diff --git a/AGENTS.md b/AGENTS.md index 064c3ccd..642b927f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -334,7 +334,7 @@ pnpm workspace (`pnpm@10.4.0`) orchestrated by **Nx**. Four workspace packages: | `apps/client` | `client` | React 18 + Vite + Mantine 8 + TanStack Query + Jotai | SPA frontend | | `packages/editor-ext` | `@docmost/editor-ext` | Tiptap/ProseMirror | Shared Tiptap node/mark extensions, imported by both the client and the server | | `packages/mcp` | `@docmost/mcp` | MCP SDK, Tiptap, Yjs | Standalone MCP server, also bundled into the server at `/mcp`. Consumes the shared converter/schema from `@docmost/prosemirror-markdown` (#293) — it no longer carries its own vendored converter/schema copy | -| `packages/prosemirror-markdown` | `@docmost/prosemirror-markdown` | Tiptap, marked, jsdom | The single, canonical ProseMirror↔Markdown converter + Docmost schema mirror (#293). Consumed by `mcp`, `git-sync`, AND `apps/server` (server-side markdown import/export, #345); there is exactly ONE copy of the converter now | +| `packages/prosemirror-markdown` | `@docmost/prosemirror-markdown` | Tiptap, marked; jsdom (Node only) | The single, canonical ProseMirror↔Markdown converter + Docmost schema mirror (#293). Consumed by `mcp`, `git-sync`, `apps/server` (server-side markdown import/export, #345), AND `apps/client` (markdown paste/copy + AI-chat render, via the `browser` entry — native `DOMParser`, no jsdom in the client bundle, #347); there is exactly ONE copy of the converter now | `build` targets are Nx-cached and dependency-ordered (`dependsOn: ["^build"]`), so `editor-ext` builds before the apps. `nx.json` sets `affected.defaultBase: main`. @@ -460,7 +460,7 @@ The API server is a Fastify app with a global `/api` prefix (`main.ts` excludes ### Client structure Vite SPA. Code is organized by feature under `apps/client/src/features/*` (mirrors the server domains: `page`, `space`, `comment`, `ai-chat`, `editor`, …). Conventions: - **TanStack Query** for server state (one `queries/` file per feature), **Jotai** atoms for local/shared UI state, **Mantine 8** + CSS modules (`*.module.css`) + `postcss-preset-mantine` for UI. -- The editor is Tiptap; shared node/mark extensions live in `packages/editor-ext` and are imported by **both the client and the server** (collaboration, schema, `canonicalizeFootnotes`) — editor schema changes often need to be made in `editor-ext`, not just the client. Server-side markdown import/export no longer lives in `editor-ext`: it goes through the canonical converter (#345, see below). The ProseMirror↔Markdown converter and its Docmost schema mirror now live in a SINGLE package, `@docmost/prosemirror-markdown` (#293), consumed by `mcp`, `git-sync`, and `apps/server` (#345) — do NOT reintroduce a per-package copy. `editor-ext` is the upstream source of the Tiptap schema; the package's `docmost-schema.ts` mirrors it and a serializer-contract test (`packages/prosemirror-markdown/test/serializer-contract.test.ts`) guards the boundary (every schema node must have a converter case), so a drift surfaces as a failing test rather than silent divergence. For the converter's property-testing and counterexample→fixture process (P1–P4 invariants, the `PROPERTY_SEED`/`PROPERTY_NUM_RUNS` knobs, and the nightly fuzz workflow), see `packages/prosemirror-markdown/README.md`. +- The editor is Tiptap; shared node/mark extensions live in `packages/editor-ext` and are imported by **both the client and the server** (collaboration, schema, `canonicalizeFootnotes`) — editor schema changes often need to be made in `editor-ext`, not just the client. Server-side markdown import/export no longer lives in `editor-ext`: it goes through the canonical converter (#345, see below). The ProseMirror↔Markdown converter and its Docmost schema mirror now live in a SINGLE package, `@docmost/prosemirror-markdown` (#293), consumed by `mcp`, `git-sync`, `apps/server` (#345), and `apps/client` (#347) — do NOT reintroduce a per-package copy. The client uses the package's `browser` entry (`@docmost/prosemirror-markdown/browser`): markdown paste (`markdown-clipboard.ts`), copy-as-markdown, and AI-chat rendering now all go through the canonical converter, so the hand-written `marked`/`turndown` markdown layer that used to live in `editor-ext` was deleted (#347). The browser entry runs the HTML→DOM stage on the native `DOMParser`, so jsdom stays out of the client bundle. `editor-ext` is the upstream source of the Tiptap schema; the package's `docmost-schema.ts` mirrors it and a serializer-contract test (`packages/prosemirror-markdown/test/serializer-contract.test.ts`) guards the boundary (every schema node must have a converter case), so a drift surfaces as a failing test rather than silent divergence. For the converter's property-testing and counterexample→fixture process (P1–P4 invariants, the `PROPERTY_SEED`/`PROPERTY_NUM_RUNS` knobs, and the nightly fuzz workflow), see `packages/prosemirror-markdown/README.md`. - API access goes through `apps/client/src/lib/api-client.ts` (axios). The `@` alias maps to `apps/client/src`. - Runtime config is injected at build time by `vite.config.ts` via `define` (`APP_URL`, `COLLAB_URL`, `APP_VERSION`, …) — these come from the root `.env`, not from `import.meta.env`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b44878a..795c7502 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -270,6 +270,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- **Client markdown paste/copy and AI-chat rendering now go through the canonical + converter.** Pasting markdown into the editor, "Copy as markdown", the AI title + generator, and the AI-chat markdown renderer all now use + `@docmost/prosemirror-markdown` (via its new `browser` entry — native + `DOMParser`, no jsdom in the client bundle) instead of the hand-written + `marked`/`turndown` markdown layer in `editor-ext`, which was **deleted**. As a + result, pasting canonical markdown (`^[…]` footnotes, ``, + `> [!type]` callouts, `$…$` math, `==…==` highlight, standalone `` + comments) now produces the SAME nodes the server import produces for the same + text. Chat/reasoning markdown now renders through the editor schema (list items + are wrapped in `

`; CSS keeps them tight). (#347) + - **Enabling a public share no longer auto-shares the whole sub-tree.** Turning a page "Shared to web" now defaults to the page alone; descendant pages become public only when you explicitly turn on the dedicated "Include sub-pages" 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..fa9e4f10 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 @@ -172,6 +181,14 @@ margin: 0 0 4px; } +/* Same as `.markdown li p` above: the canonical converter wraps every list + item's content in a

, so without this each reasoning-panel list item would + pick up `.reasoningText p`'s 4px bottom margin and render too loose. Drop it + so Reasoning-panel lists stay tight, mirroring the pre-#347 marked output. */ +.reasoningText li p { + margin: 0; +} + .inputWrapper { flex: 0 0 auto; padding-top: var(--mantine-spacing-xs); 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