Skip to content
Merged
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
4 changes: 2 additions & 2 deletions packages/desktop/src/main/preferences/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
"type": "string"
},
"language": {
"description": "General--The language MarkText use.",
"description": "General--The language MarkText uses.",
"type": "string",
"default": "en"
},
Expand Down Expand Up @@ -233,7 +233,7 @@
"default": "-"
},
"orderListDelimiter": {
"description": "Markdown--The dilimiter used in order list",
"description": "Markdown--The delimiter used in order list",
"enum": [".", ")"],
"default": "."
},
Expand Down
6 changes: 3 additions & 3 deletions packages/desktop/static/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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}",
Expand Down
18 changes: 18 additions & 0 deletions packages/muya/e2e/tests/blocks/inline-math-align.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions packages/muya/src/assets/styles/inlineSyntax.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;

Expand Down
90 changes: 90 additions & 0 deletions packages/muya/src/state/__tests__/mermaidExportResilience.spec.ts
Original file line number Diff line number Diff line change
@@ -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 >');
});
});
19 changes: 16 additions & 3 deletions packages/muya/src/state/markdownToHtml.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,16 +49,29 @@ 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({
startOnLoad: false,
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',
Expand Down
Loading