diff --git a/docs/content/11.compare/9.comark-vs-markdown-to-jsx.md b/docs/content/11.compare/9.comark-vs-markdown-to-jsx.md index 555bc0a42..602f66f0f 100644 --- a/docs/content/11.compare/9.comark-vs-markdown-to-jsx.md +++ b/docs/content/11.compare/9.comark-vs-markdown-to-jsx.md @@ -29,7 +29,7 @@ links: | Named slots | Yes, `#footer` maps to a slot or prop | Nested content arrives as `children` | | Incomplete Markdown | `autoClose` completes it on every parse (on by default) | `optimizeForStreaming` hides it until it closes (off by default) | | Outputs | Vue, React, Svelte, Angular, HTML, ANSI, Markdown | React, React Native, Solid, Vue, HTML, Markdown | -| Dependencies | markdown-exit, js-yaml, entities | None | +| Dependencies | markdown-exit, tinyaml, entities | None | | Raw HTML default | Parsed into nodes; add the [security plugin](/plugins/built-in/security) with `blockedTags` for untrusted input | `tagfilter` escapes dangerous tags; raw HTML is always sanitized | | Extensions | Comark plugins and [markdown-it plugins](/plugins/custom/markdown-it) | `overrides`, `renderRule`, `createElement`; no plugin system | | Parsed output | Serializable [`MarkdownDocument`](/getting-started/document-model) | AST from `parser()`, rendered with `astToJSX()` or `astToHTML()` | diff --git a/docs/content/5.reference/0.render.md b/docs/content/5.reference/0.render.md index bf0f0a3c3..3738df502 100644 --- a/docs/content/5.reference/0.render.md +++ b/docs/content/5.reference/0.render.md @@ -73,7 +73,7 @@ For the complete parsed document and node types, see the [Document Model](/getti | --- | --- | --- | --- | | [`maxInlineAttributes`](#options-maxinlineattributes) | `number` | `3` | Maximum attributes before switching to YAML block syntax. Set to `0` to always use block syntax | | `blockAttributesStyle` | `'frontmatter' \| 'codeblock'` | `'codeblock'` | Syntax style for block attributes when they exceed `maxInlineAttributes` | -| [`frontmatterOptions`](#options-frontmatteroptions) | `DumpOptions` | `{ indent: 2, lineWidth: -1 }` | js-yaml [DumpOptions](https://github.com/nodeca/js-yaml#dump-object---options-) passed to the frontmatter serializer | +| [`frontmatterOptions`](#options-frontmatteroptions) | `StringifyOptions` | `{ indent: 2 }` | tinyaml [stringify options](https://tiny.onmax.me/tinyaml/guide/api) passed to the frontmatter serializer | | `headingIds` | `boolean \| 'nested' \| 'flat'` | `true` | The [`headingIds`](/reference/parse#options) mode the document was parsed with. A heading id equal to the auto-generated one is omitted; any other id is written as `{#id}` | | [`components`](#options-components) | `Record` | `{}` | Custom render handlers for specific elements | | [`data`](#options-data) | `Record` | `{}` | Additional data passed to render handlers | @@ -133,11 +133,11 @@ const md = await renderMarkdown(document, { ### `frontmatterOptions` -By default frontmatter strings are never line-wrapped (`lineWidth: -1`). Pass any [js-yaml DumpOptions](https://github.com/nodeca/js-yaml#dump-object---options-) via `frontmatterOptions` to override this or control other serialization behaviour: +Frontmatter is written with [tinyaml](https://tiny.onmax.me/tinyaml/guide/getting-started), and long strings are never line-wrapped. Pass its [stringify options](https://tiny.onmax.me/tinyaml/guide/api) (`indent`, `sortKeys`, `refs`) via `frontmatterOptions`: ```typescript const md = await renderMarkdown(document, { - frontmatterOptions: { lineWidth: 80, sortKeys: true }, + frontmatterOptions: { indent: 4, sortKeys: true }, }) ``` diff --git a/docs/content/5.reference/3.reference.md b/docs/content/5.reference/3.reference.md index c3a21dba7..d86894055 100644 --- a/docs/content/5.reference/3.reference.md +++ b/docs/content/5.reference/3.reference.md @@ -275,7 +275,7 @@ interface MarkdownDocument { interface RenderMarkdownOptions { maxInlineAttributes?: number // Max inline attributes before switching to YAML block (default: 3) blockAttributesStyle?: 'frontmatter' | 'codeblock' // Block attribute syntax style (default: 'codeblock') - frontmatterOptions?: DumpOptions // js-yaml options for frontmatter serialization + frontmatterOptions?: StringifyOptions // tinyaml options for frontmatter serialization components?: Record // Custom render handlers for specific elements data?: Record // Additional data passed to render handlers } diff --git a/packages/comark/package.json b/packages/comark/package.json index 70e184f51..e21852201 100644 --- a/packages/comark/package.json +++ b/packages/comark/package.json @@ -70,8 +70,8 @@ }, "dependencies": { "entities": "catalog:", - "js-yaml": "catalog:", - "markdown-exit": "catalog:" + "markdown-exit": "catalog:", + "tinyaml": "catalog:" }, "devDependencies": { "@json-render/core": "catalog:", diff --git a/packages/comark/src/internal/frontmatter.ts b/packages/comark/src/internal/frontmatter.ts index 64d8a9195..f4991ee64 100644 --- a/packages/comark/src/internal/frontmatter.ts +++ b/packages/comark/src/internal/frontmatter.ts @@ -1,4 +1,4 @@ -import type { DumpOptions } from 'js-yaml' +import type { StringifyOptions } from 'tinyaml' import { parseYaml, stringifyYaml } from './yaml.ts' const FRONTMATTER_DELIMITER_DEFAULT = '---' @@ -41,7 +41,7 @@ export function parseFrontmatter(content: string) { export function renderFrontmatter( data: Record | undefined | null, content?: string, - yamlOptions?: DumpOptions + yamlOptions?: StringifyOptions ): string { if (!data || Object.keys(data).length === 0) { return content?.trim() || '' diff --git a/packages/comark/src/internal/stringify/attributes.ts b/packages/comark/src/internal/stringify/attributes.ts index 75881c533..383aa501d 100644 --- a/packages/comark/src/internal/stringify/attributes.ts +++ b/packages/comark/src/internal/stringify/attributes.ts @@ -114,7 +114,7 @@ export function comarkAttributes(attributes: Record) { } // Coerce string `'true'`/`'false'` (how inline `{attr}` parsing stores -// boolean-like attrs) to native booleans so js-yaml v5 emits them unquoted. +// boolean-like attrs) to native booleans so tinyaml writes them unquoted. function normalizeValue(value: unknown): unknown { if (value === 'true') return true if (value === 'false') return false diff --git a/packages/comark/src/internal/yaml.ts b/packages/comark/src/internal/yaml.ts index 95ad040dc..868bf3219 100644 --- a/packages/comark/src/internal/yaml.ts +++ b/packages/comark/src/internal/yaml.ts @@ -1,4 +1,4 @@ -import { dump, JSON_SCHEMA, loadAll, YAMLException, type DumpOptions } from 'js-yaml' +import { parse, stringify, type StringifyOptions } from 'tinyaml' /** * Parse YAML content. @@ -7,12 +7,7 @@ import { dump, JSON_SCHEMA, loadAll, YAMLException, type DumpOptions } from 'js- * @returns The parsed data, or `undefined` when the content has no YAML document */ export function parseYaml(content: string): Record | undefined { - const documents = loadAll(content, { schema: JSON_SCHEMA }) as Record[] - // Preserve `load()`'s single-document guard rather than silently taking the first. - if (documents.length > 1) { - throw new YAMLException('expected a single document in the stream, but found more') - } - return documents[0] + return parse(content) as Record | undefined } /** @@ -20,37 +15,6 @@ export function parseYaml(content: string): Record | undefined * @param data - The data to stringify * @returns The stringified data */ -export function stringifyYaml(data: Record, options?: DumpOptions): string { - const yamlOutput = dump(data, { - indent: 2, - lineWidth: -1, - ...options, - }) - - /** - * js-yaml wraps keys with quotes if they start with a colon. This function removes the quotes. - * `':test': true` becomes `:test: true` - * - * Using js-yaml and this function is faster than using other libraries like yaml. - */ - const lines = yamlOutput.split('\n') - for (let i = 0; i < lines.length; i++) { - const line = lines[i] - const trimmed = line.trimStart() - - // Check if line starts with a quote followed by colon - if (trimmed[0] === "'" || trimmed[0] === '"') { - const quote = trimmed[0] - if (trimmed[1] === ':') { - // Find the closing quote - const quoteEnd = trimmed.indexOf(quote, 1) - if (quoteEnd > 1 && trimmed[quoteEnd + 1] === ':') { - // Remove quotes: keep indentation + unquoted key + rest - const indent = line.length - trimmed.length - lines[i] = ' '.repeat(indent) + trimmed.slice(1, quoteEnd) + trimmed.slice(quoteEnd + 1) - } - } - } - } - return lines.join('\n') +export function stringifyYaml(data: Record, options?: StringifyOptions): string { + return stringify(data, options) } diff --git a/packages/comark/src/types.ts b/packages/comark/src/types.ts index 36c51e3ae..f3421afb5 100644 --- a/packages/comark/src/types.ts +++ b/packages/comark/src/types.ts @@ -1,4 +1,4 @@ -import type { DumpOptions } from 'js-yaml' +import type { StringifyOptions } from 'tinyaml' import type MarkdownExit from 'markdown-exit' import type { Token } from 'markdown-exit' import type MarkdownIt from 'markdown-it' @@ -261,10 +261,10 @@ export interface RenderMarkdownOptions extends RendererOptions { */ blockAttributesStyle?: 'frontmatter' | 'codeblock' /** - * Options for YAML serialization of frontmatter (js-yaml DumpOptions). - * Defaults: indent=2, lineWidth=-1. + * Options for YAML serialization of frontmatter (tinyaml StringifyOptions). + * Defaults: indent=2. Long strings are never folded. */ - frontmatterOptions?: DumpOptions + frontmatterOptions?: StringifyOptions /** * The `headingIds` mode the document was parsed with. Heading ids equal to * the auto-generated one are implicit and omitted; any other id is written diff --git a/packages/comark/test/frontmatter.test.ts b/packages/comark/test/frontmatter.test.ts index 4e08baf07..252d3a824 100644 --- a/packages/comark/test/frontmatter.test.ts +++ b/packages/comark/test/frontmatter.test.ts @@ -310,26 +310,6 @@ describe('renderFrontmatter', () => { }) describe('renderFrontmatter with frontmatterOptions', () => { - it('should pass lineWidth to js-yaml', () => { - const data = { - description: 'This is a very long description that should be wrapped when lineWidth is set to a small value', - } - const result = renderFrontmatter(data, 'Content', { lineWidth: 40 }) - expect(result).toContain('---') - // With lineWidth: 40, js-yaml should wrap the long string - const lines = result.split('\n') - const descLines = lines.filter((l) => l.includes('description') || (l.startsWith(' ') && !l.startsWith(' -'))) - expect(descLines.length).toBeGreaterThan(1) - - expect(result).toEqual(`---\ndescription: >- - This is a very long description that - should be wrapped when lineWidth is set - to a small value ---- - -Content`) - }) - it('should sort keys when sortKeys is true', () => { const data = { zebra: 'last', alpha: 'first', middle: 'mid' } const result = renderFrontmatter(data, 'Content', { sortKeys: true }) diff --git a/packages/comark/test/render-html-bundle.test.ts b/packages/comark/test/render-html-bundle.test.ts index 5f2b66ff9..b2f6d0407 100644 --- a/packages/comark/test/render-html-bundle.test.ts +++ b/packages/comark/test/render-html-bundle.test.ts @@ -30,7 +30,7 @@ it('bundles the HTML render export without Markdown serializers or YAML', async const retained = Object.entries(chunk.modules) .filter(([, module]) => module.renderedLength > 0) .map(([id]) => id) - expect(retained.filter((id) => /js-yaml|internal\/stringify\/handlers\/(?!html\.|comment\.)/.test(id))).toEqual([]) + expect(retained.filter((id) => /tinyaml|internal\/stringify\/handlers\/(?!html\.|comment\.)/.test(id))).toEqual([]) const { renderHtmlFromDocument } = await import( `data:text/javascript;base64,${Buffer.from(chunk.code).toString('base64')}` ) @@ -50,7 +50,7 @@ it('imports the HTML render entry without loading YAML or Markdown handlers in N import assert from 'node:assert/strict' registerHooks({ load(url, context, next) { - if (url.includes('js-yaml') || + if (url.includes('tinyaml') || (url.includes('/internal/stringify/handlers/') && !/\\/(html|comment)\\.(js|ts)$/.test(url))) { throw new Error('Unexpected renderer dependency: ' + url) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aabefcc42..9768d2ed7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -326,9 +326,6 @@ catalogs: hast-util-to-string: specifier: ^3.0.1 version: 3.0.1 - js-yaml: - specifier: ^5.5.0 - version: 5.5.0 katex: specifier: ^0.19.1 version: 0.19.1 @@ -407,6 +404,9 @@ catalogs: tailwindcss: specifier: ^4.3.3 version: 4.3.3 + tinyaml: + specifier: ^0.1.0 + version: 0.1.0 tinybench: specifier: ^6.2.2 version: 6.2.2 @@ -1422,9 +1422,6 @@ importers: entities: specifier: 'catalog:' version: 8.1.0 - js-yaml: - specifier: 'catalog:' - version: 5.5.0 katex: specifier: 'catalog:' version: 0.19.1 @@ -1434,6 +1431,9 @@ importers: shiki: specifier: 'catalog:' version: 4.5.0 + tinyaml: + specifier: 'catalog:' + version: 0.1.0 devDependencies: '@json-render/core': specifier: 'catalog:' @@ -9986,6 +9986,22 @@ packages: tiny-invariant@1.3.3: resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + tinyaml@0.1.0: + resolution: {integrity: sha512-rSx4dnwW2KndEc8V7hKNo4UE6o2RieOmSkYU+9aWd5/08Jv9jrAUitX95xrFQ4bGd9G2VawJ65jRCJ9fuy8ViA==} + engines: {node: '>=22.12.0'} + hasBin: true + peerDependencies: + eslint: '>=9.15.0' + vite-doctor: '>=0.1.1' + yaml-eslint-parser: '>=2.0.0' + peerDependenciesMeta: + eslint: + optional: true + vite-doctor: + optional: true + yaml-eslint-parser: + optional: true + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -20289,6 +20305,8 @@ snapshots: tiny-invariant@1.3.3: {} + tinyaml@0.1.0: {} + tinybench@2.9.0: {} tinybench@6.2.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 51413bf85..6ad28c7ad 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -93,7 +93,6 @@ catalog: entities: ^8.1.0 github-slugger: ^2.0.0 hast-util-to-string: ^3.0.1 - js-yaml: ^5.5.0 katex: ^0.19.1 markdown-exit: 1.3.0 "markdown-it": ^15.0.3 @@ -123,6 +122,7 @@ catalog: remark-parse: ^11.0.0 remark-rehype: ^11.1.2 scule: ^1.3.0 + tinyaml: ^0.1.0 # Shared peer dependencies rangi: ^2.2.0 @@ -184,6 +184,7 @@ minimumReleaseAgeExclude: - comark-content@0.2.0 || 0.4.1 - rangi@2.2.0 - nuxt-agent-discovery@0.7.0 + - tinyaml@0.1.0 patchedDependencies: minimark: patches/minimark.patch diff --git a/test/bundle.test.ts b/test/bundle.test.ts index be4903440..1b80bcd49 100644 --- a/test/bundle.test.ts +++ b/test/bundle.test.ts @@ -67,7 +67,7 @@ describe('package bundle size', { timeout: 60_000 }, () => { "@comark/react": "38.6k (76 files)", "@comark/svelte": "46.8k (84 files)", "@comark/vue": "56.4k (80 files)", - "comark": "398k (158 files)", + "comark": "397k (158 files)", } `) })