fix(mcp): mirror the spoiler mark in the vendored MCP schema; changelog (F1,F2)
F1 (data loss): packages/mcp keeps its own copy of the document schema (AGENTS.md), and the spoiler mark was only added to editor-ext + the server tiptapExtensions, so a doc with a spoiler silently lost the mark through /mcp. Add a local Spoiler mark to docmostExtensions (span[data-spoiler] parse, data-spoiler="true"+class render) and a case "spoiler" in markdown-converter emitting the same <span data-spoiler="true">…</span> as the editor-ext turndown rule; add an MCP json->md->json round-trip test. Regenerated build/lib output. F2: add the #259 inline-spoiler entry to CHANGELOG [Unreleased] Added. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -67,6 +67,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
`nosniff` + restrictive CSP + attachment disposition for non-image mimes) and
|
`nosniff` + restrictive CSP + attachment disposition for non-image mimes) and
|
||||||
are RAM-only, bound to the instance that created them. Tunable via five
|
are RAM-only, bound to the instance that created them. Tunable via five
|
||||||
`SANDBOX_*` env vars (see `.env.example`). (#243)
|
`SANDBOX_*` env vars (see `.env.example`). (#243)
|
||||||
|
- **Inline spoiler mark — hide text behind click-to-reveal blur.** Selected text
|
||||||
|
can be marked as a spoiler from a new bubble-menu toggle, or typed Discord-style
|
||||||
|
with the `||text||` input rule; the rendered span blurs until clicked to reveal.
|
||||||
|
The mark is preserved losslessly through Markdown export/import (as a raw
|
||||||
|
`<span data-spoiler="true">…</span>`) and on public shares. (#259)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -271,6 +271,25 @@ const TextStyle = Mark.create({
|
|||||||
return ["span", HTMLAttributes, 0];
|
return ["span", HTMLAttributes, 0];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
/**
|
||||||
|
* Inline spoiler mark. Mirrors the @docmost/editor-ext `spoiler` mark so a
|
||||||
|
* document carrying a spoiler survives the MCP read -> transform -> write path
|
||||||
|
* (and markdown export) instead of silently dropping the unrecognized mark.
|
||||||
|
* packages/mcp does NOT depend on editor-ext, so the definition is kept local;
|
||||||
|
* it parses span[data-spoiler] and renders the same span[data-spoiler][class]
|
||||||
|
* the editor-ext mark emits.
|
||||||
|
*/
|
||||||
|
const Spoiler = Mark.create({
|
||||||
|
name: "spoiler",
|
||||||
|
// Don't bleed onto text typed at the boundary (mirrors editor-ext).
|
||||||
|
inclusive: false,
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "span[data-spoiler]" }];
|
||||||
|
},
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["span", { "data-spoiler": "true", class: "spoiler", ...HTMLAttributes }, 0];
|
||||||
|
},
|
||||||
|
});
|
||||||
/**
|
/**
|
||||||
* Passthrough definitions for the remaining Docmost-specific nodes.
|
* Passthrough definitions for the remaining Docmost-specific nodes.
|
||||||
*
|
*
|
||||||
@@ -1097,6 +1116,7 @@ export const docmostExtensions = [
|
|||||||
// generateJSON drops <span style="color: ...">, defeating the color import.
|
// generateJSON drops <span style="color: ...">, defeating the color import.
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Comment,
|
Comment,
|
||||||
|
Spoiler,
|
||||||
Callout,
|
Callout,
|
||||||
Table,
|
Table,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
|||||||
@@ -160,6 +160,12 @@ export function convertProseMirrorToMarkdown(content) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "spoiler":
|
||||||
|
// Markdown has no native spoiler syntax, so emit the same
|
||||||
|
// lossless raw HTML the editor-ext turndown rule produces; the
|
||||||
|
// schema's Spoiler mark parses span[data-spoiler] back on import.
|
||||||
|
textContent = `<span data-spoiler="true">${textContent}</span>`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -298,6 +298,26 @@ const TextStyle = Mark.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inline spoiler mark. Mirrors the @docmost/editor-ext `spoiler` mark so a
|
||||||
|
* document carrying a spoiler survives the MCP read -> transform -> write path
|
||||||
|
* (and markdown export) instead of silently dropping the unrecognized mark.
|
||||||
|
* packages/mcp does NOT depend on editor-ext, so the definition is kept local;
|
||||||
|
* it parses span[data-spoiler] and renders the same span[data-spoiler][class]
|
||||||
|
* the editor-ext mark emits.
|
||||||
|
*/
|
||||||
|
const Spoiler = Mark.create({
|
||||||
|
name: "spoiler",
|
||||||
|
// Don't bleed onto text typed at the boundary (mirrors editor-ext).
|
||||||
|
inclusive: false,
|
||||||
|
parseHTML() {
|
||||||
|
return [{ tag: "span[data-spoiler]" }];
|
||||||
|
},
|
||||||
|
renderHTML({ HTMLAttributes }) {
|
||||||
|
return ["span", { "data-spoiler": "true", class: "spoiler", ...HTMLAttributes }, 0];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Passthrough definitions for the remaining Docmost-specific nodes.
|
* Passthrough definitions for the remaining Docmost-specific nodes.
|
||||||
*
|
*
|
||||||
@@ -1194,6 +1214,7 @@ export const docmostExtensions = [
|
|||||||
// generateJSON drops <span style="color: ...">, defeating the color import.
|
// generateJSON drops <span style="color: ...">, defeating the color import.
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Comment,
|
Comment,
|
||||||
|
Spoiler,
|
||||||
Callout,
|
Callout,
|
||||||
Table,
|
Table,
|
||||||
TableRow,
|
TableRow,
|
||||||
|
|||||||
@@ -167,6 +167,12 @@ export function convertProseMirrorToMarkdown(content: any): string {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "spoiler":
|
||||||
|
// Markdown has no native spoiler syntax, so emit the same
|
||||||
|
// lossless raw HTML the editor-ext turndown rule produces; the
|
||||||
|
// schema's Spoiler mark parses span[data-spoiler] back on import.
|
||||||
|
textContent = `<span data-spoiler="true">${textContent}</span>`;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,6 +167,38 @@ test("export emits comment anchors and they round-trip back to a comment mark",
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("export emits a spoiler span and it round-trips back to a spoiler mark", () => {
|
||||||
|
// A small ProseMirror doc with a text run carrying a `spoiler` mark. The MCP
|
||||||
|
// schema mirrors the editor-ext mark, so a spoiler must survive json -> md ->
|
||||||
|
// json instead of being silently dropped as an unrecognized mark.
|
||||||
|
const doc = {
|
||||||
|
type: "doc",
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "paragraph",
|
||||||
|
content: [
|
||||||
|
{ type: "text", text: "plot: " },
|
||||||
|
{
|
||||||
|
type: "text",
|
||||||
|
text: "the butler did it",
|
||||||
|
marks: [{ type: "spoiler" }],
|
||||||
|
},
|
||||||
|
{ type: "text", text: " end" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const body = convertProseMirrorToMarkdown(doc);
|
||||||
|
assert.match(body, /<span data-spoiler="true">the butler did it<\/span>/);
|
||||||
|
|
||||||
|
return markdownToProseMirror(body).then((rebuilt) => {
|
||||||
|
const spoilered = findTextWithMark(rebuilt, "spoiler");
|
||||||
|
assert.ok(spoilered, "expected a text node with a spoiler mark");
|
||||||
|
assert.equal(spoilered.text, "the butler did it");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test("drawio round-trips through export and import", () => {
|
test("drawio round-trips through export and import", () => {
|
||||||
const doc = {
|
const doc = {
|
||||||
type: "doc",
|
type: "doc",
|
||||||
|
|||||||
Reference in New Issue
Block a user