From 5d8083f8ff23cc98319bb9ec560be750751c5971 Mon Sep 17 00:00:00 2001 From: agent_coder Date: Sat, 11 Jul 2026 02:11:18 +0300 Subject: [PATCH 1/3] =?UTF-8?q?refactor(client):=20=D0=B2=D1=81=D1=82?= =?UTF-8?q?=D0=B0=D0=B2=D0=BA=D0=B0=20markdown=20=D1=87=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=B7=20=D0=BA=D0=B0=D0=BD=D0=BE=D0=BD=D0=B8=D1=87=D0=B5=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D0=B9=20@docmost/prosemirror-markdown=20+=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20md-=D1=81=D0=BB?= =?UTF-8?q?=D0=BE=D1=8F=20editor-ext=20(#347)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Третий шаг #345: клиентская вставка markdown переезжает с marked-слоя editor-ext (не знал канона — ^[…], , при вставке не распознавались) на канонический пакет. По завершении слой удалён целиком. closes #347 - Browser-entry пакета (инъекция DOM-парсера): jsdom только в Node-пути. dom-parser.ts — 2 слота инъекции (HtmlDocumentParser для HTML→Document, GenerateJsonFn для @tiptap/html), без импорта DOM. dom-parser.node.ts регистрирует jsdom + @tiptap/html/server; dom-parser.browser.ts — нативный DOMParser + @tiptap/html (browser), экспонирован через exports-условие "browser" + сабпас "./browser". markdown-to-prosemirror.ts: убраны статический импорт jsdom и module-level global.window-шим. Клиент ВСЕГДА импортирует явный сабпас /browser — не полагается на порядок условий. Node-потребители (mcp/ server) идут по "." → default → index.js → jsdom, не затронуты. - markdown-clipboard.ts: конвертация через browser-entry (markdownToProseMirror → PM-JSON → HTML через живую схему редактора DOMSerializer → НЕИЗМЕНЁННЫЙ downstream-шов normalizeTableColumnWidths→parseSlice→canonicalizePastedFootnotes →dispatch). Эвристики/fragment-insertion не тронуты. Конвертер async → handle Paste захватывает диапазон, забирает событие, диспатчит на резолве; и success, и fail-open ветки защищены guard'ом doc!==startDoc (не диспатчить по устаревшему диапазону). clipboardTextSerializer (copy PM→md) — через convertProseMirror ToMarkdown. - Удалён packages/editor-ext/src/lib/markdown/ целиком (+ marked из package.json). Мигрированы ВСЕ потребители markdownToHtml/htmlToMarkdown: ai-chat/utils/ markdown.ts (→ новый markdownToProseMirrorSync + DOMSerializer), use-generate- page-title.ts / page-header-menu.tsx (→ convertProseMirrorToMarkdown(getJSON)), серверный spec. Grep: осиротевших импортов нет, editor-ext = только схема/ расширения. Turndown ушёл из бандла (был в старом htmlToMarkdown). - AI-чат теперь рендерит markdown через схему редактора (li в

); добавлен .markdown li p{margin:0} (CSS-модуль, скоуп только чата) — визуально плотно. Проверка: pmd tsc + vitest 744; client build УСПЕШЕН, grep бандла на JSDOM/parse5/happy-dom/turndown — 0 (утечки нет); клиентский suite + paste-тесты зелёные (34); editor-ext 196; node-потребители (mcp/server/git-sync) зелёные. Юнит-тесты: dual-path parity (jsdom==DOMParser), канон-формы == серверный импорт, негативы ($5/==/[^1] не корёжатся), async-paste (claim→convert→dispatch, fail-open). Ручная paste-QA полного клиентского round-trip (footnote/callout/math/image-comment; вставка из VSCode/Obsidian/GitHub в список/таблицу/callout) и Docker-сборка клиента (#333-класс) — за пределами автостенда, оставлено на ручную проверку. Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/client/package.json | 1 + .../ai-chat/components/ai-chat.module.css | 9 + .../utils/collapse-blank-lines.test.ts | 35 +- .../src/features/ai-chat/utils/markdown.ts | 61 ++- .../markdown-clipboard.paste.test.ts | 112 ++++ .../extensions/markdown-clipboard.test.ts | 270 +++++++--- .../editor/extensions/markdown-clipboard.ts | 167 ++++-- .../hooks/use-generate-page-title.test.tsx | 15 +- .../editor/hooks/use-generate-page-title.ts | 6 +- .../components/header/page-header-menu.tsx | 7 +- .../html-embed-import-detect.spec.ts | 22 +- packages/editor-ext/package.json | 3 - packages/editor-ext/src/index.ts | 1 - .../lib/footnote/footnote-markdown.test.ts | 131 ----- .../src/lib/image/image-markdown.test.ts | 68 --- .../lib/markdown/html-embed-marked.spec.ts | 105 ---- packages/editor-ext/src/lib/markdown/index.ts | 2 - .../lib/markdown/markdown-html-embed.spec.ts | 112 ---- .../src/lib/markdown/utils/basename.ts | 29 -- .../markdown/utils/callout-common.marked.ts | 33 -- .../src/lib/markdown/utils/callout.marked.ts | 37 -- .../utils/footnote.marked.orphan.test.ts | 72 --- .../src/lib/markdown/utils/footnote.marked.ts | 131 ----- .../utils/github-callout.marked.test.ts | 54 -- .../markdown/utils/github-callout.marked.ts | 81 --- .../lib/markdown/utils/html-embed.marked.ts | 41 -- .../src/lib/markdown/utils/marked.utils.ts | 76 --- .../lib/markdown/utils/math-block.marked.ts | 37 -- .../math-inline.marked.falsepositive.test.ts | 50 -- .../lib/markdown/utils/math-inline.marked.ts | 55 -- .../lib/markdown/utils/spoiler.marked.test.ts | 128 ----- .../src/lib/markdown/utils/turndown.d.ts | 12 - .../markdown/utils/turndown.dataloss.test.ts | 147 ------ .../src/lib/markdown/utils/turndown.utils.ts | 488 ------------------ packages/editor-ext/vitest.config.ts | 7 +- packages/prosemirror-markdown/package.json | 6 + packages/prosemirror-markdown/src/browser.ts | 17 + packages/prosemirror-markdown/src/index.ts | 7 + .../src/lib/dom-parser.browser.ts | 27 + .../src/lib/dom-parser.node.ts | 30 ++ .../src/lib/dom-parser.ts | 87 ++++ .../prosemirror-markdown/src/lib/index.ts | 5 +- .../src/lib/markdown-to-prosemirror.ts | 87 ++-- .../test/dom-parser-both-paths.test.ts | 101 ++++ .../test/setup.dom-parser.ts | 13 + .../prosemirror-markdown/vitest.config.ts | 4 + pnpm-lock.yaml | 12 +- 47 files changed, 909 insertions(+), 2092 deletions(-) create mode 100644 apps/client/src/features/editor/extensions/markdown-clipboard.paste.test.ts delete mode 100644 packages/editor-ext/src/lib/footnote/footnote-markdown.test.ts delete mode 100644 packages/editor-ext/src/lib/image/image-markdown.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/html-embed-marked.spec.ts delete mode 100644 packages/editor-ext/src/lib/markdown/index.ts delete mode 100644 packages/editor-ext/src/lib/markdown/markdown-html-embed.spec.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/basename.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/callout-common.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/callout.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/footnote.marked.orphan.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/footnote.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/github-callout.marked.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/github-callout.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/html-embed.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/marked.utils.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/math-block.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/math-inline.marked.falsepositive.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/math-inline.marked.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/spoiler.marked.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/turndown.d.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/turndown.dataloss.test.ts delete mode 100644 packages/editor-ext/src/lib/markdown/utils/turndown.utils.ts create mode 100644 packages/prosemirror-markdown/src/browser.ts create mode 100644 packages/prosemirror-markdown/src/lib/dom-parser.browser.ts create mode 100644 packages/prosemirror-markdown/src/lib/dom-parser.node.ts create mode 100644 packages/prosemirror-markdown/src/lib/dom-parser.ts create mode 100644 packages/prosemirror-markdown/test/dom-parser-both-paths.test.ts create mode 100644 packages/prosemirror-markdown/test/setup.dom-parser.ts diff --git a/apps/client/package.json b/apps/client/package.json index ba042fd2..a9fb48f7 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -21,6 +21,7 @@ "@atlaskit/pragmatic-drag-and-drop-live-region": "1.3.4", "@casl/react": "5.0.1", "@docmost/editor-ext": "workspace:*", + "@docmost/prosemirror-markdown": "workspace:*", "@excalidraw/excalidraw": "0.18.0-3a5ef40", "@mantine/core": "8.3.18", "@mantine/dates": "8.3.18", diff --git a/apps/client/src/features/ai-chat/components/ai-chat.module.css b/apps/client/src/features/ai-chat/components/ai-chat.module.css index 7b99178c..94affdb9 100644 --- a/apps/client/src/features/ai-chat/components/ai-chat.module.css +++ b/apps/client/src/features/ai-chat/components/ai-chat.module.css @@ -55,6 +55,15 @@ padding-inline-start: 1.4em; } +/* The canonical converter renders list items through the editor schema, which + wraps each item's content in a

(listItem content is `paragraph+`). Drop + that paragraph's block margin so list items render TIGHT (no extra vertical + gap), matching the previous marked output — same rule already applied to + table cells above (issue #347). */ +.markdown li p { + margin: 0; +} + /* GFM tables in assistant markdown. The chat lives in a NARROW side panel, so a wide LLM table must scroll horizontally instead of collapsing its columns: `.markdown` sets `word-break: break-word`, which (with the default table diff --git a/apps/client/src/features/ai-chat/utils/collapse-blank-lines.test.ts b/apps/client/src/features/ai-chat/utils/collapse-blank-lines.test.ts index d61315dd..0ed68095 100644 --- a/apps/client/src/features/ai-chat/utils/collapse-blank-lines.test.ts +++ b/apps/client/src/features/ai-chat/utils/collapse-blank-lines.test.ts @@ -33,29 +33,44 @@ describe("collapseBlankLines", () => { }); }); -describe("collapseBlankLines + renderChatMarkdown (tight reasoning rendering)", () => { - it("renders a blank-line-separated list as a TIGHT list (no

  • )", () => { +describe("collapseBlankLines + renderChatMarkdown (canonical converter)", () => { + // Chat markdown now renders through @docmost/prosemirror-markdown (issue #347): + // the SAME converter the editor/import use. Its list items are schema-shaped — + // each

  • 's content is wrapped in a

    (listItem content is `paragraph+`) — + // so the HTML always carries `

  • ` regardless of blank-line + // looseness in the source (the converter has no tight/loose distinction). The + // visual tightness that `collapseBlankLines` used to buy is now provided by + // CSS (`.markdown li p { margin: 0 }`), not the HTML shape. + it("renders a blank-line-separated bullet list as a real