Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,28 @@ 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"
- '.github/workflows/ci.yml'
- 'packages/markdown-parser/**'
- 'packages/markdown-parser-lit/**'
- 'packages/markdown-parser-vue/**'
- 'packages/playground/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'Dockerfile'

pull_request:
paths:
- '.github/workflows/ci.yml'
- 'packages/markdown-parser/**'
- 'packages/markdown-parser-lit/**'
- 'packages/markdown-parser-vue/**'
- 'packages/playground/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'Dockerfile'
workflow_dispatch:
# Allow manual trigger

Expand All @@ -18,7 +33,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 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: 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

build-markdown-playground:
needs: test-markdown-parser
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
**/node_modules
**/tests.failed.json
**/dist
**/tests.failed.json
File renamed without changes.
3 changes: 3 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
9 changes: 2 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,11 @@
"name": "fuyeor-markdown-parser",
"license": "MIT",
"author": "Fuyeor <https://www.fuyeor.com/@Fuyeor>",
"type": "module",
"main": "./src/index.ts",
"sideEffects": false,
"imports": {
"#/*": "./src/*"
},
"packageManager": "pnpm@11.20.0",
"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-playground dev"
},
"devDependencies": {
"@types/node": "^25.6.0",
Expand Down
4 changes: 3 additions & 1 deletion packages/markdown-parser-lit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"license": "MIT",
"author": "Fuyeor <https://www.fuyeor.com/@Fuyeor>",
"type": "module",
"main": "./src/index.ts",
"sideEffects": false,
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@fuyeor/markdown-parser": "workspace:*"
},
Expand Down
58 changes: 39 additions & 19 deletions packages/markdown-parser-lit/src/render.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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`<span>${render(node.children)}</span>`;

const tagName = `h${level}`;
return staticHtml`
<${unsafeStatic(tagName)}>
${render(node.children)}
Expand All @@ -33,36 +46,43 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] {
return html`<em>${render(node.children)}</em>`;

case 'underline':
return html`<ins>${render(node.children)}</ins>`;
return html`<u>${render(node.children)}</u>`;

case 'strike':
return html`<del>${render(node.children)}</del>`;

case 'link':
return html`<a href="${node.url}">${render(node.children)}</a>`;
case 'link': {
const url = String(node.url ?? '').trim();
return isSafeLinkUrl(url)
? html`<a href="${url}">${render(node.children)}</a>`
: html`${render(node.children)}`;
}

case 'inline_code':
return html`<code>${node.content}</code>`;

case 'color_code':
case 'color_code': {
const color = String(node.content ?? '');
if (!isSafeColorValue(color)) return html`${color}`;
return html`
<code class="ffm-color-code">
<span
class="ffm-color-swatch"
style="
display: inline-block;
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: ${node.content};
vertical-align: middle;
margin-right: 0.3em;
display: inline-block;
width: 0.8em;
height: 0.8em;
border-radius: 50%;
background-color: ${color};
vertical-align: middle;
margin-right: 0.3em;
border: 1px solid #00000030;
"
></span
>${node.content}
>${color}
</code>
`;
}

case 'code_block':
return html`
Expand Down Expand Up @@ -105,17 +125,17 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] {
<table>
<thead>
<tr>
${node.headers.map(
(cell: any) => html`<th>${render(cell.children)}</th>`,
${(node.headers ?? []).map(
(cell) => html`<th>${render(cell.children)}</th>`,
)}
</tr>
</thead>
<tbody>
${node.children?.map(
(row: any) => html`
(row) => html`
<tr>
${row.children.map(
(cell: any) => html`<td>${render(cell.children)}</td>`,
${(row.children ?? []).map(
(cell) => html`<td>${render(cell.children)}</td>`,
)}
</tr>
`,
Expand Down
4 changes: 3 additions & 1 deletion packages/markdown-parser-vue/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
"license": "MIT",
"author": "Fuyeor <https://www.fuyeor.com/@Fuyeor>",
"type": "module",
"main": "./src/index.ts",
"sideEffects": false,
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@fuyeor/markdown-parser": "workspace:*"
},
Expand Down
115 changes: 102 additions & 13 deletions packages/markdown-parser-vue/src/render.ts
Original file line number Diff line number Diff line change
@@ -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':
Expand All @@ -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,
Expand Down Expand Up @@ -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 || [])),
),
),
Expand All @@ -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 || [])),
),
),
Expand All @@ -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 || []));
}
Expand Down
Loading
Loading