From d35c28748b42ede160ee5cb15074fd59fc0c8559 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Sat, 4 Jul 2026 15:08:08 +0800 Subject: [PATCH 1/3] fix(muya): stop inline math showing a scrollbar for trailing sub/superscripts (#4837) (#4839) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(muya): stop inline math from showing a scrollbar for trailing sub/superscripts (#4837) Committed inline math renders in a `.mu-math-render` box that #4615 gave `overflow: auto hidden` so genuinely long formulas scroll instead of overflowing the editor. KaTeX renders every sub/superscript with a 2px `.vlist-s` strut that `.vlist-t2 { margin-right: -2px }` cancels visually — but the negative margin doesn't shrink `scrollWidth`, so the scroll extent stays 2px wider than the visible width. `overflow-x: auto` then draws a spurious scrollbar under any short formula ending in a sub/superscript (e.g. `$x_1$`). Zero the strut and its compensating margin inside the inline-math render. This is net-zero visually (top/height/width unchanged) and a genuinely long formula still overflows and stays scrollable. Co-Authored-By: Claude Opus 4.8 (1M context) * test(muya): assert short inline math ending in a subscript has no scrollbar (#4837) Adds a regression test that a hidden inline `$x_1$` render has zero horizontal overflow (scrollWidth === clientWidth), so `overflow-x: auto` draws no scrollbar. Fails with `overflowX === 2` before the CSS fix. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .../e2e/tests/blocks/inline-math-align.spec.ts | 18 ++++++++++++++++++ .../muya/src/assets/styles/inlineSyntax.css | 15 +++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/packages/muya/e2e/tests/blocks/inline-math-align.spec.ts b/packages/muya/e2e/tests/blocks/inline-math-align.spec.ts index 20ed300e6c..d8226dfeef 100644 --- a/packages/muya/e2e/tests/blocks/inline-math-align.spec.ts +++ b/packages/muya/e2e/tests/blocks/inline-math-align.spec.ts @@ -45,6 +45,24 @@ test('a long hidden inline math stays scrollable, not truncated', async ({ page expect(r.scrollable).toBe(true) // content is reachable by scrolling, not cut off }) +test('a short inline math ending in a subscript shows no scrollbar (#4837)', async ({ page }) => { + // KaTeX gives every sub/superscript a 2px `.vlist-s` strut that its + // `.vlist-t2` margin cancels visually but not in scrollWidth; the popup's + // `overflow: auto` then drew a spurious scrollbar under any short formula + // ending in one. The rendered scroll extent must match the visible width. + await page.evaluate(() => window.muya!.setContent('inline $x_1$ here')) + await page.waitForTimeout(150) + const r = await page.evaluate(() => { + const render = document.querySelector('.mu-math > .mu-math-render') as HTMLElement + return { + hidden: render.closest('.mu-math')!.classList.contains('mu-hide'), + overflowX: render.scrollWidth - render.clientWidth, + } + }) + expect(r.hidden).toBe(true) + expect(r.overflowX).toBe(0) // no horizontal overflow, so no scrollbar +}) + test('the inline-math scrollbar is thin (6px, matching code blocks)', async ({ page }) => { await page.evaluate(() => window.muya!.setContent('x')) await page.waitForTimeout(100) diff --git a/packages/muya/src/assets/styles/inlineSyntax.css b/packages/muya/src/assets/styles/inlineSyntax.css index c27236e250..c1d60ad71c 100644 --- a/packages/muya/src/assets/styles/inlineSyntax.css +++ b/packages/muya/src/assets/styles/inlineSyntax.css @@ -206,6 +206,21 @@ code.mu-inline-rule { height: 6px; } +/* KaTeX gives every sub/superscript a 2px `.vlist-s` strut that + `.vlist-t2 { margin-right: -2px }` cancels visually. The negative margin fixes + layout width but not scrollWidth, so the `overflow: auto` above draws a + spurious scrollbar under any short formula ending in a sub/superscript + (#4837). Zero both to match the scroll extent to the visible width — net-zero + visually; a genuinely long formula still overflows and stays scrollable. */ +.mu-math > .mu-math-render .katex .vlist-s { + width: 0; + min-width: 0; +} + +.mu-math > .mu-math-render .katex .vlist-t2 { + margin-right: 0; +} + .mu-ruby > .mu-ruby-render { left: 50%; From 75b5949f055a77cc60368efd0e756b097a21dd80 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Sat, 4 Jul 2026 15:59:00 +0800 Subject: [PATCH 2/3] fix(muya): isolate mermaid render failures during export (#4812) (#4840) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The styled-HTML / PDF export path renders every mermaid diagram with a single batched `mermaid.run({ nodes: [...] })`. That call rejects the whole batch on the first parse error, so one diagram with invalid syntax (e.g. a `|` inside a `[label]`) threw all the way up to the desktop export wrapper — the entire export failed with "Failed to export document" and no file was written. Render each mermaid diagram in its own `mermaid.run` inside a try/catch, falling back to the same `< Invalid Diagram >` placeholder the other diagram renderers (`_renderDiagram`) and the inline editor already use. A broken diagram now degrades locally and the rest of the document exports. Also skip loading mermaid entirely when the document has none. Co-authored-by: Claude Opus 4.8 (1M context) --- .../__tests__/mermaidExportResilience.spec.ts | 90 +++++++++++++++++++ packages/muya/src/state/markdownToHtml.ts | 19 +++- 2 files changed, 106 insertions(+), 3 deletions(-) create mode 100644 packages/muya/src/state/__tests__/mermaidExportResilience.spec.ts diff --git a/packages/muya/src/state/__tests__/mermaidExportResilience.spec.ts b/packages/muya/src/state/__tests__/mermaidExportResilience.spec.ts new file mode 100644 index 0000000000..ee2a784856 --- /dev/null +++ b/packages/muya/src/state/__tests__/mermaidExportResilience.spec.ts @@ -0,0 +1,90 @@ +// @vitest-environment jsdom + +// Regression for #4812: a single mermaid diagram with a syntax error must not +// abort the whole document export. The styled-HTML / PDF export path renders +// every `code.language-mermaid` via `mermaid.run`; a batch run rejects entirely +// on the first parse error, so one bad diagram threw all the way up to the +// desktop wrapper ("Failed to export document") and no file was written. +// +// `mermaid` can't run under jsdom, so we mock the diagram-renderer loader to +// return a fake mermaid whose `run` throws like the real parser does on invalid +// input. That isolates the behaviour under test — per-diagram error containment. + +import { beforeEach, describe, expect, it, vi } from 'vitest'; + +const mermaidRun = vi.fn(); + +vi.mock('../../utils/diagram', () => ({ + default: vi.fn(async (name: string) => { + if (name === 'mermaid') { + return { + initialize: vi.fn(), + run: mermaidRun, + }; + } + throw new Error(`unexpected renderer ${name}`); + }), +})); + +// Import AFTER the mock is registered. +const { MarkdownToHtml } = await import('../markdownToHtml'); + +beforeEach(() => { + mermaidRun.mockReset(); +}); + +const INVALID_MERMAID = [ + '# Title', + '', + 'Intro paragraph.', + '', + '```mermaid', + 'graph LR', + 'H[a|b|c]', + '```', + '', + 'Trailing paragraph.', + '', +].join('\n'); + +describe('#4812: mermaid syntax error must not abort export', () => { + it('renderHtml resolves even when a mermaid diagram fails to parse', async () => { + // Real mermaid rejects on a parse error; emulate that. + mermaidRun.mockRejectedValue(new Error('Parse error on line 2: ... got \'PIPE\'')); + + const md2html = new MarkdownToHtml(INVALID_MERMAID); + const html = await md2html.renderHtml(); + + // The surrounding document still exports. + expect(html).toContain('Title'); + expect(html).toContain('Intro paragraph.'); + expect(html).toContain('Trailing paragraph.'); + // The broken diagram degrades to the same placeholder the other + // diagram renderers use, instead of throwing. + expect(html).toContain('< Invalid Diagram >'); + }); + + it('one broken diagram does not stop a later valid diagram from rendering', async () => { + // First diagram throws, second succeeds. A batch run would abort both. + mermaidRun + .mockRejectedValueOnce(new Error('Parse error')) + .mockResolvedValueOnce(undefined); + + const TWO = [ + '```mermaid', + 'graph LR', + 'H[a|b|c]', + '```', + '', + '```mermaid', + 'graph TD; A-->B', + '```', + '', + ].join('\n'); + + const html = await new MarkdownToHtml(TWO).renderHtml(); + + expect(mermaidRun).toHaveBeenCalledTimes(2); + expect(html).toContain('< Invalid Diagram >'); + }); +}); diff --git a/packages/muya/src/state/markdownToHtml.ts b/packages/muya/src/state/markdownToHtml.ts index 58de94bc0c..615f4805c7 100644 --- a/packages/muya/src/state/markdownToHtml.ts +++ b/packages/muya/src/state/markdownToHtml.ts @@ -49,6 +49,10 @@ export class MarkdownToHtml { mermaidContainer.classList.add('mermaid'); preEle.replaceWith(mermaidContainer); } + const nodes = [...this._exportContainer!.querySelectorAll('div.mermaid')]; + if (nodes.length === 0) + return; + const mermaid = await loadRenderer('mermaid'); // We only export light theme, so set mermaid theme to `default`, in the future, we can choose which theme to export. mermaid.initialize({ @@ -56,9 +60,18 @@ export class MarkdownToHtml { securityLevel: 'strict', theme: 'default', }); - await mermaid.run({ - nodes: [...this._exportContainer!.querySelectorAll('div.mermaid')], - }); + // Render each diagram in isolation: `mermaid.run` rejects the whole + // batch on the first parse error, so one invalid diagram used to abort + // the entire export (#4812). Contain the failure to that diagram and + // fall back to the same placeholder the other diagram renderers use. + for (const node of nodes) { + try { + await mermaid.run({ nodes: [node] }); + } + catch { + node.innerHTML = '< Invalid Diagram >'; + } + } if (this._muya) { mermaid.initialize({ securityLevel: 'strict', From c0d839cb38774b6ac265df6b75a8a825205bccb0 Mon Sep 17 00:00:00 2001 From: Ran Luo Date: Sat, 4 Jul 2026 16:32:14 +0800 Subject: [PATCH 3/3] fix(desktop): correct English typos in locale and preference schema (#4787) (#4845) Three user-facing English strings had typos (reported by a translator working from the English base): - "The language MarkText use" -> "uses" - "The dilimiter used in order list" -> "delimiter" - "loading the file change because the tab..." -> "file changes" Fixed in the source `en.json` locale and the matching preference `schema.json` descriptions. `en.min.json` is generated by `minify-locales` at build time, so it is not edited here. Co-authored-by: Claude Opus 4.8 (1M context) --- packages/desktop/src/main/preferences/schema.json | 4 ++-- packages/desktop/static/locales/en.json | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/desktop/src/main/preferences/schema.json b/packages/desktop/src/main/preferences/schema.json index fadc3f0ea7..c202785544 100644 --- a/packages/desktop/src/main/preferences/schema.json +++ b/packages/desktop/src/main/preferences/schema.json @@ -67,7 +67,7 @@ "type": "string" }, "language": { - "description": "General--The language MarkText use.", + "description": "General--The language MarkText uses.", "type": "string", "default": "en" }, @@ -233,7 +233,7 @@ "default": "-" }, "orderListDelimiter": { - "description": "Markdown--The dilimiter used in order list", + "description": "Markdown--The delimiter used in order list", "enum": [".", ")"], "default": "." }, diff --git a/packages/desktop/static/locales/en.json b/packages/desktop/static/locales/en.json index 20bf05b44d..8fcefb828b 100644 --- a/packages/desktop/static/locales/en.json +++ b/packages/desktop/static/locales/en.json @@ -374,7 +374,7 @@ "startUpAction": "The action after MarkText startup, open the last edited content, open the specified folder or blank page", "restoreLayoutState": "Restore previous editor state on startup", "defaultDirectoryToOpen": "The default directory that should be opened on startup when startUp=folder", - "language": "The language MarkText use", + "language": "The language MarkText uses", "editorFontFamily": "Editor font family", "fontSize": "Font size in pixels", "lineHeight": "Line Height", @@ -397,7 +397,7 @@ "autoCheck": "Whether to automatically check related task", "preferLooseListItem": "The preferred list type", "bulletListMarker": "The marker used in bullet list", - "orderListDelimiter": "The dilimiter used in order list", + "orderListDelimiter": "The delimiter used in order list", "preferHeadingStyle": "The preferred heading style in MarkText", "tabSize": "Replace the tab with x spaces", "listIndentation": "Select the indent of list", @@ -913,7 +913,7 @@ "tabNotFound": "Tab not found", "tocItemNotFound": "Table of contents {key} not found", "errorLoadingTabTitle": "Error loading tab", - "errorLoadingTabMessage": "There was an error while loading the file change because the tab cannot be found.", + "errorLoadingTabMessage": "There was an error while loading the file changes because the tab cannot be found.", "mixedLineEndingsNormalized": "\"{name}\" has mixed line endings which are automatically normalized to {lineEnding}.", "imageDeletionUrlTitle": "Image deletion URL", "imageDeletionUrlMessage": "Click to copy the deletion URL of the uploaded image to the clipboard {url}",