From 77cf6a9212de6aa6fe1fddfd64e3563471f1f0a8 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 18 Aug 2026 10:45:46 +0000 Subject: [PATCH 1/3] perf: Optimize parser safety, rendering consistency, and regression coverage --- .github/workflows/ci.yml | 55 ++++++++- .gitignore | 3 +- Dockerfile | 3 + package.json | 2 +- packages/markdown-parser-lit/src/render.ts | 58 ++++++--- packages/markdown-parser-vue/src/render.ts | 115 ++++++++++++++++-- packages/markdown-parser/package.json | 20 ++- packages/markdown-parser/src/core/color.ts | 9 ++ packages/markdown-parser/src/core/parser.ts | 81 +++++++++--- packages/markdown-parser/src/core/render.ts | 73 +++++++++-- packages/markdown-parser/src/core/state.ts | 28 ++++- packages/markdown-parser/src/core/url.ts | 21 ++++ packages/markdown-parser/src/default.ts | 11 +- packages/markdown-parser/src/index.spec.ts | 19 +++ packages/markdown-parser/src/index.ts | 3 + packages/markdown-parser/src/rules/blocks.ts | 42 +++++-- packages/markdown-parser/src/rules/ffm.ts | 20 +-- packages/markdown-parser/src/rules/inlines.ts | 38 +++--- .../src/test/specs/fuyeorMark.json | 48 ++++++++ packages/markdown-parser/src/types.ts | 26 +++- packages/markdown-parser/tsconfig.build.json | 12 ++ packages/markdown-parser/tsconfig.json | 3 +- packages/markdown-parser/tsconfig.test.json | 9 ++ packages/markdown-parser/vite.config.ts | 19 +++ pnpm-lock.yaml | 3 + 25 files changed, 601 insertions(+), 120 deletions(-) create mode 100644 packages/markdown-parser/src/core/color.ts create mode 100644 packages/markdown-parser/src/core/url.ts create mode 100644 packages/markdown-parser/tsconfig.build.json create mode 100644 packages/markdown-parser/tsconfig.test.json create mode 100644 packages/markdown-parser/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a72732..d777434 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,13 +3,27 @@ name: Build and Push Playground on: push: - branches: [ "main" ] - # Only trigger when relevant code changes to save resources + branches: ['main'] + # Only trigger when relevant code changes to save resources paths: - - "packages/markdown-parser/src" - - "packages/markdown-parser-lit/src" - - "packages/playground/**" - - "Dockerfile" + - 'packages/markdown-parser/**' + - 'packages/markdown-parser-lit/**' + - 'packages/markdown-parser-vue/**' + - 'packages/playground/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'Dockerfile' + - '.github/workflows/ci.yml' + pull_request: + paths: + - 'packages/markdown-parser/**' + - 'packages/markdown-parser-lit/**' + - 'packages/markdown-parser-vue/**' + - 'packages/playground/**' + - 'package.json' + - 'pnpm-lock.yaml' + - 'Dockerfile' + - '.github/workflows/ci.yml' workflow_dispatch: # Allow manual trigger @@ -18,7 +32,36 @@ env: IMAGE_NAME: fuyeor/flavored-front-end jobs: + test-markdown-parser: + name: Test markdown parser + runs-on: ubuntu-latest + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - name: Enable pnpm + run: corepack enable + - name: Install dependencies + run: pnpm install --frozen-lockfile --ignore-scripts + - name: Typecheck parser package + run: pnpm --filter @fuyeor/markdown-parser typecheck + - name: Run parser unit tests + run: pnpm --filter @fuyeor/markdown-parser test:unit + - name: Run FFM regression tests + run: pnpm --filter @fuyeor/markdown-parser test:ffm + - name: Build parser package + run: pnpm --filter @fuyeor/markdown-parser build + build-markdown-playground: + needs: test-markdown-parser + if: github.event_name != 'pull_request' runs-on: ubuntu-latest permissions: contents: read diff --git a/.gitignore b/.gitignore index 712444c..0fcfe27 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ **/node_modules -**/tests.failed.json \ No newline at end of file +**/dist +**/tests.failed.json diff --git a/Dockerfile b/Dockerfile index d249216..d9352d3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,6 +23,9 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile # copy source code COPY . . +# build parser package before workspace consumers resolve its package exports +RUN pnpm --filter @fuyeor/markdown-parser build + # build playground RUN pnpm --filter @fuyeor/markdown-parser-playground build diff --git a/package.json b/package.json index ce05cda..25003a0 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ "scripts": { "format": "prettier --write \"**/*.ts\"", "test": "pnpm -F @fuyeor/markdown-parser test", - "playground": "pnpm -F @fuyeor/markdown-parser-playground dev" + "playground": "pnpm -F @fuyeor/markdown-parser build && pnpm -F @fuyeor/markdown-parser-playground dev" }, "devDependencies": { "@types/node": "^25.6.0", diff --git a/packages/markdown-parser-lit/src/render.ts b/packages/markdown-parser-lit/src/render.ts index 7c817c3..f055ad7 100644 --- a/packages/markdown-parser-lit/src/render.ts +++ b/packages/markdown-parser-lit/src/render.ts @@ -1,7 +1,11 @@ // @fuyeor/markdown-parser-lit/src/render.ts import { html, type TemplateResult } from 'lit'; import { html as staticHtml, unsafeStatic } from 'lit/static-html.js'; -import type { ASTNode } from '@fuyeor/markdown-parser'; +import { + isSafeColorValue, + isSafeLinkUrl, + type ASTNode, +} from '@fuyeor/markdown-parser'; /** * recursively render AST nodes into Lit TemplateResult @@ -12,7 +16,16 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { return nodes.map((node) => { switch (node.type) { case 'heading': { - const tagName = `h${node.level}`; + const level = + typeof node.level === 'number' && + Number.isInteger(node.level) && + node.level >= 1 && + node.level <= 6 + ? node.level + : null; + if (level === null) return html`${render(node.children)}`; + + const tagName = `h${level}`; return staticHtml` <${unsafeStatic(tagName)}> ${render(node.children)} @@ -33,36 +46,43 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { return html`${render(node.children)}`; case 'underline': - return html`${render(node.children)}`; + return html`${render(node.children)}`; case 'strike': return html`${render(node.children)}`; - case 'link': - return html`${render(node.children)}`; + case 'link': { + const url = String(node.url ?? '').trim(); + return isSafeLinkUrl(url) + ? html`${render(node.children)}` + : html`${render(node.children)}`; + } case 'inline_code': return html`${node.content}`; - case 'color_code': + case 'color_code': { + const color = String(node.content ?? ''); + if (!isSafeColorValue(color)) return html`${color}`; return html` ${node.content} + >${color} `; + } case 'code_block': return html` @@ -105,17 +125,17 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { - ${node.headers.map( - (cell: any) => html``, + ${(node.headers ?? []).map( + (cell) => html``, )} ${node.children?.map( - (row: any) => html` + (row) => html` - ${row.children.map( - (cell: any) => html``, + ${(row.children ?? []).map( + (cell) => html``, )} `, diff --git a/packages/markdown-parser-vue/src/render.ts b/packages/markdown-parser-vue/src/render.ts index 00d866d..c0c8180 100644 --- a/packages/markdown-parser-vue/src/render.ts +++ b/packages/markdown-parser-vue/src/render.ts @@ -1,12 +1,26 @@ -// @fuyeor/markdown-parser-vue/src/renderer.ts +// @fuyeor/markdown-parser-vue/src/render.ts import { h, type VNode } from 'vue'; -import type { ASTNode } from '@fuyeor/markdown-parser'; +import { + isSafeColorValue, + isSafeLinkUrl, + type ASTNode, +} from '@fuyeor/markdown-parser'; export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { return nodes.map((node) => { switch (node.type) { - case 'heading': - return h(`h${node.level}`, renderToVue(node.children || [])); + case 'heading': { + const level = + typeof node.level === 'number' && + Number.isInteger(node.level) && + node.level >= 1 && + node.level <= 6 + ? node.level + : null; + return level === null + ? h('span', renderToVue(node.children || [])) + : h(`h${level}`, renderToVue(node.children || [])); + } case 'paragraph': return h('p', renderToVue(node.children || [])); case 'text': @@ -21,12 +35,36 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { return h('del', renderToVue(node.children || [])); case 'inline_code': return h('code', node.content || ''); - case 'link': - return h( - 'a', - { href: node.url, target: '_blank', rel: 'noopener noreferrer' }, - renderToVue(node.children || []), - ); + case 'color_code': { + const color = String(node.content ?? ''); + if (!isSafeColorValue(color)) return color; + return h('code', { class: 'ffm-color-code' }, [ + h('span', { + class: 'ffm-color-swatch', + style: { + display: 'inline-block', + width: '0.8em', + height: '0.8em', + borderRadius: '50%', + backgroundColor: color, + verticalAlign: 'middle', + marginRight: '0.3em', + border: '1px solid #00000030', + }, + }), + color, + ]); + } + case 'link': { + const url = String(node.url ?? '').trim(); + return isSafeLinkUrl(url) + ? h( + 'a', + { href: url, target: '_blank', rel: 'noopener noreferrer' }, + renderToVue(node.children || []), + ) + : h('span', renderToVue(node.children || [])); + } case 'code_block': return h('div', { class: 'code-block-wrapper' }, [ node.lang ? h('div', { class: 'code-lang' }, node.lang) : null, @@ -58,7 +96,7 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { h( 'tr', null, - node.headers.map((cell: any) => + (node.headers ?? []).map((cell) => h('th', renderToVue(cell.children || [])), ), ), @@ -67,11 +105,11 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { ? h( 'tbody', null, - node.children.map((row: any) => + node.children.map((row) => h( 'tr', null, - row.children.map((cell: any) => + (row.children ?? []).map((cell) => h('td', renderToVue(cell.children || [])), ), ), @@ -85,6 +123,57 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { return h('blockquote', renderToVue(node.children || [])); case 'hardbreak': return h('br'); + case 'accordion': + return h( + 'div', + { class: 'ffm-accordion' }, + renderToVue(node.children || []), + ); + case 'accordion_item': + return h('details', { name: node.name }, [ + h('summary', renderToVue(node.title || [])), + h( + 'div', + { class: 'accordion-content' }, + renderToVue(node.children || []), + ), + ]); + case 'chain': + return h( + 'div', + { class: 'chain-container' }, + renderToVue(node.children || []), + ); + case 'chain_item': { + const statusClass = node.hasCheckbox + ? node.isCompleted + ? 'is-completed' + : 'is-pending' + : ''; + return h('div', { class: ['chain-item', statusClass] }, [ + h('div', { class: 'chain-marker' }), + h('div', { class: 'chain-content-wrapper' }, [ + node.title && node.title.length > 0 + ? h('div', { class: 'chain-title' }, renderToVue(node.title)) + : null, + h('div', { class: 'chain-body' }, renderToVue(node.children || [])), + ]), + ]); + } + case 'slide': + return h('div', { class: 'slide-container-wrapper' }, [ + h( + 'div', + { class: 'slide-container' }, + renderToVue(node.children || []), + ), + ]); + case 'slide_item': + return h( + 'div', + { class: 'slide-item' }, + renderToVue(node.children || []), + ); default: return h('span', renderToVue(node.children || [])); } diff --git a/packages/markdown-parser/package.json b/packages/markdown-parser/package.json index e75f500..cfbb208 100644 --- a/packages/markdown-parser/package.json +++ b/packages/markdown-parser/package.json @@ -3,18 +3,34 @@ "license": "MIT", "author": "Fuyeor ", "type": "module", - "main": "./src/index.ts", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "import": "./dist/index.js" + } + }, + "files": [ + "dist" + ], "sideEffects": false, "imports": { "#/*": "./src/*" }, "scripts": { "test": "vitest run", - "build": "vite build" + "test:unit": "vitest run src/index.spec.ts", + "test:ffm": "vitest run src/test/index.spec.ts -t \"Fuyeor Mark\"", + "test:compat": "vitest run src/test/index.spec.ts", + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", + "build": "vite build && tsc -p tsconfig.build.json" }, "devDependencies": { "@types/node": "^25.6.0", "typescript": "^6.0.3", + "vite": "^8.0.1", "vitest": "^4.1.0" } } diff --git a/packages/markdown-parser/src/core/color.ts b/packages/markdown-parser/src/core/color.ts new file mode 100644 index 0000000..0389697 --- /dev/null +++ b/packages/markdown-parser/src/core/color.ts @@ -0,0 +1,9 @@ +// @fuyeor/markdown-parser/src/core/color.ts + +const COLOR_VALUE_REGEX = + /^(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgb|hsl)a?\([\d\s,%.]+\))$/; + +// Keep color_code values within the supported CSS color grammar. +export function isSafeColorValue(value: string): boolean { + return COLOR_VALUE_REGEX.test(value); +} diff --git a/packages/markdown-parser/src/core/parser.ts b/packages/markdown-parser/src/core/parser.ts index d53ae2f..b4b0b61 100644 --- a/packages/markdown-parser/src/core/parser.ts +++ b/packages/markdown-parser/src/core/parser.ts @@ -5,8 +5,10 @@ import type { BlockRule, InlineRule, MarkdownPlugin, + MarkdownParserOptions, ParserContext, } from '#/types'; +import { isSafeLinkUrl } from '#/core/url'; const LINKIFY_REGEX = /(https?:\/\/[^\s]+|(?(); #inlineRuleMap = new Map(); + #idSequence = 0; + #isPreflight = false; + readonly #maxNestingDepth: number; + + constructor(options: MarkdownParserOptions = {}) { + const maxNestingDepth = options.maxNestingDepth ?? 64; + if (!Number.isInteger(maxNestingDepth) || maxNestingDepth < 1) + throw new RangeError('maxNestingDepth must be a positive integer'); + + this.#maxNestingDepth = maxNestingDepth; + } // construct a context for recursive rule calls - readonly #context: ParserContext = { - parseInline: (content: string) => - this.#parseInline(new InlineState(content)), - parseBlocks: (content: string) => - this.#parseBlocks(new BlockState(content)), - }; + readonly #context: ParserContext = this.#createContext(0); + + #createContext(depth: number): ParserContext { + return { + parseInline: (content: string) => + this.#parseInline(new InlineState(content), depth + 1), + parseBlocks: (content: string) => + this.#parseBlocks(new BlockState(content), depth + 1), + createId: (prefix: string) => + this.#isPreflight + ? `${prefix}-probe` + : `${prefix}-${this.#idSequence++}`, + }; + } // register block rule // By default, it inserts at the end, or before/after a specified rule. @@ -54,12 +75,28 @@ export class MarkdownParser { build(): (content: string) => ASTNode[] { return (content: string) => { + this.#idSequence = 0; const state = new BlockState(content); return this.#parseBlocks(state); }; } - #parseBlocks(state: BlockState): ASTNode[] { + #parseBlocks(state: BlockState, depth = 0): ASTNode[] { + if (depth > this.#maxNestingDepth) { + return [ + { + type: 'paragraph', + children: [ + { + type: 'text', + content: state.remainingLines.join('\n'), + }, + ], + }, + ]; + } + + const context = depth === 0 ? this.#context : this.#createContext(depth); const nodes: ASTNode[] = []; while (state.lineIndex < state.lineCount) { const line = state.currentLine; @@ -81,7 +118,7 @@ export class MarkdownParser { if (rules) { for (const rule of rules) { - const result = rule.parse(state, this.#context); + const result = rule.parse(state, context); if (result) { nodes.push(result.node); state.advance(result.consumedLines); @@ -139,9 +176,14 @@ export class MarkdownParser { 'ffm_blocks', ].includes(rule.name) ) { - if (rule.parse(state, this.#context) !== null) { - isInterrupted = true; - break; + this.#isPreflight = true; + try { + if (rule.parse(state, context) !== null) { + isInterrupted = true; + break; + } + } finally { + this.#isPreflight = false; } } } @@ -159,6 +201,7 @@ export class MarkdownParser { type: 'paragraph', children: this.#parseInline( new InlineState(paragraphLines.join('\n')), + depth, ), }); } @@ -167,7 +210,12 @@ export class MarkdownParser { return nodes; } - #parseInline(state: InlineState): ASTNode[] { + #parseInline(state: InlineState, depth = 0): ASTNode[] { + if (depth > this.#maxNestingDepth) { + return [{ type: 'text', content: state.content.slice(state.pos) }]; + } + + const context = depth === 0 ? this.#context : this.#createContext(depth); const nodes: ASTNode[] = []; // record the start position of plain text @@ -204,12 +252,7 @@ export class MarkdownParser { ? urlStr : `https://${urlStr}`; - const isValid = - fullUrl.startsWith('http://') || - fullUrl.startsWith('https://') || - globalThis.URL.canParse(fullUrl); - - if (isValid) { + if (isSafeLinkUrl(fullUrl)) { if (matchIdx > lastIdx) { nodes.push({ type: 'text', @@ -242,7 +285,7 @@ export class MarkdownParser { // only check rules that are registered for this marker character for (const rule of rules) { - const result = rule.parse(state, this.#context); + const result = rule.parse(state, context); if (result) { flushText(state.pos); nodes.push(result.node); diff --git a/packages/markdown-parser/src/core/render.ts b/packages/markdown-parser/src/core/render.ts index fae2394..b0f82f9 100644 --- a/packages/markdown-parser/src/core/render.ts +++ b/packages/markdown-parser/src/core/render.ts @@ -1,5 +1,7 @@ // @fuyeor/markdown-parser/src/core/render.ts import type { ASTNode } from '#/types'; +import { isSafeColorValue } from '#/core/color'; +import { isSafeLinkUrl } from '#/core/url'; const escapeHtml = (str: string) => str.replace( @@ -14,6 +16,15 @@ const escapeHtml = (str: string) => })[m]!, ); +// Keep dynamic HTML tag names and CSS values within their supported grammar. +const getHeadingLevel = (level: unknown) => + typeof level === 'number' && + Number.isInteger(level) && + level >= 1 && + level <= 6 + ? level + : null; + export function render(nodes?: ASTNode[]): string { let html = ''; @@ -21,9 +32,14 @@ export function render(nodes?: ASTNode[]): string { for (const node of nodes) { switch (node.type) { - case 'heading': - html += `${render(node.children)}\n`; + case 'heading': { + const level = getHeadingLevel(node.level); + html += + level === null + ? `${render(node.children)}` + : `${render(node.children)}\n`; break; + } case 'paragraph': html += `

${render(node.children)}

\n`; break; @@ -37,7 +53,7 @@ export function render(nodes?: ASTNode[]): string { html += `${render(node.children)}`; break; case 'underline': - html += `${render(node.children)}`; + html += `${render(node.children)}`; break; case 'strike': html += `${render(node.children)}`; @@ -45,9 +61,22 @@ export function render(nodes?: ASTNode[]): string { case 'inline_code': html += `${node.content ? escapeHtml(node.content) : ''}`; break; - case 'link': - html += `${render(node.children)}`; + case 'color_code': { + const color = String(node.content ?? ''); + if (!isSafeColorValue(color)) { + html += escapeHtml(color); + break; + } + html += `${escapeHtml(color)}`; break; + } + case 'link': { + const url = String(node.url ?? '').trim(); + html += isSafeLinkUrl(url) + ? `${render(node.children)}` + : render(node.children); + break; + } case 'code_block': html += `
`; if (node.lang) { @@ -69,15 +98,15 @@ export function render(nodes?: ASTNode[]): string { break; case 'table': html += '
${render(cell.children)}${render(cell.children)}
${render(cell.children)}${render(cell.children)}
\n\n\n'; - node.headers.forEach((cell: any) => { + (node.headers ?? []).forEach((cell) => { html += `\n`; }); html += '\n\n'; if (node.children && node.children.length > 0) { html += '\n'; - node.children.forEach((row: any) => { + node.children.forEach((row) => { html += '\n'; - row.children.forEach((cell: any) => { + row.children?.forEach((cell) => { html += `\n`; }); html += '\n'; @@ -92,6 +121,34 @@ export function render(nodes?: ASTNode[]): string { case 'blockquote': html += `
\n${render(node.children)}
\n`; break; + case 'accordion': + html += `
${render(node.children)}
`; + break; + case 'accordion_item': + html += `
${render(node.title)}
${render(node.children)}
`; + break; + case 'chain': + html += `
${render(node.children)}
`; + break; + case 'chain_item': { + const statusClass = node.hasCheckbox + ? node.isCompleted + ? 'is-completed' + : 'is-pending' + : ''; + const title = + node.title && node.title.length > 0 + ? `
${render(node.title)}
` + : ''; + html += `
${title}
${render(node.children)}
`; + break; + } + case 'slide': + html += `
${render(node.children)}
`; + break; + case 'slide_item': + html += `
${render(node.children)}
`; + break; case 'hardbreak': html += '
\n'; break; diff --git a/packages/markdown-parser/src/core/state.ts b/packages/markdown-parser/src/core/state.ts index 312ba7b..e942df0 100644 --- a/packages/markdown-parser/src/core/state.ts +++ b/packages/markdown-parser/src/core/state.ts @@ -28,9 +28,10 @@ export class BlockState { } export class InlineState { - content: string; + readonly content: string; pos: number = 0; readonly length: number; + readonly #tokenPositions = new Map(); constructor(content: string) { this.content = content; @@ -42,6 +43,31 @@ export class InlineState { return this.content[this.pos]; } + findNextToken(token: string, from: number): number { + if (!token) throw new RangeError('token must not be empty'); + + let positions = this.#tokenPositions.get(token); + if (!positions) { + positions = []; + let position = this.content.indexOf(token); + while (position !== -1) { + positions.push(position); + position = this.content.indexOf(token, position + 1); + } + this.#tokenPositions.set(token, positions); + } + + let low = 0; + let high = positions.length; + while (low < high) { + const middle = low + ((high - low) >> 1); + if (positions[middle] < from) low = middle + 1; + else high = middle; + } + + return positions[low] ?? -1; + } + advance(count: number = 1): void { this.pos += count; } diff --git a/packages/markdown-parser/src/core/url.ts b/packages/markdown-parser/src/core/url.ts new file mode 100644 index 0000000..133f4ee --- /dev/null +++ b/packages/markdown-parser/src/core/url.ts @@ -0,0 +1,21 @@ +// @fuyeor/markdown-parser/src/core/url.ts + +const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']); + +// Validate URL schemes separately from URL syntax parsing. +export function isSafeLinkUrl(url: string): boolean { + const value = url.trim(); + if (!value) return false; + + if ( + value.startsWith('/') || + value.startsWith('#') || + value.startsWith('./') || + value.startsWith('../') + ) + return true; + + if (!globalThis.URL.canParse(value)) return false; + + return SAFE_PROTOCOLS.has(new globalThis.URL(value).protocol); +} diff --git a/packages/markdown-parser/src/default.ts b/packages/markdown-parser/src/default.ts index 327938e..8c5ac97 100644 --- a/packages/markdown-parser/src/default.ts +++ b/packages/markdown-parser/src/default.ts @@ -1,5 +1,6 @@ // @fuyeor/markdown-parser/src/default.ts import { MarkdownParser } from './core/parser'; +import type { MarkdownParserOptions } from './types'; import { headingRule, tableRule, @@ -19,9 +20,9 @@ import { } from './rules/inlines'; import { ffmBlockRule } from './rules/ffm'; -export function createMarkdownParser() { +export function createMarkdownParser(options: MarkdownParserOptions = {}) { return ( - new MarkdownParser() + new MarkdownParser(options) // block order: Code block -> List -> Title -> Table -> Delete line -> Quote .addBlockRule(codeBlockRule) .addBlockRule(listRule) @@ -43,9 +44,11 @@ export function createMarkdownParser() { ); } -export function createFuyeorMarkdownParser() { +export function createFuyeorMarkdownParser( + options: MarkdownParserOptions = {}, +) { return ( - new MarkdownParser() + new MarkdownParser(options) // block order: Code block -> List -> Title -> Table -> Delete line -> Quote .addBlockRule(ffmBlockRule) .addBlockRule(codeBlockRule) diff --git a/packages/markdown-parser/src/index.spec.ts b/packages/markdown-parser/src/index.spec.ts index cebadff..1f828f2 100644 --- a/packages/markdown-parser/src/index.spec.ts +++ b/packages/markdown-parser/src/index.spec.ts @@ -1,6 +1,7 @@ // @fuyeor/markdown-parser/src/index.spec.ts import { describe, it, expect } from 'vitest'; import { MarkdownParser } from './core/parser'; +import { createFuyeorMarkdownParser } from './default'; import { headingRule, codeBlockRule, tableRule } from './rules/blocks'; import { boldRule, linkRule } from './rules/inlines'; @@ -68,4 +69,22 @@ describe('test @fuyeor/markdown-parser', () => { expect(tableNode.headers).toHaveLength(2); expect(tableNode.children![0].type).toBe('table_row'); }); + + it('rejects unsafe link schemes', () => { + const ast = parse('[click](javascript:alert(1))'); + + expect(ast[0].children?.some((node) => node.type === 'link')).toBe(false); + }); + + it('bounds recursive block parsing', () => { + const boundedParse = createFuyeorMarkdownParser({ maxNestingDepth: 8 }); + + expect(() => boundedParse(`${'>'.repeat(100)} value`)).not.toThrow(); + }); + + it('fails fast for an invalid nesting depth', () => { + expect(() => new MarkdownParser({ maxNestingDepth: 0 })).toThrow( + RangeError, + ); + }); }); diff --git a/packages/markdown-parser/src/index.ts b/packages/markdown-parser/src/index.ts index 14a83c7..2451ef8 100644 --- a/packages/markdown-parser/src/index.ts +++ b/packages/markdown-parser/src/index.ts @@ -1,6 +1,8 @@ // @fuyeor/markdown-parser/src/index.ts export { MarkdownParser } from './core/parser'; export { render } from './core/render'; +export { isSafeColorValue } from './core/color'; +export { isSafeLinkUrl } from './core/url'; export { headingRule, tableRule, @@ -25,5 +27,6 @@ export type { ParserContext, BlockRule, InlineRule, + MarkdownParserOptions, MarkdownPlugin, } from './types'; diff --git a/packages/markdown-parser/src/rules/blocks.ts b/packages/markdown-parser/src/rules/blocks.ts index 1d4d0f2..e1e01aa 100644 --- a/packages/markdown-parser/src/rules/blocks.ts +++ b/packages/markdown-parser/src/rules/blocks.ts @@ -1,7 +1,18 @@ // @fuyeor/markdown-parser/src/rules/blocks.ts -import type { BlockRule } from '#/types'; +import type { ASTNode, BlockRule } from '#/types'; import { BlockState } from '#/core/state'; +type FencedBlock = { + lang: string; + content: string; + consumedLines: number; +}; + +const fencedBlockCache = new WeakMap< + BlockState, + Map +>(); + /** * parse ATX # title syntax */ @@ -46,15 +57,30 @@ export const codeBlockRule: BlockRule = { /** * helper function: extract code block */ -export function extractFencedBlock(state: BlockState) { +export function extractFencedBlock(state: BlockState): FencedBlock | null { + let cache = fencedBlockCache.get(state); + if (!cache) { + cache = new Map(); + fencedBlockCache.set(state, cache); + } + + if (cache.has(state.lineIndex)) return cache.get(state.lineIndex) ?? null; + const line = state.currentLine; - if (!line) return null; + if (!line) { + cache.set(state.lineIndex, null); + return null; + } // matches 0-3 spaces starting with ``` or ~~~, followed by the info string const match = line.match(/^(\s{0,3})(`{3,}|~{3,})([^`]*)$/); - if (!match) return null; + if (!match) { + cache.set(state.lineIndex, null); + return null; + } const indent = match[1].length; + const indentPrefix = ' '.repeat(indent); const fenceMarker = match[2]; const lang = match[3].trim(); @@ -77,14 +103,16 @@ export function extractFencedBlock(state: BlockState) { } // remove indentation - if (nextLine.startsWith(' '.repeat(indent))) { + if (nextLine.startsWith(indentPrefix)) { contentLines.push(nextLine.slice(indent)); } else { contentLines.push(nextLine); } } - return { lang, content: contentLines.join('\n'), consumedLines }; + const result = { lang, content: contentLines.join('\n'), consumedLines }; + cache.set(state.lineIndex, result); + return result; } /** @@ -225,7 +253,7 @@ export const listRule: BlockRule = { const isOrdered = /\d/.test(marker); const startNumber = isOrdered ? parseInt(marker, 10) : undefined; - const items: any[] = []; + const items: ASTNode[] = []; let consumedLines = 0; while (state.lineIndex + consumedLines < state.lineCount) { diff --git a/packages/markdown-parser/src/rules/ffm.ts b/packages/markdown-parser/src/rules/ffm.ts index 01d1beb..b1e8cb9 100644 --- a/packages/markdown-parser/src/rules/ffm.ts +++ b/packages/markdown-parser/src/rules/ffm.ts @@ -55,22 +55,21 @@ export const ffmBlockRule: BlockRule = { // ffm chain and accordion if (type === 'accordion' || type === 'chain') { const items: ASTNode[] = []; - let currentItem: any = null; + let currentItem: ASTNode | null = null; let currentLines: string[] = []; // collect the text before the first title let preambleLines: string[] = []; // generate a unique name for mutually exclusive folding const accordionName = - type === 'accordion' - ? `acc-${Math.random().toString(36).slice(2, 8)}` - : undefined; + type === 'accordion' ? ctx.createId('acc') : undefined; - rawContent.split('\n').forEach((l) => { + for (const l of rawContent.split('\n')) { const titleMatch = l.match(TITLE_REGEX); if (titleMatch) { // archive the previous item, or archive free content - if (!currentItem) { + const previousItem = currentItem; + if (!previousItem) { if ( currentLines.length > 0 && currentLines.join('').trim() !== '' @@ -79,7 +78,7 @@ export const ffmBlockRule: BlockRule = { preambleLines = currentLines; } } else { - currentItem.children = ctx.parseBlocks( + previousItem.children = ctx.parseBlocks( currentLines.join('\n').trim(), ); } @@ -104,11 +103,12 @@ export const ffmBlockRule: BlockRule = { } else { currentLines.push(l); } - }); + } // archive the last item or go back - if (currentItem) { - currentItem.children = ctx.parseBlocks(currentLines.join('\n').trim()); + const lastItem = currentItem; + if (lastItem) { + lastItem.children = ctx.parseBlocks(currentLines.join('\n').trim()); } else { // if no valid title is found from beginning to end // revert to displaying regular content diff --git a/packages/markdown-parser/src/rules/inlines.ts b/packages/markdown-parser/src/rules/inlines.ts index 127ec8d..d5c83fb 100644 --- a/packages/markdown-parser/src/rules/inlines.ts +++ b/packages/markdown-parser/src/rules/inlines.ts @@ -1,6 +1,8 @@ // @fuyeor/markdown-parser/src/rules/inlines.ts import type { InlineRule } from '#/types'; import { InlineState } from '#/core/state'; +import { isSafeColorValue } from '#/core/color'; +import { isSafeLinkUrl } from '#/core/url'; /** * Support backslash at line end as
@@ -30,7 +32,7 @@ export const boldRule: InlineRule = { // find directly for the next **; // if not found, treat it as plain text. - const endIdx = state.content.indexOf('**', state.pos + 2); + const endIdx = state.findNextToken('**', state.pos + 2); if (endIdx === -1) return null; const innerContent = state.content.slice(state.pos + 2, endIdx); @@ -59,7 +61,7 @@ export const italicRule: InlineRule = { // (to prevent it from overriding the bold/underline). if (char === '*' && state.content[state.pos + 1] !== char) { // find the next matching closing symbol - const endIdx = state.content.indexOf(char, state.pos + 1); + const endIdx = state.findNextToken(char, state.pos + 1); if (endIdx === -1 || endIdx === state.pos + 1) return null; @@ -75,10 +77,6 @@ export const italicRule: InlineRule = { }, }; -// GFM color regex: support 3/4/6/8 HEX and rgb/hsl -const COLOR_REGEX = - /^(?:#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})|(?:rgb|hsl)a?\([\d\s,%.]+\))$/; - /** * parse `inlineCode` syntax */ @@ -98,7 +96,7 @@ export const inlineCodeRule: InlineRule = { let endIdx = -1; while (currentPos < state.length) { - const foundIdx = state.content.indexOf(marker, currentPos); + const foundIdx = state.findNextToken(marker, currentPos); if (foundIdx === -1) break; if (state.content[foundIdx + markerLength] === '`') { @@ -129,7 +127,7 @@ export const inlineCodeRule: InlineRule = { } // if it is a color, directly change it to a `color_code` node - const isColor = COLOR_REGEX.test(rawContent); + const isColor = isSafeColorValue(rawContent); return { node: { @@ -151,28 +149,20 @@ export const linkRule: InlineRule = { if (state.currentChar !== '[') return null; // find the nearest `](` - const textEnd = state.content.indexOf('](', state.pos + 1); + const textEnd = state.findNextToken('](', state.pos + 1); if (textEnd === -1) return null; // find the nearest `)` - const urlEnd = state.content.indexOf(')', textEnd + 2); + const urlEnd = state.findNextToken(')', textEnd + 2); if (urlEnd === -1) return null; const innerText = state.content.slice(state.pos + 1, textEnd); let url = state.content.slice(textEnd + 2, urlEnd).trim(); - const isValid = - url.startsWith('http://') || - url.startsWith('https://') || - url.startsWith('/') || - globalThis.URL.canParse(url); - - if (!isValid) { - // try verifying as a relative or completed HTTP response - const testUrl = url.startsWith('www.') ? `http://${url}` : url; - if (globalThis.URL.canParse(testUrl)) url = testUrl; - else return null; - } + // normalize www. links before applying the shared protocol policy + const normalizedUrl = url.startsWith('www.') ? `http://${url}` : url; + if (!isSafeLinkUrl(normalizedUrl)) return null; + url = normalizedUrl; return { node: { @@ -194,7 +184,7 @@ export const underlineRule: InlineRule = { parse(state: InlineState, ctx) { if (!state.content.startsWith('__', state.pos)) return null; - const endIdx = state.content.indexOf('__', state.pos + 2); + const endIdx = state.findNextToken('__', state.pos + 2); if (endIdx === -1) return null; return { @@ -216,7 +206,7 @@ export const strikeRule: InlineRule = { parse(state: InlineState, ctx) { if (!state.content.startsWith('--', state.pos)) return null; - const endIdx = state.content.indexOf('--', state.pos + 2); + const endIdx = state.findNextToken('--', state.pos + 2); if (endIdx === -1) return null; return { diff --git a/packages/markdown-parser/src/test/specs/fuyeorMark.json b/packages/markdown-parser/src/test/specs/fuyeorMark.json index df503c6..832a234 100644 --- a/packages/markdown-parser/src/test/specs/fuyeorMark.json +++ b/packages/markdown-parser/src/test/specs/fuyeorMark.json @@ -10,5 +10,53 @@ "html": "

Fuyeor

", "example": 1, "section": "Underline" + }, + { + "markdown": "`#ff0000`", + "html": "

#ff0000

", + "example": 1, + "section": "Color code" + }, + { + "markdown": "```quote\n## Title\n```", + "html": "
\n

Title

\n
", + "example": 1, + "section": "Quote block" + }, + { + "markdown": "```accordion\n**One**\nContent\n```", + "html": "
One

Content

\n
", + "example": 1, + "section": "Accordion" + }, + { + "markdown": "```chain\n**[x] Done**\nBody\n```", + "html": "
Done

Body

\n
", + "example": 1, + "section": "Chain" + }, + { + "markdown": "```slide\nFirst\n\n---\n\nSecond\n```", + "html": "

First

\n

Second

\n
", + "example": 1, + "section": "Slide" + }, + { + "markdown": "[x](javascript:alert(1))", + "html": "

[x](javascript:alert(1))

", + "example": 1, + "section": "Unsafe link schemes" + }, + { + "markdown": "[x](data:text/html,)", + "html": "

[x](data:text/html,<script>alert(1)</script>)

", + "example": 1, + "section": "Unsafe link schemes" + }, + { + "markdown": "[x](file:///etc/passwd)", + "html": "

[x](file:///etc/passwd)

", + "example": 1, + "section": "Unsafe link schemes" } ] diff --git a/packages/markdown-parser/src/types.ts b/packages/markdown-parser/src/types.ts index 11fda40..26ca104 100644 --- a/packages/markdown-parser/src/types.ts +++ b/packages/markdown-parser/src/types.ts @@ -1,5 +1,8 @@ // @fuyeor/markdown-parser/src/types.ts +import type { MarkdownParser } from './core/parser'; +import type { BlockState, InlineState } from './core/state'; + // built-in type; plugins can extend the string union type export type NodeType = | 'root' @@ -24,14 +27,25 @@ export interface ASTNode { type: NodeType; content?: string; children?: ASTNode[]; + level?: number; + lang?: string; + url?: string; + ordered?: boolean; + start?: number; + headers?: ASTNode[]; + name?: string; + title?: ASTNode[]; + isCompleted?: boolean; + hasCheckbox?: boolean; // allow plugins to attach arbitrary attributes - [key: string]: any; + [key: string]: unknown; } // inject context into rules allows them to recursively call the parser export interface ParserContext { parseInline: (content: string) => ASTNode[]; parseBlocks: (content: string) => ASTNode[]; + createId: (prefix: string) => string; } // block parsing rule (such as header, code block, table) @@ -42,7 +56,7 @@ export interface BlockRule { // Returns the generated Node and the number of rows consumed if the match is successful; // returns null if the match fails. parse: ( - state: any, + state: BlockState, ctx: ParserContext, ) => { node: ASTNode; consumedLines: number } | null; } @@ -54,9 +68,13 @@ export interface InlineRule { // Returns the generated Node and the number of chars consumed if the match is successful; // returns null if the match fails. parse: ( - state: any, + state: InlineState, ctx: ParserContext, ) => { node: ASTNode; consumedChars: number } | null; } -export type MarkdownPlugin = (parser: any) => void; +export interface MarkdownParserOptions { + maxNestingDepth?: number; +} + +export type MarkdownPlugin = (parser: MarkdownParser) => void; diff --git a/packages/markdown-parser/tsconfig.build.json b/packages/markdown-parser/tsconfig.build.json new file mode 100644 index 0000000..2ee5513 --- /dev/null +++ b/packages/markdown-parser/tsconfig.build.json @@ -0,0 +1,12 @@ +// packages/markdown-parser/tsconfig.build.json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": true, + "emitDeclarationOnly": true, + "noEmit": false, + "rootDir": "src", + "outDir": "dist/types" + }, + "exclude": ["src/**/*.spec.ts", "src/test/**/*.ts"] +} diff --git a/packages/markdown-parser/tsconfig.json b/packages/markdown-parser/tsconfig.json index 140752e..0840522 100644 --- a/packages/markdown-parser/tsconfig.json +++ b/packages/markdown-parser/tsconfig.json @@ -14,5 +14,6 @@ "skipLibCheck": true, "noEmit": true }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts", "src/test/**/*.ts"] } diff --git a/packages/markdown-parser/tsconfig.test.json b/packages/markdown-parser/tsconfig.test.json new file mode 100644 index 0000000..f8ad5a9 --- /dev/null +++ b/packages/markdown-parser/tsconfig.test.json @@ -0,0 +1,9 @@ +// packages/markdown-parser/tsconfig.test.json +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node", "vitest/globals"] + }, + "include": ["src/**/*.spec.ts", "src/test/**/*.ts"], + "exclude": [] +} diff --git a/packages/markdown-parser/vite.config.ts b/packages/markdown-parser/vite.config.ts new file mode 100644 index 0000000..08b62d2 --- /dev/null +++ b/packages/markdown-parser/vite.config.ts @@ -0,0 +1,19 @@ +// packages/markdown-parser/vite.config.ts + +import { fileURLToPath } from 'node:url'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + build: { + lib: { + entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)), + formats: ['es'], + fileName: 'index', + }, + rollupOptions: { + external: [], + }, + sourcemap: true, + minify: false, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68f79c8..b4b4cf3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vite: + specifier: ^8.0.1 + version: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0) vitest: specifier: ^4.1.0 version: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)) From ba85378e09b4b5779413215c64db8aa7d1595998 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Tue, 18 Aug 2026 10:47:12 +0000 Subject: [PATCH 2/3] ci: Fix pnpm setup before dependency caching --- .github/workflows/ci.yml | 6 ++++-- package.json | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d777434..5ead908 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,13 +41,15 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up pnpm + uses: pnpm/action-setup@v4 + with: + version: 11.20.0 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: 24 cache: pnpm - - name: Enable pnpm - run: corepack enable - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - name: Typecheck parser package diff --git a/package.json b/package.json index 25003a0..5c513d0 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "license": "MIT", "author": "Fuyeor ", "type": "module", + "packageManager": "pnpm@11.20.0", "main": "./src/index.ts", "sideEffects": false, "imports": { From 2382bd2bc2d2719916523c92b0e83d3c8158915b Mon Sep 17 00:00:00 2001 From: Fuyeor Date: Wed, 19 Aug 2026 07:18:50 +0800 Subject: [PATCH 3/3] chore: Remove unnecessary build and add version --- .github/workflows/ci.yml | 7 +++--- .prettierrc => .prettierrc.json | 0 package.json | 8 +----- packages/markdown-parser-lit/package.json | 4 ++- packages/markdown-parser-vue/package.json | 4 ++- packages/markdown-parser/package.json | 26 ++++++-------------- packages/markdown-parser/tsconfig.build.json | 12 --------- packages/markdown-parser/vite.config.ts | 19 -------------- packages/playground/package.json | 12 +++++---- pnpm-lock.yaml | 5 +--- pnpm-workspace.yaml | 2 +- 11 files changed, 27 insertions(+), 72 deletions(-) rename .prettierrc => .prettierrc.json (100%) delete mode 100644 packages/markdown-parser/tsconfig.build.json delete mode 100644 packages/markdown-parser/vite.config.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ead908..753516c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: ['main'] # Only trigger when relevant code changes to save resources paths: + - '.github/workflows/ci.yml' - 'packages/markdown-parser/**' - 'packages/markdown-parser-lit/**' - 'packages/markdown-parser-vue/**' @@ -13,9 +14,10 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'Dockerfile' - - '.github/workflows/ci.yml' + pull_request: paths: + - '.github/workflows/ci.yml' - 'packages/markdown-parser/**' - 'packages/markdown-parser-lit/**' - 'packages/markdown-parser-vue/**' @@ -23,7 +25,6 @@ on: - 'package.json' - 'pnpm-lock.yaml' - 'Dockerfile' - - '.github/workflows/ci.yml' workflow_dispatch: # Allow manual trigger @@ -58,8 +59,6 @@ jobs: run: pnpm --filter @fuyeor/markdown-parser test:unit - name: Run FFM regression tests run: pnpm --filter @fuyeor/markdown-parser test:ffm - - name: Build parser package - run: pnpm --filter @fuyeor/markdown-parser build build-markdown-playground: needs: test-markdown-parser diff --git a/.prettierrc b/.prettierrc.json similarity index 100% rename from .prettierrc rename to .prettierrc.json diff --git a/package.json b/package.json index 5c513d0..74cf8fd 100644 --- a/package.json +++ b/package.json @@ -2,17 +2,11 @@ "name": "fuyeor-markdown-parser", "license": "MIT", "author": "Fuyeor ", - "type": "module", "packageManager": "pnpm@11.20.0", - "main": "./src/index.ts", - "sideEffects": false, - "imports": { - "#/*": "./src/*" - }, "scripts": { "format": "prettier --write \"**/*.ts\"", "test": "pnpm -F @fuyeor/markdown-parser test", - "playground": "pnpm -F @fuyeor/markdown-parser build && pnpm -F @fuyeor/markdown-parser-playground dev" + "playground": "pnpm -F @fuyeor/markdown-playground dev" }, "devDependencies": { "@types/node": "^25.6.0", diff --git a/packages/markdown-parser-lit/package.json b/packages/markdown-parser-lit/package.json index 37acc89..57c861e 100644 --- a/packages/markdown-parser-lit/package.json +++ b/packages/markdown-parser-lit/package.json @@ -3,8 +3,10 @@ "license": "MIT", "author": "Fuyeor ", "type": "module", - "main": "./src/index.ts", "sideEffects": false, + "exports": { + ".": "./src/index.ts" + }, "dependencies": { "@fuyeor/markdown-parser": "workspace:*" }, diff --git a/packages/markdown-parser-vue/package.json b/packages/markdown-parser-vue/package.json index cfb362a..1e216a6 100644 --- a/packages/markdown-parser-vue/package.json +++ b/packages/markdown-parser-vue/package.json @@ -3,8 +3,10 @@ "license": "MIT", "author": "Fuyeor ", "type": "module", - "main": "./src/index.ts", "sideEffects": false, + "exports": { + ".": "./src/index.ts" + }, "dependencies": { "@fuyeor/markdown-parser": "workspace:*" }, diff --git a/packages/markdown-parser/package.json b/packages/markdown-parser/package.json index cfbb208..035cbe7 100644 --- a/packages/markdown-parser/package.json +++ b/packages/markdown-parser/package.json @@ -1,36 +1,26 @@ { "name": "@fuyeor/markdown-parser", + "version": "0.1.0", "license": "MIT", "author": "Fuyeor ", "type": "module", - "main": "./dist/index.js", - "module": "./dist/index.js", - "types": "./dist/types/index.d.ts", - "exports": { - ".": { - "types": "./dist/types/index.d.ts", - "import": "./dist/index.js" - } - }, - "files": [ - "dist" - ], "sideEffects": false, - "imports": { - "#/*": "./src/*" - }, "scripts": { "test": "vitest run", "test:unit": "vitest run src/index.spec.ts", "test:ffm": "vitest run src/test/index.spec.ts -t \"Fuyeor Mark\"", "test:compat": "vitest run src/test/index.spec.ts", - "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit", - "build": "vite build && tsc -p tsconfig.build.json" + "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit" + }, + "imports": { + "#/*": "./src/*" + }, + "exports": { + ".": "./src/index.ts" }, "devDependencies": { "@types/node": "^25.6.0", "typescript": "^6.0.3", - "vite": "^8.0.1", "vitest": "^4.1.0" } } diff --git a/packages/markdown-parser/tsconfig.build.json b/packages/markdown-parser/tsconfig.build.json deleted file mode 100644 index 2ee5513..0000000 --- a/packages/markdown-parser/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -// packages/markdown-parser/tsconfig.build.json -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "emitDeclarationOnly": true, - "noEmit": false, - "rootDir": "src", - "outDir": "dist/types" - }, - "exclude": ["src/**/*.spec.ts", "src/test/**/*.ts"] -} diff --git a/packages/markdown-parser/vite.config.ts b/packages/markdown-parser/vite.config.ts deleted file mode 100644 index 08b62d2..0000000 --- a/packages/markdown-parser/vite.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -// packages/markdown-parser/vite.config.ts - -import { fileURLToPath } from 'node:url'; -import { defineConfig } from 'vite'; - -export default defineConfig({ - build: { - lib: { - entry: fileURLToPath(new URL('./src/index.ts', import.meta.url)), - formats: ['es'], - fileName: 'index', - }, - rollupOptions: { - external: [], - }, - sourcemap: true, - minify: false, - }, -}); diff --git a/packages/playground/package.json b/packages/playground/package.json index bd023bf..fa5166f 100644 --- a/packages/playground/package.json +++ b/packages/playground/package.json @@ -1,17 +1,19 @@ { - "name": "@fuyeor/markdown-parser-playground", + "name": "@fuyeor/markdown-playground", "license": "MIT", "author": "Fuyeor ", "type": "module", - "main": "./src/index.ts", "sideEffects": false, - "imports": { - "#/*": "./src/*" - }, "scripts": { "dev": "vite dev", "build": "vite build" }, + "imports": { + "#/*": "./src/*" + }, + "exports": { + ".": "./src/main.ts" + }, "dependencies": { "@fuyeor/markdown-parser-lit": "workspace:*", "@fuyeor/locale": "https://github.com/Fuyeor/webroamer.git#path:packages/locale", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b4b4cf3..79aae31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,9 +51,6 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 - vite: - specifier: ^8.0.1 - version: 8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0) vitest: specifier: ^4.1.0 version: 4.1.4(@types/node@25.6.0)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2)(vite@8.0.9(@types/node@25.6.0)(esbuild@0.27.7)(tsx@4.21.0)) @@ -365,7 +362,7 @@ packages: optional: true '@fuyeor/locale@https://codeload.github.com/Fuyeor/webroamer/tar.gz/c502f739f4b633ab24386fe9f90fb77ed9f6ca9d#path:packages/locale': - resolution: {path: packages/locale, tarball: https://codeload.github.com/Fuyeor/webroamer/tar.gz/c502f739f4b633ab24386fe9f90fb77ed9f6ca9d} + resolution: {gitHosted: true, path: packages/locale, tarball: https://codeload.github.com/Fuyeor/webroamer/tar.gz/c502f739f4b633ab24386fe9f90fb77ed9f6ca9d} version: 1.0.0 peerDependencies: '@lit-labs/signals': '*' diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index d9c769b..72b69d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,3 @@ # /pnpm-workspace.yaml packages: - - "packages/*" + - 'packages/*'
${render(cell.children)}
${render(cell.children)}