| 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, "&").replace(/</g, "<").replace(/>/g, ">"); |
| 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 |
|