From 6ed5578d99448e9c20baadec6f72aa14bc740537 Mon Sep 17 00:00:00 2001
From: Fuyeor
Date: Mon, 21 Sep 2026 14:49:39 +0800
Subject: [PATCH 1/7] refactor: rename directory to singular form and update
scope to @ffm
---
.gitignore | 6 +-
.localerc.json | 8 +-
.vscode/settings.json | 7 +-
.../markdown-parser-rust => crate}/Cargo.toml | 1 -
.../markdown-parser-rust => crate}/src/ast.rs | 2 +-
.../markdown-parser-rust => crate}/src/lib.rs | 2 +-
.../src/linkify.rs | 2 +-
.../src/parser.rs | 2 +-
.../src/plain_text.rs | 2 +-
.../src/render.rs | 2 +-
.../src/rules.rs | 2 +-
.../src/safety.rs | 2 +-
.../src/state.rs | 2 +-
.../vscode/.gitignore | 0
.../vscode/.vscode/launch.json | 0
LICENSE => extension/vscode/LICENSE | 2 +-
.../vscode/example}/en.ffm | 0
.../vscode/example}/zh-hans.ffm | 0
.../vscode/extension/icon.png | Bin
.../vscode/language-configuration.json | 0
.../vscode/package.json | 12 +-
.../README.md => extension/vscode/readme.md | 0
.../vscode/src/extension.spec.ts | 2 +-
.../vscode/src/extension.ts | 4 +-
.../vscode/src/ffm-grammar.spec.ts | 2 +-
.../vscode/src/vscode.mock.ts | 2 +-
.../vscode/syntax}/ffm.tmLanguage.json | 0
.../vscode/tsconfig.json | 0
{fixtures => fixture}/ffm.json | 0
{fixtures => fixture}/linkify.json | 0
{fixtures => fixture}/markdown.json | 0
{fixtures => fixture}/safety.json | 0
packages/extensions/vscode/LICENSE => license | 2 +-
package.json | 6 +-
.../benchmark/fixture/linkify-it/link.txt | 0
.../benchmark/fixture/linkify-it/not-link.txt | 0
.../benchmark}/linkify.ts | 67 +++--
.../benchmark}/memory.ts | 20 +-
.../benchmark}/package.json | 6 +-
.../samples.ts => package/benchmark/sample.ts | 10 +-
package/benchmark/speed.ts | 34 +++
.../converter}/bench/index.ts | 4 +-
.../converter}/package.json | 4 +-
.../README.md => package/converter/readme.md | 12 +-
.../converter}/src/color.ts | 6 +-
.../converter/src/constant.ts | 4 +-
.../converter/src/fixture/spec.json | 0
.../converter}/src/index.spec.ts | 10 +-
.../converter}/src/index.ts | 10 +-
.../converter}/src/render.ts | 6 +-
.../converter}/src/style.ts | 6 +-
.../types.ts => package/converter/src/type.ts | 6 +-
.../html2ffm => package/converter}/test.ts | 2 +-
.../converter}/tsconfig.json | 0
.../converter}/tsconfig.test.json | 1 -
.../formatter}/package.json | 2 +-
.../README.md => package/formatter/readme.md | 6 +-
.../formatter/src/block.ts | 15 +-
.../formatter/src/constant.ts | 6 +-
.../formatter/src/fixture/spec.json | 0
.../formatter}/src/index.spec.ts | 14 +-
.../formatter}/src/index.ts | 18 +-
.../formatter}/src/text.ts | 4 +-
.../types.ts => package/formatter/src/type.ts | 5 +-
.../formatter}/tsconfig.json | 2 +-
{packages => package}/linkify/benchmark.js | 5 +-
.../linkify/build/generate-tlds.ts | 23 +-
{packages => package}/linkify/package.json | 2 +-
{packages => package}/linkify/src/cases.json | 0
.../linkify/src/cc-tld-bitmap.ts | 5 +-
.../linkify/src/index.spec.ts | 4 +-
{packages => package}/linkify/src/index.ts | 11 +-
{packages => package}/linkify/tsconfig.json | 0
.../lit-renderer}/package.json | 4 +-
package/lit-renderer/src/index.ts | 3 +
.../lit-renderer}/src/render.ts | 16 +-
.../parser}/package.json | 12 +-
.../parser}/src/core/color.ts | 2 +-
.../parser}/src/core/parser.ts | 10 +-
.../parser}/src/core/plain-text.ts | 24 +-
.../parser}/src/core/render.ts | 4 +-
.../parser}/src/core/state.ts | 2 +-
.../parser}/src/core/url.ts | 2 +-
.../parser}/src/default.ts | 24 +-
.../parser/src/index.spec.ts | 7 +-
.../parser}/src/index.ts | 18 +-
.../parser/src/plugin}/ast.ts | 4 +-
.../parser/src/plugin}/async-blocks.ts | 9 +-
.../parser/src/plugin}/extensions.ts | 4 +-
.../parser/src/plugin}/highlight.ts | 4 +-
.../parser/src/plugin}/latex.ts | 4 +-
.../parser/src/plugin}/twemoji.ts | 4 +-
.../parser/src/rule}/blocks.ts | 4 +-
.../rules => package/parser/src/rule}/ffm.ts | 16 +-
.../parser/src/rule}/inlines.ts | 4 +-
.../parser}/src/test/index.spec.ts | 9 +-
.../parser/src/test/spec}/commonMark.json | 0
.../parser/src/test/spec}/githubMark.json | 0
.../types.ts => package/parser/src/type.ts | 7 +-
.../parser}/tsconfig.json | 0
.../parser}/tsconfig.test.json | 1 -
{packages => package}/playground/index.html | 0
{packages => package}/playground/package.json | 8 +-
.../playground/public/assets/css/style.css | 0
.../playground/public/assets/example}/en.ffm | 0
.../playground/public/assets/example}/fr.ffm | 0
.../playground/public/assets/example}/ja.ffm | 0
.../public/assets/example}/zh-hans.ffm | 0
.../playground/public/favicon.svg | 0
.../playground/public/robots.txt | 0
.../src/@app/component/left-sidebar.vue | 20 +-
.../playground/src/@app/config/locale.ts | 8 +-
.../src/@app/config/sidebar/menu.ts | 4 +-
.../playground/src/@app/http.ts | 0
.../playground/src/@app}/locale.json | 0
.../playground/src/@app}/router/index.ts | 8 +-
{packages => package}/playground/src/App.vue | 2 +-
.../src/home/component/campaign.vue | 6 +-
.../playground/src/home/component/feature.vue | 4 +-
.../playground/src/home/component/hero.vue | 4 +-
.../src/home/component/image-placehoder.vue | 0
.../playground/src/home/component/intro.vue | 0
.../src/home/component/section-label.vue | 0
.../src/home/component/statement.vue | 4 +-
.../playground/src/home/index.vue | 12 +-
package/playground/src/main.ts | 21 ++
package/playground/src/playground/api.ts | 11 +
.../src/playground/component/editor.vue | 17 +-
.../src/playground/component/history.vue | 7 +-
.../src/playground/component/preview.vue | 14 +-
.../src/playground/component/share-modal.vue | 40 +--
.../src/playground/component/stats-bar.vue | 20 +-
.../src/playground/component/toolbar.vue | 14 +-
.../playground/composable}/useCompression.ts | 2 +-
.../composable}/useDocumentStats.ts | 2 +-
.../playground/composable}/useIndexedDb.ts | 4 +-
.../composable}/useMarkdownEditor.ts | 4 +-
.../composable}/useMarkdownHighlighter.ts | 2 +-
.../composable}/usePlaygroundLink.ts | 2 +-
.../composable}/usePlaygroundSource.ts | 2 +-
.../playground/src/playground/index.vue | 40 +--
.../playground/tsconfig.json | 9 +-
.../playground/vite.config.ts | 6 +
.../vue-renderer}/package.json | 4 +-
package/vue-renderer/src/index.ts | 2 +
.../vue-renderer}/src/render.ts | 8 +-
packages/benchmarks/speed.ts | 34 ---
packages/markdown-parser-lit/src/index.ts | 3 -
packages/markdown-parser-vue/src/index.ts | 2 -
packages/playground/src/api/examples.ts | 11 -
packages/playground/src/main.ts | 25 --
pnpm-lock.yaml | 267 +++++-------------
pnpm-workspace.yaml | 3 +-
README.md => readme.md | 12 +-
vitest.config.ts | 2 +-
155 files changed, 569 insertions(+), 701 deletions(-)
rename {packages/markdown-parser-rust => crate}/Cargo.toml (91%)
rename {packages/markdown-parser-rust => crate}/src/ast.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/lib.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/linkify.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/parser.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/plain_text.rs (98%)
rename {packages/markdown-parser-rust => crate}/src/render.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/rules.rs (99%)
rename {packages/markdown-parser-rust => crate}/src/safety.rs (97%)
rename {packages/markdown-parser-rust => crate}/src/state.rs (98%)
rename {packages/extensions => extension}/vscode/.gitignore (100%)
rename {packages/extensions => extension}/vscode/.vscode/launch.json (100%)
rename LICENSE => extension/vscode/LICENSE (94%)
rename {packages/extensions/vscode/examples => extension/vscode/example}/en.ffm (100%)
rename {packages/extensions/vscode/examples => extension/vscode/example}/zh-hans.ffm (100%)
rename {packages/extensions => extension}/vscode/extension/icon.png (100%)
rename {packages/extensions => extension}/vscode/language-configuration.json (100%)
rename {packages/extensions => extension}/vscode/package.json (87%)
rename packages/extensions/vscode/README.md => extension/vscode/readme.md (100%)
rename {packages/extensions => extension}/vscode/src/extension.spec.ts (98%)
rename {packages/extensions => extension}/vscode/src/extension.ts (89%)
rename {packages/extensions => extension}/vscode/src/ffm-grammar.spec.ts (98%)
rename {packages/extensions => extension}/vscode/src/vscode.mock.ts (87%)
rename {packages/extensions/vscode/syntaxes => extension/vscode/syntax}/ffm.tmLanguage.json (100%)
rename {packages/extensions => extension}/vscode/tsconfig.json (100%)
rename {fixtures => fixture}/ffm.json (100%)
rename {fixtures => fixture}/linkify.json (100%)
rename {fixtures => fixture}/markdown.json (100%)
rename {fixtures => fixture}/safety.json (100%)
rename packages/extensions/vscode/LICENSE => license (94%)
rename packages/benchmarks/fixtures/linkify-it/links.txt => package/benchmark/fixture/linkify-it/link.txt (100%)
rename packages/benchmarks/fixtures/linkify-it/not_links.txt => package/benchmark/fixture/linkify-it/not-link.txt (100%)
rename {packages/benchmarks => package/benchmark}/linkify.ts (70%)
rename {packages/benchmarks => package/benchmark}/memory.ts (74%)
rename {packages/benchmarks => package/benchmark}/package.json (74%)
rename packages/benchmarks/samples.ts => package/benchmark/sample.ts (92%)
create mode 100644 package/benchmark/speed.ts
rename {packages/html2ffm => package/converter}/bench/index.ts (94%)
rename {packages/html2ffm => package/converter}/package.json (89%)
rename packages/html2ffm/README.md => package/converter/readme.md (89%)
rename {packages/html2ffm => package/converter}/src/color.ts (98%)
rename packages/html2ffm/src/constants.ts => package/converter/src/constant.ts (98%)
rename packages/html2ffm/src/fixtures/conversions.json => package/converter/src/fixture/spec.json (100%)
rename {packages/html2ffm => package/converter}/src/index.spec.ts (80%)
rename {packages/html2ffm => package/converter}/src/index.ts (80%)
rename {packages/html2ffm => package/converter}/src/render.ts (99%)
rename {packages/html2ffm => package/converter}/src/style.ts (96%)
rename packages/html2ffm/src/types.ts => package/converter/src/type.ts (86%)
rename {packages/html2ffm => package/converter}/test.ts (91%)
rename {packages/html2ffm => package/converter}/tsconfig.json (100%)
rename {packages/html2ffm => package/converter}/tsconfig.test.json (81%)
rename {packages/markdown-formatter => package/formatter}/package.json (92%)
rename packages/markdown-formatter/README.md => package/formatter/readme.md (81%)
rename packages/markdown-formatter/src/blocks.ts => package/formatter/src/block.ts (96%)
rename packages/markdown-formatter/src/constants.ts => package/formatter/src/constant.ts (79%)
rename packages/markdown-formatter/src/fixtures/format.json => package/formatter/src/fixture/spec.json (100%)
rename {packages/markdown-formatter => package/formatter}/src/index.spec.ts (65%)
rename {packages/markdown-formatter => package/formatter}/src/index.ts (85%)
rename {packages/markdown-formatter => package/formatter}/src/text.ts (98%)
rename packages/markdown-formatter/src/types.ts => package/formatter/src/type.ts (83%)
rename {packages/markdown-formatter => package/formatter}/tsconfig.json (70%)
rename {packages => package}/linkify/benchmark.js (96%)
rename {packages => package}/linkify/build/generate-tlds.ts (72%)
rename {packages => package}/linkify/package.json (93%)
rename {packages => package}/linkify/src/cases.json (100%)
rename {packages => package}/linkify/src/cc-tld-bitmap.ts (78%)
rename packages/linkify/src/cross-language-fixtures.spec.ts => package/linkify/src/index.spec.ts (88%)
rename {packages => package}/linkify/src/index.ts (93%)
rename {packages => package}/linkify/tsconfig.json (100%)
rename {packages/markdown-parser-lit => package/lit-renderer}/package.json (79%)
create mode 100644 package/lit-renderer/src/index.ts
rename {packages/markdown-parser-lit => package/lit-renderer}/src/render.ts (93%)
rename {packages/markdown-parser => package/parser}/package.json (55%)
rename {packages/markdown-parser => package/parser}/src/core/color.ts (86%)
rename {packages/markdown-parser => package/parser}/src/core/parser.ts (98%)
rename {packages/markdown-parser => package/parser}/src/core/plain-text.ts (78%)
rename {packages/markdown-parser => package/parser}/src/core/render.ts (98%)
rename {packages/markdown-parser => package/parser}/src/core/state.ts (97%)
rename {packages/markdown-parser => package/parser}/src/core/url.ts (92%)
rename {packages/markdown-parser => package/parser}/src/default.ts (75%)
rename packages/markdown-parser/src/cross-language-fixtures.spec.ts => package/parser/src/index.spec.ts (96%)
rename {packages/markdown-parser => package/parser}/src/index.ts (76%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/ast.ts (92%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/async-blocks.ts (95%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/extensions.ts (95%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/highlight.ts (95%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/latex.ts (98%)
rename {packages/markdown-parser/src/plugins => package/parser/src/plugin}/twemoji.ts (97%)
rename {packages/markdown-parser/src/rules => package/parser/src/rule}/blocks.ts (99%)
rename {packages/markdown-parser/src/rules => package/parser/src/rule}/ffm.ts (90%)
rename {packages/markdown-parser/src/rules => package/parser/src/rule}/inlines.ts (98%)
rename {packages/markdown-parser => package/parser}/src/test/index.spec.ts (93%)
rename {packages/markdown-parser/src/test/specs => package/parser/src/test/spec}/commonMark.json (100%)
rename {packages/markdown-parser/src/test/specs => package/parser/src/test/spec}/githubMark.json (100%)
rename packages/markdown-parser/src/types.ts => package/parser/src/type.ts (94%)
rename {packages/markdown-parser => package/parser}/tsconfig.json (100%)
rename {packages/markdown-parser => package/parser}/tsconfig.test.json (78%)
rename {packages => package}/playground/index.html (100%)
rename {packages => package}/playground/package.json (79%)
rename {packages => package}/playground/public/assets/css/style.css (100%)
rename {packages/playground/public/assets/examples => package/playground/public/assets/example}/en.ffm (100%)
rename {packages/playground/public/assets/examples => package/playground/public/assets/example}/fr.ffm (100%)
rename {packages/playground/public/assets/examples => package/playground/public/assets/example}/ja.ffm (100%)
rename {packages/playground/public/assets/examples => package/playground/public/assets/example}/zh-hans.ffm (100%)
rename {packages => package}/playground/public/favicon.svg (100%)
rename {packages => package}/playground/public/robots.txt (100%)
rename packages/playground/src/layout/Left.vue => package/playground/src/@app/component/left-sidebar.vue (74%)
rename packages/playground/src/config/locales.ts => package/playground/src/@app/config/locale.ts (51%)
rename packages/playground/src/config/sidebar/menu.config.ts => package/playground/src/@app/config/sidebar/menu.ts (67%)
rename packages/playground/src/api/index.ts => package/playground/src/@app/http.ts (100%)
rename {packages/playground/src/locale => package/playground/src/@app}/locale.json (100%)
rename {packages/playground/src => package/playground/src/@app}/router/index.ts (92%)
rename {packages => package}/playground/src/App.vue (87%)
rename packages/playground/src/components/Home/HomeCampaign.vue => package/playground/src/home/component/campaign.vue (94%)
rename packages/playground/src/components/Home/HomeFeatures.vue => package/playground/src/home/component/feature.vue (97%)
rename packages/playground/src/components/Home/HomeHero.vue => package/playground/src/home/component/hero.vue (97%)
rename packages/playground/src/components/Home/HomeImagePlaceholder.vue => package/playground/src/home/component/image-placehoder.vue (100%)
rename packages/playground/src/components/Home/HomeIntro.vue => package/playground/src/home/component/intro.vue (100%)
rename packages/playground/src/components/Home/HomeSectionLabel.vue => package/playground/src/home/component/section-label.vue (100%)
rename packages/playground/src/components/Home/HomeStatement.vue => package/playground/src/home/component/statement.vue (94%)
rename packages/playground/src/views/Home.vue => package/playground/src/home/index.vue (90%)
create mode 100644 package/playground/src/main.ts
create mode 100644 package/playground/src/playground/api.ts
rename packages/playground/src/components/Playground/PlaygroundEditor.vue => package/playground/src/playground/component/editor.vue (92%)
rename packages/playground/src/components/Playground/DocumentHistory.vue => package/playground/src/playground/component/history.vue (98%)
rename packages/playground/src/components/Playground/PlaygroundPreview.vue => package/playground/src/playground/component/preview.vue (88%)
rename packages/playground/src/components/Playground/PlaygroundShare.vue => package/playground/src/playground/component/share-modal.vue (75%)
rename packages/playground/src/components/Playground/DocumentStatsBar.vue => package/playground/src/playground/component/stats-bar.vue (80%)
rename packages/playground/src/components/Playground/MarkdownToolbar.vue => package/playground/src/playground/component/toolbar.vue (92%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/useCompression.ts (97%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/useDocumentStats.ts (96%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/useIndexedDb.ts (98%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/useMarkdownEditor.ts (98%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/useMarkdownHighlighter.ts (98%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/usePlaygroundLink.ts (90%)
rename {packages/playground/src/composables => package/playground/src/playground/composable}/usePlaygroundSource.ts (71%)
rename packages/playground/src/views/Playground.vue => package/playground/src/playground/index.vue (86%)
rename {packages => package}/playground/tsconfig.json (56%)
rename {packages => package}/playground/vite.config.ts (68%)
rename {packages/markdown-parser-vue => package/vue-renderer}/package.json (80%)
create mode 100644 package/vue-renderer/src/index.ts
rename {packages/markdown-parser-vue => package/vue-renderer}/src/render.ts (97%)
delete mode 100644 packages/benchmarks/speed.ts
delete mode 100644 packages/markdown-parser-lit/src/index.ts
delete mode 100644 packages/markdown-parser-vue/src/index.ts
delete mode 100644 packages/playground/src/api/examples.ts
delete mode 100644 packages/playground/src/main.ts
rename README.md => readme.md (77%)
diff --git a/.gitignore b/.gitignore
index efc251d..465a3ed 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,6 @@
**/node_modules
**/dist
**/out
-**/tests.failed.json
-packages/playground/public/assets/locale/*
-**/target
\ No newline at end of file
+**/test.failed.json
+**/target
+package/playground/public/assets/locale/*
\ No newline at end of file
diff --git a/.localerc.json b/.localerc.json
index dfa6dda..d2e3e4f 100644
--- a/.localerc.json
+++ b/.localerc.json
@@ -1,10 +1,10 @@
{
"basic": {
- "source": "packages/playground/node_modules/@fuyeor/locale/src/common.json",
- "outDir": "packages/playground/public/assets/locale"
+ "source": "package/playground/node_modules/@fuyeor/locale/src/common.json",
+ "outDir": "package/playground/public/assets/locale"
},
"main": {
- "source": "packages/playground/src/locale/locale.json",
- "outDir": "packages/playground/public/assets/locale"
+ "source": "package/playground/src/@app/locale.json",
+ "outDir": "package/playground/public/assets/locale"
}
}
diff --git a/.vscode/settings.json b/.vscode/settings.json
index af9c146..1bc62cf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -15,9 +15,14 @@
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
+ "[markdown]": {
+ "editor.formatOnSave": false,
+ "editor.formatOnType": false
+ },
"search.exclude": {
"**/node_modules": true,
"**/dist": true,
- "**/build": true
+ "**/build": true,
+ "**/target": true
}
}
diff --git a/packages/markdown-parser-rust/Cargo.toml b/crate/Cargo.toml
similarity index 91%
rename from packages/markdown-parser-rust/Cargo.toml
rename to crate/Cargo.toml
index 1a5fb92..3ddaa61 100644
--- a/packages/markdown-parser-rust/Cargo.toml
+++ b/crate/Cargo.toml
@@ -1,4 +1,3 @@
-# packages/markdown-parser-rust/Cargo.toml
[package]
name = "ffm-parser"
version = "0.1.0"
diff --git a/packages/markdown-parser-rust/src/ast.rs b/crate/src/ast.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/ast.rs
rename to crate/src/ast.rs
index 02a88e9..b6cf84a 100644
--- a/packages/markdown-parser-rust/src/ast.rs
+++ b/crate/src/ast.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/ast.rs
+// src/ast.rs
use std::collections::BTreeMap;
#[cfg(feature = "serde")]
diff --git a/packages/markdown-parser-rust/src/lib.rs b/crate/src/lib.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/lib.rs
rename to crate/src/lib.rs
index e795c1d..901b88e 100644
--- a/packages/markdown-parser-rust/src/lib.rs
+++ b/crate/src/lib.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/lib.rs
+// src/lib.rs
//! Fuyeor Flavored Markdown parser and safe HTML renderer for Rust.
//!
//! The implementation mirrors the rule order and AST semantics of the
diff --git a/packages/markdown-parser-rust/src/linkify.rs b/crate/src/linkify.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/linkify.rs
rename to crate/src/linkify.rs
index 9ac1581..a86a602 100644
--- a/packages/markdown-parser-rust/src/linkify.rs
+++ b/crate/src/linkify.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/linkify.rs
+// src/linkify.rs
/// A link match with byte offsets into the original UTF-8 string.
#[derive(Clone, Debug, PartialEq, Eq)]
diff --git a/packages/markdown-parser-rust/src/parser.rs b/crate/src/parser.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/parser.rs
rename to crate/src/parser.rs
index aba575c..dd49196 100644
--- a/packages/markdown-parser-rust/src/parser.rs
+++ b/crate/src/parser.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/parser.rs
+// src/parser.rs
use std::cell::RefCell;
use std::collections::HashMap;
diff --git a/packages/markdown-parser-rust/src/plain_text.rs b/crate/src/plain_text.rs
similarity index 98%
rename from packages/markdown-parser-rust/src/plain_text.rs
rename to crate/src/plain_text.rs
index 0d44eaa..4d8df76 100644
--- a/packages/markdown-parser-rust/src/plain_text.rs
+++ b/crate/src/plain_text.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/plain_text.rs
+// src/plain_text.rs
use std::fmt::Write;
use crate::ast::{AstNode, NodeType};
diff --git a/packages/markdown-parser-rust/src/render.rs b/crate/src/render.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/render.rs
rename to crate/src/render.rs
index 58bf07f..1f9f691 100644
--- a/packages/markdown-parser-rust/src/render.rs
+++ b/crate/src/render.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/render.rs
+// src/render.rs
use std::fmt::Write;
use crate::ast::{AstNode, NodeType};
diff --git a/packages/markdown-parser-rust/src/rules.rs b/crate/src/rules.rs
similarity index 99%
rename from packages/markdown-parser-rust/src/rules.rs
rename to crate/src/rules.rs
index 0d5646e..049751b 100644
--- a/packages/markdown-parser-rust/src/rules.rs
+++ b/crate/src/rules.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/rules.rs
+// src/rules.rs
use crate::ast::{Alignment, AstNode, NodeType};
use crate::parser::ParserContext;
use crate::safety::{is_safe_color_value, is_safe_link_url};
diff --git a/packages/markdown-parser-rust/src/safety.rs b/crate/src/safety.rs
similarity index 97%
rename from packages/markdown-parser-rust/src/safety.rs
rename to crate/src/safety.rs
index 7d78909..9680bb6 100644
--- a/packages/markdown-parser-rust/src/safety.rs
+++ b/crate/src/safety.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/safety.rs
+// src/safety.rs
/// Validates links using the same relative-URL and protocol policy as FFM.
pub fn is_safe_link_url(value: &str) -> bool {
diff --git a/packages/markdown-parser-rust/src/state.rs b/crate/src/state.rs
similarity index 98%
rename from packages/markdown-parser-rust/src/state.rs
rename to crate/src/state.rs
index 0a94e09..0a05288 100644
--- a/packages/markdown-parser-rust/src/state.rs
+++ b/crate/src/state.rs
@@ -1,4 +1,4 @@
-// packages/markdown-parser-rust/src/state.rs
+// src/state.rs
use std::collections::HashMap;
/// Holds normalized lines and the current block-parser position.
diff --git a/packages/extensions/vscode/.gitignore b/extension/vscode/.gitignore
similarity index 100%
rename from packages/extensions/vscode/.gitignore
rename to extension/vscode/.gitignore
diff --git a/packages/extensions/vscode/.vscode/launch.json b/extension/vscode/.vscode/launch.json
similarity index 100%
rename from packages/extensions/vscode/.vscode/launch.json
rename to extension/vscode/.vscode/launch.json
diff --git a/LICENSE b/extension/vscode/LICENSE
similarity index 94%
rename from LICENSE
rename to extension/vscode/LICENSE
index f91ef59..e9c7e0d 100644
--- a/LICENSE
+++ b/extension/vscode/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 Fuyeor
+Copyright (c) 2026 Fuyeor
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the โSoftwareโ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/packages/extensions/vscode/examples/en.ffm b/extension/vscode/example/en.ffm
similarity index 100%
rename from packages/extensions/vscode/examples/en.ffm
rename to extension/vscode/example/en.ffm
diff --git a/packages/extensions/vscode/examples/zh-hans.ffm b/extension/vscode/example/zh-hans.ffm
similarity index 100%
rename from packages/extensions/vscode/examples/zh-hans.ffm
rename to extension/vscode/example/zh-hans.ffm
diff --git a/packages/extensions/vscode/extension/icon.png b/extension/vscode/extension/icon.png
similarity index 100%
rename from packages/extensions/vscode/extension/icon.png
rename to extension/vscode/extension/icon.png
diff --git a/packages/extensions/vscode/language-configuration.json b/extension/vscode/language-configuration.json
similarity index 100%
rename from packages/extensions/vscode/language-configuration.json
rename to extension/vscode/language-configuration.json
diff --git a/packages/extensions/vscode/package.json b/extension/vscode/package.json
similarity index 87%
rename from packages/extensions/vscode/package.json
rename to extension/vscode/package.json
index 645ae78..9d4bd61 100644
--- a/packages/extensions/vscode/package.json
+++ b/extension/vscode/package.json
@@ -9,7 +9,7 @@
"repository": {
"type": "git",
"url": "https://github.com/Fuyeor/markdown-parser.git",
- "directory": "packages/extensions/vscode"
+ "directory": "extension/vscode"
},
"engines": {
"vscode": "^1.80.0"
@@ -37,7 +37,7 @@
{
"language": "ffm",
"scopeName": "text.ffm",
- "path": "./syntaxes/ffm.tmLanguage.json"
+ "path": "./syntax/ffm.tmLanguage.json"
}
],
"configurationDefaults": {
@@ -50,11 +50,9 @@
},
"files": [
"out",
- "examples",
- "syntaxes",
+ "syntax",
"extension",
- "language-configuration.json",
- "LICENSE"
+ "language-configuration.json"
],
"scripts": {
"compile": "esbuild src/extension.ts --bundle --platform=node --format=cjs --external:vscode --outfile=out/extension.js",
@@ -63,7 +61,7 @@
"test": "vitest run --config ../../vitest.config.ts src/*.spec.ts"
},
"dependencies": {
- "@fuyeor/markdown-formatter": "workspace:*"
+ "@ffm/formatter": "workspace:*"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
diff --git a/packages/extensions/vscode/README.md b/extension/vscode/readme.md
similarity index 100%
rename from packages/extensions/vscode/README.md
rename to extension/vscode/readme.md
diff --git a/packages/extensions/vscode/src/extension.spec.ts b/extension/vscode/src/extension.spec.ts
similarity index 98%
rename from packages/extensions/vscode/src/extension.spec.ts
rename to extension/vscode/src/extension.spec.ts
index 4a4c235..b8ac0b6 100644
--- a/packages/extensions/vscode/src/extension.spec.ts
+++ b/extension/vscode/src/extension.spec.ts
@@ -1,4 +1,4 @@
-// packages/vscode-extension/src/extension.spec.ts
+// extension/vscode/src/extension.spec.ts
import { beforeEach, describe, expect, it, vi } from 'vitest';
import * as vscode from 'vscode';
import { activate } from './extension';
diff --git a/packages/extensions/vscode/src/extension.ts b/extension/vscode/src/extension.ts
similarity index 89%
rename from packages/extensions/vscode/src/extension.ts
rename to extension/vscode/src/extension.ts
index ae9d4ad..4ad14a6 100644
--- a/packages/extensions/vscode/src/extension.ts
+++ b/extension/vscode/src/extension.ts
@@ -1,6 +1,6 @@
-// packages/vscode-extension/src/extension.ts
+// extension/vscode/src/extension.ts
import * as vscode from 'vscode';
-import { format } from '@fuyeor/markdown-formatter';
+import { format } from '@ffm/formatter';
/** Replace the complete document only when formatting changes its content. */
const formatter: vscode.DocumentFormattingEditProvider = {
diff --git a/packages/extensions/vscode/src/ffm-grammar.spec.ts b/extension/vscode/src/ffm-grammar.spec.ts
similarity index 98%
rename from packages/extensions/vscode/src/ffm-grammar.spec.ts
rename to extension/vscode/src/ffm-grammar.spec.ts
index 8489701..f22375a 100644
--- a/packages/extensions/vscode/src/ffm-grammar.spec.ts
+++ b/extension/vscode/src/ffm-grammar.spec.ts
@@ -1,4 +1,4 @@
-// packages/vscode-extension/src/ffm-grammar.spec.ts
+// extension/vscode/src/ffm-grammar.spec.ts
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
diff --git a/packages/extensions/vscode/src/vscode.mock.ts b/extension/vscode/src/vscode.mock.ts
similarity index 87%
rename from packages/extensions/vscode/src/vscode.mock.ts
rename to extension/vscode/src/vscode.mock.ts
index 61537ae..3721009 100644
--- a/packages/extensions/vscode/src/vscode.mock.ts
+++ b/extension/vscode/src/vscode.mock.ts
@@ -1,4 +1,4 @@
-// packages/vscode-extension/src/vscode.mock.ts
+// extension/vscode/src/vscode.mock.ts
import { vi } from 'vitest';
export class Range {
diff --git a/packages/extensions/vscode/syntaxes/ffm.tmLanguage.json b/extension/vscode/syntax/ffm.tmLanguage.json
similarity index 100%
rename from packages/extensions/vscode/syntaxes/ffm.tmLanguage.json
rename to extension/vscode/syntax/ffm.tmLanguage.json
diff --git a/packages/extensions/vscode/tsconfig.json b/extension/vscode/tsconfig.json
similarity index 100%
rename from packages/extensions/vscode/tsconfig.json
rename to extension/vscode/tsconfig.json
diff --git a/fixtures/ffm.json b/fixture/ffm.json
similarity index 100%
rename from fixtures/ffm.json
rename to fixture/ffm.json
diff --git a/fixtures/linkify.json b/fixture/linkify.json
similarity index 100%
rename from fixtures/linkify.json
rename to fixture/linkify.json
diff --git a/fixtures/markdown.json b/fixture/markdown.json
similarity index 100%
rename from fixtures/markdown.json
rename to fixture/markdown.json
diff --git a/fixtures/safety.json b/fixture/safety.json
similarity index 100%
rename from fixtures/safety.json
rename to fixture/safety.json
diff --git a/packages/extensions/vscode/LICENSE b/license
similarity index 94%
rename from packages/extensions/vscode/LICENSE
rename to license
index f91ef59..e9c7e0d 100644
--- a/packages/extensions/vscode/LICENSE
+++ b/license
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2026 Fuyeor
+Copyright (c) 2026 Fuyeor
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the โSoftwareโ), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
diff --git a/package.json b/package.json
index 6786f01..5537265 100644
--- a/package.json
+++ b/package.json
@@ -1,12 +1,12 @@
{
- "name": "fuyeor-markdown-parser",
+ "name": "fuyeor-flavored-markdown",
"license": "MIT",
"author": "Fuyeor ",
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
"scripts": {
"format": "prettier --write \"**/*.ts\"",
- "test": "pnpm -F @fuyeor/markdown-parser test",
- "playground": "pnpm -F @fuyeor/markdown-playground dev",
+ "test": "pnpm -r --parallel test",
+ "playground": "pnpm -F @ffm/playground dev",
"locale": "locale-cli"
},
"devDependencies": {
diff --git a/packages/benchmarks/fixtures/linkify-it/links.txt b/package/benchmark/fixture/linkify-it/link.txt
similarity index 100%
rename from packages/benchmarks/fixtures/linkify-it/links.txt
rename to package/benchmark/fixture/linkify-it/link.txt
diff --git a/packages/benchmarks/fixtures/linkify-it/not_links.txt b/package/benchmark/fixture/linkify-it/not-link.txt
similarity index 100%
rename from packages/benchmarks/fixtures/linkify-it/not_links.txt
rename to package/benchmark/fixture/linkify-it/not-link.txt
diff --git a/packages/benchmarks/linkify.ts b/package/benchmark/linkify.ts
similarity index 70%
rename from packages/benchmarks/linkify.ts
rename to package/benchmark/linkify.ts
index f1180f5..9e97ea2 100644
--- a/packages/benchmarks/linkify.ts
+++ b/package/benchmark/linkify.ts
@@ -1,7 +1,7 @@
-// @fuyeor/markdown-parser-benchmark/linkify.ts
+// @ffm/benchmark/linkify.ts
import { readFileSync } from 'node:fs';
import { bench, group, run } from 'mitata';
-import { linkify } from '@fuyeor/linkify';
+import { linkify } from '@ffm/linkify';
import { LinkifyIt } from 'linkify-it';
type PositiveFixture = {
@@ -28,36 +28,40 @@ type FixtureReport = {
negative: FixtureChecks;
};
-const linksFixture = readFileSync(
- new URL('./fixtures/linkify-it/links.txt', import.meta.url),
+const linkFixture = readFileSync(
+ new URL('./fixture/linkify-it/link.txt', import.meta.url),
'utf8',
);
-const notLinksFixture = readFileSync(
- new URL('./fixtures/linkify-it/not_links.txt', import.meta.url),
+const notLinkFixture = readFileSync(
+ new URL('./fixture/linkify-it/not-link.txt', import.meta.url),
'utf8',
);
-const linkifyIt = new LinkifyIt({ fuzzyLink: true, fuzzyIP: true, urlAuth: true });
+const linkifyIt = new LinkifyIt({
+ fuzzyLink: true,
+ fuzzyIP: true,
+ urlAuth: true,
+});
linkifyIt.normalize = () => {};
// Parse linkify-it's alternating input/expected-output fixture format.
function parsePositiveFixtures(source: string): PositiveFixture[] {
- const lines = source.split(/\r?\n/g);
- const fixtures: PositiveFixture[] = [];
+ const line = source.split(/\r?\n/g);
+ const fixture: PositiveFixture[] = [];
- for (let index = 0; index < lines.length; index++) {
- const text = lines[index].replace(/^%.*/, '');
- const next = (lines[index + 1] ?? '').replace(/^%.*/, '');
+ for (let index = 0; index < line.length; index++) {
+ const text = line[index].replace(/^%.*/, '');
+ const next = (line[index + 1] ?? '').replace(/^%.*/, '');
if (!text.trim()) continue;
if (next.trim()) {
- fixtures.push({ line: index + 1, text, expected: next });
+ fixture.push({ line: index + 1, text, expected: next });
index++;
} else {
- fixtures.push({ line: index + 1, text, expected: text });
+ fixture.push({ line: index + 1, text, expected: text });
}
}
- return fixtures;
+ return fixture;
}
// Convert a fixture file into the exact text corpus used by both benchmarks.
@@ -68,15 +72,15 @@ function fixtureCorpus(source: string): string {
.join('\n');
}
-// Check one implementation against a list of positive fixtures.
+// Check one implementation against a list of positive fixture.
function evaluatePositiveFixtures(
- fixtures: PositiveFixture[],
+ fixture: PositiveFixture[],
matcher: (text: string) => string[],
): FixtureChecks {
const failures: FixtureFailure[] = [];
let passed = 0;
- for (const fixture of fixtures) {
+ for (const fixture of fixture) {
const actual = matcher(fixture.text);
if (actual.includes(fixture.expected)) {
passed++;
@@ -90,10 +94,10 @@ function evaluatePositiveFixtures(
}
}
- return { total: fixtures.length, passed, failures };
+ return { total: fixture.length, passed, failures };
}
-// Check one implementation against the official negative fixtures.
+// Check one implementation against the official negative fixture.
function evaluateNegativeFixtures(
source: string,
matcher: (text: string) => string[],
@@ -120,8 +124,11 @@ function evaluateNegativeFixtures(
// Evaluate both official fixture files without requiring behavioral parity.
function evaluateFixtures(matcher: (text: string) => string[]): FixtureReport {
return {
- positive: evaluatePositiveFixtures(parsePositiveFixtures(linksFixture), matcher),
- negative: evaluateNegativeFixtures(notLinksFixture, matcher),
+ positive: evaluatePositiveFixtures(
+ parsePositiveFixtures(linkFixture),
+ matcher,
+ ),
+ negative: evaluateNegativeFixtures(notLinkFixture, matcher),
};
}
@@ -143,8 +150,8 @@ function summarizeReport(report: FixtureReport) {
};
}
-const positiveFixtures = parsePositiveFixtures(linksFixture);
-const fixtureText = `${fixtureCorpus(linksFixture)}\n${fixtureCorpus(notLinksFixture)}`;
+const positiveFixtures = parsePositiveFixtures(linkFixture);
+const fixtureText = `${fixtureCorpus(linkFixture)}\n${fixtureCorpus(notLinkFixture)}`;
const syntheticText = `
Here is a test with multiple links.
Visit http://example.com or https://example.org.
@@ -156,11 +163,11 @@ Links with punctuation: https://example.com/path., (https://example.org)!
console.log(
JSON.stringify(
{
- fixtureSource: 'markdown-it/linkify-it/test/fixtures',
+ fixtureSource: 'markdown-it/linkify-it/test/fixture',
positiveCases: positiveFixtures.length,
- negativeCases: evaluateNegativeFixtures(notLinksFixture, () => []).total,
+ negativeCases: evaluateNegativeFixtures(notLinkFixture, () => []).total,
corpusBytes: Buffer.byteLength(fixtureText),
- '@fuyeor/linkify': summarizeReport(
+ '@ffm/linkify': summarizeReport(
evaluateFixtures((text) => linkify(text).map((match) => match.text)),
),
'linkify-it': summarizeReport(
@@ -175,7 +182,7 @@ console.log(
);
group('Synthetic corpus (276 KB)', () => {
- bench('@fuyeor/linkify', () => {
+ bench('@ffm/linkify', () => {
linkify(syntheticText);
});
@@ -184,8 +191,8 @@ group('Synthetic corpus (276 KB)', () => {
});
});
-group('Official linkify-it fixtures', () => {
- bench('@fuyeor/linkify', () => {
+group('Official linkify-it fixture', () => {
+ bench('@ffm/linkify', () => {
linkify(fixtureText);
});
diff --git a/packages/benchmarks/memory.ts b/package/benchmark/memory.ts
similarity index 74%
rename from packages/benchmarks/memory.ts
rename to package/benchmark/memory.ts
index 8545332..5b131bb 100644
--- a/packages/benchmarks/memory.ts
+++ b/package/benchmark/memory.ts
@@ -1,10 +1,10 @@
-// @fuyeor/markdown-parser-benchmark/memory.ts
-// pnpm -F @fuyeor/markdown-parser-benchmark bench:memory
+// @ffm/benchmark/memory.ts
+// pnpm -F @ffm/benchmark bench:memory
// @ts-nocheck
-import { createFuyeorMarkdownParser, render } from '@fuyeor/markdown-parser';
+import { createFuyeorMarkdownParser, render } from '@ffm/parser';
import MarkdownIt from 'markdown-it';
import { marked } from 'marked';
-import { socialSample, blogSample } from './samples';
+import { social, article } from './samples';
// Polyfill for window if needed by some specific logic
if (typeof window === 'undefined') global.window = globalThis;
@@ -32,7 +32,7 @@ async function measure(name: string, content: string) {
`\n--- Test Scenario: ${name} (${(content.length / 1024).toFixed(1)} KB) ---`,
);
- // @fuyeor/markdown-parser
+ // @ffm/parser
const ffmStart = getMemory();
let ffmResult = ffmIt(content);
const ffmEnd = getMemory();
@@ -54,21 +54,21 @@ async function measure(name: string, content: string) {
const mkEnd = getMemory();
const mkImpact = Math.max(0, mkEnd - mkStart);
- console.log(`[@fuyeor/markdown-parser] Impact: ${ffmImpact.toFixed(2)} MB`);
+ console.log(`[@ffm/parser] Impact: ${ffmImpact.toFixed(2)} MB`);
console.log(`[markdown-it] Impact: ${mdImpact.toFixed(2)} MB`);
console.log(`[marked] Impact: ${mkImpact.toFixed(2)} MB`);
const ratio = (mdImpact / (ffmImpact || 0.01)).toFixed(2);
- console.log(`๐ markdown-it relative to @fuyeor/markdown-parser: ${ratio}x`);
+ console.log(`๐ markdown-it relative to @ffm/parser: ${ratio}x`);
const mkRatio = (mkImpact / (ffmImpact || 0.01)).toFixed(2);
- console.log(`๐ marked relative to @fuyeor/markdown-parser: ${mkRatio}x`);
+ console.log(`๐ marked relative to @ffm/parser: ${mkRatio}x`);
}
const runTests = async () => {
console.log('๐ Starting Memory Benchmark...');
- await measure('Social', socialSample);
- await measure('Blog', blogSample);
+ await measure('Social', social);
+ await measure('Blog', article);
};
runTests();
diff --git a/packages/benchmarks/package.json b/package/benchmark/package.json
similarity index 74%
rename from packages/benchmarks/package.json
rename to package/benchmark/package.json
index 417d502..a16f689 100644
--- a/packages/benchmarks/package.json
+++ b/package/benchmark/package.json
@@ -1,10 +1,10 @@
{
- "name": "@fuyeor/markdown-parser-benchmark",
+ "name": "@ffm/benchmark",
"private": true,
"type": "module",
"dependencies": {
- "@fuyeor/linkify": "workspace:*",
- "@fuyeor/markdown-parser": "workspace:*",
+ "@ffm/linkify": "workspace:*",
+ "@ffm/parser": "workspace:*",
"linkify-it": "^6.1.0",
"markdown-it": "^14.0.2",
"marked": "^18.0.2",
diff --git a/packages/benchmarks/samples.ts b/package/benchmark/sample.ts
similarity index 92%
rename from packages/benchmarks/samples.ts
rename to package/benchmark/sample.ts
index 4e00ce7..48fe4c6 100644
--- a/packages/benchmarks/samples.ts
+++ b/package/benchmark/sample.ts
@@ -1,13 +1,13 @@
-// @fuyeor/markdown-parser-benchmark/samples.ts
+// @ffm/benchmark/sample.ts
/**
* Social Media Scenario
* Focus: Short, heavy formatting (bold, italic), many links, and mentions.
*/
-export const socialSample = `
-I'm currently focused on refining the benchmarking process for **@fuyeor/markdown-parser**.
+export const social = `
+I'm currently focused on refining the benchmarking process for **@ffm/parser**.
-I've initiated *memory* and *speed tests*, using files like \`@fuyeor/markdown-parser-benchmark/memory.ts\` and \`@fuyeor/markdown-parser-benchmark/speed.ts\`.
+I've initiated *memory* and *speed tests*, using files like \`@ffm/benchmark/memory.ts\` and \`@ffm/benchmark/speed.ts\`.
Further, I'm working to __create content scenarios__ like [social](https://fuyeor.com).
@@ -18,7 +18,7 @@ Further, I'm working to __create content scenarios__ like [social](https://fuyeo
* Blog Post Scenario
* Focus: Structured headers, nested lists, code blocks, and tables.
*/
-export const blogSample = `
+export const article = `
### The \`translate\` attribute [link](https://html.spec.whatwg.org/multipage/dom.html#the-translate-attribute)
> The **\`translate\` attribute** is used to specify whether an element's attribute values and the text content of its \`Text\` node children should be translated when the page is localized, or left unchanged. It is an **[enumerated attribute](https://html.spec.whatwg.org/multipage/common-microsyntaxes.html#enumerated-attribute)** with the following keywords and states:
diff --git a/package/benchmark/speed.ts b/package/benchmark/speed.ts
new file mode 100644
index 0000000..c143668
--- /dev/null
+++ b/package/benchmark/speed.ts
@@ -0,0 +1,34 @@
+// @ffm/benchmark/speed.ts
+// pnpm -F @ffm/benchmark bench:speed
+// @ts-nocheck
+import { run, bench, group } from 'mitata';
+import { createFuyeorMarkdownParser, render } from '@ffm/parser';
+import MarkdownIt from 'markdown-it';
+import { marked } from 'marked';
+import { social, article } from './samples';
+
+const ffm = createFuyeorMarkdownParser();
+const mdIt = new MarkdownIt({ linkify: true });
+
+const ffmIt = (content: string) => {
+ const ast = ffm(content);
+ return render(ast);
+};
+
+// Benchmarking Social scenario
+group('50 Social Post (Small/Frequent)', () => {
+ bench('@ffm/parser (AST)', () => ffm(social));
+ bench('@ffm/parser (HTML)', () => ffmIt(social));
+ bench('markdown-it (HTML)', () => mdIt.render(social));
+ bench('marked (HTML)', () => marked.parse(social));
+});
+
+// Benchmarking Blog scenario
+group('20 Blog Post (Structured/Nested)', () => {
+ bench('@ffm/parser (AST)', () => ffm(article));
+ bench('@ffm/parser (HTML)', () => ffmIt(article));
+ bench('markdown-it (HTML)', () => mdIt.render(article));
+ bench('marked (HTML)', () => marked.parse(article));
+});
+
+await run();
diff --git a/packages/html2ffm/bench/index.ts b/package/converter/bench/index.ts
similarity index 94%
rename from packages/html2ffm/bench/index.ts
rename to package/converter/bench/index.ts
index cc711c0..8646a8d 100644
--- a/packages/html2ffm/bench/index.ts
+++ b/package/converter/bench/index.ts
@@ -1,5 +1,5 @@
-// packages/html2ffm/bench/index.ts
-import { toFFM } from '@fuyeor/html2ffm';
+// @ffm/converter/bench/index.ts
+import { toFFM } from '@ffm/converter';
const ITERATIONS = 1_000;
const html = `
diff --git a/packages/html2ffm/package.json b/package/converter/package.json
similarity index 89%
rename from packages/html2ffm/package.json
rename to package/converter/package.json
index 29450d0..1af09d8 100644
--- a/packages/html2ffm/package.json
+++ b/package/converter/package.json
@@ -1,5 +1,5 @@
{
- "name": "@fuyeor/html2ffm",
+ "name": "@ffm/converter",
"version": "0.1.3",
"description": "Convert HTML fragments to Fuyeor Flavored Markdown.",
"license": "MIT",
@@ -18,7 +18,7 @@
".": "./src/index.ts"
},
"dependencies": {
- "@fuyeor/markdown-formatter": "workspace:*",
+ "@ffm/formatter": "workspace:*",
"htmlparser2": "^10.1.0"
},
"devDependencies": {
diff --git a/packages/html2ffm/README.md b/package/converter/readme.md
similarity index 89%
rename from packages/html2ffm/README.md
rename to package/converter/readme.md
index 2810896..dd28b42 100644
--- a/packages/html2ffm/README.md
+++ b/package/converter/readme.md
@@ -1,11 +1,11 @@
-# @fuyeor/html2ffm
+# @ffm/converter
Convert HTML fragments into formatted [Fuyeor Flavored Markdown](https://reference.fuyeor.com/zh-hans/ffm/tutorials/syntax/basic) (FFM).
## Installation
```sh
-pnpm add @fuyeor/html2ffm
+pnpm add @ffm/converter
```
The package is source-first and can be consumed by modern Node.js or browser applications through a normal ESM import.
@@ -13,7 +13,7 @@ The package is source-first and can be consumed by modern Node.js or browser app
## Usage
```ts
-import { toFFM } from '@fuyeor/html2ffm';
+import { toFFM } from '@ffm/converter';
const output = toFFM('Hello
World
');
@@ -43,17 +43,17 @@ The parser decodes HTML entities. Unknown ordinary element wrappers are removed
Run the package tests:
```sh
-pnpm --filter @fuyeor/html2ffm test
+pnpm --filter @ffm/converter test
```
Run the package typecheck:
```sh
-pnpm --filter @fuyeor/html2ffm typecheck
+pnpm --filter @ffm/converter typecheck
```
Run the standalone benchmark:
```sh
-pnpm --filter @fuyeor/html2ffm bench
+pnpm --filter @ffm/converter bench
```
diff --git a/packages/html2ffm/src/color.ts b/package/converter/src/color.ts
similarity index 98%
rename from packages/html2ffm/src/color.ts
rename to package/converter/src/color.ts
index d8bb0b0..21c9150 100644
--- a/packages/html2ffm/src/color.ts
+++ b/package/converter/src/color.ts
@@ -1,6 +1,6 @@
-// @fuyeor/html2ffm/src/color.ts
-import { cssNamedColors } from './constants';
-import type { Rgba } from './types';
+// @ffm/converter/src/color.ts
+import { cssNamedColors } from './constant';
+import type { Rgba } from './type';
// Convert one clamped color channel to a two-digit lowercase hexadecimal value.
function normalizeHexChannel(channel: number): string {
diff --git a/packages/html2ffm/src/constants.ts b/package/converter/src/constant.ts
similarity index 98%
rename from packages/html2ffm/src/constants.ts
rename to package/converter/src/constant.ts
index c40d15d..ffbab2a 100644
--- a/packages/html2ffm/src/constants.ts
+++ b/package/converter/src/constant.ts
@@ -1,5 +1,5 @@
-// @fuyeor/html2ffm/src/constants.ts
-import type { Marks } from './types';
+// @ffm/converter/src/constant.ts
+import type { Marks } from './type';
export const blockElements = new Set([
'article',
diff --git a/packages/html2ffm/src/fixtures/conversions.json b/package/converter/src/fixture/spec.json
similarity index 100%
rename from packages/html2ffm/src/fixtures/conversions.json
rename to package/converter/src/fixture/spec.json
diff --git a/packages/html2ffm/src/index.spec.ts b/package/converter/src/index.spec.ts
similarity index 80%
rename from packages/html2ffm/src/index.spec.ts
rename to package/converter/src/index.spec.ts
index afe858d..27884ad 100644
--- a/packages/html2ffm/src/index.spec.ts
+++ b/package/converter/src/index.spec.ts
@@ -1,13 +1,13 @@
-// @fuyeor/html2ffm/src/index.spec.ts
-// pnpm --filter @fuyeor/html2ffm test
+// @ffm/converter/src/index.spec.ts
+// pnpm --filter @ffm/converter test
import { describe, expect, it } from 'vitest';
import { toFFM } from './index';
-import fixtures from './fixtures/conversions.json' with { type: 'json' };
+import fixtures from './fixture/spec.json' with { type: 'json' };
describe('toFFM conversions', () => {
- for (const [section, cases] of Object.entries(fixtures)) {
+ for (const [section, item] of Object.entries(fixtures)) {
describe(section, () => {
- for (const [index, { desc, text, expect: expected }] of cases.entries()) {
+ for (const [index, { desc, text, expect: expected }] of item.entries()) {
it(desc ?? `case ${index + 1}: ${text.slice(0, 30)}`, () => {
expect(toFFM(text)).toBe(expected);
});
diff --git a/packages/html2ffm/src/index.ts b/package/converter/src/index.ts
similarity index 80%
rename from packages/html2ffm/src/index.ts
rename to package/converter/src/index.ts
index 2633e6e..474a70d 100644
--- a/packages/html2ffm/src/index.ts
+++ b/package/converter/src/index.ts
@@ -1,14 +1,14 @@
-// @fuyeor/html2ffm/src/index.ts
-import { format } from '@fuyeor/markdown-formatter';
+// @ffm/converter/src/index.ts
+import { format } from '@ffm/formatter';
import { parseDocument } from 'htmlparser2';
import { renderFlow } from './render';
-import type { ParsedDocument, ToFFMOptions } from './types';
+import type { ParsedDocument, ToFFMOptions } from './type';
export * from './color';
-export * from './constants';
+export * from './constant';
export * from './render';
export * from './style';
-export * from './types';
+export * from './type';
/** Convert an HTML fragment into formatted Fuyeor Flavored Markdown. */
export function toFFM(input: string, options?: ToFFMOptions): string {
diff --git a/packages/html2ffm/src/render.ts b/package/converter/src/render.ts
similarity index 99%
rename from packages/html2ffm/src/render.ts
rename to package/converter/src/render.ts
index 2986695..5a18fd8 100644
--- a/packages/html2ffm/src/render.ts
+++ b/package/converter/src/render.ts
@@ -1,4 +1,4 @@
-// @fuyeor/html2ffm/src/render.ts
+// @ffm/converter/src/render.ts
import {
blockElements,
unsafeSchemePattern,
@@ -6,7 +6,7 @@ import {
emptyMarks,
headingPattern,
safeSchemePattern,
-} from './constants';
+} from './constant';
import {
cloneMarks,
cloneStyle,
@@ -24,7 +24,7 @@ import type {
Style,
TableRow,
TextNode,
-} from './types';
+} from './type';
export function isElement(node: ChildNode): node is ElementNode {
return 'name' in node && 'attribs' in node && 'children' in node;
diff --git a/packages/html2ffm/src/style.ts b/package/converter/src/style.ts
similarity index 96%
rename from packages/html2ffm/src/style.ts
rename to package/converter/src/style.ts
index c6ee144..2c9f42a 100644
--- a/packages/html2ffm/src/style.ts
+++ b/package/converter/src/style.ts
@@ -1,7 +1,7 @@
-// @fuyeor/html2ffm/src/style.ts
+// @ffm/converter/src/style.ts
import { isTransparentColor, parseColor } from './color';
-import { cssLengthPattern } from './constants';
-import type { Marks, Style } from './types';
+import { cssLengthPattern } from './constant';
+import type { Marks, Style } from './type';
export function parseFontSize(value: string): string | null {
const normalized = value.trim().replace(/\s*!important\s*$/iu, '');
diff --git a/packages/html2ffm/src/types.ts b/package/converter/src/type.ts
similarity index 86%
rename from packages/html2ffm/src/types.ts
rename to package/converter/src/type.ts
index d265d99..b17806f 100644
--- a/packages/html2ffm/src/types.ts
+++ b/package/converter/src/type.ts
@@ -1,5 +1,5 @@
-// @fuyeor/html2ffm/src/types.ts
-import type { FormatOptions } from '@fuyeor/markdown-formatter';
+// @ffm/converter/src/type.ts
+import type { FormatOption } from '@ffm/formatter';
import type { parseDocument } from 'htmlparser2';
export type ParsedDocument = ReturnType;
@@ -48,4 +48,4 @@ export type TableRow = {
};
/** Options for HTML to FFM conversion. */
-export type ToFFMOptions = FormatOptions;
+export type ToFFMOptions = FormatOption;
diff --git a/packages/html2ffm/test.ts b/package/converter/test.ts
similarity index 91%
rename from packages/html2ffm/test.ts
rename to package/converter/test.ts
index 3310bb8..d92fbdf 100644
--- a/packages/html2ffm/test.ts
+++ b/package/converter/test.ts
@@ -1,4 +1,4 @@
-// @fuyeor/html2ffm/src/test.ts
+// @ffm/converter/src/test.ts
// npx tsx test.ts
import { toFFM } from './src/index';
diff --git a/packages/html2ffm/tsconfig.json b/package/converter/tsconfig.json
similarity index 100%
rename from packages/html2ffm/tsconfig.json
rename to package/converter/tsconfig.json
diff --git a/packages/html2ffm/tsconfig.test.json b/package/converter/tsconfig.test.json
similarity index 81%
rename from packages/html2ffm/tsconfig.test.json
rename to package/converter/tsconfig.test.json
index cd95de0..fac772b 100644
--- a/packages/html2ffm/tsconfig.test.json
+++ b/package/converter/tsconfig.test.json
@@ -1,4 +1,3 @@
-// packages/html2ffm/tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
diff --git a/packages/markdown-formatter/package.json b/package/formatter/package.json
similarity index 92%
rename from packages/markdown-formatter/package.json
rename to package/formatter/package.json
index 5e5b8ee..b55a719 100644
--- a/packages/markdown-formatter/package.json
+++ b/package/formatter/package.json
@@ -1,5 +1,5 @@
{
- "name": "@fuyeor/markdown-formatter",
+ "name": "@ffm/formatter",
"version": "0.1.2",
"description": "Formatter for Fuyeor Flavored Markdown documents.",
"license": "MIT",
diff --git a/packages/markdown-formatter/README.md b/package/formatter/readme.md
similarity index 81%
rename from packages/markdown-formatter/README.md
rename to package/formatter/readme.md
index 07dae16..f53f55e 100644
--- a/packages/markdown-formatter/README.md
+++ b/package/formatter/readme.md
@@ -1,4 +1,4 @@
-@fuyeor/markdown-formatter โ A lightweight, deterministic Markdown and FFM (FuYeor Flavored Markdown) formatter with built-in CJK typographic spacing, table alignment, list indentation normalization, and semantic fence processing.
+@ffm/formatter โ A lightweight, deterministic Markdown and FFM (Fuyeor Flavored Markdown) formatter with built-in CJK typographic spacing, table alignment, list indentation normalization, and semantic fence processing.
## Features
@@ -12,7 +12,7 @@
## Quick Start
```ts
-import { format } from '@fuyeor/markdown-formatter';
+import { format } from '@ffm/formatter';
const markdown = `
# Title
@@ -49,7 +49,7 @@ console.log(formatted);
### Example with Options
```ts
-import { format } from '@fuyeor/markdown-formatter';
+import { format } from '@ffm/formatter';
const source = 'First paragraph\n\n\n\nSecond paragraph';
diff --git a/packages/markdown-formatter/src/blocks.ts b/package/formatter/src/block.ts
similarity index 96%
rename from packages/markdown-formatter/src/blocks.ts
rename to package/formatter/src/block.ts
index 1f3cfa6..591f722 100644
--- a/packages/markdown-formatter/src/blocks.ts
+++ b/package/formatter/src/block.ts
@@ -1,12 +1,7 @@
-// @fuyeor/markdown-formatter/src/blocks.ts
-import { semanticFenceLanguages } from './constants';
+// @ffm/formatter/src/block.ts
+import { semanticFenceLanguages } from './constant';
import { formatText } from './text';
-import type {
- Fence,
- FormatOptions,
- ListIndentContext,
- QuoteLine,
-} from './types';
+import type { Fence, FormatOption, ListIndentContext, QuoteLine } from './type';
/** Normalize line endings before applying deterministic line-based formatting. */
export function normalizeLineEndings(content: string): string {
@@ -235,8 +230,8 @@ export function formatSemanticFence(
lines: readonly string[],
start: number,
fence: Fence,
- formatFn: (content: string, options?: FormatOptions) => string,
- options?: FormatOptions,
+ formatFn: (content: string, options?: FormatOption) => string,
+ options?: FormatOption,
): { lines: string[]; next: number } | null {
if (!fence.language || !semanticFenceLanguages.has(fence.language)) {
return null;
diff --git a/packages/markdown-formatter/src/constants.ts b/package/formatter/src/constant.ts
similarity index 79%
rename from packages/markdown-formatter/src/constants.ts
rename to package/formatter/src/constant.ts
index c4ac029..aa0c0a4 100644
--- a/packages/markdown-formatter/src/constants.ts
+++ b/package/formatter/src/constant.ts
@@ -1,6 +1,6 @@
-// @fuyeor/markdown-formatter/src/constants.ts
+// @ffm/formatter/src/constant.ts
// NOTE: Avoid medieval SCREAMING_SNAKE_CASE; use camelCase for modern readability.
-import type { FormatOptions } from './types';
+import type { FormatOption } from './type';
export const cjkCharacter = '\\p{Script=Han}';
export const latinOrDigit = 'A-Za-z0-9';
@@ -18,6 +18,6 @@ export const semanticFenceLanguages = new Set([
]);
/** Default formatting configuration options. */
-export const defaultFormatOptions: Readonly> = {
+export const defaultFormatOption: Readonly> = {
maxConsecutiveBlankLines: 1,
};
diff --git a/packages/markdown-formatter/src/fixtures/format.json b/package/formatter/src/fixture/spec.json
similarity index 100%
rename from packages/markdown-formatter/src/fixtures/format.json
rename to package/formatter/src/fixture/spec.json
diff --git a/packages/markdown-formatter/src/index.spec.ts b/package/formatter/src/index.spec.ts
similarity index 65%
rename from packages/markdown-formatter/src/index.spec.ts
rename to package/formatter/src/index.spec.ts
index 91912d7..db098be 100644
--- a/packages/markdown-formatter/src/index.spec.ts
+++ b/package/formatter/src/index.spec.ts
@@ -1,13 +1,13 @@
-// @fuyeor/markdown-formatter/src/index.spec.ts
-// pnpm --filter @fuyeor/markdown-formatter test
+// @ffm/formatter/src/index.spec.ts
+// pnpm --filter @ffm/formatter test
import { describe, expect, it } from 'vitest';
import { format } from './index';
-import fixtures from './fixtures/format.json' with { type: 'json' };
+import fixture from './fixture/spec.json' with { type: 'json' };
-describe('format fixtures', () => {
- for (const fixture of fixtures) {
- it(fixture.section, () => {
- expect(format(fixture.origin)).toBe(fixture.formatted);
+describe('format fixture', () => {
+ for (const item of fixture) {
+ it(item.section, () => {
+ expect(format(item.origin)).toBe(item.formatted);
});
}
diff --git a/packages/markdown-formatter/src/index.ts b/package/formatter/src/index.ts
similarity index 85%
rename from packages/markdown-formatter/src/index.ts
rename to package/formatter/src/index.ts
index 0177602..7cbdc4f 100644
--- a/packages/markdown-formatter/src/index.ts
+++ b/package/formatter/src/index.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-formatter/src/index.ts
+// @ffm/formatter/src/index.ts
import {
formatDeepQuote,
formatOrdinaryLine,
@@ -8,23 +8,23 @@ import {
isFenceClose,
normalizeLineEndings,
trimDocumentBoundary,
-} from './blocks';
-import { defaultFormatOptions } from './constants';
-import type { Fence, FormatOptions, ListIndentContext } from './types';
+} from './block';
+import { defaultFormatOption } from './constant';
+import type { Fence, FormatOption, ListIndentContext } from './type';
-export * from './blocks';
-export * from './constants';
+export * from './block';
+export * from './constant';
export * from './text';
-export * from './types';
+export * from './type';
/** Format a complete FFM document according to the editor's canonical style. */
-export function format(content: string, options?: FormatOptions): string {
+export function format(content: string, options?: FormatOption): string {
if (typeof content !== 'string')
throw new TypeError('content must be a string');
const maxBlank =
options?.maxConsecutiveBlankLines ??
- defaultFormatOptions.maxConsecutiveBlankLines;
+ defaultFormatOption.maxConsecutiveBlankLines;
if (!Number.isInteger(maxBlank) || maxBlank < 0)
throw new TypeError(
diff --git a/packages/markdown-formatter/src/text.ts b/package/formatter/src/text.ts
similarity index 98%
rename from packages/markdown-formatter/src/text.ts
rename to package/formatter/src/text.ts
index 09cf329..503eee4 100644
--- a/packages/markdown-formatter/src/text.ts
+++ b/package/formatter/src/text.ts
@@ -1,9 +1,9 @@
-// @fuyeor/markdown-formatter/src/text.ts
+// @ffm/formatter/src/text.ts
import {
cjkLatinBoundary,
inlineMarkupPattern,
linkTargetPattern,
-} from './constants';
+} from './constant';
/** Check whether a single character is a Han-script character. */
export function isCjkCharacter(character: string | undefined): boolean {
diff --git a/packages/markdown-formatter/src/types.ts b/package/formatter/src/type.ts
similarity index 83%
rename from packages/markdown-formatter/src/types.ts
rename to package/formatter/src/type.ts
index 020e910..4c80934 100644
--- a/packages/markdown-formatter/src/types.ts
+++ b/package/formatter/src/type.ts
@@ -1,5 +1,4 @@
-// @fuyeor/markdown-formatter/src/types.ts
-
+// @ffm/formatter/src/type.ts
export type Fence = {
character: '`' | '~';
length: number;
@@ -15,7 +14,7 @@ export type ListIndentContext = {
};
/** Configuration options for the Markdown formatter. */
-export type FormatOptions = {
+export type FormatOption = {
/** Maximum allowable consecutive blank lines between blocks. @default 1 */
maxConsecutiveBlankLines?: number;
};
diff --git a/packages/markdown-formatter/tsconfig.json b/package/formatter/tsconfig.json
similarity index 70%
rename from packages/markdown-formatter/tsconfig.json
rename to package/formatter/tsconfig.json
index 6c7b3e6..7257dd9 100644
--- a/packages/markdown-formatter/tsconfig.json
+++ b/package/formatter/tsconfig.json
@@ -1,5 +1,5 @@
{
- "extends": "../markdown-parser/tsconfig.json",
+ "extends": "../parser/tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"noEmit": true
diff --git a/packages/linkify/benchmark.js b/package/linkify/benchmark.js
similarity index 96%
rename from packages/linkify/benchmark.js
rename to package/linkify/benchmark.js
index e75d6ba..172eb2f 100644
--- a/packages/linkify/benchmark.js
+++ b/package/linkify/benchmark.js
@@ -1,4 +1,4 @@
-// packages/linkify/benchmark.js
+// @ffm/linkify/benchmark.js
import { statSync } from 'node:fs';
const { linkify } = await import('./dist/index.js');
@@ -40,6 +40,7 @@ console.log(
p95Ms: Number(percentile(0.95).toFixed(3)),
averageMs: Number((sum / samples.length).toFixed(3)),
distBytes: statSync(new URL('./dist/index.js', import.meta.url)).size,
- declarationBytes: statSync(new URL('./dist/index.d.ts', import.meta.url)).size,
+ declarationBytes: statSync(new URL('./dist/index.d.ts', import.meta.url))
+ .size,
}),
);
diff --git a/packages/linkify/build/generate-tlds.ts b/package/linkify/build/generate-tlds.ts
similarity index 72%
rename from packages/linkify/build/generate-tlds.ts
rename to package/linkify/build/generate-tlds.ts
index 063c4b2..1631855 100644
--- a/packages/linkify/build/generate-tlds.ts
+++ b/package/linkify/build/generate-tlds.ts
@@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync } from 'node:fs';
import { dirname, resolve } from 'node:path';
// md is intentionally omitted so Markdown filenames are not fuzzy links.
-const CC_TLDS = `
+const ccTld = `
ac ad ae af ag ai al am ao aq ar as at au aw ax az
ba bb bd be bf bg bh bi bj bm bn bo br bs bt bv bw by bz
ca cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz
@@ -33,12 +33,12 @@ za zm zw
.trim()
.split(/\s+/);
-const OUTPUT_PATH = resolve(import.meta.dirname, '../src/cc-tld-bitmap.ts');
-const BIT_COUNT = 26 * 26;
-const WORD_COUNT = Math.ceil(BIT_COUNT / 32);
-const bitmap = new Uint32Array(WORD_COUNT);
+const bitCount = 26 * 26;
+const wordCount = Math.ceil(bitCount / 32);
+const bitmap = new Uint32Array(wordCount);
+const outputPath = resolve(import.meta.dirname, '../src/cc-tld-bitmap.ts');
-for (const tld of CC_TLDS) {
+for (const tld of ccTld) {
if (!/^[a-z]{2}$/.test(tld) || tld === 'md') {
throw new Error(`Invalid ccTLD entry: ${tld}`);
}
@@ -48,12 +48,11 @@ for (const tld of CC_TLDS) {
}
const words = Array.from(bitmap, (word) => `0x${word.toString(16)}`);
-const source = `// packages/linkify/src/cc-tld-bitmap.ts
+const source = `// @ffm/linkify/src/cc-tld-bitmap.ts
// This file is generated by build/generate-tlds.ts. Do not edit manually.
-
-export const CC_TLD_BITMAP = new Uint32Array([${words.join(', ')}]);
+export const ccBitmap = new Uint32Array([${words.join(', ')}]);
`;
-mkdirSync(dirname(OUTPUT_PATH), { recursive: true });
-writeFileSync(OUTPUT_PATH, source);
-console.log(`Generated ${OUTPUT_PATH} from ${CC_TLDS.length} ccTLDs.`);
+mkdirSync(dirname(outputPath), { recursive: true });
+writeFileSync(outputPath, source);
+console.log(`Generated ${outputPath} from ${ccTld.length} ccTLDs.`);
diff --git a/packages/linkify/package.json b/package/linkify/package.json
similarity index 93%
rename from packages/linkify/package.json
rename to package/linkify/package.json
index 0db404b..e2b7b96 100644
--- a/packages/linkify/package.json
+++ b/package/linkify/package.json
@@ -1,5 +1,5 @@
{
- "name": "@fuyeor/linkify",
+ "name": "@ffm/linkify",
"version": "1.0.0",
"description": "High performance and minimal size linkifier",
"type": "module",
diff --git a/packages/linkify/src/cases.json b/package/linkify/src/cases.json
similarity index 100%
rename from packages/linkify/src/cases.json
rename to package/linkify/src/cases.json
diff --git a/packages/linkify/src/cc-tld-bitmap.ts b/package/linkify/src/cc-tld-bitmap.ts
similarity index 78%
rename from packages/linkify/src/cc-tld-bitmap.ts
rename to package/linkify/src/cc-tld-bitmap.ts
index 4e7a7bb..77e62f7 100644
--- a/packages/linkify/src/cc-tld-bitmap.ts
+++ b/package/linkify/src/cc-tld-bitmap.ts
@@ -1,7 +1,6 @@
-// packages/linkify/src/cc-tld-bitmap.ts
+// @ffm/linkify/src/cc-tld-bitmap.ts
// This file is generated by build/generate-tlds.ts. Do not edit manually.
-
-export const CC_TLD_BITMAP = new Uint32Array([
+export const ccBitmap = new Uint32Array([
0xeedf597c, 0xdeddb9cf, 0x15843f27, 0x1e005480, 0xb0095c00, 0x15fb9f,
0x7818068d, 0x340400f, 0xf42b1d00, 0xd54f8141, 0x25d7fffc, 0x100084b,
0x538f3c40, 0x40000001, 0xfdf15100, 0x9fbb3be7, 0x404419a, 0x408557, 0x4002,
diff --git a/packages/linkify/src/cross-language-fixtures.spec.ts b/package/linkify/src/index.spec.ts
similarity index 88%
rename from packages/linkify/src/cross-language-fixtures.spec.ts
rename to package/linkify/src/index.spec.ts
index 035c605..0ff6a99 100644
--- a/packages/linkify/src/cross-language-fixtures.spec.ts
+++ b/package/linkify/src/index.spec.ts
@@ -1,4 +1,4 @@
-// packages/linkify/src/cross-language-fixtures.spec.ts
+// @ffm/linkify/src/index.spec.ts
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
@@ -22,7 +22,7 @@ type LinkFixtureFile = {
const fixtures = JSON.parse(
readFileSync(
- resolve(import.meta.dirname, '../../../fixtures/linkify.json'),
+ resolve(import.meta.dirname, '../../../fixture/linkify.json'),
'utf8',
),
) as LinkFixtureFile;
diff --git a/packages/linkify/src/index.ts b/package/linkify/src/index.ts
similarity index 93%
rename from packages/linkify/src/index.ts
rename to package/linkify/src/index.ts
index 42c4c4d..a7a6e79 100644
--- a/packages/linkify/src/index.ts
+++ b/package/linkify/src/index.ts
@@ -1,6 +1,5 @@
-// src/index.ts
-
-import { CC_TLD_BITMAP } from './cc-tld-bitmap.js';
+// @ffm/linkify/src/index.ts
+import { ccBitmap } from './cc-tld-bitmap.js';
export interface LinkMatch {
url: string;
@@ -10,7 +9,7 @@ export interface LinkMatch {
}
// ๅ้ๆซๆๅช่ด่ดฃๆพๅบๅๅๅฝข็ถ๏ผๅฎ้
ไธคๅญๆฏ TLD ๅๆณๆง็ฑไฝๅพๆ ก้ชใ
-const LINKIFY_REGEX =
+const linkifyRegex =
/(https?:\/\/[^\s]+|(?> 5] & (1 << (bitIndex & 31))) !== 0;
+ return (ccBitmap[bitIndex >> 5] & (1 << (bitIndex & 31))) !== 0;
}
// Validate only fuzzy two-letter TLDs; explicit URLs and hand-written TLDs are already gated.
@@ -61,7 +60,7 @@ export function linkify(text: string): LinkMatch[] {
const matches: LinkMatch[] = [];
const punctuation = '.,:;?!';
- for (const match of text.matchAll(LINKIFY_REGEX)) {
+ for (const match of text.matchAll(linkifyRegex)) {
let urlStr = match[0];
const matchIdx = match.index!;
const isExplicitUrl =
diff --git a/packages/linkify/tsconfig.json b/package/linkify/tsconfig.json
similarity index 100%
rename from packages/linkify/tsconfig.json
rename to package/linkify/tsconfig.json
diff --git a/packages/markdown-parser-lit/package.json b/package/lit-renderer/package.json
similarity index 79%
rename from packages/markdown-parser-lit/package.json
rename to package/lit-renderer/package.json
index 57c861e..d95e333 100644
--- a/packages/markdown-parser-lit/package.json
+++ b/package/lit-renderer/package.json
@@ -1,5 +1,5 @@
{
- "name": "@fuyeor/markdown-parser-lit",
+ "name": "@ffm/lit-renderer",
"license": "MIT",
"author": "Fuyeor ",
"type": "module",
@@ -8,7 +8,7 @@
".": "./src/index.ts"
},
"dependencies": {
- "@fuyeor/markdown-parser": "workspace:*"
+ "@ffm/parser": "workspace:*"
},
"peerDependencies": {
"lit": "*"
diff --git a/package/lit-renderer/src/index.ts b/package/lit-renderer/src/index.ts
new file mode 100644
index 0000000..635c5a9
--- /dev/null
+++ b/package/lit-renderer/src/index.ts
@@ -0,0 +1,3 @@
+// @ffm/lit-renderer/src/index.ts
+export * from '@ffm/parser';
+export { render } from './render';
diff --git a/packages/markdown-parser-lit/src/render.ts b/package/lit-renderer/src/render.ts
similarity index 93%
rename from packages/markdown-parser-lit/src/render.ts
rename to package/lit-renderer/src/render.ts
index f055ad7..62453f5 100644
--- a/packages/markdown-parser-lit/src/render.ts
+++ b/package/lit-renderer/src/render.ts
@@ -1,19 +1,15 @@
-// @fuyeor/markdown-parser-lit/src/render.ts
+// @ffm/lit-renderer/src/render.ts
import { html, type TemplateResult } from 'lit';
import { html as staticHtml, unsafeStatic } from 'lit/static-html.js';
-import {
- isSafeColorValue,
- isSafeLinkUrl,
- type ASTNode,
-} from '@fuyeor/markdown-parser';
+import { isSafeColorValue, isSafeLinkUrl, type ASTNode } from '@ffm/parser';
/**
- * recursively render AST nodes into Lit TemplateResult
+ * recursively render AST node into Lit TemplateResult
*/
-export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] {
- if (!nodes || nodes.length === 0) return [];
+export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
+ if (!node || node.length === 0) return [];
- return nodes.map((node) => {
+ return node.map((node) => {
switch (node.type) {
case 'heading': {
const level =
diff --git a/packages/markdown-parser/package.json b/package/parser/package.json
similarity index 55%
rename from packages/markdown-parser/package.json
rename to package/parser/package.json
index c48c2b3..88d8473 100644
--- a/packages/markdown-parser/package.json
+++ b/package/parser/package.json
@@ -1,15 +1,15 @@
{
- "name": "@fuyeor/markdown-parser",
+ "name": "@ffm/parser",
"version": "0.1.0",
"license": "MIT",
"author": "Fuyeor ",
"type": "module",
"sideEffects": false,
"scripts": {
- "test": "vitest run src/cross-language-fixtures.spec.ts",
- "test:unit": "vitest run src/cross-language-fixtures.spec.ts -t \"standard Markdown fixtures\"",
- "test:ffm": "vitest run src/cross-language-fixtures.spec.ts -t \"ffm fixtures\"",
- "test:safety": "vitest run src/cross-language-fixtures.spec.ts -t \"safety fixtures\"",
+ "test": "vitest run src/index.spec.ts",
+ "test:unit": "vitest run src/index.spec.ts -t \"cfm fixture\"",
+ "test:ffm": "vitest run src/index.spec.ts -t \"ffm fixture\"",
+ "test:safety": "vitest run src/index.spec.ts -t \"safety fixture\"",
"test:compat": "vitest run src/test/index.spec.ts",
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit"
},
@@ -20,7 +20,7 @@
".": "./src/index.ts"
},
"dependencies": {
- "@fuyeor/linkify": "workspace:*"
+ "@ffm/linkify": "workspace:*"
},
"devDependencies": {
"@types/node": "^25.6.0",
diff --git a/packages/markdown-parser/src/core/color.ts b/package/parser/src/core/color.ts
similarity index 86%
rename from packages/markdown-parser/src/core/color.ts
rename to package/parser/src/core/color.ts
index 0389697..db1ca23 100644
--- a/packages/markdown-parser/src/core/color.ts
+++ b/package/parser/src/core/color.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-parser/src/core/color.ts
+// @ffm/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,%.]+\))$/;
diff --git a/packages/markdown-parser/src/core/parser.ts b/package/parser/src/core/parser.ts
similarity index 98%
rename from packages/markdown-parser/src/core/parser.ts
rename to package/parser/src/core/parser.ts
index c105f8a..7f4b043 100644
--- a/packages/markdown-parser/src/core/parser.ts
+++ b/package/parser/src/core/parser.ts
@@ -1,6 +1,6 @@
-// @fuyeor/markdown-parser/src/core/parser.ts
+// @ffm/parser/src/core/parser.ts
import { BlockState, InlineState } from './state';
-import { linkify } from '@fuyeor/linkify';
+import { linkify } from '@ffm/linkify';
import type {
ASTNode,
ASTTransform,
@@ -8,9 +8,9 @@ import type {
InlineRule,
Linkifier,
MarkdownPlugin,
- MarkdownParserOptions,
+ MarkdownParserOption,
ParserContext,
-} from '#/types';
+} from '#/type';
import { isSafeLinkUrl } from '#/core/url';
const LINKIFY_CANDIDATE_REGEX = /(?:https?:\/\/|[A-Za-z0-9-]+\.)/u;
@@ -30,7 +30,7 @@ export class MarkdownParser {
readonly #maxNestingDepth: number;
readonly #linkifier: Linkifier;
- constructor(options: MarkdownParserOptions = {}) {
+ constructor(options: MarkdownParserOption = {}) {
const maxNestingDepth = options.maxNestingDepth ?? 64;
if (!Number.isInteger(maxNestingDepth) || maxNestingDepth < 1)
throw new RangeError('maxNestingDepth must be a positive integer');
diff --git a/packages/markdown-parser/src/core/plain-text.ts b/package/parser/src/core/plain-text.ts
similarity index 78%
rename from packages/markdown-parser/src/core/plain-text.ts
rename to package/parser/src/core/plain-text.ts
index 1b62e3c..ee71b96 100644
--- a/packages/markdown-parser/src/core/plain-text.ts
+++ b/package/parser/src/core/plain-text.ts
@@ -1,9 +1,9 @@
-// @fuyeor/markdown-parser/src/core/plain-text.ts
-import type { ASTNode } from '../types';
+// @ffm/parser/src/core/plain-text.ts
+import type { ASTNode } from '#/type';
-/// Converts Markdown/FFM AST nodes into search-oriented plain text.
-export function toPlainText(nodes: readonly ASTNode[]): string {
- return blockTextList(nodes).replace(/\n+$/u, '');
+/// Converts Markdown/FFM AST node into search-oriented plain text.
+export function toPlainText(node: readonly ASTNode[]): string {
+ return blockTextList(node).replace(/\n+$/u, '');
}
/// Converts one AST node without interpreting HTML or CSS content.
@@ -38,7 +38,8 @@ function blockText(node: ASTNode): string {
case 'chain_item':
return joinTitleAndBody(node.title ?? [], node.children ?? []);
case 'table': {
- const header = node.headers === undefined ? '' : blockTextList(node.headers);
+ const header =
+ node.headers === undefined ? '' : blockTextList(node.headers);
const body = blockTextList(node.children ?? []);
return header.length > 0 && body.length > 0
? `${header}\n${body}`
@@ -56,15 +57,16 @@ function blockText(node: ASTNode): string {
}
/// Converts inline children without inserting formatting boundaries.
-function inlineText(nodes: readonly ASTNode[]): string {
- return nodes.map(blockText).join('');
+function inlineText(node: readonly ASTNode[]): string {
+ return node.map(blockText).join('');
}
/// Converts block children with a stable newline boundary.
function blockTextList(nodes: readonly ASTNode[]): string {
let output = '';
for (const [index, node] of nodes.entries()) {
- if (index > 0 && output.length > 0 && !output.endsWith('\n')) output += '\n';
+ if (index > 0 && output.length > 0 && !output.endsWith('\n'))
+ output += '\n';
output += blockText(node);
}
return output;
@@ -83,6 +85,6 @@ function joinTitleAndBody(
}
/// Joins table cells with boundaries that preserve searchable word separation.
-function joinNodes(nodes: readonly ASTNode[]): string {
- return nodes.map(blockText).join('\n');
+function joinNodes(node: readonly ASTNode[]): string {
+ return node.map(blockText).join('\n');
}
diff --git a/packages/markdown-parser/src/core/render.ts b/package/parser/src/core/render.ts
similarity index 98%
rename from packages/markdown-parser/src/core/render.ts
rename to package/parser/src/core/render.ts
index 5ce79d7..9ce5509 100644
--- a/packages/markdown-parser/src/core/render.ts
+++ b/package/parser/src/core/render.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/core/render.ts
-import type { ASTNode } from '#/types';
+// @ffm/parser/src/core/render.ts
+import type { ASTNode } from '#/type';
import { isSafeColorValue } from '#/core/color';
import { isSafeLinkUrl } from '#/core/url';
diff --git a/packages/markdown-parser/src/core/state.ts b/package/parser/src/core/state.ts
similarity index 97%
rename from packages/markdown-parser/src/core/state.ts
rename to package/parser/src/core/state.ts
index e942df0..bb1073b 100644
--- a/packages/markdown-parser/src/core/state.ts
+++ b/package/parser/src/core/state.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-parser/src/core/state.ts
+// @ffm/parser/src/core/state.ts
export class BlockState {
lines: string[];
lineIndex: number = 0;
diff --git a/packages/markdown-parser/src/core/url.ts b/package/parser/src/core/url.ts
similarity index 92%
rename from packages/markdown-parser/src/core/url.ts
rename to package/parser/src/core/url.ts
index 133f4ee..aa583b2 100644
--- a/packages/markdown-parser/src/core/url.ts
+++ b/package/parser/src/core/url.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-parser/src/core/url.ts
+// @ffm/parser/src/core/url.ts
const SAFE_PROTOCOLS = new Set(['http:', 'https:', 'mailto:', 'tel:']);
diff --git a/packages/markdown-parser/src/default.ts b/package/parser/src/default.ts
similarity index 75%
rename from packages/markdown-parser/src/default.ts
rename to package/parser/src/default.ts
index e44aa12..085b929 100644
--- a/packages/markdown-parser/src/default.ts
+++ b/package/parser/src/default.ts
@@ -1,6 +1,6 @@
-// @fuyeor/markdown-parser/src/default.ts
+// @ffm/parser/src/default.ts
import { MarkdownParser } from './core/parser';
-import type { MarkdownParserOptions } from './types';
+import type { MarkdownParserOption } from './type';
import {
headingRule,
tableRule,
@@ -8,7 +8,7 @@ import {
listRule,
hrRule,
blockquoteRule,
-} from './rules/blocks';
+} from './rule/blocks';
import {
hardBreakRule,
inlineCodeRule,
@@ -17,14 +17,14 @@ import {
underlineRule,
italicRule,
strikeRule,
-} from './rules/inlines';
-import { ffmBlockRule } from './rules/ffm';
-import { latexPlugin } from './plugins/latex';
-import { extensionsPlugin } from './plugins/extensions';
+} from './rule/inlines';
+import { ffmBlockRule } from './rule/ffm';
+import { latexPlugin } from './plugin/latex';
+import { extensionsPlugin } from './plugin/extensions';
-export function createMarkdownParser(options: MarkdownParserOptions = {}) {
+export function createMarkdownParser(option: MarkdownParserOption = {}) {
return (
- new MarkdownParser(options)
+ new MarkdownParser(option)
// block order: Code block -> List -> Title -> Table -> Delete line -> Quote
.addBlockRule(codeBlockRule)
.addBlockRule(listRule)
@@ -46,11 +46,9 @@ export function createMarkdownParser(options: MarkdownParserOptions = {}) {
);
}
-export function createFuyeorMarkdownParser(
- options: MarkdownParserOptions = {},
-) {
+export function createFuyeorMarkdownParser(option: MarkdownParserOption = {}) {
return (
- new MarkdownParser(options)
+ new MarkdownParser(option)
// block order: Code block -> List -> Title -> Table -> Delete line -> Quote
.addBlockRule(ffmBlockRule)
.addBlockRule(codeBlockRule)
diff --git a/packages/markdown-parser/src/cross-language-fixtures.spec.ts b/package/parser/src/index.spec.ts
similarity index 96%
rename from packages/markdown-parser/src/cross-language-fixtures.spec.ts
rename to package/parser/src/index.spec.ts
index 1100248..1982881 100644
--- a/packages/markdown-parser/src/cross-language-fixtures.spec.ts
+++ b/package/parser/src/index.spec.ts
@@ -1,4 +1,4 @@
-// packages/markdown-parser/src/cross-language-fixtures.spec.ts
+// @ffm/parser/src/index.spec.ts
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { describe, expect, it } from 'vitest';
@@ -41,7 +41,10 @@ type SafetyFixtureFile = {
const loadJson = (name: string): T =>
JSON.parse(
- readFileSync(resolve(import.meta.dirname, `../../../fixtures/${name}`), 'utf8'),
+ readFileSync(
+ resolve(import.meta.dirname, `../../../fixture/${name}`),
+ 'utf8',
+ ),
) as T;
const standardFixtures = loadJson('markdown.json');
diff --git a/packages/markdown-parser/src/index.ts b/package/parser/src/index.ts
similarity index 76%
rename from packages/markdown-parser/src/index.ts
rename to package/parser/src/index.ts
index 1ae0548..e59dfa1 100644
--- a/packages/markdown-parser/src/index.ts
+++ b/package/parser/src/index.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-parser/src/index.ts
+// @ffm/parser/src/index.ts
export { MarkdownParser } from './core/parser';
export { render } from './core/render';
export { toPlainText } from './core/plain-text';
@@ -11,7 +11,7 @@ export {
listRule,
hrRule,
blockquoteRule,
-} from './rules/blocks';
+} from './rule/blocks';
export {
hardBreakRule,
inlineCodeRule,
@@ -19,16 +19,16 @@ export {
linkRule,
underlineRule,
strikeRule,
-} from './rules/inlines';
-export { latexPlugin, latexInlineRule, latexBlockRule } from './plugins/latex';
+} from './rule/inlines';
+export { latexPlugin, latexInlineRule, latexBlockRule } from './plugin/latex';
export {
twemojiPlugin,
nativeEmojiPlugin,
twemojiTransform,
transformTwemojiNode,
getTwemojiUrl,
-} from './plugins/twemoji';
-export { highlightPlugin, highlightTransform } from './plugins/highlight';
+} from './plugin/twemoji';
+export { highlightPlugin, highlightTransform } from './plugin/highlight';
export {
abcPlugin,
abcTransform,
@@ -43,8 +43,8 @@ export {
specialBlockPlugin,
specialBlockTransform,
transformSpecialBlockNode,
-} from './plugins/async-blocks';
-export { extensionsPlugin, extensionsTransform } from './plugins/extensions';
+} from './plugin/async-blocks';
+export { extensionsPlugin, extensionsTransform } from './plugin/extensions';
export { createMarkdownParser, createFuyeorMarkdownParser } from './default';
export type {
@@ -55,6 +55,6 @@ export type {
BlockRule,
InlineRule,
Linkifier,
- MarkdownParserOptions,
+ MarkdownParserOption,
MarkdownPlugin,
} from './types';
diff --git a/packages/markdown-parser/src/plugins/ast.ts b/package/parser/src/plugin/ast.ts
similarity index 92%
rename from packages/markdown-parser/src/plugins/ast.ts
rename to package/parser/src/plugin/ast.ts
index db9fd36..94c568c 100644
--- a/packages/markdown-parser/src/plugins/ast.ts
+++ b/package/parser/src/plugin/ast.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/plugins/ast.ts
-import type { ASTNode } from '#/types';
+// @ffm/parser/src/plugin/ast.ts
+import type { ASTNode } from '#/type';
type ASTNodeReplacement = ASTNode | readonly ASTNode[];
diff --git a/packages/markdown-parser/src/plugins/async-blocks.ts b/package/parser/src/plugin/async-blocks.ts
similarity index 95%
rename from packages/markdown-parser/src/plugins/async-blocks.ts
rename to package/parser/src/plugin/async-blocks.ts
index 1af6b5a..eac7bd2 100644
--- a/packages/markdown-parser/src/plugins/async-blocks.ts
+++ b/package/parser/src/plugin/async-blocks.ts
@@ -1,11 +1,6 @@
-// @fuyeor/markdown-parser/src/plugins/async-blocks.ts
+// @ffm/parser/src/plugin/async-blocks.ts
import { InlineState } from '#/core/state';
-import type {
- ASTNode,
- ASTTransform,
- InlineRule,
- MarkdownPlugin,
-} from '#/types';
+import type { ASTNode, ASTTransform, InlineRule, MarkdownPlugin } from '#/type';
import { mapAstNodes } from './ast';
import { normalizeHighlightLanguage } from './highlight';
diff --git a/packages/markdown-parser/src/plugins/extensions.ts b/package/parser/src/plugin/extensions.ts
similarity index 95%
rename from packages/markdown-parser/src/plugins/extensions.ts
rename to package/parser/src/plugin/extensions.ts
index 0086a1e..400f131 100644
--- a/packages/markdown-parser/src/plugins/extensions.ts
+++ b/package/parser/src/plugin/extensions.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/plugins/extensions.ts
-import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/types';
+// @ffm/parser/src/plugin/extensions.ts
+import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/type';
import { mapAstNodes } from './ast';
import { transformTwemojiNode } from './twemoji';
import {
diff --git a/packages/markdown-parser/src/plugins/highlight.ts b/package/parser/src/plugin/highlight.ts
similarity index 95%
rename from packages/markdown-parser/src/plugins/highlight.ts
rename to package/parser/src/plugin/highlight.ts
index 442c76d..348d9ff 100644
--- a/packages/markdown-parser/src/plugins/highlight.ts
+++ b/package/parser/src/plugin/highlight.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/plugins/highlight.ts
-import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/types';
+// @ffm/parser/src/plugin/highlight.ts
+import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/type';
import { mapAstNodes } from './ast';
const languageAliases: Record = {
diff --git a/packages/markdown-parser/src/plugins/latex.ts b/package/parser/src/plugin/latex.ts
similarity index 98%
rename from packages/markdown-parser/src/plugins/latex.ts
rename to package/parser/src/plugin/latex.ts
index 8e561c7..4042326 100644
--- a/packages/markdown-parser/src/plugins/latex.ts
+++ b/package/parser/src/plugin/latex.ts
@@ -1,6 +1,6 @@
-// @fuyeor/markdown-parser/src/plugins/latex.ts
+// @ffm/parser/src/plugin/latex.ts
import { BlockState, InlineState } from '#/core/state';
-import type { BlockRule, InlineRule, MarkdownPlugin } from '#/types';
+import type { BlockRule, InlineRule, MarkdownPlugin } from '#/type';
const MATH_INLINE_MARKER = '$';
const MATH_BLOCK_MARKER = '$$';
diff --git a/packages/markdown-parser/src/plugins/twemoji.ts b/package/parser/src/plugin/twemoji.ts
similarity index 97%
rename from packages/markdown-parser/src/plugins/twemoji.ts
rename to package/parser/src/plugin/twemoji.ts
index cd311db..7621318 100644
--- a/packages/markdown-parser/src/plugins/twemoji.ts
+++ b/package/parser/src/plugin/twemoji.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/plugins/twemoji.ts
-import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/types';
+// @ffm/parser/src/plugin/twemoji.ts
+import type { ASTNode, ASTTransform, MarkdownPlugin } from '#/type';
import { mapAstNodes } from './ast';
const r = String.raw;
diff --git a/packages/markdown-parser/src/rules/blocks.ts b/package/parser/src/rule/blocks.ts
similarity index 99%
rename from packages/markdown-parser/src/rules/blocks.ts
rename to package/parser/src/rule/blocks.ts
index 40364df..627f3f4 100644
--- a/packages/markdown-parser/src/rules/blocks.ts
+++ b/package/parser/src/rule/blocks.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/rules/blocks.ts
-import type { ASTNode, BlockRule } from '#/types';
+// @ffm/parser/src/rule/blocks.ts
+import type { ASTNode, BlockRule } from '#/type';
import { BlockState } from '#/core/state';
type FencedBlock = {
diff --git a/packages/markdown-parser/src/rules/ffm.ts b/package/parser/src/rule/ffm.ts
similarity index 90%
rename from packages/markdown-parser/src/rules/ffm.ts
rename to package/parser/src/rule/ffm.ts
index b1e8cb9..bf5ef1e 100644
--- a/packages/markdown-parser/src/rules/ffm.ts
+++ b/package/parser/src/rule/ffm.ts
@@ -1,16 +1,16 @@
-// @fuyeor/markdown-parser/src/rules/ffm.ts
+// @ffm/parser/src/rule/ffm.ts
import { BlockState } from '#/core/state';
import { extractFencedBlock } from './blocks';
-import type { BlockRule, ASTNode } from '#/types';
+import type { BlockRule, ASTNode } from '#/type';
-const FFM_KEYWORDS = new Set(['quote', 'slide', 'chain', 'accordion']);
+const ffmKeyword = new Set(['quote', 'slide', 'chain', 'accordion']);
// matches two horizontal newline characters before and after the match
// allows spaces in between, e.g., \n\n --- \n\n
-const SLIDE_SEPARATOR_REGEX = /\n\n\s*---\s*\n\n/g;
+const slideSeparatorRegex = /\n\n\s*---\s*\n\n/g;
// ffm block title
-const TITLE_REGEX = /^\s*\*\*(?:\[([ xX])\]\s*)?(.+?)\*\*\s*$/;
+const nodeTitleRegex = /^\s*\*\*(?:\[([ xX])\]\s*)?(.+?)\*\*\s*$/;
/**
* intercept special fuyeor flavored markdown blocks (accordion, slide, chain, quote)
@@ -26,7 +26,7 @@ export const ffmBlockRule: BlockRule = {
const type = block.lang.split(/\s+/)[0];
// skip if it's not a specific keyword, leave it to codeBlockRule.
- if (!FFM_KEYWORDS.has(type)) return null;
+ if (!ffmKeyword.has(type)) return null;
const { content: rawContent, consumedLines } = block;
@@ -41,7 +41,7 @@ export const ffmBlockRule: BlockRule = {
// ffm slide
if (type === 'slide') {
const slideContents = rawContent
- .split(SLIDE_SEPARATOR_REGEX)
+ .split(slideSeparatorRegex)
.filter((s) => s.trim().length > 0);
const slides = slideContents.map((s) => ({
@@ -65,7 +65,7 @@ export const ffmBlockRule: BlockRule = {
type === 'accordion' ? ctx.createId('acc') : undefined;
for (const l of rawContent.split('\n')) {
- const titleMatch = l.match(TITLE_REGEX);
+ const titleMatch = l.match(nodeTitleRegex);
if (titleMatch) {
// archive the previous item, or archive free content
const previousItem = currentItem;
diff --git a/packages/markdown-parser/src/rules/inlines.ts b/package/parser/src/rule/inlines.ts
similarity index 98%
rename from packages/markdown-parser/src/rules/inlines.ts
rename to package/parser/src/rule/inlines.ts
index d5c83fb..bc27bfc 100644
--- a/packages/markdown-parser/src/rules/inlines.ts
+++ b/package/parser/src/rule/inlines.ts
@@ -1,5 +1,5 @@
-// @fuyeor/markdown-parser/src/rules/inlines.ts
-import type { InlineRule } from '#/types';
+// @ffm/parser/src/rule/inlines.ts
+import type { InlineRule } from '#/type';
import { InlineState } from '#/core/state';
import { isSafeColorValue } from '#/core/color';
import { isSafeLinkUrl } from '#/core/url';
diff --git a/packages/markdown-parser/src/test/index.spec.ts b/package/parser/src/test/index.spec.ts
similarity index 93%
rename from packages/markdown-parser/src/test/index.spec.ts
rename to package/parser/src/test/index.spec.ts
index 44250d0..5fe6b60 100644
--- a/packages/markdown-parser/src/test/index.spec.ts
+++ b/package/parser/src/test/index.spec.ts
@@ -1,4 +1,4 @@
-// @fuyeor/markdown-parser/test/index.spec.ts
+// @ffm/parser/test/index.spec.ts
// pnpm test
import { readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
@@ -12,11 +12,11 @@ const failures: any[] = [];
// load test JSON
const commonMark = JSON.parse(
// commonMark: 0.13.2 edition
- readFileSync(resolve(__dirname, './specs/commonMark.json'), 'utf-8'),
+ readFileSync(resolve(__dirname, './spec/commonMark.json'), 'utf-8'),
);
const githubMark = JSON.parse(
// githubMark: 0.29 edition
- readFileSync(resolve(__dirname, './specs/githubMark.json'), 'utf-8'),
+ readFileSync(resolve(__dirname, './spec/githubMark.json'), 'utf-8'),
);
/**
* skip unimplemented test cases
@@ -113,12 +113,11 @@ describe('markdown compatibility test', () => {
});
});
-
// after test complete, write error details into JSON
afterAll(() => {
if (failures.length > 0) {
writeFileSync(
- resolve(__dirname, './tests.failed.json'),
+ resolve(__dirname, './test.failed.json'),
JSON.stringify(failures, null, 2),
'utf-8',
);
diff --git a/packages/markdown-parser/src/test/specs/commonMark.json b/package/parser/src/test/spec/commonMark.json
similarity index 100%
rename from packages/markdown-parser/src/test/specs/commonMark.json
rename to package/parser/src/test/spec/commonMark.json
diff --git a/packages/markdown-parser/src/test/specs/githubMark.json b/package/parser/src/test/spec/githubMark.json
similarity index 100%
rename from packages/markdown-parser/src/test/specs/githubMark.json
rename to package/parser/src/test/spec/githubMark.json
diff --git a/packages/markdown-parser/src/types.ts b/package/parser/src/type.ts
similarity index 94%
rename from packages/markdown-parser/src/types.ts
rename to package/parser/src/type.ts
index bec0595..88c18aa 100644
--- a/packages/markdown-parser/src/types.ts
+++ b/package/parser/src/type.ts
@@ -1,6 +1,5 @@
-// @fuyeor/markdown-parser/src/types.ts
-
-import type { LinkMatch } from '@fuyeor/linkify';
+// @ffm/parser/src/type.ts
+import type { LinkMatch } from '@ffm/linkify';
import type { MarkdownParser } from './core/parser';
import type { BlockState, InlineState } from './core/state';
@@ -83,7 +82,7 @@ export interface InlineRule {
export type Linkifier = (text: string) => readonly LinkMatch[];
-export interface MarkdownParserOptions {
+export interface MarkdownParserOption {
maxNestingDepth?: number;
linkifier?: Linkifier;
}
diff --git a/packages/markdown-parser/tsconfig.json b/package/parser/tsconfig.json
similarity index 100%
rename from packages/markdown-parser/tsconfig.json
rename to package/parser/tsconfig.json
diff --git a/packages/markdown-parser/tsconfig.test.json b/package/parser/tsconfig.test.json
similarity index 78%
rename from packages/markdown-parser/tsconfig.test.json
rename to package/parser/tsconfig.test.json
index f8ad5a9..b59851f 100644
--- a/packages/markdown-parser/tsconfig.test.json
+++ b/package/parser/tsconfig.test.json
@@ -1,4 +1,3 @@
-// packages/markdown-parser/tsconfig.test.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
diff --git a/packages/playground/index.html b/package/playground/index.html
similarity index 100%
rename from packages/playground/index.html
rename to package/playground/index.html
diff --git a/packages/playground/package.json b/package/playground/package.json
similarity index 79%
rename from packages/playground/package.json
rename to package/playground/package.json
index 6dd0db8..78baea6 100644
--- a/packages/playground/package.json
+++ b/package/playground/package.json
@@ -1,5 +1,5 @@
{
- "name": "@fuyeor/markdown-playground",
+ "name": "@ffm/playground",
"license": "MIT",
"author": "Fuyeor ",
"type": "module",
@@ -16,9 +16,9 @@
"@fuyeor/config": "^0.1.1",
"@fuyeor/interactify": "^0.1.1",
"@fuyeor/locale": "^0.1.0",
- "@fuyeor/markdown-formatter": "workspace:*",
- "@fuyeor/markdown-parser": "workspace:*",
- "@fuyeor/markdown-parser-vue": "workspace:*",
+ "@ffm/formatter": "workspace:*",
+ "@ffm/parser": "workspace:*",
+ "@ffm/vue-renderer": "workspace:*",
"@fuyeor/vue-router": "^0.0.9",
"vue": "^3.5.41"
},
diff --git a/packages/playground/public/assets/css/style.css b/package/playground/public/assets/css/style.css
similarity index 100%
rename from packages/playground/public/assets/css/style.css
rename to package/playground/public/assets/css/style.css
diff --git a/packages/playground/public/assets/examples/en.ffm b/package/playground/public/assets/example/en.ffm
similarity index 100%
rename from packages/playground/public/assets/examples/en.ffm
rename to package/playground/public/assets/example/en.ffm
diff --git a/packages/playground/public/assets/examples/fr.ffm b/package/playground/public/assets/example/fr.ffm
similarity index 100%
rename from packages/playground/public/assets/examples/fr.ffm
rename to package/playground/public/assets/example/fr.ffm
diff --git a/packages/playground/public/assets/examples/ja.ffm b/package/playground/public/assets/example/ja.ffm
similarity index 100%
rename from packages/playground/public/assets/examples/ja.ffm
rename to package/playground/public/assets/example/ja.ffm
diff --git a/packages/playground/public/assets/examples/zh-hans.ffm b/package/playground/public/assets/example/zh-hans.ffm
similarity index 100%
rename from packages/playground/public/assets/examples/zh-hans.ffm
rename to package/playground/public/assets/example/zh-hans.ffm
diff --git a/packages/playground/public/favicon.svg b/package/playground/public/favicon.svg
similarity index 100%
rename from packages/playground/public/favicon.svg
rename to package/playground/public/favicon.svg
diff --git a/packages/playground/public/robots.txt b/package/playground/public/robots.txt
similarity index 100%
rename from packages/playground/public/robots.txt
rename to package/playground/public/robots.txt
diff --git a/packages/playground/src/layout/Left.vue b/package/playground/src/@app/component/left-sidebar.vue
similarity index 74%
rename from packages/playground/src/layout/Left.vue
rename to package/playground/src/@app/component/left-sidebar.vue
index d317117..d21f6ba 100644
--- a/packages/playground/src/layout/Left.vue
+++ b/package/playground/src/@app/component/left-sidebar.vue
@@ -1,8 +1,8 @@
-
+
-
+
-
+
{{ t('playground') }}
-
+
-
+
"
}
],
@@ -45,15 +45,15 @@
"value": "table"
},
{
- "path": "/0/headers/0/type",
+ "path": "/0/header/0/type",
"value": "table_cell"
},
{
- "path": "/0/headers/1/type",
+ "path": "/0/header/1/type",
"value": "table_cell"
},
{
- "path": "/0/children/0/type",
+ "path": "/0/content/0/type",
"value": "table_row"
}
],
diff --git a/package/lit-renderer/src/render.ts b/package/lit-renderer/src/render.ts
index 62453f5..5bea102 100644
--- a/package/lit-renderer/src/render.ts
+++ b/package/lit-renderer/src/render.ts
@@ -19,46 +19,46 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
node.level <= 6
? node.level
: null;
- if (level === null) return html`${render(node.children)}`;
+ if (level === null) return html`${render(node.content)}`;
const tagName = `h${level}`;
return staticHtml`
<${unsafeStatic(tagName)}>
- ${render(node.children)}
+ ${render(node.content)}
${unsafeStatic(tagName)}>
`;
}
case 'paragraph':
- return html`${render(node.children)}
`;
+ return html`${render(node.content)}
`;
case 'text':
- return node.content || '';
+ return node.value || '';
case 'bold':
- return html`${render(node.children)}`;
+ return html`${render(node.content)}`;
case 'italic':
- return html`${render(node.children)}`;
+ return html`${render(node.content)}`;
case 'underline':
- return html`${render(node.children)}`;
+ return html`${render(node.content)}`;
case 'strike':
- return html`${render(node.children)}`;
+ return html`${render(node.content)}`;
case 'link': {
const url = String(node.url ?? '').trim();
return isSafeLinkUrl(url)
- ? html`${render(node.children)}`
- : html`${render(node.children)}`;
+ ? html`${render(node.content)}`
+ : html`${render(node.content)}`;
}
case 'inline_code':
- return html`${node.content}`;
+ return html`${node.value}`;
case 'color_code': {
- const color = String(node.content ?? '');
+ const color = String(node.value ?? '');
if (!isSafeColorValue(color)) return html`${color}`;
return html`
@@ -86,7 +86,7 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
${node.lang ? html`${node.lang}
` : ''}
${node.content}
+ : ''}">${node.value}
`;
@@ -98,16 +98,16 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
: undefined;
return staticHtml`
<${unsafeStatic(tag)} start="${startValue || ''}">
- ${render(node.children)}
+ ${render(node.content)}
${unsafeStatic(tag)}>
`;
}
case 'list_item':
- return html`${render(node.children)}`;
+ return html`${render(node.content)}`;
case 'blockquote':
- return html`${render(node.children)}
`;
+ return html`${render(node.content)}
`;
case 'hr':
return html`
`;
@@ -121,17 +121,17 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
- ${(node.headers ?? []).map(
- (cell) => html`| ${render(cell.children)} | `,
+ ${(node.header ?? []).map(
+ (cell) => html`${render(cell.content)} | `,
)}
- ${node.children?.map(
+ ${node.content?.map(
(row) => html`
- ${(row.children ?? []).map(
- (cell) => html`| ${render(cell.children)} | `,
+ ${(row.content ?? []).map(
+ (cell) => html`${render(cell.content)} | `,
)}
`,
@@ -142,13 +142,13 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
`;
case 'accordion':
- return html`${render(node.children)}
`;
+ return html`${render(node.content)}
`;
case 'accordion_item':
return html`
${render(node.title)}
- ${render(node.children)}
+ ${render(node.content)}
`;
@@ -156,18 +156,16 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
case 'slide':
return html`
-
${render(node.children)}
+
${render(node.content)}
`;
case 'slide_item':
- return html`${render(node.children)}
`;
+ return html`${render(node.content)}
`;
// FFM Chain
case 'chain':
- return html`
- ${render(node.children)}
-
`;
+ return html`${render(node.content)}
`;
case 'chain_item': {
const statusClass = node.hasCheckbox
@@ -183,7 +181,7 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
${node.title && node.title.length > 0
? html`${render(node.title)}
`
: ''}
- ${render(node.children)}
+ ${render(node.content)}
`;
@@ -191,9 +189,7 @@ export function render(node?: ASTNode[]): (TemplateResult | string | null)[] {
default:
// fallback render
- return node.children
- ? html`${render(node.children)}`
- : null;
+ return node.content ? html`${render(node.content)}` : null;
}
});
}
diff --git a/package/parser/src/core/parser.ts b/package/parser/src/core/parser.ts
index 7f4b043..004e986 100644
--- a/package/parser/src/core/parser.ts
+++ b/package/parser/src/core/parser.ts
@@ -107,10 +107,10 @@ export class MarkdownParser {
return [
{
type: 'paragraph',
- children: [
+ content: [
{
type: 'text',
- content: state.remainingLines.join('\n'),
+ value: state.remainingLines.join('\n'),
},
],
},
@@ -223,7 +223,7 @@ export class MarkdownParser {
if (paragraphLines.length > 0) {
nodes.push({
type: 'paragraph',
- children: this.#parseInline(
+ content: this.#parseInline(
new InlineState(paragraphLines.join('\n')),
depth,
),
@@ -236,7 +236,7 @@ export class MarkdownParser {
#parseInline(state: InlineState, depth = 0): ASTNode[] {
if (depth > this.#maxNestingDepth) {
- return [{ type: 'text', content: state.content.slice(state.pos) }];
+ return [{ type: 'text', value: state.content.slice(state.pos) }];
}
const context = depth === 0 ? this.#context : this.#createContext(depth);
@@ -256,20 +256,20 @@ export class MarkdownParser {
if (match.index > lastIdx) {
nodes.push({
type: 'text',
- content: textBuffer.slice(lastIdx, match.index),
+ value: textBuffer.slice(lastIdx, match.index),
});
}
nodes.push({
type: 'link',
url: match.url,
- children: [{ type: 'text', content: match.text }],
+ content: [{ type: 'text', value: match.text }],
});
lastIdx = match.lastIndex;
}
}
if (lastIdx < textBuffer.length) {
- nodes.push({ type: 'text', content: textBuffer.slice(lastIdx) });
+ nodes.push({ type: 'text', value: textBuffer.slice(lastIdx) });
}
}
};
diff --git a/package/parser/src/core/plain-text.ts b/package/parser/src/core/plain-text.ts
index ee71b96..231fc1d 100644
--- a/package/parser/src/core/plain-text.ts
+++ b/package/parser/src/core/plain-text.ts
@@ -12,9 +12,9 @@ function blockText(node: ASTNode): string {
case 'text':
case 'inline_code':
case 'color_code':
- return node.content ?? '';
+ return node.value ?? '';
case 'code_block':
- return node.content ?? '';
+ return node.value ?? '';
case 'heading':
case 'paragraph':
case 'bold':
@@ -23,7 +23,7 @@ function blockText(node: ASTNode): string {
case 'strike':
case 'link':
case 'table_cell':
- return inlineText(node.children ?? []);
+ return inlineText(node.content ?? []);
case 'blockquote':
case 'list':
case 'list_item':
@@ -32,27 +32,27 @@ function blockText(node: ASTNode): string {
case 'accordion':
case 'chain':
case 'root':
- return blockTextList(node.children ?? []);
+ return blockTextList(node.content ?? []);
case 'accordion_item':
- return joinTitleAndBody(node.title ?? [], node.children ?? []);
+ return joinTitleAndBody(node.title ?? [], node.content ?? []);
case 'chain_item':
- return joinTitleAndBody(node.title ?? [], node.children ?? []);
+ return joinTitleAndBody(node.title ?? [], node.content ?? []);
case 'table': {
const header =
- node.headers === undefined ? '' : blockTextList(node.headers);
- const body = blockTextList(node.children ?? []);
+ node.header === undefined ? '' : blockTextList(node.header);
+ const body = blockTextList(node.content ?? []);
return header.length > 0 && body.length > 0
? `${header}\n${body}`
: header || body;
}
case 'table_row':
- return joinNodes(node.children ?? []);
+ return joinNodes(node.content ?? []);
case 'hardbreak':
return '\n';
case 'hr':
return '';
default:
- return blockTextList(node.children ?? []);
+ return blockTextList(node.content ?? []);
}
}
diff --git a/package/parser/src/core/render.ts b/package/parser/src/core/render.ts
index 9ce5509..ebce92e 100644
--- a/package/parser/src/core/render.ts
+++ b/package/parser/src/core/render.ts
@@ -65,46 +65,46 @@ export function render(nodes?: ASTNode[]): string {
const level = getHeadingLevel(node.level);
html +=
level === null
- ? `${render(node.children)}`
- : `${render(node.children)}\n`;
+ ? `${render(node.content)}`
+ : `${render(node.content)}\n`;
break;
}
case 'paragraph':
- html += `${render(node.children)}
\n`;
+ html += `${render(node.content)}
\n`;
break;
case 'text':
- html += node.content ? escapeHtml(node.content) : '';
+ html += node.value ? escapeHtml(node.value) : '';
break;
case 'emoji': {
- const emoji = String(node.content ?? '');
+ const emoji = String(node.value ?? '');
const alt = String(node.alt ?? emoji);
const src = String(node.src ?? '');
html += `
`;
break;
}
case 'math_inline':
- html += `${escapeHtml(String(node.content ?? ''))}`;
+ html += `${escapeHtml(String(node.value ?? ''))}`;
break;
case 'math_block':
- html += `${escapeHtml(String(node.content ?? ''))}
\n`;
+ html += `${escapeHtml(String(node.value ?? ''))}
\n`;
break;
case 'bold':
- html += `${render(node.children)}`;
+ html += `${render(node.content)}`;
break;
case 'italic':
- html += `${render(node.children)}`;
+ html += `${render(node.content)}`;
break;
case 'underline':
- html += `${render(node.children)}`;
+ html += `${render(node.content)}`;
break;
case 'strike':
- html += `${render(node.children)}`;
+ html += `${render(node.content)}`;
break;
case 'inline_code':
- html += `${node.content ? escapeHtml(node.content) : ''}`;
+ html += `${node.value ? escapeHtml(node.value) : ''}`;
break;
case 'color_code': {
- const color = String(node.content ?? '');
+ const color = String(node.value ?? '');
if (!isSafeColorValue(color)) {
html += escapeHtml(color);
break;
@@ -115,8 +115,8 @@ export function render(nodes?: ASTNode[]): string {
case 'link': {
const url = String(node.url ?? '').trim();
html += isSafeLinkUrl(url)
- ? `${render(node.children)}`
- : render(node.children);
+ ? `${render(node.content)}`
+ : render(node.content);
break;
}
case 'code_block': {
@@ -129,7 +129,7 @@ export function render(nodes?: ASTNode[]): string {
if (language) {
html += `${escapeHtml(displayLanguage)}
`;
}
- html += `${escapeHtml(String(node.content ?? ''))}\n
\n`;
+ html += `${escapeHtml(String(node.value ?? ''))}\n
\n`;
break;
}
case 'list': {
@@ -138,23 +138,23 @@ export function render(nodes?: ASTNode[]): string {
node.ordered && node.start && node.start !== 1
? ` start="${node.start}"`
: '';
- html += `<${tag}${start}>\n${render(node.children)}${tag}>\n`;
+ html += `<${tag}${start}>\n${render(node.content)}${tag}>\n`;
break;
}
case 'list_item':
- html += `${render(node.children)}\n`;
+ html += `${render(node.content)}\n`;
break;
case 'mermaid':
- html += `${escapeHtml(String(node.content ?? ''))}
\n`;
+ html += `${escapeHtml(String(node.value ?? ''))}
\n`;
break;
case 'abc':
- html += `${escapeHtml(String(node.content ?? ''))}
\n`;
+ html += `${escapeHtml(String(node.value ?? ''))}
\n`;
break;
case 'smiles_inline':
- html += ``;
+ html += ``;
break;
case 'smiles_block': {
- const blocks = String(node.content ?? '')
+ const blocks = String(node.value ?? '')
.split('\n')
.filter((line) => line.trim().length > 0)
.map(
@@ -166,19 +166,19 @@ export function render(nodes?: ASTNode[]): string {
}
case 'table': {
html += '\n';
- if (node.headers) {
+ if (node.header) {
html += '\n\n';
- node.headers.forEach((cell) => {
- html += `| ${render(cell.children)} | \n`;
+ node.header.forEach((cell) => {
+ html += `${render(cell.content)} | \n`;
});
html += '
\n\n';
}
- if (node.children && node.children.length > 0) {
+ if (node.content && node.content.length > 0) {
html += '\n';
- node.children.forEach((row) => {
+ node.content.forEach((row) => {
html += '\n';
- row.children?.forEach((cell) => {
- html += `| ${render(cell.children)} | \n`;
+ row.content?.forEach((cell) => {
+ html += `${render(cell.content)} | \n`;
});
html += '
\n';
});
@@ -191,16 +191,16 @@ export function render(nodes?: ASTNode[]): string {
html += '
\n';
break;
case 'blockquote':
- html += `\n${render(node.children)}
\n`;
+ html += `\n${render(node.content)}
\n`;
break;
case 'accordion':
- html += `${render(node.children)}
`;
+ html += `${render(node.content)}
`;
break;
case 'accordion_item':
- html += `${render(node.title)}
${render(node.children)}
`;
+ html += `${render(node.title)}
${render(node.content)}
`;
break;
case 'chain':
- html += `${render(node.children)}
`;
+ html += `${render(node.content)}
`;
break;
case 'chain_item': {
const statusClass = node.hasCheckbox
@@ -212,20 +212,20 @@ export function render(nodes?: ASTNode[]): string {
node.title && node.title.length > 0
? `${render(node.title)}
`
: '';
- html += `${title}
${render(node.children)}
`;
+ html += `${title}
${render(node.content)}
`;
break;
}
case 'slide':
- html += ``;
+ html += ``;
break;
case 'slide_item':
- html += `${render(node.children)}
`;
+ html += `${render(node.content)}
`;
break;
case 'hardbreak':
html += '
\n';
break;
default:
- html += `${render(node.children)}`;
+ html += `${render(node.content)}`;
break;
}
}
diff --git a/package/parser/src/index.spec.ts b/package/parser/src/index.spec.ts
index 1982881..a8d73f1 100644
--- a/package/parser/src/index.spec.ts
+++ b/package/parser/src/index.spec.ts
@@ -123,6 +123,44 @@ function executeMarkdownSuite(
executeMarkdownSuite('standard Markdown fixtures', standardFixtures, false);
executeMarkdownSuite('FFM fixtures', ffmFixtures, true);
+// Verify the public AST shape uses the modern scalar and structural field names.
+describe('AST field names', () => {
+ it('uses value, content, and header fields', () => {
+ const ast = createMarkdownParser()('# Title\n\nhello');
+ expect(ast).toEqual([
+ {
+ type: 'heading',
+ level: 1,
+ content: [{ type: 'text', value: 'Title' }],
+ },
+ {
+ type: 'paragraph',
+ content: [{ type: 'text', value: 'hello' }],
+ },
+ ]);
+
+ const table = createMarkdownParser()(
+ '\n| A | B |\n|---|---|\n| C | D |',
+ );
+ expect(table[0]).toMatchObject({
+ type: 'table',
+ header: [
+ { type: 'table_cell', content: [{ type: 'text', value: 'A' }] },
+ { type: 'table_cell', content: [{ type: 'text', value: 'B' }] },
+ ],
+ content: [
+ {
+ type: 'table_row',
+ content: [
+ { type: 'table_cell', content: [{ type: 'text', value: 'C' }] },
+ { type: 'table_cell', content: [{ type: 'text', value: 'D' }] },
+ ],
+ },
+ ],
+ });
+ });
+});
+
describe('safety fixtures', () => {
expect(safetyFixtures.schema_version).toBe(1);
for (const fixture of safetyFixtures.links)
diff --git a/package/parser/src/index.ts b/package/parser/src/index.ts
index e59dfa1..bfe848b 100644
--- a/package/parser/src/index.ts
+++ b/package/parser/src/index.ts
@@ -57,4 +57,4 @@ export type {
Linkifier,
MarkdownParserOption,
MarkdownPlugin,
-} from './types';
+} from './type';
diff --git a/package/parser/src/plugin/ast.ts b/package/parser/src/plugin/ast.ts
index 94c568c..781dce1 100644
--- a/package/parser/src/plugin/ast.ts
+++ b/package/parser/src/plugin/ast.ts
@@ -12,12 +12,12 @@ export const mapAstNodes = (
for (let index = 0; index < nodes.length; index++) {
const node = nodes[index];
- const transformedChildren = node.children
- ? mapAstNodes(node.children, transformNode)
- : node.children;
+ const transformedChildren = node.content
+ ? mapAstNodes(node.content, transformNode)
+ : node.content;
const transformedNode =
- transformedChildren && transformedChildren !== node.children
- ? { ...node, children: transformedChildren }
+ transformedChildren && transformedChildren !== node.content
+ ? { ...node, content: transformedChildren }
: node;
const replacement = transformNode(transformedNode);
const replacementNodes = Array.isArray(replacement)
diff --git a/package/parser/src/plugin/async-blocks.ts b/package/parser/src/plugin/async-blocks.ts
index eac7bd2..ec79c44 100644
--- a/package/parser/src/plugin/async-blocks.ts
+++ b/package/parser/src/plugin/async-blocks.ts
@@ -18,7 +18,7 @@ export const smilesInlineRule: InlineRule = {
if (!match) return null;
return {
- node: { type: 'smiles_inline', content: match[1].trim() },
+ node: { type: 'smiles_inline', value: match[1].trim() },
consumedChars: match[0].length,
};
},
@@ -36,7 +36,7 @@ export const transformSpecialBlockNode = (
return {
type: language === 'smiles' ? 'smiles_block' : language,
- content: String(node.content ?? ''),
+ value: String(node.value ?? ''),
} satisfies ASTNode;
};
diff --git a/package/parser/src/plugin/latex.ts b/package/parser/src/plugin/latex.ts
index 4042326..092cf98 100644
--- a/package/parser/src/plugin/latex.ts
+++ b/package/parser/src/plugin/latex.ts
@@ -44,7 +44,7 @@ export const latexInlineRule: InlineRule = {
return {
node: {
type: 'math_inline',
- content: state.content
+ value: state.content
.slice(state.pos + MATH_INLINE_MARKER.length, endIndex)
.trim(),
},
@@ -86,7 +86,7 @@ export const latexBlockRule: BlockRule = {
return {
node: {
type: 'math_block',
- content: contentLines.join('\n').trim(),
+ value: contentLines.join('\n').trim(),
},
consumedLines,
};
diff --git a/package/parser/src/plugin/twemoji.ts b/package/parser/src/plugin/twemoji.ts
index 7621318..fb01ac1 100644
--- a/package/parser/src/plugin/twemoji.ts
+++ b/package/parser/src/plugin/twemoji.ts
@@ -18,7 +18,7 @@ export const getTwemojiUrl = (emoji: string): string => {
// Split text nodes into plain text and emoji image nodes without touching code.
export const transformTwemojiNode = (node: ASTNode): ASTNode[] => {
- const content = node.content;
+ const content = node.value;
if (typeof content !== 'string' || content.length === 0) return [node];
emojiRegex.lastIndex = 0;
@@ -35,12 +35,12 @@ export const transformTwemojiNode = (node: ASTNode): ASTNode[] => {
if (matchIndex > lastIndex) {
nodes.push({
type: 'text',
- content: content.slice(lastIndex, matchIndex),
+ value: content.slice(lastIndex, matchIndex),
});
}
nodes.push({
type: 'emoji',
- content: emoji,
+ value: emoji,
alt: emoji,
src: getTwemojiUrl(emoji),
});
@@ -48,7 +48,7 @@ export const transformTwemojiNode = (node: ASTNode): ASTNode[] => {
}
if (lastIndex < content.length) {
- nodes.push({ type: 'text', content: content.slice(lastIndex) });
+ nodes.push({ type: 'text', value: content.slice(lastIndex) });
}
return nodes;
};
diff --git a/package/parser/src/rule/blocks.ts b/package/parser/src/rule/blocks.ts
index 627f3f4..0c9a372 100644
--- a/package/parser/src/rule/blocks.ts
+++ b/package/parser/src/rule/blocks.ts
@@ -30,7 +30,7 @@ export const headingRule: BlockRule = {
node: {
type: 'heading',
level: match[1].length,
- children: match[2] ? ctx.parseInline(match[2].trim()) : [],
+ content: match[2] ? ctx.parseInline(match[2].trim()) : [],
},
consumedLines: 1,
};
@@ -48,7 +48,7 @@ export const codeBlockRule: BlockRule = {
if (!block) return null;
return {
- node: { type: 'code_block', lang: block.lang, content: block.content },
+ node: { type: 'code_block', lang: block.lang, value: block.content },
consumedLines: block.consumedLines,
};
},
@@ -191,7 +191,7 @@ const createTableCell = (
): ASTNode => {
const cell: ASTNode = {
type: 'table_cell',
- children: parseInline(content),
+ content: parseInline(content),
};
if (alignment) cell.align = alignment;
return cell;
@@ -238,7 +238,7 @@ export const tableRule: BlockRule = {
rows.push({
type: 'table_row',
- children: normalizeTableCells(
+ content: normalizeTableCells(
extractTableCells(rowLine),
columnCount,
).map((cell, index) =>
@@ -253,12 +253,12 @@ export const tableRule: BlockRule = {
type: 'table',
...(headers
? {
- headers: headers.map((header, index) =>
+ header: headers.map((header, index) =>
createTableCell(header, alignments[index], ctx.parseInline),
),
}
: {}),
- children: rows,
+ content: rows,
},
consumedLines,
};
@@ -319,7 +319,7 @@ export const blockquoteRule: BlockRule = {
return {
node: {
type: 'blockquote',
- children: ctx.parseBlocks(contentLines.join('\n')),
+ content: ctx.parseBlocks(contentLines.join('\n')),
},
consumedLines,
};
@@ -418,7 +418,7 @@ export const listRule: BlockRule = {
items.push({
type: 'list_item',
- children: ctx.parseBlocks(itemLines.join('\n')),
+ content: ctx.parseBlocks(itemLines.join('\n')),
});
consumedLines += itemConsumedLines;
} else {
@@ -431,7 +431,7 @@ export const listRule: BlockRule = {
type: 'list',
ordered: isOrdered,
start: startNumber,
- children: items,
+ content: items,
},
consumedLines,
};
diff --git a/package/parser/src/rule/ffm.ts b/package/parser/src/rule/ffm.ts
index bf5ef1e..53a0b22 100644
--- a/package/parser/src/rule/ffm.ts
+++ b/package/parser/src/rule/ffm.ts
@@ -33,7 +33,7 @@ export const ffmBlockRule: BlockRule = {
// ffm quote: recursively render the internal Markdown
if (type === 'quote') {
return {
- node: { type: 'blockquote', children: ctx.parseBlocks(rawContent) },
+ node: { type: 'blockquote', content: ctx.parseBlocks(rawContent) },
consumedLines,
};
}
@@ -46,10 +46,10 @@ export const ffmBlockRule: BlockRule = {
const slides = slideContents.map((s) => ({
type: 'slide_item',
- children: ctx.parseBlocks(s.trim()),
+ content: ctx.parseBlocks(s.trim()),
}));
- return { node: { type: 'slide', children: slides }, consumedLines };
+ return { node: { type: 'slide', content: slides }, consumedLines };
}
// ffm chain and accordion
@@ -78,7 +78,7 @@ export const ffmBlockRule: BlockRule = {
preambleLines = currentLines;
}
} else {
- previousItem.children = ctx.parseBlocks(
+ previousItem.content = ctx.parseBlocks(
currentLines.join('\n').trim(),
);
}
@@ -88,7 +88,7 @@ export const ffmBlockRule: BlockRule = {
type: type === 'accordion' ? 'accordion_item' : 'chain_item',
name: accordionName,
title: ctx.parseInline(titleMatch[2]),
- children: [],
+ content: [],
...(type === 'chain'
? {
isCompleted: checkboxMark
@@ -108,7 +108,7 @@ export const ffmBlockRule: BlockRule = {
// archive the last item or go back
const lastItem = currentItem;
if (lastItem) {
- lastItem.children = ctx.parseBlocks(currentLines.join('\n').trim());
+ lastItem.content = ctx.parseBlocks(currentLines.join('\n').trim());
} else {
// if no valid title is found from beginning to end
// revert to displaying regular content
@@ -117,18 +117,18 @@ export const ffmBlockRule: BlockRule = {
}
}
- const children: ASTNode[] = [];
+ const content: ASTNode[] = [];
if (preambleLines.length > 0) {
- children.push(...ctx.parseBlocks(preambleLines.join('\n').trim()));
+ content.push(...ctx.parseBlocks(preambleLines.join('\n').trim()));
}
- children.push(...items);
+ content.push(...items);
- return { node: { type, name: accordionName, children }, consumedLines };
+ return { node: { type, name: accordionName, content }, consumedLines };
}
// fallback
return {
- node: { type: 'code_block', lang: type, content: rawContent },
+ node: { type: 'code_block', lang: type, value: rawContent },
consumedLines,
};
},
diff --git a/package/parser/src/rule/inlines.ts b/package/parser/src/rule/inlines.ts
index bc27bfc..c9acf35 100644
--- a/package/parser/src/rule/inlines.ts
+++ b/package/parser/src/rule/inlines.ts
@@ -40,7 +40,7 @@ export const boldRule: InlineRule = {
node: {
type: 'bold',
// supports nested parsing
- children: ctx.parseInline(innerContent),
+ content: ctx.parseInline(innerContent),
},
// +2 is because the followed `**` was consumed
consumedChars: endIdx - state.pos + 2,
@@ -68,7 +68,7 @@ export const italicRule: InlineRule = {
return {
node: {
type: 'italic',
- children: ctx.parseInline(state.content.slice(state.pos + 1, endIdx)),
+ content: ctx.parseInline(state.content.slice(state.pos + 1, endIdx)),
},
consumedChars: endIdx - state.pos + 1,
};
@@ -111,7 +111,7 @@ export const inlineCodeRule: InlineRule = {
if (endIdx === -1) {
return {
- node: { type: 'text', content: marker },
+ node: { type: 'text', value: marker },
consumedChars: markerLength,
};
}
@@ -132,7 +132,7 @@ export const inlineCodeRule: InlineRule = {
return {
node: {
type: isColor ? 'color_code' : 'inline_code',
- content: rawContent,
+ value: rawContent,
},
consumedChars: endIdx + markerLength - state.pos,
};
@@ -168,7 +168,7 @@ export const linkRule: InlineRule = {
node: {
type: 'link',
url,
- children: ctx.parseInline(innerText),
+ content: ctx.parseInline(innerText),
},
consumedChars: urlEnd - state.pos + 1,
};
@@ -190,7 +190,7 @@ export const underlineRule: InlineRule = {
return {
node: {
type: 'underline',
- children: ctx.parseInline(state.content.slice(state.pos + 2, endIdx)),
+ content: ctx.parseInline(state.content.slice(state.pos + 2, endIdx)),
},
consumedChars: endIdx - state.pos + 2,
};
@@ -212,7 +212,7 @@ export const strikeRule: InlineRule = {
return {
node: {
type: 'strike',
- children: ctx.parseInline(state.content.slice(state.pos + 2, endIdx)),
+ content: ctx.parseInline(state.content.slice(state.pos + 2, endIdx)),
},
consumedChars: endIdx - state.pos + 2,
};
diff --git a/package/parser/src/type.ts b/package/parser/src/type.ts
index 88c18aa..0c545b7 100644
--- a/package/parser/src/type.ts
+++ b/package/parser/src/type.ts
@@ -32,14 +32,14 @@ export type NodeType =
export interface ASTNode {
type: NodeType;
- content?: string;
- children?: ASTNode[];
+ value?: string;
+ content?: ASTNode[];
level?: number;
lang?: string;
url?: string;
ordered?: boolean;
start?: number;
- headers?: ASTNode[];
+ header?: ASTNode[];
name?: string;
title?: ASTNode[];
isCompleted?: boolean;
diff --git a/package/vue-renderer/src/render.ts b/package/vue-renderer/src/render.ts
index 70782ea..8b8dbe0 100644
--- a/package/vue-renderer/src/render.ts
+++ b/package/vue-renderer/src/render.ts
@@ -14,25 +14,25 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
? node.level
: null;
return level === null
- ? h('span', renderToVue(node.children || []))
- : h(`h${level}`, renderToVue(node.children || []));
+ ? h('span', renderToVue(node.content || []))
+ : h(`h${level}`, renderToVue(node.content || []));
}
case 'paragraph':
- return h('p', renderToVue(node.children || []));
+ return h('p', renderToVue(node.content || []));
case 'text':
- return node.content || ''; // Vue ๅคฉ็ถ้ฒ XSS๏ผ็ดๆฅ่ฟๅๅญ็ฌฆไธฒ
+ return node.value || ''; // Vue ๅคฉ็ถ้ฒ XSS๏ผ็ดๆฅ่ฟๅๅญ็ฌฆไธฒ
case 'bold':
- return h('strong', renderToVue(node.children || []));
+ return h('strong', renderToVue(node.content || []));
case 'italic':
- return h('em', renderToVue(node.children || []));
+ return h('em', renderToVue(node.content || []));
case 'underline':
- return h('u', renderToVue(node.children || []));
+ return h('u', renderToVue(node.content || []));
case 'strike':
- return h('del', renderToVue(node.children || []));
+ return h('del', renderToVue(node.content || []));
case 'inline_code':
- return h('code', node.content || '');
+ return h('code', node.value || '');
case 'color_code': {
- const color = String(node.content ?? '');
+ const color = String(node.value ?? '');
if (!isSafeColorValue(color)) return color;
return h('code', { class: 'ffm-color-code' }, [
h('span', {
@@ -57,9 +57,9 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
? h(
'a',
{ href: url, target: '_blank', rel: 'noopener noreferrer' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
)
- : h('span', renderToVue(node.children || []));
+ : h('span', renderToVue(node.content || []));
}
case 'code_block':
return h('div', { class: 'code-block-wrapper' }, [
@@ -70,7 +70,7 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
h(
'code',
{ class: node.lang ? `language-${node.lang}` : '' },
- node.content || '',
+ node.value || '',
),
),
]);
@@ -80,10 +80,10 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
node.ordered && node.start && node.start !== 1
? { start: node.start }
: {},
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
);
case 'list_item':
- return h('li', renderToVue(node.children || []));
+ return h('li', renderToVue(node.content || []));
case 'table':
return h('table', null, [
h(
@@ -92,21 +92,21 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
h(
'tr',
null,
- (node.headers ?? []).map((cell) =>
- h('th', renderToVue(cell.children || [])),
+ (node.header ?? []).map((cell) =>
+ h('th', renderToVue(cell.content || [])),
),
),
),
- node.children && node.children.length > 0
+ node.content && node.content.length > 0
? h(
'tbody',
null,
- node.children.map((row) =>
+ node.content.map((row) =>
h(
'tr',
null,
- (row.children ?? []).map((cell) =>
- h('td', renderToVue(cell.children || [])),
+ (row.content ?? []).map((cell) =>
+ h('td', renderToVue(cell.content || [])),
),
),
),
@@ -116,14 +116,14 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
case 'hr':
return h('hr');
case 'blockquote':
- return h('blockquote', renderToVue(node.children || []));
+ return h('blockquote', renderToVue(node.content || []));
case 'hardbreak':
return h('br');
case 'accordion':
return h(
'div',
{ class: 'ffm-accordion' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
);
case 'accordion_item':
return h('details', { name: node.name }, [
@@ -131,14 +131,14 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
h(
'div',
{ class: 'accordion-content' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
),
]);
case 'chain':
return h(
'div',
{ class: 'chain-container' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
);
case 'chain_item': {
const statusClass = node.hasCheckbox
@@ -152,7 +152,7 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
node.title && node.title.length > 0
? h('div', { class: 'chain-title' }, renderToVue(node.title))
: null,
- h('div', { class: 'chain-body' }, renderToVue(node.children || [])),
+ h('div', { class: 'chain-body' }, renderToVue(node.content || [])),
]),
]);
}
@@ -161,17 +161,17 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] {
h(
'div',
{ class: 'slide-container' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
),
]);
case 'slide_item':
return h(
'div',
{ class: 'slide-item' },
- renderToVue(node.children || []),
+ renderToVue(node.content || []),
);
default:
- return h('span', renderToVue(node.children || []));
+ return h('span', renderToVue(node.content || []));
}
});
}
From 67c6bff392adb3bfa80bbe9162fe512d953d50d8 Mon Sep 17 00:00:00 2001
From: Fuyeor
Date: Tue, 22 Sep 2026 13:25:20 +0800
Subject: [PATCH 5/7] singularize marker array in parser
---
package/parser/src/core/parser.ts | 8 ++---
package/parser/src/default.ts | 4 +--
package/parser/src/plugin/async-blocks.ts | 2 +-
package/parser/src/plugin/latex.ts | 30 +++++++++----------
.../parser/src/rule/{blocks.ts => block.ts} | 16 +++++-----
package/parser/src/rule/ffm.ts | 4 +--
.../parser/src/rule/{inlines.ts => inline.ts} | 16 +++++-----
package/parser/src/type.ts | 6 ++--
8 files changed, 43 insertions(+), 43 deletions(-)
rename package/parser/src/rule/{blocks.ts => block.ts} (97%)
rename package/parser/src/rule/{inlines.ts => inline.ts} (97%)
diff --git a/package/parser/src/core/parser.ts b/package/parser/src/core/parser.ts
index 004e986..e7d416b 100644
--- a/package/parser/src/core/parser.ts
+++ b/package/parser/src/core/parser.ts
@@ -1,6 +1,6 @@
// @ffm/parser/src/core/parser.ts
-import { BlockState, InlineState } from './state';
import { linkify } from '@ffm/linkify';
+import { BlockState, InlineState } from './state';
import type {
ASTNode,
ASTTransform,
@@ -58,7 +58,7 @@ export class MarkdownParser {
// register block rule
// By default, it inserts at the end, or before/after a specified rule.
addBlockRule(rule: BlockRule): this {
- for (const marker of rule.markers) {
+ for (const marker of rule.marker) {
const list = this.#blockRuleMap.get(marker) || [];
list.push(rule);
this.#blockRuleMap.set(marker, list);
@@ -68,8 +68,8 @@ export class MarkdownParser {
// register inline rule
addInlineRule(rule: InlineRule): this {
- // register the rule under its markers for quick lookup during parsing
- for (const marker of rule.markers) {
+ // register the rule under its marker for quick lookup during parsing
+ for (const marker of rule.marker) {
const rulesForMarker = this.#inlineRuleMap.get(marker) || [];
// ensure the rule is in the list for this marker
rulesForMarker.unshift(rule);
diff --git a/package/parser/src/default.ts b/package/parser/src/default.ts
index 085b929..f258866 100644
--- a/package/parser/src/default.ts
+++ b/package/parser/src/default.ts
@@ -8,7 +8,7 @@ import {
listRule,
hrRule,
blockquoteRule,
-} from './rule/blocks';
+} from './rule/block';
import {
hardBreakRule,
inlineCodeRule,
@@ -17,7 +17,7 @@ import {
underlineRule,
italicRule,
strikeRule,
-} from './rule/inlines';
+} from './rule/inline';
import { ffmBlockRule } from './rule/ffm';
import { latexPlugin } from './plugin/latex';
import { extensionsPlugin } from './plugin/extensions';
diff --git a/package/parser/src/plugin/async-blocks.ts b/package/parser/src/plugin/async-blocks.ts
index ec79c44..6145159 100644
--- a/package/parser/src/plugin/async-blocks.ts
+++ b/package/parser/src/plugin/async-blocks.ts
@@ -10,7 +10,7 @@ export const SPECIAL_BLOCK_LANGUAGES = new Set(['mermaid', 'abc', 'smiles']);
// Parse the FFM #[smiles = `...`] inline placeholder syntax.
export const smilesInlineRule: InlineRule = {
name: 'smiles_inline',
- markers: ['#'],
+ marker: ['#'],
parse(state: InlineState) {
if (state.currentChar !== '#') return null;
diff --git a/package/parser/src/plugin/latex.ts b/package/parser/src/plugin/latex.ts
index 092cf98..4936f0a 100644
--- a/package/parser/src/plugin/latex.ts
+++ b/package/parser/src/plugin/latex.ts
@@ -2,8 +2,8 @@
import { BlockState, InlineState } from '#/core/state';
import type { BlockRule, InlineRule, MarkdownPlugin } from '#/type';
-const MATH_INLINE_MARKER = '$';
-const MATH_BLOCK_MARKER = '$$';
+const inlineMathMarker = '$';
+const blockMathMarker = '$$';
// Find an unescaped dollar delimiter without interpreting LaTeX escapes.
const findMathDelimiter = (
@@ -29,15 +29,15 @@ const findMathDelimiter = (
// Parse inline $...$ formulas while leaving unmatched dollars as plain text.
export const latexInlineRule: InlineRule = {
name: 'math_inline',
- markers: [MATH_INLINE_MARKER],
+ marker: [inlineMathMarker],
parse(state: InlineState) {
- if (state.currentChar !== MATH_INLINE_MARKER) return null;
- if (state.content.startsWith(MATH_BLOCK_MARKER, state.pos)) return null;
+ if (state.currentChar !== inlineMathMarker) return null;
+ if (state.content.startsWith(blockMathMarker, state.pos)) return null;
const endIndex = findMathDelimiter(
state.content,
- state.pos + MATH_INLINE_MARKER.length,
- MATH_INLINE_MARKER,
+ state.pos + inlineMathMarker.length,
+ inlineMathMarker,
);
if (endIndex === -1) return null;
@@ -45,10 +45,10 @@ export const latexInlineRule: InlineRule = {
node: {
type: 'math_inline',
value: state.content
- .slice(state.pos + MATH_INLINE_MARKER.length, endIndex)
+ .slice(state.pos + inlineMathMarker.length, endIndex)
.trim(),
},
- consumedChars: endIndex - state.pos + MATH_INLINE_MARKER.length,
+ consumedChars: endIndex - state.pos + inlineMathMarker.length,
};
},
};
@@ -56,30 +56,30 @@ export const latexInlineRule: InlineRule = {
// Parse a $$...$$ formula as one block, including formulas spanning lines.
export const latexBlockRule: BlockRule = {
name: 'math_block',
- markers: [MATH_INLINE_MARKER],
+ marker: [inlineMathMarker],
parse(state: BlockState) {
const firstLine = state.currentLine;
if (!firstLine) return null;
const trimmedLine = firstLine.trimStart();
- if (!trimmedLine.startsWith(MATH_BLOCK_MARKER)) return null;
+ if (!trimmedLine.startsWith(blockMathMarker)) return null;
const contentLines: string[] = [];
let consumedLines = 1;
- let currentContent = trimmedLine.slice(MATH_BLOCK_MARKER.length);
- let endIndex = findMathDelimiter(currentContent, 0, MATH_BLOCK_MARKER);
+ let currentContent = trimmedLine.slice(blockMathMarker.length);
+ let endIndex = findMathDelimiter(currentContent, 0, blockMathMarker);
while (endIndex === -1) {
contentLines.push(currentContent);
if (state.lineIndex + consumedLines >= state.lineCount) return null;
currentContent = state.lines[state.lineIndex + consumedLines];
consumedLines++;
- endIndex = findMathDelimiter(currentContent, 0, MATH_BLOCK_MARKER);
+ endIndex = findMathDelimiter(currentContent, 0, blockMathMarker);
}
contentLines.push(currentContent.slice(0, endIndex));
const trailingContent = currentContent.slice(
- endIndex + MATH_BLOCK_MARKER.length,
+ endIndex + blockMathMarker.length,
);
if (trailingContent.trim() !== '') return null;
diff --git a/package/parser/src/rule/blocks.ts b/package/parser/src/rule/block.ts
similarity index 97%
rename from package/parser/src/rule/blocks.ts
rename to package/parser/src/rule/block.ts
index 0c9a372..da7261c 100644
--- a/package/parser/src/rule/blocks.ts
+++ b/package/parser/src/rule/block.ts
@@ -1,4 +1,4 @@
-// @ffm/parser/src/rule/blocks.ts
+// @ffm/parser/src/rule/block.ts
import type { ASTNode, BlockRule } from '#/type';
import { BlockState } from '#/core/state';
@@ -18,7 +18,7 @@ const fencedBlockCache = new WeakMap<
*/
export const headingRule: BlockRule = {
name: 'heading',
- markers: ['#'],
+ marker: ['#'],
parse(state: BlockState, ctx) {
const line = state.currentLine;
if (!line) return null;
@@ -42,7 +42,7 @@ export const headingRule: BlockRule = {
*/
export const codeBlockRule: BlockRule = {
name: 'code_block',
- markers: ['`', '~'],
+ marker: ['`', '~'],
parse(state: BlockState) {
const block = extractFencedBlock(state);
if (!block) return null;
@@ -202,7 +202,7 @@ const createTableCell = (
*/
export const tableRule: BlockRule = {
name: 'table',
- markers: ['|'],
+ marker: ['|'],
parse(state: BlockState, ctx) {
const line = state.currentLine;
if (!line || !line.includes('|')) return null;
@@ -270,7 +270,7 @@ export const tableRule: BlockRule = {
*/
export const hrRule: BlockRule = {
name: 'hr',
- markers: ['-', '*', '_'],
+ marker: ['-', '*', '_'],
parse(state: BlockState) {
const line = state.currentLine;
if (!line) return null;
@@ -291,7 +291,7 @@ export const hrRule: BlockRule = {
*/
export const blockquoteRule: BlockRule = {
name: 'blockquote',
- markers: ['>'],
+ marker: ['>'],
parse(state: BlockState, ctx) {
const line = state.currentLine;
if (!line || !line.trimStart().startsWith('>')) return null;
@@ -334,7 +334,7 @@ const LIST_INDENT_STEP = 2;
*/
export const listRule: BlockRule = {
name: 'list',
- markers: ['-', '*', '+', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
+ marker: ['-', '*', '+', '1', '2', '3', '4', '5', '6', '7', '8', '9'],
parse(state: BlockState, ctx) {
const line = state.currentLine;
if (!line) return null;
@@ -364,7 +364,7 @@ export const listRule: BlockRule = {
// detect subsequent lines belonging to this item
// (lines with indentation deeper than the marker).
const markerTotalWidth = baseIndent + marker.length + 1;
- // Normalize nested list markers to two-space logical levels, tolerating odd legacy indentation.
+ // Normalize nested list marker to two-space logical levels, tolerating odd legacy indentation.
const nestedListIndent = baseIndent + LIST_INDENT_STEP;
while (
diff --git a/package/parser/src/rule/ffm.ts b/package/parser/src/rule/ffm.ts
index 53a0b22..1a06ec5 100644
--- a/package/parser/src/rule/ffm.ts
+++ b/package/parser/src/rule/ffm.ts
@@ -1,6 +1,6 @@
// @ffm/parser/src/rule/ffm.ts
import { BlockState } from '#/core/state';
-import { extractFencedBlock } from './blocks';
+import { extractFencedBlock } from './block';
import type { BlockRule, ASTNode } from '#/type';
const ffmKeyword = new Set(['quote', 'slide', 'chain', 'accordion']);
@@ -18,7 +18,7 @@ const nodeTitleRegex = /^\s*\*\*(?:\[([ xX])\]\s*)?(.+?)\*\*\s*$/;
export const ffmBlockRule: BlockRule = {
name: 'ffm_blocks',
// same as code block to leverage fenced block parsing
- markers: ['`', '~'],
+ marker: ['`', '~'],
parse(state: BlockState, ctx) {
const block = extractFencedBlock(state);
if (!block) return null;
diff --git a/package/parser/src/rule/inlines.ts b/package/parser/src/rule/inline.ts
similarity index 97%
rename from package/parser/src/rule/inlines.ts
rename to package/parser/src/rule/inline.ts
index c9acf35..67385d1 100644
--- a/package/parser/src/rule/inlines.ts
+++ b/package/parser/src/rule/inline.ts
@@ -1,4 +1,4 @@
-// @ffm/parser/src/rule/inlines.ts
+// @ffm/parser/src/rule/inline.ts
import type { InlineRule } from '#/type';
import { InlineState } from '#/core/state';
import { isSafeColorValue } from '#/core/color';
@@ -9,7 +9,7 @@ import { isSafeLinkUrl } from '#/core/url';
*/
export const hardBreakRule: InlineRule = {
name: 'hardbreak',
- markers: ['\\', ' '],
+ marker: ['\\', ' '],
parse(state: InlineState) {
if (state.currentChar === '\\' && state.content[state.pos + 1] === '\n') {
return {
@@ -26,7 +26,7 @@ export const hardBreakRule: InlineRule = {
*/
export const boldRule: InlineRule = {
name: 'bold',
- markers: ['*'],
+ marker: ['*'],
parse(state: InlineState, ctx) {
if (!state.content.startsWith('**', state.pos)) return null;
@@ -53,7 +53,7 @@ export const boldRule: InlineRule = {
*/
export const italicRule: InlineRule = {
name: 'italic',
- markers: ['*'],
+ marker: ['*'],
parse(state: InlineState, ctx) {
const char = state.currentChar;
// only triggers when it starts with * and
@@ -82,7 +82,7 @@ export const italicRule: InlineRule = {
*/
export const inlineCodeRule: InlineRule = {
name: 'inline_code',
- markers: ['`'],
+ marker: ['`'],
parse(state: InlineState) {
if (state.currentChar !== '`') return null;
@@ -144,7 +144,7 @@ export const inlineCodeRule: InlineRule = {
*/
export const linkRule: InlineRule = {
name: 'link',
- markers: ['['],
+ marker: ['['],
parse(state: InlineState, ctx) {
if (state.currentChar !== '[') return null;
@@ -180,7 +180,7 @@ export const linkRule: InlineRule = {
*/
export const underlineRule: InlineRule = {
name: 'underline',
- markers: ['_'],
+ marker: ['_'],
parse(state: InlineState, ctx) {
if (!state.content.startsWith('__', state.pos)) return null;
@@ -202,7 +202,7 @@ export const underlineRule: InlineRule = {
*/
export const strikeRule: InlineRule = {
name: 'strike',
- markers: ['-'],
+ marker: ['-'],
parse(state: InlineState, ctx) {
if (!state.content.startsWith('--', state.pos)) return null;
diff --git a/package/parser/src/type.ts b/package/parser/src/type.ts
index 0c545b7..34e8499 100644
--- a/package/parser/src/type.ts
+++ b/package/parser/src/type.ts
@@ -58,8 +58,8 @@ export interface ParserContext {
// block parsing rule (such as header, code block, table)
export interface BlockRule {
name: string;
- // optional markers to quickly identify potential matches (e.g., '#' for headings)
- markers: string[];
+ // optional marker to quickly identify potential matches (e.g., '#' for headings)
+ marker: string[];
// Returns the generated Node and the number of rows consumed if the match is successful;
// returns null if the match fails.
parse: (
@@ -71,7 +71,7 @@ export interface BlockRule {
// inline parsing rule (such as bold, italics, links)
export interface InlineRule {
name: string;
- markers: string[];
+ marker: string[];
// Returns the generated Node and the number of chars consumed if the match is successful;
// returns null if the match fails.
parse: (
From cfa5880a30b09fff64b1e8e56b66866f826ff61f Mon Sep 17 00:00:00 2001
From: Fuyeor
Date: Tue, 22 Sep 2026 09:18:16 +0000
Subject: [PATCH 6/7] align rust ffm-parser with new AST
---
Cargo.toml | 2 +-
crate/src/ast.rs | 22 ++---
crate/src/lib.rs | 65 +++++++--------
crate/src/parser.rs | 98 +++++++++++-----------
crate/src/plain_text.rs | 36 ++++----
crate/src/render.rs | 68 +++++++--------
crate/src/{rules.rs => rule.rs} | 101 +++++++++++------------
extension/vscode/src/ffm-grammar.spec.ts | 92 ---------------------
fixture/ffm.json | 4 +-
9 files changed, 196 insertions(+), 292 deletions(-)
rename crate/src/{rules.rs => rule.rs} (91%)
delete mode 100644 extension/vscode/src/ffm-grammar.spec.ts
diff --git a/Cargo.toml b/Cargo.toml
index 9926708..db32fd8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,4 +1,4 @@
# /Cargo.toml
[workspace]
-members = ["packages/markdown-parser-rust"]
+members = ["crate"]
resolver = "3"
diff --git a/crate/src/ast.rs b/crate/src/ast.rs
index b6cf84a..3d18350 100644
--- a/crate/src/ast.rs
+++ b/crate/src/ast.rs
@@ -128,10 +128,10 @@ impl Alignment {
pub struct AstNode {
/// The semantic node type.
pub node_type: NodeType,
- /// Literal text or code content, when present.
- pub content: Option,
+ /// Literal text or code content value, when present.
+ pub value: Option,
/// Child nodes in source order.
- pub children: Vec,
+ pub content: Vec,
/// ATX heading level.
pub level: Option,
/// Fenced code info string.
@@ -143,7 +143,7 @@ pub struct AstNode {
/// Ordered-list starting number.
pub start: Option,
/// Table header cells.
- pub headers: Option>,
+ pub header: Option>,
/// Accordion or chain group name.
pub name: Option,
/// Accordion or chain item title nodes.
@@ -163,14 +163,14 @@ impl AstNode {
pub fn new(node_type: NodeType) -> Self {
Self {
node_type,
- content: None,
- children: Vec::new(),
+ value: None,
+ content: Vec::new(),
level: None,
lang: None,
url: None,
ordered: None,
start: None,
- headers: None,
+ header: None,
name: None,
title: None,
is_completed: None,
@@ -181,16 +181,16 @@ impl AstNode {
}
/// Creates a text node with the supplied content.
- pub fn text(content: impl Into) -> Self {
+ pub fn text(value: impl Into) -> Self {
let mut node = Self::new(NodeType::Text);
- node.content = Some(content.into());
+ node.value = Some(value.into());
node
}
/// Creates a node with child nodes.
- pub fn with_children(node_type: NodeType, children: Vec) -> Self {
+ pub fn with_content(node_type: NodeType, content: Vec) -> Self {
let mut node = Self::new(node_type);
- node.children = children;
+ node.content = content;
node
}
}
diff --git a/crate/src/lib.rs b/crate/src/lib.rs
index 901b88e..e0b8b28 100644
--- a/crate/src/lib.rs
+++ b/crate/src/lib.rs
@@ -11,37 +11,36 @@ mod linkify;
mod parser;
mod plain_text;
mod render;
-mod rules;
+mod rule;
mod safety;
mod state;
pub use ast::{Alignment, AstNode, NodeType};
pub use linkify::{LinkMatch, linkify};
-pub use parser::{Linkifier, MarkdownParser, ParserContext, ParserError, ParserOptions};
+pub use parser::{Linkifier, MarkdownParser, ParserContext, ParserError, ParserOption};
pub use plain_text::to_plain_text;
pub use render::{render, render_optional};
pub use safety::{is_safe_color_value, is_safe_link_url};
pub use state::{BlockState, InlineState};
/// Constructs a parser with the base Markdown rule set.
-pub fn create_markdown_parser(options: ParserOptions) -> MarkdownParser {
- MarkdownParser::create_standard(options)
+pub fn create_common_parser(option: ParserOption) -> MarkdownParser {
+ MarkdownParser::create_common(option)
}
/// Constructs a parser with the base Markdown rules and FFM extensions.
-pub fn create_fuyeor_markdown_parser(options: ParserOptions) -> MarkdownParser {
- MarkdownParser::create_ffm(options)
+pub fn create_ffm_parser(option: ParserOption) -> MarkdownParser {
+ MarkdownParser::create_ffm(option)
}
#[cfg(test)]
-mod cross_language_fixtures {
+mod shared_fixture {
use serde::Deserialize;
use serde_json::{Map, Value, json};
use super::{
- AstNode, MarkdownParser, ParserOptions, create_fuyeor_markdown_parser,
- create_markdown_parser, is_safe_color_value, is_safe_link_url, linkify, render,
- to_plain_text,
+ AstNode, MarkdownParser, ParserOption, create_common_parser, create_ffm_parser,
+ is_safe_color_value, is_safe_link_url, linkify, render, to_plain_text,
};
#[derive(Deserialize)]
@@ -59,7 +58,7 @@ mod cross_language_fixtures {
assert: Vec,
error: Option,
no_throw: Option,
- options: Option,
+ option: Option,
}
#[derive(Deserialize)]
@@ -70,7 +69,7 @@ mod cross_language_fixtures {
}
#[derive(Deserialize)]
- struct FixtureOptions {
+ struct FixtureOption {
max_nesting_depth: Option,
}
@@ -107,7 +106,7 @@ mod cross_language_fixtures {
// Load a shared JSON fixture from the repository root.
fn read_fixture Deserialize<'de>>(name: &str) -> T {
- let path = format!("{}/../../fixtures/{name}", env!("CARGO_MANIFEST_DIR"));
+ let path = format!("{}/../fixture/{name}", env!("CARGO_MANIFEST_DIR"));
serde_json::from_str(&std::fs::read_to_string(path).expect("fixture must exist"))
.expect("fixture must be valid JSON")
}
@@ -116,13 +115,13 @@ mod cross_language_fixtures {
fn normalize_node(node: &AstNode) -> Value {
let mut object = Map::new();
object.insert("type".into(), json!(node.node_type.as_str()));
- if let Some(content) = &node.content {
- object.insert("content".into(), json!(content));
+ if let Some(value) = &node.value {
+ object.insert("value".into(), json!(value));
}
- if !node.children.is_empty() {
+ if !node.content.is_empty() {
object.insert(
- "children".into(),
- Value::Array(node.children.iter().map(normalize_node).collect()),
+ "content".into(),
+ Value::Array(node.content.iter().map(normalize_node).collect()),
);
}
if let Some(level) = node.level {
@@ -140,10 +139,10 @@ mod cross_language_fixtures {
if let Some(start) = node.start {
object.insert("start".into(), json!(start));
}
- if let Some(headers) = &node.headers {
+ if let Some(header) = &node.header {
object.insert(
- "headers".into(),
- Value::Array(headers.iter().map(normalize_node).collect()),
+ "header".into(),
+ Value::Array(header.iter().map(normalize_node).collect()),
);
}
if let Some(name) = &node.name {
@@ -179,29 +178,29 @@ mod cross_language_fixtures {
})
}
- // Translate fixture options into the Rust parser API.
- fn parser_options(case: &MarkdownCase) -> ParserOptions {
- ParserOptions {
+ // Translate fixture option into the Rust parser API.
+ fn parser_option(case: &MarkdownCase) -> ParserOption {
+ ParserOption {
max_nesting_depth: case
- .options
+ .option
.as_ref()
- .and_then(|options| options.max_nesting_depth)
+ .and_then(|option| option.max_nesting_depth)
.unwrap_or(64),
- ..ParserOptions::default()
+ ..ParserOption::default()
}
}
// Execute one standard or FFM Markdown case against the Rust parser.
fn execute_markdown_case(case: &MarkdownCase, ffm: bool) {
- let options = parser_options(case);
+ let option = parser_option(case);
if case.error.as_deref() == Some("invalid_nesting_depth") {
- assert!(MarkdownParser::try_new(options).is_err());
+ assert!(MarkdownParser::try_new(option).is_err());
return;
}
let parser = if ffm {
- create_fuyeor_markdown_parser(options)
+ create_ffm_parser(option)
} else {
- create_markdown_parser(options)
+ create_common_parser(option)
};
let ast = parser.parse(case.input.as_deref().unwrap_or(""));
if let Some(expected_html) = &case.html {
@@ -230,7 +229,7 @@ mod cross_language_fixtures {
}
#[test]
- fn executes_shared_markdown_fixtures() {
+ fn test_markdown() {
let standard = read_fixture::("markdown.json");
assert_eq!(standard.schema_version, 2);
for case in &standard.cases {
@@ -244,7 +243,7 @@ mod cross_language_fixtures {
}
#[test]
- fn executes_shared_safety_and_linkify_fixtures() {
+ fn test_linkify() {
let safety = read_fixture::("safety.json");
assert_eq!(safety.schema_version, 1);
for case in safety.links {
diff --git a/crate/src/parser.rs b/crate/src/parser.rs
index dd49196..ec66940 100644
--- a/crate/src/parser.rs
+++ b/crate/src/parser.rs
@@ -4,7 +4,7 @@ use std::collections::HashMap;
use crate::ast::{AstNode, NodeType};
use crate::linkify::{LinkMatch, linkify};
-use crate::rules::{BlockRule, FfmBlockRule, InlineRule};
+use crate::rule::{BlockRule, FuyeorBlockRule, InlineRule};
use crate::safety::is_safe_link_url;
use crate::state::{BlockState, InlineState};
@@ -21,16 +21,16 @@ const PRELIGHT_BLOCK_RULES: &[&str] = &[
/// A linkifier callback used by the parser's inline text projection.
pub type Linkifier = fn(&str) -> Vec;
-/// Parser options matching the TypeScript parser constructor.
+/// Parser option matching the TypeScript parser constructor.
#[derive(Clone, Copy)]
-pub struct ParserOptions {
+pub struct ParserOption {
/// Maximum recursive block/inline parsing depth.
pub max_nesting_depth: usize,
/// Callback used to discover links in plain text.
pub linkifier: Linkifier,
}
-impl Default for ParserOptions {
+impl Default for ParserOption {
fn default() -> Self {
Self {
max_nesting_depth: 64,
@@ -109,14 +109,14 @@ pub struct MarkdownParser {
}
impl MarkdownParser {
- /// Creates a parser and fails fast if its options are invalid.
- pub fn new(options: ParserOptions) -> Self {
- Self::try_new(options).expect("invalid MarkdownParser options")
+ /// Creates a parser and fails fast if its option are invalid.
+ pub fn new(option: ParserOption) -> Self {
+ Self::try_new(option).expect("invalid MarkdownParser option")
}
/// Creates a parser without panicking on invalid configuration.
- pub fn try_new(options: ParserOptions) -> Result {
- if options.max_nesting_depth == 0 {
+ pub fn try_new(option: ParserOption) -> Result {
+ if option.max_nesting_depth == 0 {
return Err(ParserError::InvalidMaxNestingDepth);
}
Ok(Self {
@@ -124,8 +124,8 @@ impl MarkdownParser {
inline_rules: Vec::new(),
block_rule_map: HashMap::new(),
inline_rule_map: HashMap::new(),
- max_nesting_depth: options.max_nesting_depth,
- linkifier: options.linkifier,
+ max_nesting_depth: option.max_nesting_depth,
+ linkifier: option.linkifier,
})
}
@@ -135,9 +135,9 @@ impl MarkdownParser {
R: BlockRule + 'static,
{
let rule_index = self.block_rules.len();
- let markers = rule.markers();
+ let marker = rule.marker();
self.block_rules.push(Box::new(rule));
- for marker in markers {
+ for marker in marker {
self.block_rule_map
.entry(*marker)
.or_default()
@@ -152,9 +152,9 @@ impl MarkdownParser {
R: InlineRule + 'static,
{
let rule_index = self.inline_rules.len();
- let markers = rule.markers();
+ let marker = rule.marker();
self.inline_rules.push(Box::new(rule));
- for marker in markers {
+ for marker in marker {
self.inline_rule_map
.entry(*marker)
.or_default()
@@ -176,41 +176,41 @@ impl MarkdownParser {
}
/// Parses the input with the standard rule set.
- pub fn create_standard(options: ParserOptions) -> Self {
- let mut parser = Self::new(options);
- parser.add_block_rule(crate::rules::CodeBlockRule);
- parser.add_block_rule(crate::rules::ListRule);
- parser.add_block_rule(crate::rules::HeadingRule);
- parser.add_block_rule(crate::rules::TableRule);
- parser.add_block_rule(crate::rules::HrRule);
- parser.add_block_rule(crate::rules::BlockquoteRule);
- parser.add_inline_rule(crate::rules::HardBreakRule);
- parser.add_inline_rule(crate::rules::InlineCodeRule);
- parser.add_inline_rule(crate::rules::LinkRule);
- parser.add_inline_rule(crate::rules::BoldRule);
- parser.add_inline_rule(crate::rules::UnderlineRule);
- parser.add_inline_rule(crate::rules::ItalicRule);
- parser.add_inline_rule(crate::rules::StrikeRule);
+ pub fn create_common(option: ParserOption) -> Self {
+ let mut parser = Self::new(option);
+ parser.add_block_rule(crate::rule::CodeBlockRule);
+ parser.add_block_rule(crate::rule::ListRule);
+ parser.add_block_rule(crate::rule::HeadingRule);
+ parser.add_block_rule(crate::rule::TableRule);
+ parser.add_block_rule(crate::rule::HrRule);
+ parser.add_block_rule(crate::rule::BlockquoteRule);
+ parser.add_inline_rule(crate::rule::HardBreakRule);
+ parser.add_inline_rule(crate::rule::InlineCodeRule);
+ parser.add_inline_rule(crate::rule::LinkRule);
+ parser.add_inline_rule(crate::rule::BoldRule);
+ parser.add_inline_rule(crate::rule::UnderlineRule);
+ parser.add_inline_rule(crate::rule::ItalicRule);
+ parser.add_inline_rule(crate::rule::StrikeRule);
parser
}
/// Parses the input with the standard rule set plus FFM fenced blocks.
- pub fn create_ffm(options: ParserOptions) -> Self {
- let mut parser = Self::new(options);
- parser.add_block_rule(FfmBlockRule);
- parser.add_block_rule(crate::rules::CodeBlockRule);
- parser.add_block_rule(crate::rules::ListRule);
- parser.add_block_rule(crate::rules::HeadingRule);
- parser.add_block_rule(crate::rules::TableRule);
- parser.add_block_rule(crate::rules::HrRule);
- parser.add_block_rule(crate::rules::BlockquoteRule);
- parser.add_inline_rule(crate::rules::HardBreakRule);
- parser.add_inline_rule(crate::rules::InlineCodeRule);
- parser.add_inline_rule(crate::rules::LinkRule);
- parser.add_inline_rule(crate::rules::BoldRule);
- parser.add_inline_rule(crate::rules::UnderlineRule);
- parser.add_inline_rule(crate::rules::ItalicRule);
- parser.add_inline_rule(crate::rules::StrikeRule);
+ pub fn create_ffm(option: ParserOption) -> Self {
+ let mut parser = Self::new(option);
+ parser.add_block_rule(FuyeorBlockRule);
+ parser.add_block_rule(crate::rule::CodeBlockRule);
+ parser.add_block_rule(crate::rule::ListRule);
+ parser.add_block_rule(crate::rule::HeadingRule);
+ parser.add_block_rule(crate::rule::TableRule);
+ parser.add_block_rule(crate::rule::HrRule);
+ parser.add_block_rule(crate::rule::BlockquoteRule);
+ parser.add_inline_rule(crate::rule::HardBreakRule);
+ parser.add_inline_rule(crate::rule::InlineCodeRule);
+ parser.add_inline_rule(crate::rule::LinkRule);
+ parser.add_inline_rule(crate::rule::BoldRule);
+ parser.add_inline_rule(crate::rule::UnderlineRule);
+ parser.add_inline_rule(crate::rule::ItalicRule);
+ parser.add_inline_rule(crate::rule::StrikeRule);
parser
}
@@ -233,7 +233,7 @@ impl MarkdownParser {
runtime: &RefCell,
) -> Vec {
if depth > self.max_nesting_depth {
- return vec![AstNode::with_children(
+ return vec![AstNode::with_content(
NodeType::Paragraph,
vec![AstNode::text(state.remaining_lines().join("\n"))],
)];
@@ -316,7 +316,7 @@ impl MarkdownParser {
if !paragraph_lines.is_empty() {
let inline_content = paragraph_lines.join("\n");
- nodes.push(AstNode::with_children(
+ nodes.push(AstNode::with_content(
NodeType::Paragraph,
self.parse_inline_inner(&inline_content, depth, runtime),
));
@@ -415,7 +415,7 @@ impl MarkdownParser {
}
let mut link = AstNode::new(NodeType::Link);
link.url = Some(matched.url);
- link.children = vec![AstNode::text(matched.text)];
+ link.content = vec![AstNode::text(matched.text)];
nodes.push(link);
last_index = matched.last_index;
}
diff --git a/crate/src/plain_text.rs b/crate/src/plain_text.rs
index 4d8df76..56ed2de 100644
--- a/crate/src/plain_text.rs
+++ b/crate/src/plain_text.rs
@@ -27,8 +27,8 @@ fn append_block_nodes(nodes: &[AstNode], output: &mut String) {
fn append_block_node(node: &AstNode, output: &mut String) {
match &node.node_type {
NodeType::Text | NodeType::InlineCode | NodeType::ColorCode => {
- if let Some(content) = &node.content {
- output.push_str(content);
+ if let Some(value) = &node.value {
+ output.push_str(value);
}
}
NodeType::Heading
@@ -37,10 +37,10 @@ fn append_block_node(node: &AstNode, output: &mut String) {
| NodeType::Italic
| NodeType::Underline
| NodeType::Strike
- | NodeType::Link => append_inline_nodes(&node.children, output),
+ | NodeType::Link => append_inline_nodes(&node.content, output),
NodeType::CodeBlock => {
- if let Some(content) = &node.content {
- output.push_str(content);
+ if let Some(value) = &node.value {
+ output.push_str(value);
}
}
NodeType::Blockquote
@@ -49,43 +49,43 @@ fn append_block_node(node: &AstNode, output: &mut String) {
| NodeType::Slide
| NodeType::SlideItem
| NodeType::Accordion
- | NodeType::Chain => append_block_nodes(&node.children, output),
+ | NodeType::Chain => append_block_nodes(&node.content, output),
NodeType::AccordionItem => {
append_inline_nodes(node.title.as_deref().unwrap_or_default(), output);
- if !output.is_empty() && !output.ends_with('\n') && !node.children.is_empty() {
+ if !output.is_empty() && !output.ends_with('\n') && !node.content.is_empty() {
output.push('\n');
}
- append_block_nodes(&node.children, output);
+ append_block_nodes(&node.content, output);
}
NodeType::ChainItem => {
if let Some(title) = &node.title {
append_inline_nodes(title, output);
- if !output.is_empty() && !output.ends_with('\n') && !node.children.is_empty() {
+ if !output.is_empty() && !output.ends_with('\n') && !node.content.is_empty() {
output.push('\n');
}
}
- append_block_nodes(&node.children, output);
+ append_block_nodes(&node.content, output);
}
NodeType::Table => {
- if let Some(headers) = &node.headers {
- append_table_cells(headers, output);
+ if let Some(header) = &node.header {
+ append_table_cells(header, output);
}
- if !node.children.is_empty() {
+ if !node.content.is_empty() {
if !output.is_empty() {
push_separator(output);
}
- append_block_nodes(&node.children, output);
+ append_block_nodes(&node.content, output);
}
}
- NodeType::TableRow => append_table_cells(&node.children, output),
- NodeType::TableCell => append_inline_nodes(&node.children, output),
+ NodeType::TableRow => append_table_cells(&node.content, output),
+ NodeType::TableCell => append_inline_nodes(&node.content, output),
NodeType::Hardbreak => output.push('\n'),
NodeType::Hr => {}
- NodeType::Root | NodeType::Custom(_) => append_block_nodes(&node.children, output),
+ NodeType::Root | NodeType::Custom(_) => append_block_nodes(&node.content, output),
}
}
-/// Appends inline children without inserting separators between formatting nodes.
+/// Appends inline content without inserting separators between formatting nodes.
fn append_inline_nodes(nodes: &[AstNode], output: &mut String) {
for node in nodes {
append_block_node(node, output);
diff --git a/crate/src/render.rs b/crate/src/render.rs
index 1f9f691..5d9e9b1 100644
--- a/crate/src/render.rs
+++ b/crate/src/render.rs
@@ -33,53 +33,53 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
NodeType::Heading => {
if let Some(level @ 1..=6) = node.level {
let _ = write!(output, "");
- render_into(output, &node.children);
+ render_into(output, &node.content);
let _ = writeln!(output, "");
} else {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
}
NodeType::Paragraph => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
\n");
}
NodeType::Text => {
- if let Some(content) = &node.content {
- output.push_str(&escape_html(content));
+ if let Some(value) = &node.value {
+ output.push_str(&escape_html(value));
}
}
NodeType::Bold => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
NodeType::Italic => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
NodeType::Underline => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
NodeType::Strike => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
NodeType::InlineCode => {
output.push_str("");
- if let Some(content) = &node.content {
- output.push_str(&escape_html(content));
+ if let Some(value) = &node.value {
+ output.push_str(&escape_html(value));
}
output.push_str("");
}
NodeType::ColorCode => {
- let color = node.content.as_deref().unwrap_or_default();
+ let color = node.value.as_deref().unwrap_or_default();
if !is_safe_color_value(color) {
output.push_str(&escape_html(color));
} else {
@@ -95,10 +95,10 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
if is_safe_link_url(url) {
let escaped = escape_html(url);
let _ = write!(output, "");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
} else {
- render_into(output, &node.children);
+ render_into(output, &node.content);
}
}
NodeType::CodeBlock => {
@@ -115,8 +115,8 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
let _ = write!(output, " class=\"language-{}\"", escape_html(lang));
}
output.push('>');
- if let Some(content) = &node.content {
- output.push_str(&escape_html(content));
+ if let Some(value) = &node.value {
+ output.push_str(&escape_html(value));
}
output.push_str("\n\n");
}
@@ -134,32 +134,32 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
String::new()
};
let _ = writeln!(output, "<{tag}{start}>");
- render_into(output, &node.children);
+ render_into(output, &node.content);
let _ = writeln!(output, "{tag}>");
}
NodeType::ListItem => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("\n");
}
NodeType::Table => {
output.push_str("\n");
- if let Some(headers) = &node.headers {
+ if let Some(header) = &node.header {
output.push_str("\n\n");
- for cell in headers {
+ for cell in header {
let _ = write!(output, "| ", table_alignment(cell));
- render_into(output, &cell.children);
+ render_into(output, &cell.content);
output.push_str(" | \n");
}
output.push_str("
\n\n");
}
- if !node.children.is_empty() {
+ if !node.content.is_empty() {
output.push_str("\n");
- for row in &node.children {
+ for row in &node.content {
output.push_str("\n");
- for cell in &row.children {
+ for cell in &row.content {
let _ = write!(output, "| ", table_alignment(cell));
- render_into(output, &cell.children);
+ render_into(output, &cell.content);
output.push_str(" | \n");
}
output.push_str("
\n");
@@ -171,12 +171,12 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
NodeType::Hr => output.push_str("
\n"),
NodeType::Blockquote => {
output.push_str("\n");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
\n");
}
NodeType::Accordion => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::AccordionItem => {
@@ -184,12 +184,12 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
let _ = write!(output, "");
render_optional_into(output, node.title.as_deref());
output.push_str("
");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::Chain => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::ChainItem => {
@@ -210,32 +210,32 @@ fn render_into(output: &mut String, nodes: &[AstNode]) {
output,
"{title}
"
);
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::Slide => {
output.push_str(
"",
);
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::SlideItem => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("
");
}
NodeType::Hardbreak => output.push_str("
\n"),
NodeType::Root | NodeType::TableRow | NodeType::TableCell | NodeType::Custom(_) => {
output.push_str("");
- render_into(output, &node.children);
+ render_into(output, &node.content);
output.push_str("");
}
}
}
}
-/// Renders optional title children without allocating when absent.
+/// Renders optional title content without allocating when absent.
fn render_optional_into(output: &mut String, nodes: Option<&[AstNode]>) {
if let Some(nodes) = nodes {
render_into(output, nodes);
diff --git a/crate/src/rules.rs b/crate/src/rule.rs
similarity index 91%
rename from crate/src/rules.rs
rename to crate/src/rule.rs
index 049751b..2d01402 100644
--- a/crate/src/rules.rs
+++ b/crate/src/rule.rs
@@ -1,4 +1,4 @@
-// src/rules.rs
+// src/rule.rs
use crate::ast::{Alignment, AstNode, NodeType};
use crate::parser::ParserContext;
use crate::safety::{is_safe_color_value, is_safe_link_url};
@@ -19,14 +19,14 @@ pub struct InlineMatch {
/// Extensible block-rule interface matching the TypeScript parser contract.
pub trait BlockRule {
fn name(&self) -> &'static str;
- fn markers(&self) -> &'static [char];
+ fn marker(&self) -> &'static [char];
fn parse(&self, state: &BlockState, ctx: &ParserContext<'_>) -> Option;
}
/// Extensible inline-rule interface matching the TypeScript parser contract.
pub trait InlineRule {
fn name(&self) -> &'static str;
- fn markers(&self) -> &'static [char];
+ fn marker(&self) -> &'static [char];
fn parse(&self, state: &mut InlineState, ctx: &ParserContext<'_>) -> Option;
}
@@ -125,7 +125,7 @@ impl BlockRule for HeadingRule {
"heading"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['#']
}
@@ -156,7 +156,7 @@ impl BlockRule for HeadingRule {
let mut node = AstNode::new(NodeType::Heading);
node.level = Some(level as u8);
- node.children = if text.is_empty() {
+ node.content = if text.is_empty() {
Vec::new()
} else {
ctx.parse_inline(&text)
@@ -176,7 +176,7 @@ impl BlockRule for CodeBlockRule {
"code_block"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['`', '~']
}
@@ -184,7 +184,7 @@ impl BlockRule for CodeBlockRule {
let block = extract_fenced_block(state)?;
let mut node = AstNode::new(NodeType::CodeBlock);
node.lang = Some(block.lang);
- node.content = Some(block.content);
+ node.value = Some(block.content);
Some(BlockMatch {
node,
consumed_lines: block.consumed_lines,
@@ -297,7 +297,7 @@ fn create_table_cell(
alignment: Option,
ctx: &ParserContext<'_>,
) -> AstNode {
- let mut node = AstNode::with_children(NodeType::TableCell, ctx.parse_inline(content));
+ let mut node = AstNode::with_content(NodeType::TableCell, ctx.parse_inline(content));
node.align = alignment;
node
}
@@ -310,7 +310,7 @@ impl BlockRule for TableRule {
"table"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['|']
}
@@ -326,33 +326,33 @@ impl BlockRule for TableRule {
if alignments.is_none() {
let next = state.lines.get(state.line_index + 1)?;
alignments = parse_table_alignments(next);
- let parsed_headers = extract_table_cells(line);
- if alignments.as_ref()?.len() != parsed_headers.len() {
+ let parsed_header = extract_table_cells(line);
+ if alignments.as_ref()?.len() != parsed_header.len() {
return None;
}
- header_cells = Some(parsed_headers);
+ header_cells = Some(parsed_header);
consumed_lines = 2;
}
let alignments = alignments?;
let column_count = alignments.len();
- let headers = header_cells.map(|cells| normalize_table_cells(cells, column_count));
+ let header = header_cells.map(|cells| normalize_table_cells(cells, column_count));
let mut rows = Vec::new();
while let Some(row_line) = state.lines.get(state.line_index + consumed_lines)
&& row_line.contains('|')
{
let cells = normalize_table_cells(extract_table_cells(row_line), column_count);
- let row_children = cells
+ let row_content = cells
.iter()
.enumerate()
.map(|(index, cell)| create_table_cell(cell, alignments[index], ctx))
.collect();
- rows.push(AstNode::with_children(NodeType::TableRow, row_children));
+ rows.push(AstNode::with_content(NodeType::TableRow, row_content));
consumed_lines += 1;
}
- let mut node = AstNode::with_children(NodeType::Table, rows);
- node.headers = headers.map(|cells| {
+ let mut node = AstNode::with_content(NodeType::Table, rows);
+ node.header = header.map(|cells| {
cells
.iter()
.enumerate()
@@ -374,7 +374,7 @@ impl BlockRule for HrRule {
"hr"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['-', '*', '_']
}
@@ -409,7 +409,7 @@ impl BlockRule for BlockquoteRule {
"blockquote"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['>']
}
@@ -431,7 +431,7 @@ impl BlockRule for BlockquoteRule {
consumed_lines += 1;
}
let mut node = AstNode::new(NodeType::Blockquote);
- node.children = ctx.parse_blocks(&content_lines.join("\n"));
+ node.content = ctx.parse_blocks(&content_lines.join("\n"));
Some(BlockMatch {
node,
consumed_lines,
@@ -439,7 +439,7 @@ impl BlockRule for BlockquoteRule {
}
}
-const LIST_MARKERS: &[char] = &['-', '*', '+', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
+const LIST_MARKER: &[char] = &['-', '*', '+', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
/// Parses a list-item prefix and returns indentation, marker and content.
fn parse_list_item(line: &str) -> Option<(usize, String, String)> {
@@ -479,8 +479,8 @@ impl BlockRule for ListRule {
"list"
}
- fn markers(&self) -> &'static [char] {
- LIST_MARKERS
+ fn marker(&self) -> &'static [char] {
+ LIST_MARKER
}
fn parse(&self, state: &BlockState, ctx: &ParserContext<'_>) -> Option {
@@ -553,13 +553,13 @@ impl BlockRule for ListRule {
}
let mut item = AstNode::new(NodeType::ListItem);
- item.children = ctx.parse_blocks(&item_lines.join("\n"));
+ item.content = ctx.parse_blocks(&item_lines.join("\n"));
items.push(item);
consumed_lines += item_consumed_lines;
let _ = item_marker;
}
- let mut node = AstNode::with_children(NodeType::List, items);
+ let mut node = AstNode::with_content(NodeType::List, items);
node.ordered = Some(ordered);
node.start = start_number;
Some(BlockMatch {
@@ -577,7 +577,7 @@ impl InlineRule for HardBreakRule {
"hardbreak"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['\\', ' ']
}
@@ -601,7 +601,7 @@ impl InlineRule for InlineCodeRule {
"inline_code"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['`']
}
@@ -648,7 +648,7 @@ impl InlineRule for InlineCodeRule {
NodeType::InlineCode
};
let mut node = AstNode::new(node_type);
- node.content = Some(raw);
+ node.value = Some(raw);
Some(InlineMatch {
node,
consumed_bytes: end + marker_len - state.pos,
@@ -664,7 +664,7 @@ impl InlineRule for LinkRule {
"link"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['[']
}
@@ -684,7 +684,7 @@ impl InlineRule for LinkRule {
}
let mut node = AstNode::new(NodeType::Link);
node.url = Some(url);
- node.children = ctx.parse_inline(&inner);
+ node.content = ctx.parse_inline(&inner);
Some(InlineMatch {
node,
consumed_bytes: url_end + 1 - state.pos,
@@ -700,7 +700,7 @@ impl InlineRule for UnderlineRule {
"underline"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['_']
}
@@ -710,7 +710,7 @@ impl InlineRule for UnderlineRule {
}
let end = state.find_next_token("__", state.pos + 2)?;
let mut node = AstNode::new(NodeType::Underline);
- node.children = ctx.parse_inline(&state.content[state.pos + 2..end]);
+ node.content = ctx.parse_inline(&state.content[state.pos + 2..end]);
Some(InlineMatch {
node,
consumed_bytes: end + 2 - state.pos,
@@ -726,7 +726,7 @@ impl InlineRule for StrikeRule {
"strike"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['-']
}
@@ -736,7 +736,7 @@ impl InlineRule for StrikeRule {
}
let end = state.find_next_token("--", state.pos + 2)?;
let mut node = AstNode::new(NodeType::Strike);
- node.children = ctx.parse_inline(&state.content[state.pos + 2..end]);
+ node.content = ctx.parse_inline(&state.content[state.pos + 2..end]);
Some(InlineMatch {
node,
consumed_bytes: end + 2 - state.pos,
@@ -752,7 +752,7 @@ impl InlineRule for BoldRule {
"bold"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['*']
}
@@ -762,7 +762,7 @@ impl InlineRule for BoldRule {
}
let end = state.find_next_token("**", state.pos + 2)?;
let mut node = AstNode::new(NodeType::Bold);
- node.children = ctx.parse_inline(&state.content[state.pos + 2..end]);
+ node.content = ctx.parse_inline(&state.content[state.pos + 2..end]);
Some(InlineMatch {
node,
consumed_bytes: end + 2 - state.pos,
@@ -778,7 +778,7 @@ impl InlineRule for ItalicRule {
"italic"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['*']
}
@@ -791,7 +791,7 @@ impl InlineRule for ItalicRule {
return None;
}
let mut node = AstNode::new(NodeType::Italic);
- node.children = ctx.parse_inline(&state.content[state.pos + 1..end]);
+ node.content = ctx.parse_inline(&state.content[state.pos + 1..end]);
Some(InlineMatch {
node,
consumed_bytes: end + 1 - state.pos,
@@ -802,14 +802,14 @@ impl InlineRule for ItalicRule {
const FFM_KEYWORDS: &[&str] = &["quote", "slide", "chain", "accordion"];
/// Parses FFM fenced blocks before generic code blocks.
-pub struct FfmBlockRule;
+pub struct FuyeorBlockRule;
-impl BlockRule for FfmBlockRule {
+impl BlockRule for FuyeorBlockRule {
fn name(&self) -> &'static str {
"ffm_blocks"
}
- fn markers(&self) -> &'static [char] {
+ fn marker(&self) -> &'static [char] {
&['`', '~']
}
@@ -822,20 +822,17 @@ impl BlockRule for FfmBlockRule {
let node = match block_type {
"quote" => {
- AstNode::with_children(NodeType::Blockquote, ctx.parse_blocks(&block.content))
+ AstNode::with_content(NodeType::Blockquote, ctx.parse_blocks(&block.content))
}
"slide" => {
let slides = split_slide_contents(&block.content)
.into_iter()
.filter(|content| !content.trim().is_empty())
.map(|content| {
- AstNode::with_children(
- NodeType::SlideItem,
- ctx.parse_blocks(content.trim()),
- )
+ AstNode::with_content(NodeType::SlideItem, ctx.parse_blocks(content.trim()))
})
.collect();
- AstNode::with_children(NodeType::Slide, slides)
+ AstNode::with_content(NodeType::Slide, slides)
}
"accordion" | "chain" => parse_foldable_block(block_type, &block.content, ctx),
_ => unreachable!("FFM keywords are exhaustive"),
@@ -910,7 +907,7 @@ fn parse_foldable_block(block_type: &str, content: &str, ctx: &ParserContext<'_>
for line in content.split('\n') {
if let Some((checkbox, title)) = parse_ffm_title(line) {
if let Some(mut previous) = current_item.take() {
- previous.children = ctx.parse_blocks(current_lines.join("\n").trim());
+ previous.content = ctx.parse_blocks(current_lines.join("\n").trim());
items.push(previous);
} else if !current_lines.is_empty() && !current_lines.join("").trim().is_empty() {
preamble_lines = std::mem::take(&mut current_lines);
@@ -936,24 +933,24 @@ fn parse_foldable_block(block_type: &str, content: &str, ctx: &ParserContext<'_>
}
if let Some(mut last) = current_item {
- last.children = ctx.parse_blocks(current_lines.join("\n").trim());
+ last.content = ctx.parse_blocks(current_lines.join("\n").trim());
items.push(last);
} else if !current_lines.is_empty() && !current_lines.join("").trim().is_empty() {
preamble_lines = current_lines;
}
- let mut children = if preamble_lines.is_empty() {
+ let mut content = if preamble_lines.is_empty() {
Vec::new()
} else {
ctx.parse_blocks(preamble_lines.join("\n").trim())
};
- children.extend(items);
+ content.extend(items);
let node_type = if block_type == "accordion" {
NodeType::Accordion
} else {
NodeType::Chain
};
- let mut node = AstNode::with_children(node_type, children);
+ let mut node = AstNode::with_content(node_type, content);
node.name = accordion_name;
node
}
diff --git a/extension/vscode/src/ffm-grammar.spec.ts b/extension/vscode/src/ffm-grammar.spec.ts
deleted file mode 100644
index f22375a..0000000
--- a/extension/vscode/src/ffm-grammar.spec.ts
+++ /dev/null
@@ -1,92 +0,0 @@
-// extension/vscode/src/ffm-grammar.spec.ts
-import { readFileSync } from 'node:fs';
-import { resolve } from 'node:path';
-
-const packageDirectory = resolve(import.meta.dirname, '..');
-const readJson = (relativePath: string) =>
- JSON.parse(
- readFileSync(resolve(packageDirectory, relativePath), 'utf8'),
- ) as Record;
-
-describe('FFM VS Code extension source', () => {
- it('declares a valid FFM language contribution', () => {
- const manifest = readJson('package.json');
- const contributes = manifest.contributes as Record;
- const languages = contributes.languages as Array>;
- const language = languages.find((entry) => entry.id === 'ffm');
-
- expect(language).toBeDefined();
- expect(language?.extensions).toEqual(['.ffm', '.fmd']);
- expect(language?.configuration).toBe('./language-configuration.json');
- });
-
- it('points the grammar contribution to the FFM TextMate grammar', () => {
- const manifest = readJson('package.json');
- const contributes = manifest.contributes as Record;
- const grammars = contributes.grammars as Array>;
-
- expect(grammars).toContainEqual({
- language: 'ffm',
- scopeName: 'text.html.markdown.ffm',
- path: './syntaxes/ffm.tmLanguage.json',
- });
- });
-
- it('covers the syntax families defined by the FFM tutorials', () => {
- const grammar = readJson('syntaxes/ffm.tmLanguage.json');
- const repository = grammar.repository as Record;
- const expectedRules = [
- 'heading',
- 'bold_italic',
- 'bold',
- 'italic',
- 'underline',
- 'strikethrough',
- 'list_marker',
- 'link',
- 'image',
- 'blockquote',
- 'fenced_code_block',
- 'special_fenced_blocks',
- 'math_block',
- 'inline_math',
- 'table_row',
- ];
-
- for (const ruleName of expectedRules)
- expect(repository[ruleName]).toBeDefined();
- });
-
- it('configures .ffm documents as wrapped natural text without ambiguous-character highlights', () => {
- const manifest = readJson('package.json');
- const configurationDefaults = manifest.configurationDefaults as Record<
- string,
- unknown
- >;
- const ffmDefaults = configurationDefaults['[ffm]'] as Record<
- string,
- unknown
- >;
-
- expect(ffmDefaults).toEqual({
- 'editor.wordWrap': 'on',
- 'editor.wrappingStrategy': 'advanced',
- 'editor.unicodeHighlight.ambiguousCharacters': false,
- });
- });
-
- it('recognizes FFM-only fenced block keywords and task titles', () => {
- const grammar = readJson('syntaxes/ffm.tmLanguage.json');
- const repository = grammar.repository as Record;
- const specialBlocks = repository.special_fenced_blocks as Record<
- string,
- unknown
- >;
- const title = repository.special_block_title as Record;
-
- expect(specialBlocks.begin).toContain(
- 'quote|slide|chain|accordion|mermaid|smiles|abc',
- );
- expect(title.match).toContain('\\[[ xX]\\]');
- });
-});
diff --git a/fixture/ffm.json b/fixture/ffm.json
index 836351e..60a544e 100644
--- a/fixture/ffm.json
+++ b/fixture/ffm.json
@@ -125,14 +125,14 @@
},
{
"input": ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> value",
- "options": {
+ "option": {
"max_nesting_depth": 8
},
"no_throw": true,
"plain_text": ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> value"
},
{
- "options": {
+ "option": {
"max_nesting_depth": 0
},
"error": "invalid_nesting_depth"
From 276f4bbab311d62832b3e8488003bc14a77f22e3 Mon Sep 17 00:00:00 2001
From: Fuyeor
Date: Tue, 22 Sep 2026 09:39:26 +0000
Subject: [PATCH 7/7] fix test in @ffm/parser
---
fixture/markdown.json | 5 -----
package/parser/src/index.spec.ts | 32 +++++++++++++++-----------------
package/parser/src/index.ts | 4 ++--
3 files changed, 17 insertions(+), 24 deletions(-)
diff --git a/fixture/markdown.json b/fixture/markdown.json
index f59fb4d..c987556 100644
--- a/fixture/markdown.json
+++ b/fixture/markdown.json
@@ -7,11 +7,6 @@
"html": "Hello World
",
"plain_text": "Hello World"
},
- {
- "input": "```ts\nconst a = 1;\n```",
- "html": "",
- "plain_text": "const a = 1;"
- },
{
"input": "visit www.fuyeor.com or [click here](https://fuyeor.com)",
"html": "visit www.fuyeor.com or click here
",
diff --git a/package/parser/src/index.spec.ts b/package/parser/src/index.spec.ts
index a8d73f1..0d72632 100644
--- a/package/parser/src/index.spec.ts
+++ b/package/parser/src/index.spec.ts
@@ -22,7 +22,7 @@ type MarkdownCase = {
assert?: MarkdownAssertion[];
error?: 'invalid_nesting_depth';
no_throw?: boolean;
- options?: { max_nesting_depth?: number };
+ option?: { max_nesting_depth?: number };
};
type MarkdownFixtureFile = {
@@ -47,8 +47,8 @@ const loadJson = (name: string): T =>
),
) as T;
-const standardFixtures = loadJson('markdown.json');
-const ffmFixtures = loadJson('ffm.json');
+const cfmFixture = loadJson('markdown.json');
+const ffmFixture = loadJson('ffm.json');
const safetyFixtures = loadJson('safety.json');
// Read a portable JSON Pointer path from a TypeScript AST value.
@@ -69,24 +69,24 @@ function readJsonPointer(root: unknown, pointer: string): unknown {
return value;
}
-// Map the wire-level snake_case options to the TypeScript API.
-function parserOptions(fixture: MarkdownCase) {
- return fixture.options === undefined
+// Map the wire-level snake_case option to the TypeScript API.
+function parserOption(fixture: MarkdownCase) {
+ return fixture.option === undefined
? undefined
- : { maxNestingDepth: fixture.options.max_nesting_depth };
+ : { maxNestingDepth: fixture.option.max_nesting_depth };
}
// Execute one canonical Markdown case against a selected parser dialect.
function executeMarkdownCase(fixture: MarkdownCase, ffm: boolean): void {
- const options = parserOptions(fixture);
+ const option = parserOption(fixture);
if (fixture.error === 'invalid_nesting_depth') {
- expect(() => new MarkdownParser(options)).toThrow(RangeError);
+ expect(() => new MarkdownParser(option)).toThrow(RangeError);
return;
}
const parser = ffm
- ? createFuyeorMarkdownParser(options)
- : createMarkdownParser(options);
+ ? createFuyeorMarkdownParser(option)
+ : createMarkdownParser(option);
const ast = parser(fixture.input ?? '');
if (fixture.html !== undefined) expect(render(ast).trim()).toBe(fixture.html);
if (fixture.plain_text !== undefined)
@@ -120,8 +120,8 @@ function executeMarkdownSuite(
});
}
-executeMarkdownSuite('standard Markdown fixtures', standardFixtures, false);
-executeMarkdownSuite('FFM fixtures', ffmFixtures, true);
+executeMarkdownSuite('cfm fixture', cfmFixture, false);
+executeMarkdownSuite('ffm fixture', ffmFixture, true);
// Verify the public AST shape uses the modern scalar and structural field names.
describe('AST field names', () => {
@@ -139,9 +139,7 @@ describe('AST field names', () => {
},
]);
- const table = createMarkdownParser()(
- '\n| A | B |\n|---|---|\n| C | D |',
- );
+ const table = createMarkdownParser()('\n| A | B |\n|---|---|\n| C | D |');
expect(table[0]).toMatchObject({
type: 'table',
header: [
@@ -161,7 +159,7 @@ describe('AST field names', () => {
});
});
-describe('safety fixtures', () => {
+describe('safety fixture', () => {
expect(safetyFixtures.schema_version).toBe(1);
for (const fixture of safetyFixtures.links)
it(`link: ${fixture.input}`, () =>
diff --git a/package/parser/src/index.ts b/package/parser/src/index.ts
index bfe848b..dc0c346 100644
--- a/package/parser/src/index.ts
+++ b/package/parser/src/index.ts
@@ -11,7 +11,7 @@ export {
listRule,
hrRule,
blockquoteRule,
-} from './rule/blocks';
+} from './rule/block';
export {
hardBreakRule,
inlineCodeRule,
@@ -19,7 +19,7 @@ export {
linkRule,
underlineRule,
strikeRule,
-} from './rule/inlines';
+} from './rule/inline';
export { latexPlugin, latexInlineRule, latexBlockRule } from './plugin/latex';
export {
twemojiPlugin,