Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions third_party/muya/src/assets/styles/exportStyle.css
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@
white-space: pre-wrap;
}

/* Render soft line breaks (Shift+Enter → a bare `\n` inside a block) the way
the editor does (`.mu-content` is pre-wrap) instead of emitting a
non-standard `<br>`, so the exported HTML stays CommonMark-conformant (the
`\n` is a plain line ending) yet still shows the break (marktext#3676).

`li:not(:has(> p))` targets only tight list items, whose soft break is a
bare `\n` directly inside the `<li>`. Loose items wrap their content in
`<p>` (handled by the `p` rule); giving them `li` pre-wrap would expose
marked's pretty-printing newline between `</p>` and `</li>` as a stray blank
line, so they are deliberately excluded. Specificity stays level with the
earlier `.toc-container ul li` rule (no-descending-specificity). */
.markdown-body p,
.markdown-body li:not(:has(> p)) {
white-space: pre-wrap;
}

.markdown-body table {
display: table;
}
Expand Down
37 changes: 37 additions & 0 deletions third_party/muya/src/state/__tests__/softBreakExportHtml.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest';
import { getHighlightHtml } from '../../utils/marked';

// marktext#3676 — a soft line break (Shift+Enter, serialized as a bare `\n`
// inside a block) shows as a line break in the editor (`.mu-content` is
// pre-wrap) but was lost on export because marked renders a soft break as a
// space. Rather than emit a non-standard `<br>` (which CommonMark reserves for
// hard breaks), the export keeps the conformant `\n` and renders it with
// `white-space: pre-wrap` on `.markdown-body p` and `li:not(:has(> p))`
// (tight items only).
//
// These assert the HTML stays conformant — the soft break is a preserved
// newline, never a `<br>`, and hard breaks are untouched. The pre-wrap
// rendering itself (and the `:has()` exclusion of loose items) is a CSS
// concern verified in a real browser against the export stylesheet.

const OPTS = { math: false, superSubScript: false, footnote: false, frontMatter: false };

describe('#3676 — soft line breaks survive export as a conformant newline', () => {
it('keeps a paragraph soft break as a newline, never a <br>', () => {
const html = getHighlightHtml('line one\nline two', OPTS);
expect(html).toContain('<p>line one\nline two</p>');
expect(html).not.toMatch(/<br\s*\/?>/);
});

it('keeps a soft break inside a tight list item, never a <br>', () => {
const html = getHighlightHtml('- line A\n line B', OPTS);
expect(html).toMatch(/<li>line A\nline B<\/li>/);
expect(html).not.toMatch(/<br\s*\/?>/);
});

it('leaves a real hard break (two trailing spaces) as <br>', () => {
// Sanity: the change only touches soft breaks; hard breaks are untouched.
const html = getHighlightHtml('line one \nline two', OPTS);
expect(html).toMatch(/<br\s*\/?>/);
});
});
Loading