PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.5
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.5
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
betterdocs / react-src / gutenberg / write-with-ai / insert.js

insert.js in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.9.5, at react-src/gutenberg/write-with-ai/insert.js

147 lines 5.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 // ===== write-with-ai/insert.js — content processing + Gutenberg insertion =====
2 // Ports the proven helpers from the legacy inline template
3 // (WriteWithAI::writeWithAIForm): strip code fences, unwrap <body>, drop the
4 // first <h1>, wrap headings + keywords in <span class="highlight">, then convert
5 // to blocks via rawHandler and insert (overwrite vs. insert-after).
6
7 import { dispatch, select } from "@wordpress/data";
8 import { rawHandler } from "@wordpress/blocks";
9
10 const escapeHtml = (str) =>
11 String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
12
13 const escapeRegex = (str) => String(str).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
14
15 const stripCodeFences = (htmlString) => {
16 if (!htmlString) return "";
17 return htmlString
18 .replace(/^\s*```(?:html|HTML)?\s*\n?/, "")
19 .replace(/\n?\s*```\s*$/, "");
20 };
21
22 const getBodyContent = (htmlString) => {
23 if (!htmlString) return "";
24 const cleaned = stripCodeFences(htmlString);
25 const match = cleaned.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
26 if (match) {
27 return match[1].replace(/<p>\s+/g, "<p>");
28 }
29 return cleaned;
30 };
31
32 // Pull the AI-generated title out of the raw response: the first <h1>'s text.
33 // Used when title generation was requested; the h1 itself is stripped from the
34 // body by removeFirstHeading/cleanGeneratedHtml so it never lands in the content.
35 export const extractFirstHeading = (rawHtml) => {
36 const html = getBodyContent(rawHtml);
37 const host = document.createElement("div");
38 host.innerHTML = html;
39 const h1 = host.querySelector("h1");
40 return h1 ? (h1.textContent || "").trim() : "";
41 };
42
43 const removeFirstHeading = (htmlString) => {
44 const newDiv = document.createElement("div");
45 newDiv.innerHTML = htmlString;
46 const firstHeading = newDiv.querySelector("h1");
47 if (firstHeading) {
48 firstHeading.parentNode.removeChild(firstHeading);
49 }
50 return newDiv.innerHTML;
51 };
52
53 const wrapWithHighlight = (inputString, keywords) => {
54 if (!inputString) return "";
55
56 const container = document.createElement("div");
57 container.innerHTML = inputString;
58
59 // Wrap each heading's visible text with the highlight span (skip if present)
60 container.querySelectorAll("h1, h2, h3, h4, h5, h6").forEach((heading) => {
61 if (heading.querySelector(".highlight")) return;
62 const text = heading.textContent;
63 if (!text || !text.trim()) return;
64 heading.innerHTML = '<span class="highlight">' + escapeHtml(text) + "</span>";
65 });
66
67 // Wrap keywords only inside text nodes — never inside attributes/code/highlights
68 const keywordList = (keywords || "")
69 .split(",")
70 .map((k) => k.trim())
71 .filter(Boolean);
72
73 if (keywordList.length) {
74 const pattern = new RegExp("\\b(" + keywordList.map(escapeRegex).join("|") + ")\\b", "gi");
75 const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, {
76 acceptNode: (node) => {
77 let parent = node.parentNode;
78 while (parent && parent !== container) {
79 if (parent.nodeType === 1) {
80 const tag = parent.tagName.toLowerCase();
81 if (tag === "code" || tag === "pre") return NodeFilter.FILTER_REJECT;
82 if (parent.classList && parent.classList.contains("highlight")) return NodeFilter.FILTER_REJECT;
83 }
84 parent = parent.parentNode;
85 }
86 return NodeFilter.FILTER_ACCEPT;
87 },
88 });
89
90 const textNodes = [];
91 let current;
92 while ((current = walker.nextNode())) {
93 textNodes.push(current);
94 }
95
96 textNodes.forEach((textNode) => {
97 pattern.lastIndex = 0;
98 if (!pattern.test(textNode.nodeValue)) return;
99 pattern.lastIndex = 0;
100 const fragmentHost = document.createElement("span");
101 fragmentHost.innerHTML = escapeHtml(textNode.nodeValue).replace(pattern, '<span class="highlight">$1</span>');
102 const parent = textNode.parentNode;
103 while (fragmentHost.firstChild) {
104 parent.insertBefore(fragmentHost.firstChild, textNode);
105 }
106 parent.removeChild(textNode);
107 });
108 }
109
110 return container.innerHTML;
111 };
112
113 // Normalize the raw OpenAI HTML into clean body content (used by the preview too).
114 export const cleanGeneratedHtml = (rawHtml, keywords = "") => {
115 let html = getBodyContent(rawHtml);
116 html = removeFirstHeading(html);
117 html = wrapWithHighlight(html, keywords);
118 return html;
119 };
120
121 // Insert processed HTML into the editor. `isOverwrite` replaces all blocks;
122 // otherwise inserts after the selected block (or at the end).
123 export const insertContentToEditor = ({ title, html, isOverwrite }) => {
124 const blocks = rawHandler({ HTML: html });
125
126 if (title) {
127 dispatch("core/editor").editPost({ title });
128 }
129
130 const hasExistingContent = (select("core/block-editor").getBlocks() || []).length > 0;
131 const selectedBlockClientId = select("core/block-editor").getSelectedBlockClientId();
132
133 if (isOverwrite || !hasExistingContent) {
134 const allBlocks = select("core/block-editor").getBlocks();
135 const ids = allBlocks.map((b) => b.clientId);
136 if (ids.length) {
137 dispatch("core/block-editor").removeBlocks(ids);
138 }
139 dispatch("core/block-editor").insertBlocks(blocks);
140 } else if (selectedBlockClientId) {
141 const selectedIndex = select("core/block-editor").getBlockIndex(selectedBlockClientId);
142 dispatch("core/block-editor").insertBlocks(blocks, selectedIndex + 1);
143 } else {
144 dispatch("core/block-editor").insertBlocks(blocks);
145 }
146 };
147