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
|
||||
are RAM-only, bound to the instance that created them. Tunable via five
|
||||
`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
|
||||
|
||||
|
||||
@@ -271,6 +271,25 @@ const TextStyle = Mark.create({
|
||||
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.
|
||||
*
|
||||
@@ -1097,6 +1116,7 @@ export const docmostExtensions = [
|
||||
// generateJSON drops <span style="color: ...">, defeating the color import.
|
||||
TextStyle,
|
||||
Comment,
|
||||
Spoiler,
|
||||
Callout,
|
||||
Table,
|
||||
TableRow,
|
||||
|
||||
@@ -160,6 +160,12 @@ export function convertProseMirrorToMarkdown(content) {
|
||||
}
|
||||
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.
|
||||
*
|
||||
@@ -1194,6 +1214,7 @@ export const docmostExtensions = [
|
||||
// generateJSON drops <span style="color: ...">, defeating the color import.
|
||||
TextStyle,
|
||||
Comment,
|
||||
Spoiler,
|
||||
Callout,
|
||||
Table,
|
||||
TableRow,
|
||||
|
||||
@@ -167,6 +167,12 @@ export function convertProseMirrorToMarkdown(content: any): string {
|
||||
}
|
||||
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", () => {
|
||||
const doc = {
|
||||
type: "doc",
|
||||
|
||||
Reference in New Issue
Block a user