diff --git a/benchmarks/plugin-emoji.ts b/benchmarks/plugin-emoji.ts new file mode 100644 index 00000000..fc996e37 --- /dev/null +++ b/benchmarks/plugin-emoji.ts @@ -0,0 +1,110 @@ +import { bench, run, group, barplot } from 'mitata' +import MarkdownIt from 'markdown-it' +import MarkdownExit from 'markdown-exit' +import { markdownItComark } from 'comark/plugins/syntax' +import { full as markdownItEmoji } from 'markdown-it-emoji' +import { createParse } from 'comark' +import { unified } from 'unified' +import remarkParse from 'remark-parse' +import remarkGfm from 'remark-gfm' +import remarkEmoji from 'remark-emoji' +import emoji from '../packages/comark/src/plugins/emoji' + +const short = `Hello :wave: world :rocket: that's :100: percent :fire:` + +const medium = ` +# Welcome :tada: + +This is a **great** day :sunny: for coding :computer: + +## Features :sparkles: + +- Fast parsing :rocket: +- Easy to use :thumbsup: +- Great docs :books: + +> "Keep it simple" :smile: -- someone wise :nerd_face: + +Check out our :star: project on :octocat: GitHub! + +Don't :x: forget to :heart: the repo :pray: +` + +const long = Array.from( + { length: 50 }, + (_, i) => ` +## Section ${i + 1} :bookmark: + +Paragraph ${i + 1} has some :smile: emoji and :rocket: inline codes. + +- Item :thumbsup: with emoji +- Another :star: item +- Final :checkered_flag: item + +> Quote with :heart: and :fire: emoji :100: +` +).join('\n') + +// markdown-it: baseline vs emoji +const markdownIt = new MarkdownIt({ html: false, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) +const markdownItEm = new MarkdownIt({ html: false, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) + .use(markdownItEmoji) + +// markdown-exit: baseline vs emoji +const markdownExit = new MarkdownExit({ html: false, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) +const markdownExitEm = new MarkdownExit({ html: false, linkify: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) + .use(markdownItEmoji) + +// comark: baseline vs emoji plugin +const comark = createParse() +const comarkEm = createParse({ plugins: [emoji()] }) + +// remark (unified): baseline vs emoji +const remark = unified().use(remarkParse).use(remarkGfm) +const remarkEm = unified().use(remarkParse).use(remarkGfm).use(remarkEmoji) + +for (const [label, content] of [ + ['short text', short], + ['medium text', medium], + ['long text (50 sections)', long], +] as const) { + barplot(() => { + group(`emoji โ€” ${label}`, () => { + bench('comark', async () => { + await comark(content) + }) + bench('comark + emoji', async () => { + await comarkEm(content) + }) + bench('markdown-it', () => { + markdownIt.parse(content, {}) + }) + bench('markdown-it + emoji', () => { + markdownItEm.parse(content, {}) + }) + bench('markdown-exit', () => { + markdownExit.parse(content, {}) + }) + bench('markdown-exit + emoji', () => { + markdownExitEm.parse(content, {}) + }) + bench('remark', () => { + remark.parse(content) + }) + bench('remark + emoji', () => { + remarkEm.runSync(remarkEm.parse(content)) + }) + }) + }) +} + +console.log('๐Ÿƒ Running benchmarks...\n') +await run() diff --git a/benchmarks/plugin-punctuation.ts b/benchmarks/plugin-punctuation.ts index a15263f0..a4390f96 100644 --- a/benchmarks/plugin-punctuation.ts +++ b/benchmarks/plugin-punctuation.ts @@ -1,11 +1,15 @@ 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 { log } from '@comark/ansi' +import { unified } from 'unified' +import remarkParse from 'remark-parse' +import remarkGfm from 'remark-gfm' +import remarkSmartypants from 'remark-smartypants' import punctuation from '../packages/comark/src/plugins/punctuation' -// โ”€โ”€ Test content (exercises ALL features: quotes, dashes, ellipsis, symbols, normalization) โ”€โ”€ +// Test content (exercises ALL features: quotes, dashes, ellipsis, symbols, normalization) const short = `"Hello" -- world... (c) 2025 what???? ok,,` @@ -38,113 +42,65 @@ Really???? Wow!!!!! hmm,, ok?.... end ` ).join('\n') -// โ”€โ”€ markdown-it typographer โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -const parserTypographer = new MarkdownExit({ - html: false, - linkify: true, - typographer: true, -}) +// markdown-it: baseline vs typographer +const markdownIt = new MarkdownIt({ html: false, linkify: true, typographer: false }) .enable(['table', 'strikethrough']) .use(markdownItComark) - -const parserNoTypographer = new MarkdownExit({ - html: false, - linkify: true, - typographer: false, -}) +const markdownItTypo = new MarkdownIt({ html: false, linkify: true, typographer: true }) .enable(['table', 'strikethrough']) .use(markdownItComark) -// โ”€โ”€ comark with full punctuation plugin (all features) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -const comarkFull = createParse({ plugins: [punctuation()] }) -const comarkBaseline = createParse() - -// โ”€โ”€ Benchmarks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -barplot(() => { - group('short text (all features)', () => { - bench('markdown-it typographer', () => { - parserTypographer.parse(short, {}) - }) - bench('markdown-it baseline', () => { - parserNoTypographer.parse(short, {}) - }) - bench('comark + punctuation (full)', async () => { - await comarkFull(short) - }) - bench('comark baseline', async () => { - await comarkBaseline(short) - }) - }) -}) - -barplot(() => { - group('medium text (all features)', () => { - bench('markdown-it typographer', () => { - parserTypographer.parse(medium, {}) - }) - bench('markdown-it baseline', () => { - parserNoTypographer.parse(medium, {}) - }) - bench('comark + punctuation (full)', async () => { - await comarkFull(medium) - }) - bench('comark baseline', async () => { - await comarkBaseline(medium) - }) - }) -}) +// markdown-exit: baseline vs typographer +const markdownExit = new MarkdownExit({ html: false, linkify: true, typographer: false }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) +const markdownExitTypo = new MarkdownExit({ html: false, linkify: true, typographer: true }) + .enable(['table', 'strikethrough']) + .use(markdownItComark) -barplot(() => { - group('long text โ€” 50 sections (all features)', () => { - bench('markdown-it typographer', () => { - parserTypographer.parse(long, {}) - }) - bench('markdown-it baseline', () => { - parserNoTypographer.parse(long, {}) - }) - bench('comark + punctuation (full)', async () => { - await comarkFull(long) - }) - bench('comark baseline', async () => { - await comarkBaseline(long) +// comark: baseline vs punctuation plugin +const comark = createParse() +const comarkPunct = createParse({ plugins: [punctuation()] }) + +// remark (unified): baseline vs smartypants +const remark = unified().use(remarkParse).use(remarkGfm) +const remarkSmarty = unified().use(remarkParse).use(remarkGfm).use(remarkSmartypants) + + +for (const [label, content] of [ + ['short text', short], + ['medium text', medium], + ['long text (50 sections)', long], +] as const) { + barplot(() => { + group(`punctuation โ€” ${label}`, () => { + bench('comark', async () => { + await comark(content) + }) + bench('comark + punctuation', async () => { + await comarkPunct(content) + }) + bench('markdown-it', () => { + markdownIt.parse(content, {}) + }) + bench('markdown-it + typographer', () => { + markdownItTypo.parse(content, {}) + }) + bench('markdown-exit', () => { + markdownExit.parse(content, {}) + }) + bench('markdown-exit + typographer', () => { + markdownExitTypo.parse(content, {}) + }) + bench('remark', () => { + remark.parse(content) + }) + bench('remark + smartypants', () => { + remarkSmarty.runSync(remarkSmarty.parse(content)) + }) }) }) -}) - -// โ”€โ”€ Output comparison โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -function flattenText(nodes: any[]): string { - let text = '' - for (const node of nodes) { - if (typeof node === 'string') text += node - else if (Array.isArray(node) && node.length > 2) text += flattenText(node.slice(2)) - } - return text } -const testStr = `"Hello" -- world... (c) what???? ok,, really!.... hmm.....` - -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('') -console.log('markdown-it typographer:', JSON.stringify(miText)) - -const comarkTree = await comarkFull(testStr) -console.log('comark punctuation: ', JSON.stringify(flattenText(comarkTree.nodes))) - -console.log('\n๐Ÿƒ Running benchmarks...\n') +console.log('๐Ÿƒ Running benchmarks...\n') await run() - -await log(`> [!NOTE] -> The goal of this benchmark is to compare the additional time each parser takes when -> using punctuation plugins. -> -> Official plugin adds ~100% the execution time, while comark adds ~25% execution time`) diff --git a/docs/app/pages/benchmarks.vue b/docs/app/pages/benchmarks.vue new file mode 100644 index 00000000..7f2f7ecd --- /dev/null +++ b/docs/app/pages/benchmarks.vue @@ -0,0 +1,482 @@ + + + diff --git a/docs/public/benchmarks.json b/docs/public/benchmarks.json new file mode 100644 index 00000000..5d5fd9be --- /dev/null +++ b/docs/public/benchmarks.json @@ -0,0 +1,492 @@ +{ + "generated": "2026-06-04T02:15:31.529Z", + "runtime": "node 24.16.0 (arm64-darwin)", + "cpu": "Apple M4 Max", + "benchmarks": { + "comark-parse": [ + { + "name": "parse", + "entries": [ + { + "name": "markdown-it parse", + "avg": 22.82, + "unit": "ยตs", + "avgNs": 22820 + }, + { + "name": "markdown-exit parse", + "avg": 19.61, + "unit": "ยตs", + "avgNs": 19610 + }, + { + "name": "comark parse", + "avg": 37.42, + "unit": "ยตs", + "avgNs": 37420 + }, + { + "name": "comark parse no close", + "avg": 35.35, + "unit": "ยตs", + "avgNs": 35350 + }, + { + "name": "comark parse streaming", + "avg": 3.62, + "unit": "ยตs", + "avgNs": 3620 + } + ] + } + ], + "comark-render": [ + { + "name": "render", + "entries": [ + { + "name": "markdown-it render", + "avg": 27.42, + "unit": "ยตs", + "avgNs": 27420 + }, + { + "name": "markdown-exit render", + "avg": 24.46, + "unit": "ยตs", + "avgNs": 24460 + }, + { + "name": "comark parse + renderHTML", + "avg": 74.9, + "unit": "ยตs", + "avgNs": 74900 + }, + { + "name": "comark parse no close", + "avg": 70.54, + "unit": "ยตs", + "avgNs": 70540 + }, + { + "name": "comark parse streaming", + "avg": 37.99, + "unit": "ยตs", + "avgNs": 37990 + } + ] + } + ], + "plugin-emoji": [ + { + "name": "emoji โ€” short text", + "entries": [ + { + "name": "comark", + "avg": 3.48, + "unit": "ยตs", + "avgNs": 3480 + }, + { + "name": "comark + emoji", + "avg": 2.44, + "unit": "ยตs", + "avgNs": 2440 + }, + { + "name": "markdown-it", + "avg": 1.88, + "unit": "ยตs", + "avgNs": 1880 + }, + { + "name": "markdown-it + emoji", + "avg": 1.97, + "unit": "ยตs", + "avgNs": 1970 + }, + { + "name": "markdown-exit", + "avg": 1.68, + "unit": "ยตs", + "avgNs": 1680 + }, + { + "name": "markdown-exit + emoji", + "avg": 1.77, + "unit": "ยตs", + "avgNs": 1770 + }, + { + "name": "remark", + "avg": 30.29, + "unit": "ยตs", + "avgNs": 30290 + }, + { + "name": "remark + emoji", + "avg": 31.62, + "unit": "ยตs", + "avgNs": 31620 + } + ] + }, + { + "name": "emoji โ€” medium text", + "entries": [ + { + "name": "comark", + "avg": 19.34, + "unit": "ยตs", + "avgNs": 19340 + }, + { + "name": "comark + emoji", + "avg": 16.32, + "unit": "ยตs", + "avgNs": 16320 + }, + { + "name": "markdown-it", + "avg": 11.78, + "unit": "ยตs", + "avgNs": 11780 + }, + { + "name": "markdown-it + emoji", + "avg": 12.09, + "unit": "ยตs", + "avgNs": 12090 + }, + { + "name": "markdown-exit", + "avg": 10.37, + "unit": "ยตs", + "avgNs": 10370 + }, + { + "name": "markdown-exit + emoji", + "avg": 10.9, + "unit": "ยตs", + "avgNs": 10900 + }, + { + "name": "remark", + "avg": 142.75, + "unit": "ยตs", + "avgNs": 142750 + }, + { + "name": "remark + emoji", + "avg": 151.47, + "unit": "ยตs", + "avgNs": 151470 + } + ] + }, + { + "name": "emoji โ€” long text (50 sections)", + "entries": [ + { + "name": "comark", + "avg": 559.26, + "unit": "ยตs", + "avgNs": 559260 + }, + { + "name": "comark + emoji", + "avg": 438.11, + "unit": "ยตs", + "avgNs": 438110 + }, + { + "name": "markdown-it", + "avg": 364.89, + "unit": "ยตs", + "avgNs": 364890 + }, + { + "name": "markdown-it + emoji", + "avg": 378.86, + "unit": "ยตs", + "avgNs": 378860 + }, + { + "name": "markdown-exit", + "avg": 326.09, + "unit": "ยตs", + "avgNs": 326090 + }, + { + "name": "markdown-exit + emoji", + "avg": 344.24, + "unit": "ยตs", + "avgNs": 344240 + }, + { + "name": "remark", + "avg": 4.63, + "unit": "ms", + "avgNs": 4630000 + }, + { + "name": "remark + emoji", + "avg": 4.96, + "unit": "ms", + "avgNs": 4960000 + } + ] + } + ], + "plugin-highlight": [ + { + "name": "highlight โ€” short (2 blocks)", + "entries": [ + { + "name": "comark", + "avg": 4.5, + "unit": "ยตs", + "avgNs": 4500 + }, + { + "name": "comark + highlight", + "avg": 170.33, + "unit": "ยตs", + "avgNs": 170330 + }, + { + "name": "markdown-it + shiki", + "avg": 173.02, + "unit": "ยตs", + "avgNs": 173020 + }, + { + "name": "markdown-exit + shiki", + "avg": 174.17, + "unit": "ยตs", + "avgNs": 174170 + } + ] + }, + { + "name": "highlight โ€” medium (3 blocks)", + "entries": [ + { + "name": "comark", + "avg": 7.7, + "unit": "ยตs", + "avgNs": 7700 + }, + { + "name": "comark + highlight", + "avg": 732.79, + "unit": "ยตs", + "avgNs": 732790 + }, + { + "name": "markdown-it + shiki", + "avg": 751.98, + "unit": "ยตs", + "avgNs": 751980 + }, + { + "name": "markdown-exit + shiki", + "avg": 762.79, + "unit": "ยตs", + "avgNs": 762790 + } + ] + }, + { + "name": "highlight โ€” long (40 blocks)", + "entries": [ + { + "name": "comark", + "avg": 47.44, + "unit": "ยตs", + "avgNs": 47440 + }, + { + "name": "comark + highlight", + "avg": 7.56, + "unit": "ms", + "avgNs": 7560000 + }, + { + "name": "markdown-it + shiki", + "avg": 7.65, + "unit": "ms", + "avgNs": 7650000 + }, + { + "name": "markdown-exit + shiki", + "avg": 7.58, + "unit": "ms", + "avgNs": 7580000 + } + ] + } + ], + "plugin-punctuation": [ + { + "name": "punctuation โ€” short text", + "entries": [ + { + "name": "comark", + "avg": 1.94, + "unit": "ยตs", + "avgNs": 1940 + }, + { + "name": "comark + punctuation", + "avg": 2.49, + "unit": "ยตs", + "avgNs": 2490 + }, + { + "name": "markdown-it", + "avg": 956.48, + "unit": "ns", + "avgNs": 956.48 + }, + { + "name": "markdown-it + typographer", + "avg": 2.37, + "unit": "ยตs", + "avgNs": 2370 + }, + { + "name": "markdown-exit", + "avg": 802.68, + "unit": "ns", + "avgNs": 802.68 + }, + { + "name": "markdown-exit + typographer", + "avg": 2.06, + "unit": "ยตs", + "avgNs": 2060 + }, + { + "name": "remark", + "avg": 29.14, + "unit": "ยตs", + "avgNs": 29140 + }, + { + "name": "remark + smartypants", + "avg": 56.29, + "unit": "ยตs", + "avgNs": 56290 + } + ] + }, + { + "name": "punctuation โ€” medium text", + "entries": [ + { + "name": "comark", + "avg": 8.48, + "unit": "ยตs", + "avgNs": 8480 + }, + { + "name": "comark + punctuation", + "avg": 10.93, + "unit": "ยตs", + "avgNs": 10930 + }, + { + "name": "markdown-it", + "avg": 5.89, + "unit": "ยตs", + "avgNs": 5890 + }, + { + "name": "markdown-it + typographer", + "avg": 11.6, + "unit": "ยตs", + "avgNs": 11600 + }, + { + "name": "markdown-exit", + "avg": 5.08, + "unit": "ยตs", + "avgNs": 5080 + }, + { + "name": "markdown-exit + typographer", + "avg": 10.54, + "unit": "ยตs", + "avgNs": 10540 + }, + { + "name": "remark", + "avg": 96.22, + "unit": "ยตs", + "avgNs": 96220 + }, + { + "name": "remark + smartypants", + "avg": 255.47, + "unit": "ยตs", + "avgNs": 255470 + } + ] + }, + { + "name": "punctuation โ€” long text (50 sections)", + "entries": [ + { + "name": "comark", + "avg": 424.01, + "unit": "ยตs", + "avgNs": 424010 + }, + { + "name": "comark + punctuation", + "avg": 525.64, + "unit": "ยตs", + "avgNs": 525640 + }, + { + "name": "markdown-it", + "avg": 264.75, + "unit": "ยตs", + "avgNs": 264750 + }, + { + "name": "markdown-it + typographer", + "avg": 487.09, + "unit": "ยตs", + "avgNs": 487090 + }, + { + "name": "markdown-exit", + "avg": 234.3, + "unit": "ยตs", + "avgNs": 234300 + }, + { + "name": "markdown-exit + typographer", + "avg": 444.92, + "unit": "ยตs", + "avgNs": 444920 + }, + { + "name": "remark", + "avg": 3.81, + "unit": "ms", + "avgNs": 3810000 + }, + { + "name": "remark + smartypants", + "avg": 11.11, + "unit": "ms", + "avgNs": 11110000 + } + ] + } + ] + } +} diff --git a/docs/server/api/benchmarks.get.ts b/docs/server/api/benchmarks.get.ts new file mode 100644 index 00000000..5214ba1c --- /dev/null +++ b/docs/server/api/benchmarks.get.ts @@ -0,0 +1,15 @@ +import { readFileSync, existsSync } from 'node:fs' +import { join } from 'node:path' + +export default defineEventHandler(() => { + const filePath = join(process.cwd(), 'docs', 'public', 'benchmarks.json') + + if (existsSync(filePath)) { + return JSON.parse(readFileSync(filePath, 'utf-8')) + } + + throw createError({ + statusCode: 404, + statusMessage: 'Benchmarks data not found. run: `node scripts/bench.mjs`', + }) +}) diff --git a/package.json b/package.json index 514fab67..281c6902 100644 --- a/package.json +++ b/package.json @@ -52,17 +52,21 @@ "comark": "workspace:*", "markdown-exit": "catalog:", "markdown-it": "catalog:", + "markdown-it-emoji": "^3.0.0", "mitata": "catalog:", "nuxt": "catalog:", "oxfmt": "catalog:", "oxlint": "catalog:", "playwright": "catalog:", "release-it": "catalog:", + "remark-emoji": "^5.0.2", "remark-gfm": "catalog:", "remark-mdc": "catalog:", "remark-parse": "catalog:", "remark-rehype": "catalog:", + "remark-smartypants": "^3.0.2", "scule": "catalog:", + "shiki": "catalog:", "tsx": "catalog:", "typescript": "catalog:", "unified": "catalog:" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2695322c..f61777bc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -217,7 +217,7 @@ catalogs: specifier: ^1.3.0 version: 1.3.0 shiki: - specifier: ^4.0.0 + specifier: ^4.1.0 version: 4.1.0 splitpanes: specifier: ^4.0.4 @@ -321,6 +321,9 @@ importers: markdown-it: specifier: 'catalog:' version: 14.2.0 + markdown-it-emoji: + specifier: ^3.0.0 + version: 3.0.0 mitata: specifier: 'catalog:' version: 1.0.34 @@ -339,6 +342,9 @@ importers: release-it: specifier: 'catalog:' version: 19.2.4(@types/node@25.9.1)(magicast@0.5.3) + remark-emoji: + specifier: ^5.0.2 + version: 5.0.2 remark-gfm: specifier: 'catalog:' version: 4.0.1 @@ -351,9 +357,15 @@ importers: remark-rehype: specifier: 'catalog:' version: 11.1.2 + remark-smartypants: + specifier: ^3.0.2 + version: 3.0.2 scule: specifier: 'catalog:' version: 1.3.0 + shiki: + specifier: 'catalog:' + version: 4.1.0 tsx: specifier: 'catalog:' version: 4.22.4 @@ -7680,6 +7692,9 @@ packages: markdown-exit@1.0.0-beta.9: resolution: {integrity: sha512-5tzrMKMF367amyBly131vm6eGuWRL2DjBqWaFmPzPbLyuxP0XOmyyyroOAIXuBAMF/3kZbbfqOxvW/SotqKqbQ==} + markdown-it-emoji@3.0.0: + resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} + markdown-it-math@5.2.1: resolution: {integrity: sha512-0YJczaqvBxgOt13oKj/4HYs/34Y9FsHiTktsIBaWYqrE+k1JxmMSTxCcTezTVul0YjmhmCBDehK65vWxjzRbdg==} peerDependencies: @@ -16945,6 +16960,8 @@ snapshots: punycode.js: 2.3.1 uc.micro: 2.1.0 + markdown-it-emoji@3.0.0: {} + markdown-it-math@5.2.1: {} markdown-it@14.2.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8ef997e3..728c4f07 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -110,7 +110,7 @@ catalog: scule: ^1.3.0 # Shared peer dependencies - shiki: ^4.0.0 + shiki: ^4.1.0 splitpanes: ^4.0.4 # Svelte diff --git a/scripts/bench.mjs b/scripts/bench.mjs new file mode 100644 index 00000000..c3cd29a7 --- /dev/null +++ b/scripts/bench.mjs @@ -0,0 +1,136 @@ +#!/usr/bin/env node +/** + * Run all benchmarks and save results to docs/public/benchmarks.json + * + * Usage: + * node scripts/bench.mjs # run all benchmarks + * node scripts/bench.mjs # run only benchmarks matching "" + */ + +import { execSync } from 'node:child_process' +import { readdirSync, writeFileSync, mkdirSync, existsSync } from 'node:fs' +import { join, basename } from 'node:path' + +const root = new URL('..', import.meta.url).pathname +const benchDir = join(root, 'benchmarks') +const outFile = join(root, 'docs', 'public', 'benchmarks.json') + +const filter = process.argv[2] + +// Discover benchmark files +const files = readdirSync(benchDir) + .filter((f) => f.endsWith('.ts')) + .filter((f) => !filter || f.includes(filter)) + .sort() + +if (files.length === 0) { + console.error(`No benchmark files found${filter ? ` matching "${filter}"` : ''}.`) + process.exit(1) +} + +// Ensure output directory exists +const outDir = join(root, 'docs', 'public') +if (!existsSync(outDir)) { + mkdirSync(outDir, { recursive: true }) +} + +console.log(`Found ${files.length} benchmark(s):\n`) +files.forEach((f) => console.log(` - ${f}`)) +console.log() + +const results = {} +const errors = [] + +for (const file of files) { + const name = basename(file, '.ts') + console.log(`โ–ถ Running ${file}...`) + + try { + // Strip ANSI codes from output for clean text + const output = execSync(`npx tsx "${join(benchDir, file)}"`, { + cwd: root, + encoding: 'utf-8', + timeout: 5 * 60 * 1000, // 5 minutes per benchmark + env: { ...process.env, FORCE_COLOR: '0', NO_COLOR: '1' }, + stdio: ['pipe', 'pipe', 'pipe'], + }) + + // Strip any remaining ANSI escape codes + const clean = output.replace(/\x1B\[[0-9;]*[a-z]/gi, '') + + results[name] = clean + console.log(` โœ“ Done\n`) + } catch (err) { + const message = err.stderr?.toString() || err.message + errors.push({ file, message }) + console.error(` โœ— Failed: ${message.split('\n')[0]}\n`) + } +} + +// Generate a combined JSON summary with extracted stats +const summary = { + generated: new Date().toISOString(), + runtime: null, + cpu: null, + benchmarks: {}, +} + +for (const [name, output] of Object.entries(results)) { + // Extract runtime info from first benchmark + const cpuMatch = output.match(/cpu:\s*(.+)/) + const runtimeMatch = output.match(/runtime:\s*(.+)/) + if (cpuMatch) summary.cpu = cpuMatch[1].trim() + if (runtimeMatch) summary.runtime = runtimeMatch[1].trim() + + // Extract benchmark groups and results + const groups = [] + const lines = output.split('\n') + + let currentGroup = null + + for (const line of lines) { + // Group header: โ€ข group name + const groupMatch = line.match(/^โ€ข\s+(.+)$/) + if (groupMatch) { + currentGroup = { name: groupMatch[1], entries: [] } + groups.push(currentGroup) + continue + } + + // Bench result: name avg/iter + // Format: "name 123.45 ns/iter ..." + if (currentGroup) { + const benchMatch = line.match(/^(\S.*?)\s{2,}(\d[\d,.]*)\s+(ns|ยตs|ms|s)\/iter/) + if (benchMatch) { + const benchName = benchMatch[1].trim() + const value = Number.parseFloat(benchMatch[2].replace(/,/g, '')) + const unit = benchMatch[3] + + // Normalize to nanoseconds + const multipliers = { ns: 1, ยตs: 1e3, ms: 1e6, s: 1e9 } + const ns = value * (multipliers[unit] || 1) + + currentGroup.entries.push({ + name: benchName, + avg: value, + unit, + avgNs: ns, + }) + } + } + } + + summary.benchmarks[name] = groups +} + +writeFileSync(outFile, JSON.stringify(summary, null, 2)) +console.log('โ”€'.repeat(50)) +console.log(`\nโœ“ Results saved to docs/public/benchmarks.json`) + +if (errors.length > 0) { + console.log(`\nโœ— ${errors.length} benchmark(s) failed:`) + errors.forEach((e) => console.log(` - ${e.file}: ${e.message.split('\n')[0]}`)) + process.exit(1) +} + +console.log(`\nโœ“ All ${files.length} benchmarks completed successfully.`)