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
37 changes: 37 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,40 @@ jobs:

- name: Build typedoc
run: pnpm typedoc

browser-test:
name: Browser tests (MathJax)
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Install pnpm
uses: pnpm/action-setup@v4

- name: Install nodejs
uses: actions/setup-node@v6
with:
cache: pnpm
node-version-file: .tool-versions

- name: Install dependencies
run: pnpm install

- name: Install Playwright Chromium
run: pnpm exec playwright install --with-deps chromium

- name: Run browser tests
# Real-Chromium tests for the client-side MathJax pipeline: equation
# numbering across re-renders and a11y-enrichment performance (#2312).
run: pnpm test:browser

- name: Upload Playwright report on failure
if: ${{ failure() }}
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ out
test.js
test.mjs
.direnv
.codegraph
tsconfig.tsbuildinfo
pnpm-debug.log
.mume
Expand All @@ -17,3 +18,9 @@ styles/**/*.css
!styles/markdown-it-callout.css
!styles/twemoji.css
docs

# Playwright browser-test artifacts
/test-results
/playwright-report
/blob-report
/playwright/.cache
15 changes: 11 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@

Please visit https://github.com/shd101wyy/vscode-markdown-preview-enhanced/releases for the more changelog

## [Unreleased]
## [0.9.29] - 2026-06-05

### Bug fixes

- **Harden external file/link opening against command injection** — Opening links and files from the preview no longer goes through a shell, and untrusted inputs (the diagram `filename` attribute, imported file paths, and the `latex_engine` code-chunk attribute) are passed as literal arguments or validated before use. This closes a security issue affecting Windows. Thanks to @byte16384 for the responsible disclosure.
- **Eliminate arbitrary code execution in WaveDrom rendering** — WaveDrom diagrams were parsed by evaluating untrusted markdown content with `eval()`, enabling arbitrary JavaScript execution. This affected every render path: the live preview (`window.eval`), and presentation mode plus HTML export (the bundled `WaveDrom.ProcessAll()`/`eva()` helpers). The live preview now parses with `JSON5.parse()`, and — because a malicious `<script type="WaveDrom">` can also be injected via raw HTML in markdown — the HTML sanitizer now validates and normalizes every WaveDrom data script to inert strict JSON, so no downstream `eval`/`ProcessAll` can execute attacker-controlled code. Fixes the security vulnerability reported in [vscode-mpe#2315](https://github.com/shd101wyy/vscode-markdown-preview-enhanced/issues/2315).
- **Replace `interpretJS` with `JSON5.parse` in Bitfield renderer** — Bitfield fenced code blocks were parsed using `interpretJS()` which evaluates user input via `vm.runInNewContext`, enabling arbitrary code execution on the server side. Replaced with `JSON5.parse()` since bitfield register definitions are purely data (arrays of objects).
- **Improve MathJax 4 rendering performance** — MathJax 4's combined `tex-mml-chtml` component runs accessibility _semantic enrichment_ (the speech-rule-engine) on every typeset, which dominates per-formula cost and made formula-heavy previews re-render slowly on each edit (measured ~890 ms vs ~42 ms for 127 formulas in Chrome — a ~21× difference). Semantic enrichment is now disabled by default (`options.enableEnrichment: false`), restoring MathJax-3-like performance. Because MathJax ignores this flag when set in the config block, the engine re-applies the configured a11y toggles onto the live `MathDocument` via a `startup.ready` hook; users who need screen-reader speech output can set `enableEnrichment: true` in their `mathjaxConfig`. Addresses [vscode-mpe#2312](https://github.com/shd101wyy/vscode-markdown-preview-enhanced/issues/2312).

## [0.9.28] - 2026-05-24

Expand Down Expand Up @@ -239,12 +246,12 @@ The eager in-memory note cache is now slim: it holds metadata (title, aliases, f

### New features

- Add markdown-it callout feature with styling https://github.com/shd101wyy/crossnote/pull/387 by [@EmmetZ](https://github.com/EmmetZ).
- Add WebSequenceDiagrams support in `wsd` code blocks https://github.com/shd101wyy/vscode-markdown-preview-enhanced/pull/2228 by [@smhanov](https://github.com/smhanov).
- Add markdown-it callout feature with styling https://github.com/shd101wyy/crossnote/pull/387 by @EmmetZ.
- Add WebSequenceDiagrams support in `wsd` code blocks https://github.com/shd101wyy/vscode-markdown-preview-enhanced/pull/2228 by @smhanov.

### Bug fixes

- Remove the wrapper of custom head in HTML page https://github.com/shd101wyy/crossnote/pull/386 by [@TanShun](https://github.com/TanShun).
- Remove the wrapper of custom head in HTML page https://github.com/shd101wyy/crossnote/pull/386 by @TanShun.

### Security

Expand Down
3 changes: 3 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ module.exports = {
transformIgnorePatterns: ['/node_modules/'],
roots: ['test'],
testMatch: ['**/?(*.)(spec|test).(j|t)s?(x)'],
// Browser tests under test/browser run with Playwright (`pnpm test:browser`),
// not jest — they need a real Chromium to exercise MathJax typesetting.
testPathIgnorePatterns: ['/node_modules/', '/test/browser/'],
testEnvironment: 'node',
};
6 changes: 5 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "crossnote",
"version": "0.9.28",
"version": "0.9.29",
"description": "A powerful markdown notebook tool",
"keywords": [
"markdown"
Expand Down Expand Up @@ -43,6 +43,7 @@
"prepare": "husky",
"prepublish": "pnpm build",
"test": "jest --no-coverage",
"test:browser": "playwright test",
"test:coverage": "jest",
"typedoc": "typedoc src/index.ts"
},
Expand Down Expand Up @@ -103,6 +104,7 @@
"ignore": "^7.0.5",
"imagemagick-cli": "^0.5.0",
"jquery": "^3.7.1",
"json5": "^2.2.3",
"katex": "^0.16.47",
"less": "^4.2.0",
"markdown-it": "^14.1.1",
Expand Down Expand Up @@ -151,6 +153,7 @@
"@babel/preset-react": "^7.26.3",
"@babel/preset-typescript": "^7.26.0",
"@eslint/js": "^10.0.1",
"@playwright/test": "^1.60.0",
"@simbathesailor/use-what-changed": "^2.0.0",
"@types/crypto-js": "^4.1.1",
"@types/d3-color": "^3.1.3",
Expand Down Expand Up @@ -206,6 +209,7 @@
"jest-environment-jsdom": "^30.3.0",
"jsdom": "^23.2.0",
"lint-staged": "^16.4.0",
"mathjax": "^4.1.2",
"postcss": "^8.4.29",
"prettier": "^3.8.3",
"prettier-plugin-packagejson": "^3.0.2",
Expand Down
36 changes: 36 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineConfig, devices } from '@playwright/test';

/**
* Playwright config for the browser-backed tests under `test/browser`.
*
* These exercise the client-side MathJax pipeline in a real Chromium so we can
* lock in behavior that jsdom cannot reproduce (MathJax's speech-rule-engine
* worker, equation numbering across re-renders). Run with `pnpm test:browser`.
*
* In CI, Chromium is provided by `playwright install --with-deps chromium`.
* Locally (e.g. on NixOS where Playwright's bundled Chromium can't find system
* libs), point at a system browser via `PLAYWRIGHT_CHROMIUM_EXECUTABLE`.
*/
const executablePath = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE || undefined;

export default defineConfig({
testDir: './test/browser',
fullyParallel: false,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 1 : 0,
workers: 1,
reporter: process.env.CI ? 'github' : 'list',
use: {
...devices['Desktop Chrome'],
launchOptions: executablePath ? { executablePath } : {},
},
projects: [
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
launchOptions: executablePath ? { executablePath } : {},
},
},
],
});
56 changes: 56 additions & 0 deletions pnpm-lock.yaml

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

26 changes: 22 additions & 4 deletions src/converters/process-graphs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import * as magick from '../tools/magick';
import * as mermaidAPI from '../tools/mermaid';
import * as sharp from '../tools/sharp';
import * as wavedromAPI from '../tools/wavedrom';
import { extractCommandFromBlockInfo } from '../utility';
import { extractCommandFromBlockInfo, sanitizeImageFilename } from '../utility';

export async function processGraphs(
text: string,
Expand Down Expand Up @@ -139,6 +139,17 @@ export async function processGraphs(
}
}

// Resolve a sanitized output image name to an absolute path. A leading `/`
// means "relative to the project root" (MPE's imageFolderPath convention),
// not a filesystem-absolute path; otherwise it's relative to the image output
// directory.
function resolveOutputImagePath(name: string): string {
if (name.startsWith('/')) {
return path.resolve(projectDirectoryPath, '.' + name);
}
return path.resolve(imageDirectoryPath, name);
}

async function convertSVGToPNGFile(
outFileName: string = '',
svg: string,
Expand All @@ -150,11 +161,14 @@ export async function processGraphs(
altName: string,
optionsStr: string,
) {
// The filename comes from untrusted markdown and ends up in a converter
// that shells out; reject anything but a safe relative name.
outFileName = sanitizeImageFilename(outFileName);
if (!outFileName) {
outFileName = imageFilePrefix + imgCount + '.png';
}

const pngFilePath = path.resolve(imageDirectoryPath, outFileName);
const pngFilePath = resolveOutputImagePath(outFileName);
if (notebook.config.imageMagickPath) {
// use `magick`
await magick.svgElementToPNGFile(
Expand Down Expand Up @@ -298,11 +312,15 @@ export async function processGraphs(
} else if (def.match(/^mermaid/)) {
// mermaid-cli Ver.8.4.8 has a bug, render in png https://github.com/mermaid-js/mermaid/issues/664
try {
let pngFileName = options['filename'] as string | undefined;
// `filename` is untrusted markdown and is passed to mermaid-cli, which
// is spawned with `shell: true`; reject shell-unsafe names.
let pngFileName = sanitizeImageFilename(
options['filename'] as string | undefined,
);
if (!pngFileName) {
pngFileName = imageFilePrefix + imgCount + '.png';
}
const pngFilePath = path.resolve(imageDirectoryPath, pngFileName);
const pngFilePath = resolveOutputImagePath(pngFileName);
imgCount++;
await mermaidAPI.mermaidToPNG(
content,
Expand Down
Loading
Loading