Thanks for contributing! For a bug fix, include a reproduction and a regression test. If you are planning a new API or a larger change, an issue is a good place to discuss the scope before implementation.
Use Node.js 24 (the version used in CI) and the pnpm version pinned in the root package.json. With Corepack installed, enable it before installing dependencies:
corepack enable
pnpm installRun commands from the repository root unless a different directory is shown. Installation runs pnpm stub: package dist/ files re-export their TypeScript sources, and pnpm sync-plugins adds plugin re-exports to the renderer packages. These files are generated and should not be committed.
The default example is the Vue app in examples/2.vite/vue:
pnpm devOther examples have their own root scripts, including pnpm dev:react, pnpm dev:svelte, pnpm dev:angular, pnpm dev:html, and pnpm dev:nuxt. Use the example for the renderer you are changing.
With source stubs in place, example bundlers load local package sources directly. You do not normally need a second terminal running pnpm dev:packages.
pnpm build replaces the stubs with compiled output. After building, restore source exports before continuing development:
pnpm stubIf you want to develop against compiled output instead, run pnpm dev:packages in a separate terminal alongside the example. This runs the available package compiler watchers; packages without a dev script still need their own build command. For Svelte's compiler watcher, use pnpm --dir packages/comark-svelte run build:watch.
The documentation site uses a separate app:
pnpm docspackages/comark: parser, document model, core plugins and Markdown string rendering.packages/comark-htmlandpackages/comark-ansi: HTML and terminal renderers.packages/comark-vue,packages/comark-react,packages/comark-svelte, andpackages/comark-angular: framework renderers.packages/comark-nuxt: Nuxt integration.docs/content: user-facing documentation.
Keep parser-only behavior in the core package and framework-specific behavior in its renderer. The architecture reference has a more detailed package map.
Core plugins live in packages/comark/src/plugins and use defineComarkPlugin from comark/parse. Add tests in packages/comark/test/plugins and document the plugin in docs/content/4.plugins.
For plugins without a framework component, pnpm sync-plugins generates the renderer re-exports. If a plugin needs a component, add the wrapper in the relevant renderer package and keep the renderer APIs consistent.
See the plugin API for hooks and examples.
Run the repository checks before opening a pull request:
pnpm verifyThis runs linting, package tests and the root TypeScript check. Formatting uses oxfmt, with no semicolons, single quotes and a 120-column limit. To format a file, use pnpm exec oxfmt path/to/file; review the diff before committing.
For a focused parser test:
cd packages/comark
pnpm vitest run test/parse/sub-sup.test.tsParser fixtures live in packages/comark/SPEC and run through test/index.test.ts. Parser changes should also preserve streaming behavior in test/streaming.test.ts.
Svelte's local tests include a headless Chromium project. Install the browser if needed:
pnpm exec playwright install chromiumFor changes to build output or package exports, also run the build and bundle snapshot check used in CI:
pnpm prepack
pnpm vitest run bundleIf your change legitimately alters package sizes, update the snapshot with pnpm vitest run bundle -u after building, and review the resulting diff. Restore source stubs with pnpm stub before returning to example development.
Parse, streaming, render and plugin performance is tracked with CodSpeed. The codspeed workflow runs the Vitest benches in benchmarks/*.bench.ts on every pull request and comments with any change compared to main. CodSpeed measures in CPU simulation, so results do not depend on CI machine noise.
Run the suite locally (wall-clock timings, which are only useful for rough comparisons on your machine):
pnpm bench
pnpm bench parse # only benchmarks/parse.bench.tsWhen adding a benchmark:
- Put shared inputs in
benchmarks/fixtures.ts. Renaming a benchmark or editing a fixture resets its history in CodSpeed, so prefer adding a new one. - Create parsers once at module level and call each one before the benches run, so samples measure steady-state work, not setup.
- Give every streaming bench its own parser and reset it with a non-streaming call inside the bench. Otherwise the incremental cache returns the previous result without parsing.
The mitata scripts in benchmarks/*.ts compare Comark with markdown-it, markdown-exit and remark. Run them directly, for example pnpm tsx benchmarks/comark-parse.ts; they are not part of CodSpeed.
Link the relevant issue and follow the pull request template's What and Why sections. Describe the user-visible problem, the chosen fix and any compatibility impact. Include the checks you ran and any remaining limitations.
Use Conventional Commits, for example fix(parse): preserve inline comments, feat(html): add an option, or docs: clarify local setup. Mark breaking changes explicitly. Maintainers use these messages to generate package releases; contributors do not need to bump versions or publish packages.
All commits in a pull request that is ready for review must have a signature that GitHub verifies. Use a signing key registered with your GitHub account and check the commit's Verified status before submitting. See GitHub's commit signing guide.
Report security vulnerabilities privately using SECURITY.md.