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
1 change: 1 addition & 0 deletions .github/workflows/lint-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,5 @@ jobs:
run: |
yarn install
yarn lint
yarn typecheck
yarn test
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ To build the lite version, run `yarn build:lite` instead.

- `yarn test` — run tests
- `yarn lint` — run linting (also runs automatically before build)
- `yarn typecheck` — run TypeScript checks (also runs automatically before the full build)

## How to Use

Expand Down
159 changes: 71 additions & 88 deletions dist/lite/markedit-preview.js

Large diffs are not rendered by default.

5,438 changes: 2,740 additions & 2,698 deletions dist/markedit-preview.js

Large diffs are not rendered by default.

19 changes: 10 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"scripts": {
"test": "vitest run",
"lint": "eslint . --config eslint.config.mjs",
"build": "yarn lint && vite build && rm -f ~/Library/Group\\ Containers/group.app.cyan.markedit/Shared/scripts/$(node -p \"require('./package.json').name\").js",
"typecheck": "tsc --noEmit",
"build": "yarn lint && yarn typecheck && vite build && rm -f ~/Library/Group\\ Containers/group.app.cyan.markedit/Shared/scripts/$(node -p \"require('./package.json').name\").js",
"build:lite": "cross-env LITE_BUILD=true vite build",
"reload": "osascript -e 'quit app \"MarkEdit\"' -e 'delay 1' -e 'launch app \"MarkEdit\"'",
"uninstall": "rm ~/Library/Containers/app.cyan.markedit/Data/Documents/scripts/$(node -p \"require('./package.json').name\").js"
Expand Down Expand Up @@ -34,17 +35,17 @@
"eslint": "^9.27.0",
"happy-dom": "^20.8.9",
"jsdom": "^26.1.0",
"markedit-api": "https://github.com/MarkEdit-app/MarkEdit-api#v0.35.0",
"markedit-vite": "https://github.com/MarkEdit-app/MarkEdit-vite#v0.5.0",
"typescript": "^5.0.0",
"vite": "^7.0.0",
"markedit-api": "https://github.com/MarkEdit-app/MarkEdit-api#v0.36.0",
"markedit-vite": "https://github.com/MarkEdit-app/MarkEdit-vite#v0.6.0",
"typescript": "~6.0.0",
"vite": "^8.0.0",
"vite-plugin-singlefile": "^2.3.3",
"vitest": "^4.0.18"
},
"dependencies": {
"dompurify": "^3.4.12",
"js-yaml": "^5.2.1",
"katex": "^0.18.4",
"dompurify": "^3.4.16",
"js-yaml": "^5.4.3",
"katex": "^0.19.0",
Comment thread
cyanzhong marked this conversation as resolved.
"mark.js": "^8.11.1",
"markdown-it": "^14.3.0",
"markdown-it-anchor": "^9.2.1",
Expand All @@ -54,7 +55,7 @@
"markdown-it-highlightjs": "^4.3.0",
"markdown-it-link-attributes": "^4.0.1",
"markdown-it-task-lists": "^2.1.1",
"markedit-katex": "https://github.com/MarkEdit-app/MarkEdit-katex#v1.1.7",
"markedit-katex": "https://github.com/MarkEdit-app/MarkEdit-katex#v1.1.8",
"mermaid": "^11.16.0",
"split-grid": "^1.0.11"
}
Expand Down
4 changes: 2 additions & 2 deletions src/features/frontMatter.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { load as parseYaml, CORE_SCHEMA as schema } from 'js-yaml';
import extractFrontMatter from 'markdown-it-front-matter';
import type MarkdownIt from 'markdown-it';
import type { PluginSimple } from 'markdown-it';

/**
* Markdown-it plugin that extracts YAML frontMatter and renders it as an HTML table.
*/
export function createFrontMatterPlugin(): MarkdownIt.PluginSimple {
export function createFrontMatterPlugin(): PluginSimple {
return (mdit) => {
// Closure state is safe: the extract callback and the renderer rule fire in the same synchronous pass.
let renderedHtml = '';
Expand Down
2 changes: 1 addition & 1 deletion src/quicklook/shim.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Stub `require` for hosts that don't provide one (Quick Look, `@light`).
* Compiled to an IIFE and injected as the Rollup banner so it runs before the
* Compiled to an IIFE and injected as the build banner so it runs before the
* bundle's top-level `require(...)` calls.
*/

Expand Down
4 changes: 2 additions & 2 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,9 +227,9 @@ if (__FULL_BUILD__) {
);

pluginInits.push(
import('markedit-katex').then(mod => {
import('markedit-katex').then(async mod => {
const options = mathDelimiters ? { delimiters: mathDelimiters } : {};
mdit.use(mod.default, options);
mdit.use(mod.default, { ...options, katex: await importKatex() });
}),
);
}
Expand Down
8 changes: 6 additions & 2 deletions styles/katex.css
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@
}
/* stylelint-disable font-family-no-missing-generic-family-keyword */
.katex {
font: normal 1.21em KaTeX_Main, Times New Roman, serif;
font: normal 1.21em KaTeX_Main, math, Times New Roman, serif;
line-height: 1.2;
position: relative;
text-indent: 0;
Expand All @@ -154,7 +154,7 @@
border-color: currentColor;
}
.katex .katex-version::after {
content: "0.18.4";
content: "0.19.0";
}
.katex .katex-mathml {
/* Accessibility hack to only show to screen readers
Expand Down Expand Up @@ -1174,6 +1174,10 @@
.katex .anglpad {
padding: 0 0.03889em;
}
.katex .reflectbox {
display: inline-block;
transform: scaleX(-1);
}
.katex .eqn-num::before {
counter-increment: katexEqnNo;
content: "(" counter(katexEqnNo) ")";
Expand Down
97 changes: 96 additions & 1 deletion tests/build.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,20 @@
import { readFileSync } from 'node:fs';
import { Script } from 'node:vm';
import { createRequire } from 'node:module';
import { dirname } from 'node:path';
import { transform } from 'esbuild';
import { describe, expect, test } from 'vitest';
import { build, loadConfigFromFile } from 'vite';
import { describe, expect, test, vi } from 'vitest';

const require = createRequire(import.meta.url);

describe('build variants', () => {
test('keeps the bundled KaTeX stylesheet aligned with the installed renderer', () => {
const bundled = readFileSync(new URL('../styles/katex.css', import.meta.url), 'utf8');
const installed = readFileSync(require.resolve('katex/dist/katex.css'), 'utf8');
expect(bundled).toBe(installed);
});

test('uses no-op renderers in lite builds', async () => {
const source = readFileSync(new URL('../src/render.ts', import.meta.url), 'utf8');
const transformRenderer = (fullBuild: boolean) => transform(source, {
Expand Down Expand Up @@ -30,4 +42,87 @@ describe('build variants', () => {
expect(lite.code).toMatch(/renderToString:\s*\(\.\.\._args\) => ""/);
expect(lite.code).toMatch(/render:\s*async \(\) => \(\{\s*svg: ""\s*\}\)/);
});

test.each([false, true])('builds a single-file CommonJS bundle with host-provided externals (lite: %s)', async lite => {
vi.stubEnv('LITE_BUILD', String(lite));
try {
const loaded = await loadConfigFromFile({ command: 'build', mode: 'production' });
if (loaded === null) {
throw new Error('Vite configuration was not found');
}

const result = await build({
...loaded.config,
configFile: false,
logLevel: 'warn',
plugins: loaded.config.plugins?.filter(plugin =>
!plugin || !('name' in plugin) || plugin.name !== 'markedit-copy-dist-file',
),
build: { ...loaded.config.build, write: false },
});

if ('on' in result) {
throw new Error('Expected a non-watching build');
}

const outputs = Array.isArray(result) ? result : [result];
const files = outputs.flatMap(output => output.output);
expect(files).toHaveLength(1);

const chunk = files[0];
if (chunk.type !== 'chunk') {
throw new Error('Expected a JavaScript bundle');
}

expect(chunk.fileName).toMatch(/\.js$/);
expect(chunk.dynamicImports.filter(id => id !== chunk.fileName)).toEqual([]);
expect(chunk.code).not.toMatch(/\bimport\s*\(/);
expect(chunk.imports).toEqual(expect.arrayContaining(['markedit-api', '@codemirror/state', '@codemirror/view']));
expect(chunk.imports.every(id => id === 'markedit-api' || /^@(codemirror|lezer)\//.test(id))).toBe(true);
expect(chunk.code).toMatch(/^"use strict";/);

const script = new Script(chunk.code);
const firstImport = new Error('Reached the first host import');

// Stop at the first external import, before the bundle needs a browser DOM.
let shimRequire: ((id: string) => unknown) | undefined;
expect(() => script.runInNewContext({
get require() {
return shimRequire === undefined ? undefined : () => { throw firstImport; };
},
set require(value: ((id: string) => unknown) | undefined) {
shimRequire = value;
},
})).toThrow(firstImport);

const markeditApi = shimRequire?.('markedit-api');
expect(markeditApi).toEqual({ MarkEdit: {} });

const hostRequire = vi.fn(() => { throw firstImport; });
expect(() => script.runInNewContext({ require: hostRequire })).toThrow(firstImport);
expect(hostRequire).toHaveBeenCalledOnce();

const modules = Object.keys(chunk.modules);
expect(modules.some(id => id.includes('/node_modules/mermaid/'))).toBe(!lite);

const katexRoot = dirname(require.resolve('katex/package.json'));
expect(modules.some(id => id.startsWith(`${katexRoot}/`))).toBe(!lite);

const mermaidRequire = createRequire(require.resolve('mermaid'));
const mermaidKatexRoot = dirname(mermaidRequire.resolve('katex/package.json'));
expect(modules.some(id => id.startsWith(`${mermaidKatexRoot}/`))).toBe(!lite);

// The plugin must share the project's renderer; only Mermaid may bring its own.
const katexRenderers = modules.filter(id => /\/katex\/dist\/katex\.(?:mjs|js)$/.test(id));
const expectedRenderers = lite ? [] : [...new Set([
`${katexRoot}/dist/katex.mjs`,
`${mermaidKatexRoot}/dist/katex.mjs`,
])];

expect(katexRenderers.sort()).toEqual(expectedRenderers.sort());
expect(chunk.code.includes('https://cdn.jsdelivr.net/npm/mermaid@')).toBe(!lite);
} finally {
vi.unstubAllEnvs();
}
}, 120_000);
});
14 changes: 13 additions & 1 deletion tests/render.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect, vi } from 'vitest';
import { renderMarkdown, renderMermaid, renderKatex, renderTableBlocks } from '../src/render';
import { renderMarkdown, renderMermaid, renderKatex, renderKatexHTML, renderTableBlocks } from '../src/render';

vi.mock('markedit-api', () => {
const markEdit: Record<string, unknown> = {};
Expand Down Expand Up @@ -145,6 +145,18 @@ describe('renderMermaid', () => {
});

describe('renderKatex', () => {
it('uses KaTeX 0.19 features in both Markdown and standalone math', async () => {
const content = '\\reflectbox{R}';
for (const html of [
await renderMarkdown(`$${content}$`),
await renderMarkdown(`$$${content}$$`),
await renderKatexHTML(content),
]) {
expect(html).toMatch(/class="[^"]*\breflectbox\b/);
expect(html).not.toContain('katex-error');
}
});

it('should wrap content in a katex div', async () => {
await mockDocLines(1);
const content = 'E = mc^2';
Expand Down
3 changes: 2 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"typeRoots": ["./node_modules/@types"],
"types": ["node"],
"module": "esnext",
"target": "esnext",
"lib": ["es2019", "dom", "dom.iterable"],
Expand All @@ -13,6 +14,6 @@
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true
},
"include": ["src/**/*.ts", "tests/**/*.ts", "types/**/*.d.ts", "main.ts"],
"include": ["src/**/*.ts", "tests/**/*.ts", "types/**/*.d.ts", "*.ts", "*.mts"],
"exclude": ["dist", "node_modules"]
}
3 changes: 2 additions & 1 deletion vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export default defineConfig(mergeConfig(defaultViteConfig({ outDir }), {
__MERMAID_VERSION__: JSON.stringify(mermaidPackage.version),
},
build: {
rollupOptions: {
target: 'safari16',
rolldownOptions: {
Comment thread
cyanzhong marked this conversation as resolved.
output: {
banner: requireShim,
},
Expand Down
Loading
Loading