From 755c6ce0be305368bf8176b644a1aab0e144a609 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Fri, 15 Aug 2025 00:10:14 +0900 Subject: [PATCH 1/7] feat(wasm): add WebAssembly bindings - Add wasm-bindgen setup with Cargo.toml - Implement to_html and to_html_with_options APIs - Support CommonMark, GFM, MDX, and frontmatter --- wasm/Cargo.toml | 23 ++++++++++++++++ wasm/src/lib.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 wasm/Cargo.toml create mode 100644 wasm/src/lib.rs diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml new file mode 100644 index 00000000..69e1aa37 --- /dev/null +++ b/wasm/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "markdown-rs-wasm" +version = "0.0.1" +authors = ["markdown-rs contributors"] +edition = "2018" +description = "WebAssembly bindings for markdown-rs" +license = "MIT" +repository = "https://github.com/wooorm/markdown-rs" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +markdown = { path = ".." } +wasm-bindgen = "0.2" +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6" +console_error_panic_hook = "0.1" + +[dependencies.web-sys] +version = "0.3" +features = ["console"] + diff --git a/wasm/src/lib.rs b/wasm/src/lib.rs new file mode 100644 index 00000000..617c868e --- /dev/null +++ b/wasm/src/lib.rs @@ -0,0 +1,72 @@ +use serde::{Deserialize, Serialize}; +use wasm_bindgen::prelude::*; + +// Set panic hook for better error messages +#[wasm_bindgen(start)] +pub fn init() { + console_error_panic_hook::set_once(); +} + +#[derive(Serialize, Deserialize, Default)] +pub struct Options { + #[serde(default)] + pub gfm: bool, + + #[serde(default)] + pub mdx: bool, + + #[serde(default)] + pub frontmatter: bool, + + #[serde(rename = "allowDangerousHtml", default)] + pub allow_dangerous_html: bool, + + #[serde(rename = "allowDangerousProtocol", default)] + pub allow_dangerous_protocol: bool, +} + +/// Convert markdown to HTML with default options +#[wasm_bindgen] +pub fn to_html(input: &str) -> String { + markdown::to_html(input) +} + +/// Convert markdown to HTML with options +#[wasm_bindgen] +pub fn to_html_with_options(input: &str, options: JsValue) -> Result { + // Parse options from JavaScript + let opts: Options = if options.is_null() || options.is_undefined() { + Options::default() + } else { + serde_wasm_bindgen::from_value(options) + .map_err(|e| JsValue::from_str(&format!("Invalid options: {}", e)))? + }; + + // Build markdown options + let mut parse_options = markdown::ParseOptions::default(); + let mut compile_options = markdown::CompileOptions::default(); + + // Configure constructs based on options + if opts.gfm { + parse_options.constructs = markdown::Constructs::gfm(); + } else if opts.mdx { + parse_options.constructs = markdown::Constructs::mdx(); + } + + if opts.frontmatter { + parse_options.constructs.frontmatter = true; + } + + // Configure compile options + compile_options.allow_dangerous_html = opts.allow_dangerous_html; + compile_options.allow_dangerous_protocol = opts.allow_dangerous_protocol; + + let markdown_options = markdown::Options { + parse: parse_options, + compile: compile_options, + }; + + // Convert markdown to HTML + markdown::to_html_with_options(input, &markdown_options) + .map_err(|e| JsValue::from_str(&format!("Markdown error: {}", e))) +} From 1b36e683dab380eef2b046c40bf447587a4a7d4f Mon Sep 17 00:00:00 2001 From: jp-knj Date: Fri, 15 Aug 2025 00:10:30 +0900 Subject: [PATCH 2/7] feat(wasm): add Node.js ES module wrapper - Create async/sync JavaScript API wrapper - Configure package.json with .mjs build script - Handle WASM initialization and module loading --- wasm/lib/index.mjs | 80 ++++++++++++++++++++++++++++++++++++++++++++++ wasm/package.json | 31 ++++++++++++++++++ 2 files changed, 111 insertions(+) create mode 100644 wasm/lib/index.mjs create mode 100644 wasm/package.json diff --git a/wasm/lib/index.mjs b/wasm/lib/index.mjs new file mode 100644 index 00000000..bd5ef05a --- /dev/null +++ b/wasm/lib/index.mjs @@ -0,0 +1,80 @@ +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { dirname, join } from 'node:path'; + +// Dynamic import to handle the ES module +let wasmModule = null; + +async function loadWasmModule() { + if (!wasmModule) { + wasmModule = await import('../pkg/markdown_rs_wasm.mjs'); + } + return wasmModule; +} + +// Initialize WASM module once +let initialized = false; +let initPromise = null; + +async function ensureInitialized() { + if (initialized) return; + if (!initPromise) { + const wasm = await loadWasmModule(); + // Read WASM file for Node.js + const __dirname = dirname(fileURLToPath(import.meta.url)); + const wasmPath = join(__dirname, '..', 'pkg', 'markdown_rs_wasm_bg.wasm'); + const wasmBuffer = await readFile(wasmPath); + + initPromise = wasm.default({ module_or_path: wasmBuffer }).then(() => { + initialized = true; + }); + } + await initPromise; +} + +/** + * Convert markdown to HTML using CommonMark + * @param {string} input - Markdown text + * @returns {Promise} HTML output + */ +export async function toHtml(input) { + await ensureInitialized(); + const wasm = await loadWasmModule(); + return wasm.to_html(input); +} + +/** + * Convert markdown to HTML with options + * @param {string} input - Markdown text + * @param {Object} options - Parsing options + * @param {boolean} [options.gfm] - Enable GitHub Flavored Markdown + * @param {boolean} [options.mdx] - Enable MDX + * @param {boolean} [options.frontmatter] - Enable frontmatter + * @param {boolean} [options.allowDangerousHtml] - Allow raw HTML + * @param {boolean} [options.allowDangerousProtocol] - Allow dangerous protocols + * @returns {Promise} HTML output + */ +export async function toHtmlWithOptions(input, options) { + await ensureInitialized(); + const wasm = await loadWasmModule(); + return wasm.to_html_with_options(input, options); +} + +// Also export sync versions after initialization +export function toHtmlSync(input) { + if (!initialized || !wasmModule) { + throw new Error('WASM not initialized. Call toHtml() first or await init()'); + } + return wasmModule.to_html(input); +} + +export function toHtmlWithOptionsSync(input, options) { + if (!initialized || !wasmModule) { + throw new Error('WASM not initialized. Call toHtmlWithOptions() first or await init()'); + } + return wasmModule.to_html_with_options(input, options); +} + +export async function init() { + await ensureInitialized(); +} diff --git a/wasm/package.json b/wasm/package.json new file mode 100644 index 00000000..a36cfe5c --- /dev/null +++ b/wasm/package.json @@ -0,0 +1,31 @@ +{ + "name": "markdown-rs-wasm", + "version": "0.0.1", + "description": "WebAssembly bindings for markdown-rs", + "type": "module", + "exports": "./lib/index.mjs", + "files": [ + "lib/", + "pkg/", + "README.md" + ], + "scripts": { + "build": "wasm-pack build --target web --out-dir pkg && mv pkg/markdown_rs_wasm.js pkg/markdown_rs_wasm.mjs", + "test": "node --test test/index.test.mjs", + "example:basic": "node examples/basic.mjs", + "example:options": "node examples/with-options.mjs" + }, + "keywords": [ + "markdown", + "commonmark", + "wasm", + "webassembly", + "mdx", + "gfm" + ], + "license": "MIT", + "repository": "https://github.com/wooorm/markdown-rs", + "engines": { + "node": ">=16.0.0" + } +} From 7d5587ba97cbb8d207d641cab934776e3bc695c8 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Fri, 15 Aug 2025 00:10:46 +0900 Subject: [PATCH 3/7] test(wasm): add comprehensive test suite - Test basic markdown conversions - Test GFM, MDX, and security options - Test sync/async APIs - Cover edge cases and complex markdown --- wasm/test/index.test.mjs | 119 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 wasm/test/index.test.mjs diff --git a/wasm/test/index.test.mjs b/wasm/test/index.test.mjs new file mode 100644 index 00000000..4be8c5fb --- /dev/null +++ b/wasm/test/index.test.mjs @@ -0,0 +1,119 @@ +import assert from 'node:assert'; +import { describe, it } from 'node:test'; +import { toHtml, toHtmlWithOptions, toHtmlSync, toHtmlWithOptionsSync } from '../lib/index.mjs'; + +describe('markdown-rs WASM', () => { + describe('Basic functionality', () => { + it('converts heading to HTML', async () => { + const result = await toHtml('# Hello World'); + assert.strictEqual(result, '

Hello World

'); + }); + + it('converts paragraph to HTML', async () => { + const result = await toHtml('This is a paragraph.'); + assert.strictEqual(result, '

This is a paragraph.

'); + }); + + it('converts emphasis to HTML', async () => { + const result = await toHtml('*italic* and **bold**'); + assert.strictEqual(result, '

italic and bold

'); + }); + + it('converts links to HTML', async () => { + const result = await toHtml('[GitHub](https://github.com)'); + assert.strictEqual(result, '

GitHub

'); + }); + + it('converts code blocks to HTML', async () => { + const result = await toHtml('```js\nconst x = 1;\n```'); + assert(result.includes('
'));
+      assert(result.includes(' {
+    it('enables strikethrough with GFM', async () => {
+      const result = await toHtmlWithOptions('~strikethrough~', { gfm: true });
+      assert(result.includes('strikethrough'));
+    });
+
+    it('enables tables with GFM', async () => {
+      const markdown = '| a | b |\n|---|---|\n| c | d |';
+      const result = await toHtmlWithOptions(markdown, { gfm: true });
+      assert(result.includes(''));
+      assert(result.includes(''));
+    });
+
+    it('enables autolinks with GFM', async () => {
+      const result = await toHtmlWithOptions('https://example.com', { gfm: true });
+      assert(result.includes(''));
+    });
+  });
+
+  describe('MDX options', () => {
+    it('handles JSX with MDX enabled', async () => {
+      const result = await toHtmlWithOptions('# Hello ', { mdx: true });
+      assert(result.includes('

')); + }); + }); + + describe('Security options', () => { + it('blocks dangerous HTML by default', async () => { + const html = ''; + const result = await toHtmlWithOptions(html, {}); + assert(!result.includes(''; + const result = await toHtmlWithOptions(html, { allowDangerousHtml: true }); + assert(result.includes(' + +[Dangerous Link](javascript:alert('XSS')) +`; + +console.log('Input Markdown:'); +console.log('```markdown'); +console.log(dangerousMarkdown.trim()); +console.log('```\n'); + +console.log('Safe mode output (default):'); +console.log('```html'); +const safeHtml = await toHtmlWithOptions(dangerousMarkdown, {}); +console.log(safeHtml.trim()); +console.log('```\n'); + +console.log('Dangerous mode output (be careful!):'); +console.log('```html'); +const dangerousHtml = await toHtmlWithOptions(dangerousMarkdown, { + allowDangerousHtml: true, + allowDangerousProtocol: true +}); +console.log(dangerousHtml.trim()); +console.log('```'); + +// Example 5: Combined Options +console.log('\n┌────────────────────────────────────────────────┐'); +console.log('│ Example 5: Combined Options (GFM + MDX) │'); +console.log('└────────────────────────────────────────────────┘\n'); + +const combinedMarkdown = ` +| GFM | MDX | +|-----|-----| +| Yes | Yes | + +~strikethrough~ and +`; + +const combinedHtml = await toHtmlWithOptions(combinedMarkdown, { + gfm: true, + mdx: true +}); +console.log('Input Markdown:'); +console.log('```markdown'); +console.log(combinedMarkdown.trim()); +console.log('```\n'); +console.log('Output HTML:'); +console.log('```html'); +console.log(combinedHtml.trim()); +console.log('```'); + +console.log('\n╔════════════════════════════════════════════════╗'); +console.log('║ Examples Completed! ║'); +console.log('╚════════════════════════════════════════════════╝\n'); From 58b75291deb6a7968a130f79c54af58e457f449e Mon Sep 17 00:00:00 2001 From: jp-knj Date: Fri, 15 Aug 2025 00:11:36 +0900 Subject: [PATCH 5/7] build: configure monorepo for WASM package - Add wasm to Cargo workspace - Setup pnpm workspace with wasm directory - Add root package.json for development scripts - Update .gitignore for WASM build artifacts --- .gitignore | 3 +++ Cargo.toml | 2 +- package.json | 10 ++++++++++ pnpm-workspace.yaml | 2 ++ 4 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 package.json create mode 100644 pnpm-workspace.yaml diff --git a/.gitignore b/.gitignore index 151575c5..bce355a7 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,6 @@ fuzz/corpus fuzz/artifacts fuzz/hfuzz_target fuzz/hfuzz_workspace +node_modules/ +pnpm-lock.yaml +pnpm-debug.log* diff --git a/Cargo.toml b/Cargo.toml index 5462ac32..f4760ff7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ rust-version = "1.56" version = "1.0.0" [workspace] -members = ["generate", "mdast_util_to_markdown"] +members = ["generate", "mdast_util_to_markdown", "wasm"] [workspace.dependencies] pretty_assertions = "1" diff --git a/package.json b/package.json new file mode 100644 index 00000000..2c8265d2 --- /dev/null +++ b/package.json @@ -0,0 +1,10 @@ +{ + "name": "markdown-rs-monorepo", + "private": true, + "scripts": { + "build": "pnpm -r build", + "test": "pnpm -r test", + "lint": "cargo fmt --all && cargo clippy --all-features --all-targets --workspace" + }, + "packageManager": "pnpm@8.15.1" +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..1e9d6a7f --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'wasm' \ No newline at end of file From 67fdfb14ce24bb8a56a5275d87d7afcd216a5c18 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Fri, 15 Aug 2025 22:40:58 +0900 Subject: [PATCH 6/7] feat(wasm): prepare @wooorm/markdown-wasm for plain-npm publish - Scope package as @wooorm/markdown-wasm - Export lib/index.mjs (+ types) - Add examples script and require Node >=18 - Update Whitelist files Refs: #185 --- wasm/package.json | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/wasm/package.json b/wasm/package.json index a36cfe5c..0121a73a 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -1,19 +1,24 @@ { - "name": "markdown-rs-wasm", + "name": "@wooorm/markdown-wasm", "version": "0.0.1", "description": "WebAssembly bindings for markdown-rs", "type": "module", - "exports": "./lib/index.mjs", + "exports": { + ".": { + "types": "./lib/index.d.ts", + "default": "./lib/index.mjs" + } + }, "files": [ "lib/", "pkg/", - "README.md" + "README.md", + "LICENSE" ], "scripts": { "build": "wasm-pack build --target web --out-dir pkg && mv pkg/markdown_rs_wasm.js pkg/markdown_rs_wasm.mjs", "test": "node --test test/index.test.mjs", - "example:basic": "node examples/basic.mjs", - "example:options": "node examples/with-options.mjs" + "examples": "node examples/basic.mjs && node examples/with-options.mjs" }, "keywords": [ "markdown", @@ -26,6 +31,6 @@ "license": "MIT", "repository": "https://github.com/wooorm/markdown-rs", "engines": { - "node": ">=16.0.0" + "node": ">=18" } } From 7adf3c7e615f79d8757dac2ea8e77e5861ee894d Mon Sep 17 00:00:00 2001 From: jp-knj Date: Sat, 16 Aug 2025 00:56:49 +0900 Subject: [PATCH 7/7] feat(docs): add WASM package documentation and references - Add @wooorm/markdown-wasm - Update wasm/README.md with correct package name - Fix installation and import examples to use @wooorm/markdown-wasm --- readme.md | 2 ++ wasm/README.md | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index f3710bab..a0b27ed1 100644 --- a/readme.md +++ b/readme.md @@ -373,6 +373,8 @@ Special thanks go out to: — same as `markdown-rs` but in JavaScript * [`mdxjs-rs`][mdxjs-rs] — wraps `markdown-rs` to *compile* MDX to JavaScript +* [`@wooorm/markdown-wasm`](https://www.npmjs.com/package/@wooorm/markdown-wasm) + — WASM bindings for `markdown-rs` ## License diff --git a/wasm/README.md b/wasm/README.md index 4cb9fde5..0d840498 100644 --- a/wasm/README.md +++ b/wasm/README.md @@ -1,4 +1,4 @@ -# markdown-rs WASM Bindings +# @wooorm/markdown-wasm WebAssembly bindings for [markdown-rs](https://github.com/wooorm/markdown-rs). @@ -14,13 +14,13 @@ WebAssembly bindings for [markdown-rs](https://github.com/wooorm/markdown-rs). ## Installation ```bash -pnpm add markdown-rs-wasm +npm install @wooorm/markdown-wasm ``` ## Usage ```javascript -import { toHtml, toHtmlWithOptions } from 'markdown-rs-wasm'; +import { toHtml, toHtmlWithOptions } from '@wooorm/markdown-wasm'; // Convert markdown to HTML const html = await toHtml('# Hello World');

c