fix(html-embed): address code-review findings on the sandbox commit
Follow-up fixes to the htmlEmbed-sandbox / trackerHead change:
- share-seo: inject trackerHead via a function replacer so `$`-sequences
($&, $', $`, $$) in the admin snippet are inserted literally instead of
being treated as String.replace substitution patterns; warn when the
</head> marker is absent instead of silently skipping injection.
- mcp: register a passthrough `htmlEmbed` node in the schema mirror so an
AI/MCP edit of a page containing an embed no longer throws
"Unknown node type: htmlEmbed" in TiptapTransformer.toYdoc.
- editor-ext + client: treat a non-finite `data-height` as auto (null) so a
crafted/corrupted height cannot disable auto-resize or yield a NaN iframe
height; extract a shared clampHeight helper.
- client: rename render-raw-html.{ts,test.ts} -> html-embed-sandbox.{...} and
shouldExecute -> shouldRender so the seam name matches the sandbox model.
- client: i18n the iframe title; surface the real error reason in
tracker-settings (console.error + err.response.data.message).
- docs: note hasHtmlEmbedNode is now a test-only helper; add an Unreleased
CHANGELOG entry; drop the dangling "arbitrary HTML embed" planning-doc ref.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -97,7 +97,12 @@ export const HtmlEmbed = Node.create<HtmlEmbedOptions>({
|
||||
default: null,
|
||||
parseHTML: (el) => {
|
||||
const v = el.getAttribute("data-height");
|
||||
return v ? parseInt(v, 10) : null;
|
||||
if (!v) return null;
|
||||
const n = parseInt(v, 10);
|
||||
// A non-numeric data-height (e.g. crafted/corrupted import) must not
|
||||
// become NaN: NaN is typeof "number" and would disable auto-resize and
|
||||
// yield an unclamped iframe height downstream. Treat it as auto (null).
|
||||
return Number.isFinite(n) ? n : null;
|
||||
},
|
||||
renderHTML: (attrs: HtmlEmbedAttributes) =>
|
||||
attrs.height ? { "data-height": String(attrs.height) } : {},
|
||||
|
||||
@@ -797,6 +797,60 @@ const Embed = Node.create({
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Docmost raw HTML embed. Block atom; the client renders `source` inside a
|
||||
* sandboxed iframe. The MCP server never renders it — it only needs the
|
||||
* schema to accept and carry the node so a fromYdoc -> transform -> toYdoc
|
||||
* round-trip does not throw "Unknown node type: htmlEmbed". Mirrors the
|
||||
* @docmost/editor-ext node name, attribute keys and flags; keep in sync when
|
||||
* the editor-ext htmlEmbed schema changes.
|
||||
*
|
||||
* NOTE: unlike the canonical editor-ext node, `data-source` here is mapped as
|
||||
* plain text rather than base64-encoded. That is intentional: the MCP write
|
||||
* path carries the node through Yjs (fromYdoc -> toYdoc) on its JSON `source`
|
||||
* attribute and never invokes parseHTML/renderHTML, and htmlEmbed is not
|
||||
* produced from the markdown/HTML (generateJSON) path. If a future HTML path
|
||||
* for htmlEmbed is added here, this mapping must adopt editor-ext's base64
|
||||
* encode/decode to avoid double-encoding `source`.
|
||||
*/
|
||||
const HtmlEmbed = Node.create({
|
||||
name: "htmlEmbed",
|
||||
group: "block",
|
||||
inline: false,
|
||||
isolating: true,
|
||||
atom: true,
|
||||
defining: true,
|
||||
draggable: true,
|
||||
addAttributes() {
|
||||
return {
|
||||
source: {
|
||||
default: "",
|
||||
parseHTML: (el: HTMLElement) => el.getAttribute("data-source") ?? "",
|
||||
renderHTML: (attrs: Record<string, any>) => ({
|
||||
"data-source": attrs.source ?? "",
|
||||
}),
|
||||
},
|
||||
height: {
|
||||
default: null,
|
||||
parseHTML: (el: HTMLElement) => {
|
||||
const v = el.getAttribute("data-height");
|
||||
if (!v) return null;
|
||||
const n = parseInt(v, 10);
|
||||
return Number.isFinite(n) ? n : null;
|
||||
},
|
||||
renderHTML: (attrs: Record<string, any>) =>
|
||||
attrs.height != null ? { "data-height": String(attrs.height) } : {},
|
||||
},
|
||||
};
|
||||
},
|
||||
parseHTML() {
|
||||
return [{ tag: 'div[data-type="htmlEmbed"]' }];
|
||||
},
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return ["div", { "data-type": "htmlEmbed", ...HTMLAttributes }, 0];
|
||||
},
|
||||
});
|
||||
|
||||
/** Shared attribute set for drawio/excalidraw diagram nodes. */
|
||||
const diagramAttributes = () => ({
|
||||
src: {
|
||||
@@ -1158,6 +1212,7 @@ export const docmostExtensions = [
|
||||
Video,
|
||||
Youtube,
|
||||
Embed,
|
||||
HtmlEmbed,
|
||||
Drawio,
|
||||
Excalidraw,
|
||||
Columns,
|
||||
|
||||
Reference in New Issue
Block a user