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
32 changes: 16 additions & 16 deletions dist/markedit-preview.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

39 changes: 22 additions & 17 deletions src/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,23 +129,28 @@ export async function applyStyles(html: string) {
const { default: katexCss } = await import('../styles/katex.css?raw');
components.push(stylify(katexCss));

const mermaid = `
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs";
if (${styledHtmlColorScheme === 'auto' ? 'true' : 'false'}) {
const darkMode = matchMedia("(prefers-color-scheme: dark)");
mermaid.initialize({ theme: darkMode.matches ? "dark" : undefined });
darkMode.addEventListener("change", () => {
if (document.querySelector(".mermaid") !== null) {
location.reload();
}
});
} else {
const isDark = ${styledHtmlColorScheme === 'dark' ? 'true' : 'false'};
mermaid.initialize({ theme: isDark ? "dark" : undefined });
}
</script>`;
components.push(mermaid);
const template = document.createElement('template');
template.innerHTML = html;

if (template.content.querySelector('.mermaid') !== null) {
const mermaid = `
<script type="module">
import mermaid from "https://cdn.jsdelivr.net/npm/mermaid@${__MERMAID_VERSION__}/dist/mermaid.esm.min.mjs";
if (${styledHtmlColorScheme === 'auto' ? 'true' : 'false'}) {
const darkMode = matchMedia("(prefers-color-scheme: dark)");
mermaid.initialize({ theme: darkMode.matches ? "dark" : undefined });
darkMode.addEventListener("change", () => {
if (document.querySelector(".mermaid") !== null) {
location.reload();
}
});
} else {
const isDark = ${styledHtmlColorScheme === 'dark' ? 'true' : 'false'};
mermaid.initialize({ theme: isDark ? "dark" : undefined });
}
</script>`;
components.push(mermaid);
}
}

return components.join('\n');
Expand Down
3 changes: 3 additions & 0 deletions tests/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('build variants', () => {
loader: 'ts',
define: { __FULL_BUILD__: String(fullBuild) },
treeShaking: true,
minifySyntax: true,
});

const lite = await transformRenderer(false);
Expand All @@ -24,6 +25,8 @@ describe('build variants', () => {
expect(lite.code).not.toMatch(moduleImport);
expect(full.code).toMatch(moduleImport);
});
expect(lite.code).not.toContain('https://cdn.jsdelivr.net/npm/mermaid@');
expect(full.code).toContain('https://cdn.jsdelivr.net/npm/mermaid@');
expect(lite.code).toMatch(/renderToString:\s*\(\.\.\._args\) => ""/);
expect(lite.code).toMatch(/render:\s*async \(\) => \(\{\s*svg: ""\s*\}\)/);
});
Expand Down
117 changes: 117 additions & 0 deletions tests/render.styles.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// @vitest-environment happy-dom
import { beforeEach, describe, expect, it, vi } from 'vitest';
import mermaidPackage from 'mermaid/package.json';
import type * as Settings from '../src/support/settings';
import { applyStyles, renderMarkdown } from '../src/render';
import { generateStaticHtml, renderStaticHtml, saveStyledHtml } from '../src/view';

const mocks = vi.hoisted(() => ({
getText: vi.fn(() => '# Hello'),
getFileInfo: vi.fn(async () => ({ filePath: '/example.md' })),
showSavePanel: vi.fn(),
settings: { styledHtmlColorScheme: 'auto' },
}));

vi.mock('markedit-api', () => ({
MarkEdit: {
editorAPI: { getText: mocks.getText },
getFileInfo: mocks.getFileInfo,
showSavePanel: mocks.showSavePanel,
},
}));

vi.mock('../src/support/settings', async importOriginal => ({
...await importOriginal<typeof Settings>(),
get styledHtmlColorScheme() { return mocks.settings.styledHtmlColorScheme; },
}));

const mermaidMarkdown = '```mermaid\ngraph TD\n A --> B\n```';
const mermaidURL = `https://cdn.jsdelivr.net/npm/mermaid@${mermaidPackage.version}/dist/mermaid.esm.min.mjs`;

function scripts(html: string) {
const template = document.createElement('template');
template.innerHTML = html;
return template.content.querySelectorAll('script');
}

beforeEach(() => {
mocks.getText.mockReturnValue('# Hello');
mocks.getFileInfo.mockResolvedValue({ filePath: '/example.md' });
mocks.showSavePanel.mockClear();
mocks.settings.styledHtmlColorScheme = 'auto';
});

describe('styled HTML remote scripts', () => {
it.each([
'# Hello',
'The word mermaid is ordinary text.',
'```html\n<div class="mermaid">graph TD</div>\n```',
'<!-- <div class="mermaid">graph TD</div> -->',
'<div class="not-mermaid">Plain content</div>',
'$E = mc^2$',
])('does not add scripts when no Mermaid element is present: %s', async markdown => {
const html = await applyStyles(await renderMarkdown(markdown));
expect(scripts(html)).toHaveLength(0);
expect(html).not.toContain(mermaidURL);
expect(html).toContain('<style>');
});

it.each([
mermaidMarkdown,
"<div class='diagram mermaid'>graph TD\nA --> B</div>",
`${mermaidMarkdown}\n\n${mermaidMarkdown}`,
])('adds one exact-version module import for Mermaid elements: %s', async markdown => {
const html = await applyStyles(await renderMarkdown(markdown));
const modules = scripts(html);
expect(modules).toHaveLength(1);
expect(modules[0].type).toBe('module');
expect(modules[0].textContent).toContain(`import mermaid from "${mermaidURL}"`);
expect(html).not.toContain('mermaid@11/');
});

it.each(['auto', 'light', 'dark'])('preserves %s color scheme initialization', async scheme => {
mocks.settings.styledHtmlColorScheme = scheme;
const html = await applyStyles(await renderMarkdown(mermaidMarkdown));
const script = scripts(html)[0].textContent;
expect(script).toContain(`if (${scheme === 'auto'})`);
expect(script).toContain(`const isDark = ${scheme === 'dark'}`);
expect(script).toContain('darkMode.addEventListener("change"');
expect(script).toContain('mermaid.initialize');
});
});

describe('HTML export entry points', () => {
it('applies conditional loading to both HTML-generation APIs', async () => {
expect(scripts(await generateStaticHtml(true))).toHaveLength(0);
expect(scripts(await renderStaticHtml('# Hello', true))).toHaveLength(0);

mocks.getText.mockReturnValue(mermaidMarkdown);
expect(await generateStaticHtml(true)).toContain(mermaidURL);
expect(await renderStaticHtml(mermaidMarkdown, true)).toContain(mermaidURL);
expect(scripts(await generateStaticHtml(false))).toHaveLength(0);
expect(scripts(await renderStaticHtml(mermaidMarkdown, false))).toHaveLength(0);
});

it.each(['mmd', 'mermaid'])('includes the loader for standalone .%s exports', async extension => {
mocks.getText.mockReturnValue('graph TD\nA --> B');
mocks.getFileInfo.mockResolvedValue({ filePath: `/example.${extension}` });
expect(await generateStaticHtml(true)).toContain(mermaidURL);
});

it('does not add scripts to standalone math exports', async () => {
mocks.getText.mockReturnValue('E = mc^2');
mocks.getFileInfo.mockResolvedValue({ filePath: '/example.tex' });
const html = await generateStaticHtml(true);
expect(html).toContain('class="katex"');
expect(scripts(html)).toHaveLength(0);
});

it('saves the same pinned loader in styled HTML', async () => {
mocks.getText.mockReturnValue(mermaidMarkdown);
saveStyledHtml();
await vi.waitFor(() => expect(mocks.showSavePanel).toHaveBeenCalledWith({
fileName: 'example.html',
string: expect.stringContaining(mermaidURL),
}));
});
});
5 changes: 5 additions & 0 deletions types/env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,8 @@ declare const __PKG_VERSION__: string;
* Whether to include all extensions, such as `mermaid`, `katex`, and `highlight.js`.
*/
declare const __FULL_BUILD__: boolean;

/**
* Installed Mermaid version used by styled HTML exports.
*/
declare const __MERMAID_VERSION__: string;
2 changes: 2 additions & 0 deletions vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fileURLToPath } from 'node:url';

import mainPackage from './package.json' with { type: 'json' };
import katexPackage from 'katex/package.json' with { type: 'json' };
import mermaidPackage from 'mermaid/package.json' with { type: 'json' };

const liteBuild = process.env.LITE_BUILD === 'true';
const outDir = liteBuild ? 'dist/lite' : 'dist';
Expand All @@ -28,6 +29,7 @@ export default defineConfig(mergeConfig(defaultViteConfig({ outDir }), {
define: {
__PKG_VERSION__: JSON.stringify(mainPackage.version),
__FULL_BUILD__: JSON.stringify(!liteBuild),
__MERMAID_VERSION__: JSON.stringify(mermaidPackage.version),
},
build: {
rollupOptions: {
Expand Down
2 changes: 2 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineConfig } from 'vitest/config';
import mermaidPackage from 'mermaid/package.json' with { type: 'json' };

export default defineConfig({
test: {
Expand All @@ -9,5 +10,6 @@ export default defineConfig({
define: {
__PKG_VERSION__: JSON.stringify('test'),
__FULL_BUILD__: JSON.stringify(true),
__MERMAID_VERSION__: JSON.stringify(mermaidPackage.version),
},
});
Loading