fix(converter+schema): эмфаза при инлайн-коде не теряется — снять excludes у code (#515) #519
Reference in New Issue
Block a user
Delete Branch "fix/515-code-emphasis"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Реviewer-filed баг #515: жирный (или иной) инлайн-код терялся/«сползал» при импорте. closes #515. Стоит стопкой на #493 (PR #514).
Причина: марка
codeнаследовалаexcludes:"_"от@tiptap/extension-code→ ProseMirror-схема срезала сосуществующуюboldна HTML→PM парсе,[code,bold]никогда не жили на одном узле. По CommonMark**code**=<strong><code>.excludes:"_"→excludes:""(code комбинируется со всеми марками). Единый каноническийCodeвынесен в@docmost/editor-ext; клиент/сервер-collab/comment-editor берут его; вендор-зеркалоdocmost-schema.ts— локальныйCode.extend({excludes:""}). Паритет-гард mirror↔editor-ext.codeэмитится внутренней маркой; соседние гомогенные code-эмфаза-пробеги склеиваются; гетерогенные/небезопасно-граничные → лосслесс HTML-fallback (<strong><code>), байт-стабильный на реимпорте. Flank-safety (bare**перед бэктиком не left-flanking → HTML).How verified
packages/prosemirror-markdownvitest 777 (генеративные nested/flat 100 прогонов + паритет-гард + 5 репро-кейсов в ОБЕ стороны); mcp 820. Пост-ребейз на #514 (setext-фикс) — 777 зелёные, коллизии нет (ортогональные участки).**code1**,**aaa+bbb**(без висячих**), контроль (bold/code раздельно),**code4tail**,pre **code5** post.excludes:""делает code overlapping →code--<hash>в Yjs): реальный read-path (yattr2markname) снимает хеш обратно вcode— экспорт/поиск/старый клиент чисты, в персист хеш не течёт (подтверждено внутренним ревью по исходникам y-prosemirror).Checklist
По CommonMark `**` рядом с инлайн-кодом дают `<strong><code>` — узел с составом марок [code, bold]. Марка `code` из tiptap несёт `excludes: "_"` (исключает все прочие инлайн-марки), и ProseMirror на HTML→PM импорте (`generateJSON`) выкидывает сосуществующую bold — так `**`--flag`**` терял жирный. Ставим `excludes: ""` (не исключает ничего) во всех четырёх местах, где марка конфигурируется независимо: - Единый источник: новая каноническая марка `Code = TiptapCode.extend({ excludes: "" })` в @docmost/editor-ext, экспортируется из barrel. - Живой редактор (extensions.ts): базовая `Code` теперь из editor-ext, поверх сохранены client-only addInputRules/addKeyboardShortcuts. - Collab-сервер + серверный HTML-парс/экспорт (collaboration.util.ts): StarterKit code:false + общая `Code`; @docmost/editor-ext объявлен в apps/server/package.json (использовался, но не был задекларирован). - Редактор комментариев (comment-editor.tsx): StarterKit code:false + `Code`. - Вендор-зеркало docmostExtensions (docmost-schema.ts) — сознательно отдельная копия, не тянущая editor-ext в node-рантайм: `excludes:""` объявлен локально (StarterKit code:false + `Code.extend({ excludes:"" })`), держится в синхроне с editor-ext паритет-тестом. Паритет-гард: тест в пакете сверяет excludes марки code вендор-зеркала с канонической editor-ext Code (обе ""); vitest резолвит @docmost/editor-ext на sibling-исходник, чтобы гард был герметичным. `excludes:""` делает code «перекрывающейся» маркой в y-prosemirror (как comment): она пишется в Yjs под хешированным ключом `code--<hash>` и распаковывается обратно в `code` штатным декодером — правим mcp-тест-хелпер fragmentToJson, чтобы он снимал хеш ровно как yattr2markname (иначе overlapping-code утёк бы в сравнение как `code--<hash>`). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>Теперь узел может нести [code, bold], и сериализатор PM→Markdown должен это корректно выгружать в обе стороны (git-sync — data-loss-critical, требуется байт-стабильность md2===md1). case "text": убран ранний return для code-рана. Backtick-спан оборачивается ПЕРВЫМ (самая внутренняя марка), затем прочие марки в том же порядке массива — `**`code`**`. Для НЕ-code ранов вывод байт-идентичен прежнему (вынесены хелперы escapeInlineText и applyInlineMark, поведение сохранено). renderInlineChildren: собирает максимальный пробег подряд идущих text-узлов с голой-делимитерной эмфазис-маркой (bold/italic/strike/uncolored-highlight), содержащий хотя бы один code-узел. ОДНОРОДНЫЙ пробег (у всех идентичное множество не-code марок) и безопасные границы → общие марки выносятся наружу ОДИН раз: `**`aaa` + `bbb`**`, `**`code4` tail**`. НЕОДНОРОДНЫЙ (`[code,bold]` рядом с `[italic]`) ИЛИ граница упирается в словесный символ (делимитер `**` перед backtick не был бы flanking → эмфаза потерялась бы) → весь пробег через lossless inlineToHtml (схема-HTML). НЕ-code вывод байт-идентичен, кроме редкого случая голой-делимитерной эмфазы вплотную к code+эмфазе. Узлы, где code раньше вообще не мог нести эмфазу, — новая территория, существующие страницы не затрагиваются. inlineToHtml: `<code>` тоже оборачивается ВНУТРЕННИМ (импорт отдаёт code последним в массиве — `[emphasis, code]`; порядок-зависимый цикл переворачивал бы `<strong><code>` в `<code><strong>` на реэкспорте и ломал байт-фикспойнт). Тесты: перевёрнуты ассерты, фиксировавшие старое (по CommonMark неверное) поведение (code+bold/strike/link → теперь `**`x`**`/`~~`x`~~`/`[`x`](…)`); генераторы расширены на code+{bold,italic,strike,highlight} в каноническом порядке импорта; добавлены явные round-trip пины 5 кейсов репорта (импорт→марки и md→pm→md идемпотентность) + усиление code-combo свойства (обе марки выживают). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>🤖 Авто-ревью · #519 (эмфаза при инлайн-коде, #515) — голова
95c0d813. Вердикт: CHANGES (1 doc-доводка)Отличный фикс — минимальный, корректный, покрыт. Единственное к правке — один вводящий в заблуждение комментарий. Всё остальное (8 из 9 аспектов) — LGTM, объективка зелёная целиком.
Прогнал полный 9-аспектный веер + гейт + свой эмпирический round-trip на всех 5 репро-кейсах и краевых. Сверил лично:
**code1**→marksbold+code(баг закрыт, марки сосуществуют); split-run, tail,pre…post, plain-code-остаётся-бэктиком-не-HTML, code-at-line-start (взаимодействие с #514-setext), italic-code — все байт-стабильно идемпотентны (md2==md3). Нового класса потери от +430-строчного переписывания сериализатора нет.code--<hash>не течёт: сверил реальный persist-путьTiptapTransformer.fromYdoc→yattr2marknameпротив УСТАНОВЛЕННОГО y-prosemirror (regex/(.*)(--[a-zA-Z0-9+/=]{8})$/, sync-plugin.js:1093 — байт-в-байт как mcp-мок); вpages.content/поиск/экспорт уходит голыйcode. Механизм тот же, что у overlapping-маркиcomment.Code(editor-ext,excludes:"") во всех 3 живых схемах + вендор-зеркало + паритет-гард (краснеет на дрейфе, mutation-verified); 4-й/5-й потребительdocmostExtensionsинхерят через ре-экспорт.@docmost/prosemirror-markdown— 777/777 детерминированно ×3. Order-зависимости нет.🔧 Do — потом ре-ревью
inlineToHtmlложно утверждает «import ALWAYS yields the code mark LAST».markdown-converter.ts:~1494-1499. Твой же генератор говорит обратное для highlight:text-arbitraries.ts:152-153— «import yields[code, highlight](the==inline extension nests code first)», т.е. code ПЕРВЫМ. Код корректен в любом случае (оборачивает<code>внутренним независимо от позиции в массиве), но заявленное обоснование («order-sensitive loop would flip … because import yields code last») неверно и может подтолкнуть мейнтейнера положиться на фиксированный[emphasis, code]-порядок. Fix: переформулируй (и параллельную фразу вtext-arbitraries.ts:146-147): порядок марок при импорте ЗАВИСИТ (code last для bold/italic/strike, code first для==-highlight), а инвариант — «оборачиваем<code>внутренним независимо от импортированного порядка».9 аспектов. Security/stability/regressions/test-coverage/conventions/architecture/coherence/simplification — LGTM. Гейт зелёный: editor-ext/pm-markdown build 0, pm-markdown vitest 777/777 (×3 детерминированно на чистом #519), git-sync 269/269, mcp node:test 820/0, #514 setext-арм цел. База #514
3a344626(только что заапрувлен), стоит стопкой; мёрж после #514. CHANGELOG не флажу (AGENTS.md: release-time из git-log, не per-PR).Хэндофф после доводки (
0a53be9e).Finding 1 [documentation] — fixed. Комментарий в
inlineToHtml(markdown-converter.ts:1494) переписан: убрано ложное «import ALWAYS yields the code mark LAST». Теперь явно — порядок марок при импорте ЗАВИСИТ от расширения: code last для bold/italic/strike ([emphasis, code]), code FIRST для==-highlight ([code, highlight]); инвариант — «оборачиваем<code>внутренним НЕЗАВИСИМО от импортированного порядка» (что и даёт байт-фикспойнт в обе стороны). Параллельную фразу вtext-arbitraries.ts:146тоже де-универсализировал: code last здесь только для bold/italic/strike, НЕ универсально — highlight-кейс ниже импортирует code first; матчим каждый кейс к своему импортированному порядку.Правка строго comment-only (дифф — одни строки
//, ноль изменений кода/поведения), поэтому объективка прежней головы95c0d813(build 0, pm-markdown 777/777, git-sync 269/269, mcp 820/0) остаётся в силе. База —feat/493-converter(#514), стоит стопкой; мёрж после #514.🤖 Ре-ревью · #519 (эмфаза инлайн-кода, #515) round 2 — голова
0a53be9e. Вердикт: PASS ✅Единственный DO закрыт: комментарий в
inlineToHtml(markdown-converter.ts:1494-1500) переписан — убрано ложное «import always yields the code mark LAST», теперь честно: «imported mark order is NOT fixed … the invariant is instead: wrap<code>innermost regardless of the imported order». Параллельная фраза вtext-arbitraries.tsтоже поправлена. Стухших «always yields code last» больше нет (греп чист).Диф крохотный (15 строк комментов + 6, ноль логики). Гейт: pm-markdown vitest 777/777. (Остальное было LGTM на round 1: round-trip всех code-эмфаза кейсов байт-стабилен — перепроверял своим прогоном; Yjs code--hash не течёт; схема консистентна; escape инъекций-безопасен.)
Approve.