Paragraphs stack like lines, Shift+Enter breaks one, and PDFs have a left margin
Three things that were wrong in how a document renders, found by pasting a list out of Google Docs and looking at the result. Paragraph spacing. Docs' Normal text is 0pt before, 0pt after, 1.15 line spacing, so consecutive paragraphs stack like lines and a visible gap is an empty paragraph. Ours added 1em after every paragraph, so a pasted list came out widely spaced with a double gap where the blank line was. Now margin-block: 0 and line-height 1.4. Worth being precise about what was NOT wrong: Docs has paragraphs, its Enter makes one, and ours already matched. The difference was entirely spacing. The empty-paragraph rule that comes with it is the part that would have shipped broken. A blank paragraph was visible in the server-rendered read view only because it carried that margin; with the margin gone, <p></p> collapses to nothing, so a blank line would still show while editing — ProseMirror puts a br inside an empty textblock, which is why it is never :empty there — and vanish from the read view, the PDF and the Markdown export. Same document, two shapes. Shift+Enter. hard_break was in the schema and bound to nothing, and the Rust renderer had no case for it, so a break would have been dropped by every reader. Now bound and handled in the HTML, the text, the JSON the exporters read, the PDF and the Markdown, where it is a backslash before the line ending because that survives trailing-whitespace stripping. Verified in a real browser: ALPHA, Shift+Enter, BRAVO, Enter, CHARLIE gives one paragraph holding ALPHABRAVO plus one holding CHARLIE, and the same shape comes back after a reload through the renderer. The PDF's left margin. pdf.go drew text at x = ctx.indent, which starts at zero and is only added to for quotes and lists, while the vertical cursor correctly started at MarginPt. So body text began at the page edge and, because wrapping still used the right content width, every page had a two-inch gutter on the right. MarginPt was correct all along; nothing applied it horizontally. No test asserted where text starts, which is why a whole-page offset shipped, and there is one now. Reviewed by Fable, which verified the p:empty mechanism, all three x-computation sites, the hard_break name across six representations and the Markdown form against the libraries' own sources, and found no test that could not fail against the mistake it guards. Two findings fixed: a comment asserting Shift+Enter used to fall through to the browser, when prosemirror-view captures and preventDefaults every Enter so it did nothing at all; and three PDF spacing constants this change turned into load-bearing zeroes with nothing pinning them to the stylesheet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
12 files changed,
+706
−43
core/src/fragment.rs+25 −0core/tests/fragment.rs+59 −0fixtures/editor-entry.js+46 −0internal/core/testdata/docs_core.wasmbinaryinternal/docs/export_test.go+101 −0internal/docs/static/{editor.0c0322ca56426314.js => editor.26513ec0d7ae331b.js}+11 −11internal/docs/views/show.html+44 −6internal/export/markdown.go+47 −2internal/export/markdown_test.go+86 −0internal/export/pdf.go+78 −9internal/export/pdf_test.go+120 −0internal/export/pdfread_test.go+89 −15
landed from worktree-typography
core/src/fragment.rsLoading diff…
core/tests/fragment.rsLoading diff…
fixtures/editor-entry.jsLoading diff…
internal/core/testdata/docs_core.wasmLoading diff…
internal/docs/export_test.goLoading diff…
internal/docs/static/{editor.0c0322ca56426314.js => editor.26513ec0d7ae331b.js}Loading diff…
internal/docs/views/show.htmlLoading diff…
internal/export/markdown.goLoading diff…
internal/export/markdown_test.goLoading diff…
internal/export/pdf.goLoading diff…
internal/export/pdf_test.goLoading diff…
internal/export/pdfread_test.goLoading diff…