diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3e6c923..5d19293 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,13 +7,13 @@ on: # Only trigger when relevant code changes to save resources paths: - '.github/workflows/ci.yml' - - 'packages/linkify/**' - - 'packages/markdown-parser/**' - - 'packages/markdown-parser-rust/**' - - 'packages/markdown-parser-lit/**' - - 'packages/markdown-parser-vue/**' - - 'packages/playground/**' - - 'fixtures/**' + - 'crate/**' + - 'fixture/**' + - 'package/linkify/**' + - 'package/parser/**' + - 'package/lit-renderer**' + - 'package/vue-renderer/**' + - 'playground/**' - 'Cargo.toml' - 'Cargo.lock' - 'package.json' @@ -23,13 +23,13 @@ on: pull_request: paths: - '.github/workflows/ci.yml' - - 'packages/linkify/**' - - 'packages/markdown-parser/**' - - 'packages/markdown-parser-rust/**' - - 'packages/markdown-parser-lit/**' - - 'packages/markdown-parser-vue/**' - - 'packages/playground/**' - - 'fixtures/**' + - 'crate/**' + - 'fixture/**' + - 'package/linkify/**' + - 'package/parser/**' + - 'package/lit-renderer**' + - 'package/vue-renderer/**' + - 'playground/**' - 'Cargo.toml' - 'Cargo.lock' - 'package.json' @@ -44,7 +44,6 @@ env: jobs: test-markdown-parser: - name: Test markdown parser runs-on: ubuntu-latest permissions: contents: read @@ -67,17 +66,15 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile --ignore-scripts - name: Build linkify package - run: pnpm --filter @fuyeor/linkify build + run: pnpm --filter @ffm/linkify build - name: Typecheck parser package - run: pnpm --filter @fuyeor/markdown-parser typecheck + run: pnpm --filter @ffm/parser typecheck - name: Run parser unit tests - run: pnpm --filter @fuyeor/markdown-parser test:unit + run: pnpm --filter @ffm/parser test:unit - name: Run FFM regression tests - run: pnpm --filter @fuyeor/markdown-parser test:ffm - - name: Run safety fixtures - run: pnpm --filter @fuyeor/markdown-parser test:safety - - name: Run shared linkify fixtures - run: pnpm --filter @fuyeor/linkify exec vitest run src/cross-language-fixtures.spec.ts + run: pnpm --filter @ffm/parser test:ffm + - name: Run safety fixture + run: pnpm --filter @ffm/parser test:safety - name: Check Rust parser run: | cargo fmt --all -- --check diff --git a/.gitignore b/.gitignore index efc251d..c12008d 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 +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/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/Dockerfile b/Dockerfile index 2d35f2e..0384ec9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # monorepo/Dockerfile.frontend # ========================================== -# build @fuyeor/markdown-parser-playground +# build @ffm/playground # ========================================== FROM node:24-slim AS builder ENV PNPM_HOME="/pnpm" @@ -13,11 +13,13 @@ WORKDIR /app # prepare depends COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ -# copy shared packages -COPY packages/linkify/package.json packages/linkify/ -COPY packages/markdown-parser/package.json packages/markdown-parser/ -COPY packages/markdown-parser-lit/package.json packages/markdown-parser-lit/ -COPY packages/playground/package.json packages/playground/ +# copy shared package +COPY package/converter/package.json package/converter/ +COPY package/formatter/package.json package/formatter/ +COPY package/linkify/package.json package/linkify/ +COPY package/parser/package.json package/parser/ +COPY package/vue-renderer/package.json package/vue-renderer/ +COPY playground/package.json playground/ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile @@ -25,15 +27,15 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile COPY . . # build linkify before parser consumers resolve its package exports -RUN pnpm --filter @fuyeor/linkify build +RUN pnpm --filter @ffm/linkify build # build parser package before workspace consumers resolve its package exports -RUN pnpm --filter @fuyeor/markdown-parser build +RUN pnpm --filter @ffm/parser build # generate production locale assets before the playground build copies public files RUN pnpm locale make playground prod # build playground -RUN pnpm --filter @fuyeor/markdown-playground build +RUN pnpm --filter @ffm/playground build # ========================================== # release @@ -44,7 +46,7 @@ FROM nginx:alpine AS runner RUN rm -rf /usr/share/nginx/html/* # copy build artifacts -COPY --from=builder /app/packages/playground/dist /usr/share/nginx/html +COPY --from=builder /app/playground/dist /usr/share/nginx/html # Nginx config RUN echo 'server { \ 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 90% rename from packages/markdown-parser-rust/src/ast.rs rename to crate/src/ast.rs index 02a88e9..3d18350 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")] @@ -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/packages/markdown-parser-rust/src/lib.rs b/crate/src/lib.rs similarity index 81% rename from packages/markdown-parser-rust/src/lib.rs rename to crate/src/lib.rs index e795c1d..e0b8b28 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 @@ -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/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 82% rename from packages/markdown-parser-rust/src/parser.rs rename to crate/src/parser.rs index aba575c..ec66940 100644 --- a/packages/markdown-parser-rust/src/parser.rs +++ b/crate/src/parser.rs @@ -1,10 +1,10 @@ -// packages/markdown-parser-rust/src/parser.rs +// src/parser.rs use std::cell::RefCell; 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/packages/markdown-parser-rust/src/plain_text.rs b/crate/src/plain_text.rs similarity index 74% rename from packages/markdown-parser-rust/src/plain_text.rs rename to crate/src/plain_text.rs index 0d44eaa..56ed2de 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}; @@ -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/packages/markdown-parser-rust/src/render.rs b/crate/src/render.rs similarity index 81% rename from packages/markdown-parser-rust/src/render.rs rename to crate/src/render.rs index 58bf07f..5d9e9b1 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}; @@ -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, ""); } 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/packages/markdown-parser-rust/src/rules.rs b/crate/src/rule.rs similarity index 91% rename from packages/markdown-parser-rust/src/rules.rs rename to crate/src/rule.rs index 0d5646e..2d01402 100644 --- a/packages/markdown-parser-rust/src/rules.rs +++ b/crate/src/rule.rs @@ -1,4 +1,4 @@ -// packages/markdown-parser-rust/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/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/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 89% rename from fixtures/ffm.json rename to fixture/ffm.json index 8e7539a..60a544e 100644 --- a/fixtures/ffm.json +++ b/fixture/ffm.json @@ -67,15 +67,15 @@ "html": "

    Visit fuyeor.рф

    ", "assert": [ { - "path": "/0/children/1/type", + "path": "/0/content/1/type", "value": "link" }, { - "path": "/0/children/1/url", + "path": "/0/content/1/url", "value": "https://fuyeor.рф" }, { - "path": "/0/children/1/children/0/content", + "path": "/0/content/1/content/0/value", "value": "fuyeor.рф" } ], @@ -89,7 +89,7 @@ "value": "list" }, { - "path": "/0/children/0/children/1/type", + "path": "/0/content/0/content/1/type", "value": "list" } ], @@ -103,7 +103,7 @@ "value": "list" }, { - "path": "/0/children/0/children/1/type", + "path": "/0/content/0/content/1/type", "value": "list" } ], @@ -117,7 +117,7 @@ "value": "list" }, { - "path": "/0/children/0/children/1/children/0/children/1/type", + "path": "/0/content/0/content/1/content/0/content/1/type", "value": "list" } ], @@ -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" @@ -141,11 +141,11 @@ "input": "__under__ and --strike--\n\n```quote\n## nested\n```", "assert": [ { - "path": "/0/children/0/type", + "path": "/0/content/0/type", "value": "underline" }, { - "path": "/0/children/2/type", + "path": "/0/content/2/type", "value": "strike" }, { @@ -153,7 +153,7 @@ "value": "blockquote" }, { - "path": "/1/children/0/type", + "path": "/1/content/0/type", "value": "heading" } ], @@ -167,7 +167,7 @@ "value": "slide" }, { - "path": "/0/children", + "path": "/0/content", "length": 2 } ], @@ -181,11 +181,11 @@ "value": "accordion" }, { - "path": "/0/children", + "path": "/0/content", "length": 2 }, { - "path": "/0/children/0/name", + "path": "/0/content/0/name", "value": "acc-0" } ], @@ -199,11 +199,11 @@ "value": "chain" }, { - "path": "/0/children/0/isCompleted", + "path": "/0/content/0/isCompleted", "value": true }, { - "path": "/0/children/0/hasCheckbox", + "path": "/0/content/0/hasCheckbox", "value": true } ], 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 80% rename from fixtures/markdown.json rename to fixture/markdown.json index 96348b3..c987556 100644 --- a/fixtures/markdown.json +++ b/fixture/markdown.json @@ -7,11 +7,6 @@ "html": "

    Hello World

    ", "plain_text": "Hello World" }, - { - "input": "```ts\nconst a = 1;\n```", - "html": "
    ts
    const a = 1;\n
    ", - "plain_text": "const a = 1;" - }, { "input": "visit www.fuyeor.com or [click here](https://fuyeor.com)", "html": "

    visit www.fuyeor.com or click here

    ", @@ -31,7 +26,7 @@ "value": "paragraph" }, { - "path": "/0/children/0/content", + "path": "/0/content/0/value", "value": "text " } ], @@ -45,15 +40,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/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 63% rename from packages/markdown-parser-lit/src/render.ts rename to package/lit-renderer/src/render.ts index f055ad7..5bea102 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 = @@ -23,46 +19,46 @@ export function render(nodes?: 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)} `; } 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` @@ -90,7 +86,7 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { ${node.lang ? html`
    ${node.lang}
    ` : ''}
    ${node.content}
    + : ''}">${node.value}
    `; @@ -102,16 +98,16 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { : undefined; return staticHtml` <${unsafeStatic(tag)} start="${startValue || ''}"> - ${render(node.children)} + ${render(node.content)} `; } 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`
    `; @@ -125,17 +121,17 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] {
    - ${(node.headers ?? []).map( - (cell) => html``, + ${(node.header ?? []).map( + (cell) => html``, )} - ${node.children?.map( + ${node.content?.map( (row) => html` - ${(row.children ?? []).map( - (cell) => html``, + ${(row.content ?? []).map( + (cell) => html``, )} `, @@ -146,13 +142,13 @@ export function render(nodes?: 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)}
    `; @@ -160,18 +156,16 @@ export function render(nodes?: 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 @@ -187,7 +181,7 @@ export function render(nodes?: ASTNode[]): (TemplateResult | string | null)[] { ${node.title && node.title.length > 0 ? html`
    ${render(node.title)}
    ` : ''} -
    ${render(node.children)}
    +
    ${render(node.content)}
    `; @@ -195,9 +189,7 @@ export function render(nodes?: 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/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 92% rename from packages/markdown-parser/src/core/parser.ts rename to package/parser/src/core/parser.ts index c105f8a..e7d416b 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 { linkify } from '@ffm/linkify'; import { BlockState, InlineState } from './state'; -import { linkify } from '@fuyeor/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'); @@ -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); @@ -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/packages/markdown-parser/src/core/plain-text.ts b/package/parser/src/core/plain-text.ts similarity index 62% rename from packages/markdown-parser/src/core/plain-text.ts rename to package/parser/src/core/plain-text.ts index 1b62e3c..231fc1d 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. @@ -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,39 +32,41 @@ 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 ?? []); + const header = + 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 ?? []); } } /// 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 74% rename from packages/markdown-parser/src/core/render.ts rename to package/parser/src/core/render.ts index 5ce79d7..ebce92e 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'; @@ -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 += `${escapeHtml(alt)}`; 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)}\n`; + html += `<${tag}${start}>\n${render(node.content)}\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 += '
    ${render(cell.children)}${render(cell.content)}
    ${render(cell.children)}${render(cell.content)}
    \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 += `
    ${render(node.children)}
    `; + html += `
    ${render(node.content)}
    `; 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/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..f258866 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/block'; 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/inline'; +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 63% rename from packages/markdown-parser/src/cross-language-fixtures.spec.ts rename to package/parser/src/index.spec.ts index 1100248..0d72632 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'; @@ -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 = { @@ -41,11 +41,14 @@ 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'); -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. @@ -66,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) @@ -117,10 +120,46 @@ function executeMarkdownSuite( }); } -executeMarkdownSuite('standard Markdown fixtures', standardFixtures, false); -executeMarkdownSuite('FFM fixtures', ffmFixtures, true); +executeMarkdownSuite('cfm fixture', cfmFixture, false); +executeMarkdownSuite('ffm fixture', ffmFixture, true); -describe('safety fixtures', () => { +// 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 fixture', () => { expect(safetyFixtures.schema_version).toBe(1); for (const fixture of safetyFixtures.links) it(`link: ${fixture.input}`, () => diff --git a/packages/markdown-parser/src/index.ts b/package/parser/src/index.ts similarity index 74% rename from packages/markdown-parser/src/index.ts rename to package/parser/src/index.ts index 1ae0548..dc0c346 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/block'; export { hardBreakRule, inlineCodeRule, @@ -19,16 +19,16 @@ export { linkRule, underlineRule, strikeRule, -} from './rules/inlines'; -export { latexPlugin, latexInlineRule, latexBlockRule } from './plugins/latex'; +} from './rule/inline'; +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'; +} from './type'; diff --git a/packages/markdown-parser/src/plugins/ast.ts b/package/parser/src/plugin/ast.ts similarity index 72% rename from packages/markdown-parser/src/plugins/ast.ts rename to package/parser/src/plugin/ast.ts index db9fd36..781dce1 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[]; @@ -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/packages/markdown-parser/src/plugins/async-blocks.ts b/package/parser/src/plugin/async-blocks.ts similarity index 91% rename from packages/markdown-parser/src/plugins/async-blocks.ts rename to package/parser/src/plugin/async-blocks.ts index 1af6b5a..6145159 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'; @@ -15,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; @@ -23,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, }; }, @@ -41,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/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 69% rename from packages/markdown-parser/src/plugins/latex.ts rename to package/parser/src/plugin/latex.ts index 8e561c7..4936f0a 100644 --- a/packages/markdown-parser/src/plugins/latex.ts +++ b/package/parser/src/plugin/latex.ts @@ -1,9 +1,9 @@ -// @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 = '$$'; +const inlineMathMarker = '$'; +const blockMathMarker = '$$'; // Find an unescaped dollar delimiter without interpreting LaTeX escapes. const findMathDelimiter = ( @@ -29,26 +29,26 @@ 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; return { node: { type: 'math_inline', - content: state.content - .slice(state.pos + MATH_INLINE_MARKER.length, endIndex) + value: state.content + .slice(state.pos + inlineMathMarker.length, endIndex) .trim(), }, - consumedChars: endIndex - state.pos + MATH_INLINE_MARKER.length, + consumedChars: endIndex - state.pos + inlineMathMarker.length, }; }, }; @@ -56,37 +56,37 @@ 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; return { node: { type: 'math_block', - content: contentLines.join('\n').trim(), + value: contentLines.join('\n').trim(), }, consumedLines, }; diff --git a/packages/markdown-parser/src/plugins/twemoji.ts b/package/parser/src/plugin/twemoji.ts similarity index 89% rename from packages/markdown-parser/src/plugins/twemoji.ts rename to package/parser/src/plugin/twemoji.ts index cd311db..fb01ac1 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; @@ -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/packages/markdown-parser/src/rules/blocks.ts b/package/parser/src/rule/block.ts similarity index 93% rename from packages/markdown-parser/src/rules/blocks.ts rename to package/parser/src/rule/block.ts index 40364df..da7261c 100644 --- a/packages/markdown-parser/src/rules/blocks.ts +++ b/package/parser/src/rule/block.ts @@ -1,5 +1,5 @@ -// @fuyeor/markdown-parser/src/rules/blocks.ts -import type { ASTNode, BlockRule } from '#/types'; +// @ffm/parser/src/rule/block.ts +import type { ASTNode, BlockRule } from '#/type'; import { BlockState } from '#/core/state'; type FencedBlock = { @@ -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; @@ -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, }; @@ -42,13 +42,13 @@ export const headingRule: BlockRule = { */ export const codeBlockRule: BlockRule = { name: 'code_block', - markers: ['`', '~'], + marker: ['`', '~'], parse(state: BlockState) { const block = extractFencedBlock(state); 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; @@ -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; @@ -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, }; @@ -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; @@ -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, }; @@ -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 ( @@ -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/packages/markdown-parser/src/rules/ffm.ts b/package/parser/src/rule/ffm.ts similarity index 73% rename from packages/markdown-parser/src/rules/ffm.ts rename to package/parser/src/rule/ffm.ts index b1e8cb9..1a06ec5 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 { extractFencedBlock } from './block'; +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) @@ -18,7 +18,7 @@ const TITLE_REGEX = /^\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; @@ -26,14 +26,14 @@ 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; // 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, }; } @@ -41,15 +41,15 @@ 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) => ({ 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 @@ -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; @@ -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/packages/markdown-parser/src/rules/inlines.ts b/package/parser/src/rule/inline.ts similarity index 88% rename from packages/markdown-parser/src/rules/inlines.ts rename to package/parser/src/rule/inline.ts index d5c83fb..67385d1 100644 --- a/packages/markdown-parser/src/rules/inlines.ts +++ b/package/parser/src/rule/inline.ts @@ -1,5 +1,5 @@ -// @fuyeor/markdown-parser/src/rules/inlines.ts -import type { InlineRule } from '#/types'; +// @ffm/parser/src/rule/inline.ts +import type { InlineRule } from '#/type'; import { InlineState } from '#/core/state'; import { isSafeColorValue } from '#/core/color'; import { isSafeLinkUrl } from '#/core/url'; @@ -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; @@ -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, @@ -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 @@ -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, }; @@ -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; @@ -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, }; @@ -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; @@ -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, }; @@ -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; @@ -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, }; @@ -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; @@ -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/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 86% rename from packages/markdown-parser/src/types.ts rename to package/parser/src/type.ts index bec0595..34e8499 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'; @@ -33,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; @@ -59,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: ( @@ -72,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: ( @@ -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/markdown-parser-vue/package.json b/package/vue-renderer/package.json similarity index 80% rename from packages/markdown-parser-vue/package.json rename to package/vue-renderer/package.json index 1e216a6..5e24165 100644 --- a/packages/markdown-parser-vue/package.json +++ b/package/vue-renderer/package.json @@ -1,5 +1,5 @@ { - "name": "@fuyeor/markdown-parser-vue", + "name": "@ffm/vue-renderer", "license": "MIT", "author": "Fuyeor ", "type": "module", @@ -8,7 +8,7 @@ ".": "./src/index.ts" }, "dependencies": { - "@fuyeor/markdown-parser": "workspace:*" + "@ffm/parser": "workspace:*" }, "peerDependencies": { "vue": "*" diff --git a/package/vue-renderer/src/index.ts b/package/vue-renderer/src/index.ts new file mode 100644 index 0000000..977c027 --- /dev/null +++ b/package/vue-renderer/src/index.ts @@ -0,0 +1,2 @@ +// @ffm/vue-renderer/src/index.ts +export { renderToVue } from './render'; diff --git a/packages/markdown-parser-vue/src/render.ts b/package/vue-renderer/src/render.ts similarity index 70% rename from packages/markdown-parser-vue/src/render.ts rename to package/vue-renderer/src/render.ts index c0c8180..8b8dbe0 100644 --- a/packages/markdown-parser-vue/src/render.ts +++ b/package/vue-renderer/src/render.ts @@ -1,10 +1,6 @@ -// @fuyeor/markdown-parser-vue/src/render.ts +// @ffm/vue-renderer/src/render.ts import { h, type VNode } from 'vue'; -import { - isSafeColorValue, - isSafeLinkUrl, - type ASTNode, -} from '@fuyeor/markdown-parser'; +import { isSafeColorValue, isSafeLinkUrl, type ASTNode } from '@ffm/parser'; export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { return nodes.map((node) => { @@ -18,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', { @@ -61,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' }, [ @@ -74,7 +70,7 @@ export function renderToVue(nodes: ASTNode[]): (VNode | string)[] { h( 'code', { class: node.lang ? `language-${node.lang}` : '' }, - node.content || '', + node.value || '', ), ), ]); @@ -84,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( @@ -96,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 || [])), ), ), ), @@ -120,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 }, [ @@ -135,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 @@ -156,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 || [])), ]), ]); } @@ -165,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 || [])); } }); } diff --git a/packages/benchmarks/speed.ts b/packages/benchmarks/speed.ts deleted file mode 100644 index 6705a34..0000000 --- a/packages/benchmarks/speed.ts +++ /dev/null @@ -1,34 +0,0 @@ -// @fuyeor/markdown-parser-benchmark/speed.ts -// pnpm -F @fuyeor/markdown-parser-benchmark bench:speed -// @ts-nocheck -import { run, bench, group } from 'mitata'; -import { createFuyeorMarkdownParser, render } from '@fuyeor/markdown-parser'; -import MarkdownIt from 'markdown-it'; -import { marked } from 'marked'; -import { socialSample, blogSample } 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 Posts (Small/Frequent)', () => { - bench('@fuyeor/markdown-parser (AST)', () => ffm(socialSample)); - bench('@fuyeor/markdown-parser (HTML)', () => ffmIt(socialSample)); - bench('markdown-it (HTML)', () => mdIt.render(socialSample)); - bench('marked (HTML)', () => marked.parse(socialSample)); -}); - -// Benchmarking Blog scenario -group('20 Blog Posts (Structured/Nested)', () => { - bench('@fuyeor/markdown-parser (AST)', () => ffm(blogSample)); - bench('@fuyeor/markdown-parser (HTML)', () => ffmIt(blogSample)); - bench('markdown-it (HTML)', () => mdIt.render(blogSample)); - bench('marked (HTML)', () => marked.parse(blogSample)); -}); - -await run(); diff --git a/packages/extensions/vscode/src/ffm-grammar.spec.ts b/packages/extensions/vscode/src/ffm-grammar.spec.ts deleted file mode 100644 index 8489701..0000000 --- a/packages/extensions/vscode/src/ffm-grammar.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -// packages/vscode-extension/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/packages/markdown-parser-lit/src/index.ts b/packages/markdown-parser-lit/src/index.ts deleted file mode 100644 index 6b8f4b0..0000000 --- a/packages/markdown-parser-lit/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -// @fuyeor/markdown-parser-lit/src/index.ts -export * from '@fuyeor/markdown-parser'; -export { render } from './render'; diff --git a/packages/markdown-parser-vue/src/index.ts b/packages/markdown-parser-vue/src/index.ts deleted file mode 100644 index d54cf72..0000000 --- a/packages/markdown-parser-vue/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -// @fuyeor/markdown-parser-vue/src/index.ts -export { renderToVue } from './render'; diff --git a/packages/playground/src/api/examples.ts b/packages/playground/src/api/examples.ts deleted file mode 100644 index b06e81f..0000000 --- a/packages/playground/src/api/examples.ts +++ /dev/null @@ -1,11 +0,0 @@ -// @/api/examples.ts -import apiClient from './index'; - -export async function fetchExample(locale: string): Promise { - try { - return await apiClient.get(`assets/examples/${locale}.ffm`); - } catch (error) { - // Fallback to English example if the requested locale is not found - return await apiClient.get('assets/examples/en.ffm'); - } -} diff --git a/packages/playground/src/main.ts b/packages/playground/src/main.ts deleted file mode 100644 index 8c8b4e6..0000000 --- a/packages/playground/src/main.ts +++ /dev/null @@ -1,25 +0,0 @@ -// @/main.ts -// pnpm playground -import App from './App.vue'; -import router from './router'; - -import { createApp } from 'vue'; -import { initializeLocale, createHead } from '@fuyeor/commons'; -import { vRipple, vTooltip } from '@fuyeor/interactify'; - -async function bootstrap() { - const app = createApp(App); - const head = createHead(); - - app.use(router); - app.use(head); - - await initializeLocale({ app }); - - app.directive('ripple', vRipple); - app.directive('tooltip', vTooltip); - - app.mount('#app'); -} - -bootstrap(); diff --git a/packages/playground/index.html b/playground/index.html similarity index 100% rename from packages/playground/index.html rename to playground/index.html diff --git a/packages/playground/package.json b/playground/package.json similarity index 79% rename from packages/playground/package.json rename to playground/package.json index 6dd0db8..bf14a9a 100644 --- a/packages/playground/package.json +++ b/playground/package.json @@ -1,5 +1,5 @@ { - "name": "@fuyeor/markdown-playground", + "name": "@ffm/playground", "license": "MIT", "author": "Fuyeor ", "type": "module", @@ -12,13 +12,13 @@ ".": "./src/main.ts" }, "dependencies": { + "@ffm/formatter": "workspace:*", + "@ffm/parser": "workspace:*", + "@ffm/vue-renderer": "workspace:*", "@fuyeor/commons": "^0.1.2", "@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:*", "@fuyeor/vue-router": "^0.0.9", "vue": "^3.5.41" }, diff --git a/packages/playground/public/assets/css/style.css b/playground/public/assets/css/style.css similarity index 100% rename from packages/playground/public/assets/css/style.css rename to playground/public/assets/css/style.css diff --git a/packages/playground/public/assets/examples/en.ffm b/playground/public/assets/example/en.ffm similarity index 100% rename from packages/playground/public/assets/examples/en.ffm rename to playground/public/assets/example/en.ffm diff --git a/packages/playground/public/assets/examples/fr.ffm b/playground/public/assets/example/fr.ffm similarity index 100% rename from packages/playground/public/assets/examples/fr.ffm rename to playground/public/assets/example/fr.ffm diff --git a/packages/playground/public/assets/examples/ja.ffm b/playground/public/assets/example/ja.ffm similarity index 100% rename from packages/playground/public/assets/examples/ja.ffm rename to playground/public/assets/example/ja.ffm diff --git a/packages/playground/public/assets/examples/zh-hans.ffm b/playground/public/assets/example/zh-hans.ffm similarity index 100% rename from packages/playground/public/assets/examples/zh-hans.ffm rename to playground/public/assets/example/zh-hans.ffm diff --git a/packages/playground/public/favicon.svg b/playground/public/favicon.svg similarity index 100% rename from packages/playground/public/favicon.svg rename to playground/public/favicon.svg diff --git a/packages/playground/public/robots.txt b/playground/public/robots.txt similarity index 100% rename from packages/playground/public/robots.txt rename to playground/public/robots.txt diff --git a/packages/playground/src/layout/Left.vue b/playground/src/@app/component/left-sidebar.vue similarity index 74% rename from packages/playground/src/layout/Left.vue rename to playground/src/@app/component/left-sidebar.vue index d317117..d21f6ba 100644 --- a/packages/playground/src/layout/Left.vue +++ b/playground/src/@app/component/left-sidebar.vue @@ -1,8 +1,8 @@ - +