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}", 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%; 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',