From f9d8a6ede17b673a798da7f56536cb0313b59245 Mon Sep 17 00:00:00 2001 From: claude code agent 227 Date: Tue, 30 Jun 2026 00:09:25 +0300 Subject: [PATCH] fix(mcp): mirror the spoiler mark in the vendored MCP schema; changelog (F1,F2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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) --- CHANGELOG.md | 5 +++ packages/mcp/build/lib/docmost-schema.js | 20 ++++++++++++ packages/mcp/build/lib/markdown-converter.js | 6 ++++ packages/mcp/src/lib/docmost-schema.ts | 21 ++++++++++++ packages/mcp/src/lib/markdown-converter.ts | 6 ++++ .../test/unit/docmost-md-roundtrip.test.mjs | 32 +++++++++++++++++++ 6 files changed, 90 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8dfa172..abd1f25c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + ``) and on public shares. (#259) ### Changed diff --git a/packages/mcp/build/lib/docmost-schema.js b/packages/mcp/build/lib/docmost-schema.js index 6b6c221d..579a4304 100644 --- a/packages/mcp/build/lib/docmost-schema.js +++ b/packages/mcp/build/lib/docmost-schema.js @@ -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 , defeating the color import. TextStyle, Comment, + Spoiler, Callout, Table, TableRow, diff --git a/packages/mcp/build/lib/markdown-converter.js b/packages/mcp/build/lib/markdown-converter.js index d5d47400..625650f3 100644 --- a/packages/mcp/build/lib/markdown-converter.js +++ b/packages/mcp/build/lib/markdown-converter.js @@ -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 = `${textContent}`; + break; } } } diff --git a/packages/mcp/src/lib/docmost-schema.ts b/packages/mcp/src/lib/docmost-schema.ts index 546b9844..af79a181 100644 --- a/packages/mcp/src/lib/docmost-schema.ts +++ b/packages/mcp/src/lib/docmost-schema.ts @@ -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 , defeating the color import. TextStyle, Comment, + Spoiler, Callout, Table, TableRow, diff --git a/packages/mcp/src/lib/markdown-converter.ts b/packages/mcp/src/lib/markdown-converter.ts index 4e35c995..36b4443d 100644 --- a/packages/mcp/src/lib/markdown-converter.ts +++ b/packages/mcp/src/lib/markdown-converter.ts @@ -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 = `${textContent}`; + break; } } } diff --git a/packages/mcp/test/unit/docmost-md-roundtrip.test.mjs b/packages/mcp/test/unit/docmost-md-roundtrip.test.mjs index c80fbd53..798bac10 100644 --- a/packages/mcp/test/unit/docmost-md-roundtrip.test.mjs +++ b/packages/mcp/test/unit/docmost-md-roundtrip.test.mjs @@ -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, /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",