diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.yml similarity index 98% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/bug_report.yml index ac7e6bea..4550ba01 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,6 +1,6 @@ name: "\U0001F41E Bug Report" description: Create a report to help us improve Comark -labels: ["bug", "pending triage"] +labels: ['bug', 'pending triage'] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/documentation.md b/.github/ISSUE_TEMPLATE/documentation.yml similarity index 94% rename from .github/ISSUE_TEMPLATE/documentation.md rename to .github/ISSUE_TEMPLATE/documentation.yml index 62fa8b45..f5db4471 100644 --- a/.github/ISSUE_TEMPLATE/documentation.md +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -1,6 +1,6 @@ -name: "📖 Documentation" +name: '📖 Documentation' description: Report a documentation issue or suggest an improvement -labels: ["documentation", "pending triage"] +labels: ['documentation', 'pending triage'] body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.yml similarity index 94% rename from .github/ISSUE_TEMPLATE/feature_request.md rename to .github/ISSUE_TEMPLATE/feature_request.yml index 7a9da9ed..f44fd96d 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.md +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -1,6 +1,6 @@ -name: "✨ Feature Request" +name: '✨ Feature Request' description: Suggest a new feature or enhancement for Comark -labels: ["enhancement", "pending triage"] +labels: ['enhancement', 'pending triage'] body: - type: markdown attributes: diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..b3800489 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,9 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": false, + "printWidth": 120, + "singleQuote": true, + "trailingComma": "es5", + "singleAttributePerLine": true, + "ignorePatterns": ["*.md", "pnpm*.yaml", "**/*.gen.ts", "**/dist/**"] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 00000000..27d0959b --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,15 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "plugins": ["typescript", "oxc", "import"], + "rules": { + "typescript/no-useless-empty-export": "off" + }, + "overrides": [ + { + "files": ["**/next-env.d.ts"], + "rules": { + "typescript/triple-slash-reference": "off" + } + } + ] +} diff --git a/AGENTS.md b/AGENTS.md index b17d2724..6dd6cfdd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -27,9 +27,9 @@ This is a **monorepo** containing multiple packages related to Comark (Component │ ├── comark-svelte/ # Svelte renderer + plugins (@comark/svelte) │ └── comark-nuxt/ # Nuxt module (@comark/nuxt) ├── examples/ # Example applications -│ ├── 1.vue-vite/ # Vue + Vite + Tailwind CSS v4 -│ ├── 2.react-vite/ # React 19 + Vite + Tailwind CSS v4 -│ └── 3.plugins/ # Plugin examples (vue-vite-math, vue-vite-mermaid) +│ ├── 1.frameworks/ # Framework examples (Nuxt, Next.js, Astro, SvelteKit, ...) +│ ├── 2.vite/ # Vite examples (Vue, React, Svelte, HTML, ANSI) +│ └── 3.plugins/ # Plugin examples (math, mermaid, highlight, ...) ├── docs/ # Documentation site (Docus-based) ├── scripts/ # Build/sync scripts ├── pnpm-workspace.yaml # Workspace configuration @@ -235,11 +235,16 @@ packages/comark-svelte/ ├── src/ │ ├── index.ts # Entry point (@comark/svelte) │ ├── types.ts # Shared prop interfaces -│ ├── Comark.svelte # High-level markdown → render ($state + $effect) -│ ├── ComarkAsync.svelte # High-level markdown → render (experimental await) -│ ├── ComarkRenderer.svelte # Low-level AST → render component -│ ├── ComarkNode.svelte # Recursive AST node renderer -│ ├── async/index.ts # Async export (@comark/svelte/async) +│ ├── components/ +│ │ ├── Comark.svelte # High-level markdown → render ($state + $effect) +│ │ ├── ComarkRenderer.svelte # Low-level AST → render component +│ │ ├── ComarkNode.svelte # Recursive AST node renderer +│ │ ├── ComarkComponent.svelte # Custom component renderer with named snippets +│ │ └── Resolve.svelte # Stable promise resolver for lazy components +│ ├── async/ +│ │ ├── index.ts # Async export (@comark/svelte/async) +│ │ ├── ComarkAsync.svelte # High-level markdown → render (experimental await) +│ │ └── ResolveAsync.svelte # Async SSR resolver for lazy components │ └── plugins/ │ ├── math.ts # Re-exports comark/plugins/math │ ├── Math.svelte # Math rendering component @@ -256,7 +261,8 @@ packages/comark-svelte/ { ".": { "svelte": "./dist/index.js" }, "./async": { "svelte": "./dist/async/index.js" }, - "./plugins/*": { "svelte": "./dist/plugins/*.js" } + "./plugins/*": { "svelte": "./dist/plugins/*.js" }, + "./components/*": { "svelte": "./dist/components/*" } } ``` diff --git a/SECURITY.md b/SECURITY.md index 434db874..4baffc1f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -42,6 +42,6 @@ This policy applies to all packages in the Comark monorepo: ## Built-in Security -Comark includes a built-in [security plugin](https://comark.dev/plugins/core/security) (`comark/plugins/security`) that provides XSS sanitization. We recommend enabling it when rendering user-generated content. +Comark includes a built-in [security plugin](https://comark.dev/plugins/built-in/security) (`comark/plugins/security`) that provides XSS sanitization. We recommend enabling it when rendering user-generated content. Thank you for helping keep Comark and its users safe. diff --git a/benchmarks/comark-parse.ts b/benchmarks/comark-parse.ts index 5592dd82..bb6e4847 100644 --- a/benchmarks/comark-parse.ts +++ b/benchmarks/comark-parse.ts @@ -1,7 +1,7 @@ import { bench, run, barplot, group } from 'mitata' import MarkdownIt from 'markdown-it' import MarkdownExit from 'markdown-exit' -import pluginMdc from '@comark/markdown-it' +import { markdownItComark } from 'comark/plugins/syntax' import { createParse } from 'comark' // Sample markdown content to test with @@ -61,7 +61,7 @@ const markdownIt = new MarkdownIt({ linkify: true, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) // Initialize markdown-exit with MDC plugin const markdownExit = new MarkdownExit({ @@ -69,7 +69,7 @@ const markdownExit = new MarkdownExit({ linkify: true, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) const comark = createParse() const comarkNoClose = createParse({ autoClose: false }) diff --git a/benchmarks/comark-render.ts b/benchmarks/comark-render.ts index 8da24ab2..adc243f9 100644 --- a/benchmarks/comark-render.ts +++ b/benchmarks/comark-render.ts @@ -1,7 +1,7 @@ import { bench, run, barplot, group } from 'mitata' import MarkdownIt from 'markdown-it' import MarkdownExit from 'markdown-exit' -import pluginMdc from '@comark/markdown-it' +import { markdownItComark } from 'comark/plugins/syntax' import { createParse } from 'comark' import { renderHTML } from '../packages/comark-html/src/index.ts' @@ -62,7 +62,7 @@ const markdownIt = new MarkdownIt({ linkify: true, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) // Initialize markdown-exit with MDC plugin const markdownExit = new MarkdownExit({ @@ -70,7 +70,7 @@ const markdownExit = new MarkdownExit({ linkify: true, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) const comark = createParse() const comarkNoClose = createParse({ autoClose: false }) diff --git a/benchmarks/plugin-highlight.ts b/benchmarks/plugin-highlight.ts new file mode 100644 index 00000000..25e545c3 --- /dev/null +++ b/benchmarks/plugin-highlight.ts @@ -0,0 +1,150 @@ +import { bench, run, group, barplot } from 'mitata' +import MarkdownIt from 'markdown-it' +import MarkdownExit from 'markdown-exit' +import { markdownItComark } from 'comark/plugins/syntax' +import { createParse } from 'comark' +import highlight, { getHighlighter } from '../packages/comark/src/plugins/highlight' +import { codeToHast } from 'shiki/core' + +const short = ` +# Quick Start + +Install with: + +\`\`\`bash +npm install comark +\`\`\` + +Then use it: + +\`\`\`javascript +import { parse } from 'comark' +const tree = await parse('# Hello') +\`\`\` +` + +const medium = ` +# API Reference + +## Parse + +\`\`\`typescript +import { parse } from 'comark' + +interface ParseOptions { + autoClose?: boolean + streaming?: boolean + plugins?: ComarkPlugin[] +} + +const tree = await parse(markdown, { + autoClose: true, + plugins: [highlight()], +}) +\`\`\` + +## Render + +\`\`\`vue + + + +\`\`\` + +## Configuration + +\`\`\`json +{ + "name": "my-project", + "dependencies": { + "comark": "^0.2.0", + "@comark/vue": "^0.2.0" + } +} +\`\`\` +` + +const long = Array.from( + { length: 20 }, + (_, i) => ` +## Module ${i + 1} + +\`\`\`typescript +export function module${i + 1}(input: string): string { + const result = input.trim() + return result.length > 0 ? result : 'default' +} +\`\`\` + +\`\`\`bash +npm run build:module-${i + 1} +\`\`\` +` +).join('\n') + +// markdown-it / markdown-exit produce flat tokens — to get syntax highlighting +// they still need shiki. We benchmark both pipelines with the same shiki work. +const markdownIt = new MarkdownIt({ html: true, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) +const markdownExit = new MarkdownExit({ html: true, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) + +// comark: baseline vs highlight plugin +const comark = createParse() +const comarkHl = createParse({ plugins: [highlight()] }) + +// Pre-warm shiki so we benchmark steady-state, not cold-start +const shiki = await getHighlighter() + +// Warm up comark highlight to ensure shiki languages are loaded +await comarkHl(medium) + +// Helper: extract fence tokens from markdown-it/exit and highlight them with shiki +// This simulates what a real markdown-it + shiki pipeline does. +async function highlightTokens(tokens: any[]) { + for (const token of tokens) { + if (token.type === 'fence' && token.info) { + await codeToHast(shiki, token.content, { + lang: token.info.split(/\s/)[0], + themes: { light: 'material-theme-lighter', dark: 'material-theme-palenight' }, + }) + } + } +} + +for (const [label, content] of [ + ['short (2 blocks)', short], + ['medium (3 blocks)', medium], + ['long (40 blocks)', long], +] as const) { + barplot(() => { + group(`highlight — ${label}`, () => { + bench('comark', async () => { + await comark(content) + }) + bench('comark + highlight', async () => { + await comarkHl(content) + }) + bench('markdown-it + shiki', async () => { + const tokens = markdownIt.parse(content, {}) + await highlightTokens(tokens) + }) + bench('markdown-exit + shiki', async () => { + const tokens = markdownExit.parse(content, {}) + await highlightTokens(tokens) + }) + }) + }) +} + +console.log('🏃 Running benchmarks...\n') +await run() diff --git a/benchmarks/plugin-punctuation.ts b/benchmarks/plugin-punctuation.ts index 07692c1c..a15263f0 100644 --- a/benchmarks/plugin-punctuation.ts +++ b/benchmarks/plugin-punctuation.ts @@ -1,6 +1,6 @@ import { bench, run, group, barplot } from 'mitata' import MarkdownExit from 'markdown-exit' -import pluginMdc from '@comark/markdown-it' +import { markdownItComark } from 'comark/plugins/syntax' import { createParse } from 'comark' import { log } from '@comark/ansi' import punctuation from '../packages/comark/src/plugins/punctuation' @@ -23,7 +23,9 @@ Copyright (c) 2025 Acme Corp(r). All rights reserved... Really?.... wow!!!!! hmm,, ok ` -const long = Array.from({ length: 50 }, (_, i) => ` +const long = Array.from( + { length: 50 }, + (_, i) => ` ## Section ${i + 1} "Paragraph ${i + 1}" has some 'quoted text' and contractions like don't, won't, can't. @@ -33,7 +35,8 @@ The range is ${i}--${i + 10} and the em-dash --- is used here... plus (c) (r) (t Another line with "double quotes" and 'single quotes' and more ellipsis... Really???? Wow!!!!! hmm,, ok?.... end -`).join('\n') +` +).join('\n') // ── markdown-it typographer ───────────────────────────────────────────────── @@ -43,7 +46,7 @@ const parserTypographer = new MarkdownExit({ typographer: true, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) const parserNoTypographer = new MarkdownExit({ html: false, @@ -51,7 +54,7 @@ const parserNoTypographer = new MarkdownExit({ typographer: false, }) .enable(['table', 'strikethrough']) - .use(pluginMdc) + .use(markdownItComark) // ── comark with full punctuation plugin (all features) ────────────────────── @@ -128,7 +131,10 @@ console.log('=== Output Comparison ===\n') console.log('Input:', JSON.stringify(testStr)) const miTokens = parserTypographer.parse(testStr, {}) -const miText = miTokens.filter((t: any) => t.type === 'inline').map((t: any) => t.content).join('') +const miText = miTokens + .filter((t: any) => t.type === 'inline') + .map((t: any) => t.content) + .join('') console.log('markdown-it typographer:', JSON.stringify(miText)) const comarkTree = await comarkFull(testStr) diff --git a/docs/app/app.config.ts b/docs/app/app.config.ts index f31b9310..bba579bb 100644 --- a/docs/app/app.config.ts +++ b/docs/app/app.config.ts @@ -70,12 +70,13 @@ export default defineAppConfig({ }, codeIcon: { 'astro.config.mjs': 'i-simple-icons:astro', - 'astro': 'i-simple-icons:astro', - 'md': 'i-custom-comark', - 'mdc': 'i-custom-comark', - 'react': 'i-logos-react', - 'html': 'i-vscode-icons-file-type-html', - 'svelte': 'i-simple-icons-svelte', + astro: 'i-simple-icons:astro', + md: 'i-custom-comark', + mdc: 'i-custom-comark', + react: 'i-logos-react', + html: 'i-vscode-icons-file-type-html', + svelte: 'i-logos-svelte-icon', + nuxt: 'i-logos-nuxt-icon', }, }, }, @@ -107,15 +108,15 @@ export default defineAppConfig({ links: [ { label: 'Syntax Highlighting', - to: '/plugins/core/highlight', + to: '/plugins/built-in/syntax-highlight', }, { label: 'Math', - to: '/plugins/core/math', + to: '/plugins/built-in/math', }, { label: 'Mermaid', - to: '/plugins/core/mermaid', + to: '/plugins/built-in/mermaid', }, ], }, diff --git a/docs/app/assets/styles/main.css b/docs/app/app.css similarity index 57% rename from docs/app/assets/styles/main.css rename to docs/app/app.css index 0e1286eb..448b255c 100644 --- a/docs/app/assets/styles/main.css +++ b/docs/app/app.css @@ -1,24 +1,18 @@ -@import 'tailwindcss'; -@import '@nuxt/ui'; -@plugin "@tailwindcss/typography"; - @theme { --font-sans: 'Geist', 'Public Sans', sans-serif; --font-mono: 'Geist Mono', ui-monospace, monospace; - --breakpoint-3xl: 1920px; - - --color-green-50: #EFFDF5; - --color-green-100: #D9FBE8; - --color-green-200: #B3F5D1; - --color-green-300: #75EDAE; - --color-green-400: #00DC82; - --color-green-500: #00C16A; - --color-green-600: #00A155; - --color-green-700: #007F45; + --color-green-50: #effdf5; + --color-green-100: #d9fbe8; + --color-green-200: #b3f5d1; + --color-green-300: #75edae; + --color-green-400: #00dc82; + --color-green-500: #00c16a; + --color-green-600: #00a155; + --color-green-700: #007f45; --color-green-800: #016538; - --color-green-900: #0A5331; - --color-green-950: #052E16; + --color-green-900: #0a5331; + --color-green-950: #052e16; } :root { @@ -32,9 +26,21 @@ --ui-primary: var(--ui-color-primary-400); } +:where(code, kbd, samp, pre, .font-mono) { + font-variant-ligatures: none; + font-feature-settings: + 'liga' 0, + 'calt' 0; +} + @keyframes caret-blink { - 0%, 100% { opacity: 1; } - 50% { opacity: 0; } + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0; + } } .caret { @@ -77,16 +83,36 @@ background-color: var(--ui-primary); } -.syntax-hash { color: var(--ui-color-primary-600); } -.syntax-asterisk { color: var(--color-amber-600); } -.syntax-colon { color: var(--ui-color-primary-600); } -.syntax-bracket { color: var(--ui-color-neutral-400); } -.syntax-text { color: var(--ui-color-neutral-700); } -.dark .syntax-hash { color: var(--ui-color-primary-400); } -.dark .syntax-asterisk { color: var(--color-amber-400); } -.dark .syntax-colon { color: var(--ui-color-primary-500); } -.dark .syntax-bracket { color: var(--ui-color-neutral-500); } -.dark .syntax-text { color: var(--ui-color-neutral-200); } +.syntax-hash { + color: var(--ui-color-primary-600); +} +.syntax-asterisk { + color: var(--color-amber-600); +} +.syntax-colon { + color: var(--ui-color-primary-600); +} +.syntax-bracket { + color: var(--ui-color-neutral-400); +} +.syntax-text { + color: var(--ui-color-neutral-700); +} +.dark .syntax-hash { + color: var(--ui-color-primary-400); +} +.dark .syntax-asterisk { + color: var(--color-amber-400); +} +.dark .syntax-colon { + color: var(--ui-color-primary-500); +} +.dark .syntax-bracket { + color: var(--ui-color-neutral-500); +} +.dark .syntax-text { + color: var(--ui-color-neutral-200); +} .shiki span.line.highlight { background-color: rgba(255, 255, 0, 0.1); diff --git a/docs/app/components/ComarkDocs.ts b/docs/app/components/ComarkDocs.ts index 77c14b80..90b32821 100644 --- a/docs/app/components/ComarkDocs.ts +++ b/docs/app/components/ComarkDocs.ts @@ -13,12 +13,7 @@ import binding, { Binding } from '@comark/nuxt/plugins/binding' const BaseComarkDocs = defineComarkComponent({ name: 'BaseComarkDocs', autoClose: true, - plugins: [ - math(), - mermaid(), - emoji(), - binding(), - ], + plugins: [math(), mermaid(), emoji(), binding()], components: { Math, Mermaid, @@ -33,9 +28,7 @@ export default defineComarkComponent({ name: 'ComarkDocs', plugins: [ highlight({ - languages: [ - Python, - ], + languages: [Python], themes: { light: githubLight, dark: githubDark, diff --git a/docs/app/components/ComarkPlaygroundRenderer.ts b/docs/app/components/ComarkPlaygroundRenderer.ts new file mode 100644 index 00000000..8a3d2f1e --- /dev/null +++ b/docs/app/components/ComarkPlaygroundRenderer.ts @@ -0,0 +1,13 @@ +import { defineComarkRendererComponent } from '@comark/vue' +import { Math } from '@comark/vue/plugins/math' +import { Mermaid } from '@comark/vue/plugins/mermaid' +import { Binding } from '@comark/vue/plugins/binding' + +export default defineComarkRendererComponent({ + name: 'ComarkPlaygroundRenderer', + components: { + Math, + Mermaid, + Binding, + }, +}) diff --git a/docs/app/components/ComarkStream.vue b/docs/app/components/ComarkStream.vue index 75e6d983..05944074 100644 --- a/docs/app/components/ComarkStream.vue +++ b/docs/app/components/ComarkStream.vue @@ -29,7 +29,7 @@ async function startStream() { accumulated.value += chunk if (i + chunkSize < props.markdown.length) { - await new Promise(resolve => setTimeout(resolve, delayMs)) + await new Promise((resolve) => setTimeout(resolve, delayMs)) } } @@ -55,7 +55,6 @@ defineExpose({