diff --git a/Cargo.lock b/Cargo.lock index 4b54627..4eb985e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3,42 +3,35 @@ version = 4 [[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "autocfg" -version = "1.4.0" +name = "bumpalo" +version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" [[package]] -name = "bumpalo" -version = "3.17.0" +name = "caseless" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "8b6fd507454086c8edfd769ca6ada439193cdb209c7681712ef6275cccbfe5d8" +dependencies = [ + "unicode-normalization", +] [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" [[package]] name = "comrak" -version = "0.17.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b9638d96f93f8bf7b050f7505ba0d82b8267f601965b08faa0eb4882a9bba9" +checksum = "5ccfd7678fba9aff54a74a70352d952b345e568823ed9d5a92ebc8ad0adad8ea" dependencies = [ + "caseless", "entities", "memchr", - "once_cell", - "regex", "slug", "typed-arena", "unicode_categories", @@ -46,7 +39,7 @@ dependencies = [ [[package]] name = "comrak-wasm" -version = "0.4.2" +version = "0.4.4" dependencies = [ "bumpalo", "comrak", @@ -59,9 +52,9 @@ dependencies = [ [[package]] name = "deunicode" -version = "1.6.1" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d" +checksum = "abd57806937c9cc163efc8ea3910e00a62e2aeb0b8119f1793a978088f8f6b04" [[package]] name = "entities" @@ -81,19 +74,18 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.26" +version = "0.4.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e" +checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" [[package]] name = "lol_alloc" @@ -106,68 +98,39 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "once_cell" -version = "1.21.1" +version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d75b0bedcc4fe52caa0e03d9f1151a323e4aa5e2d78ba3580400cd3c9e2bc4bc" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "ce25767e7b499d1b604768e7cde645d14cc8584231ea6b295e9c9eb22c02e1d1" dependencies = [ "proc-macro2", ] -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "scopeguard" @@ -227,15 +190,30 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.100" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "typed-arena" version = "2.0.2" @@ -244,9 +222,18 @@ checksum = "6af6ae20167a9ece4bcb41af5b80f8a1f1df981f6391189ce00fd257af04126a" [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d" + +[[package]] +name = "unicode-normalization" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956" +dependencies = [ + "tinyvec", +] [[package]] name = "unicode_categories" diff --git a/Cargo.toml b/Cargo.toml index 685e33b..40911ac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ mt = ["alloc"] [dependencies] - comrak = { version = "=0.17.0", default-features = false } + comrak = { version = "=0.43.0", default-features = false } js-sys = "=0.3.77" serde = { version = "=1.0.219", features = ["derive"] } wasm-bindgen = { version = "=0.2.100", features = [] } diff --git a/mod.ts b/mod.ts index 187750b..3451b49 100644 --- a/mod.ts +++ b/mod.ts @@ -1,5 +1,5 @@ // @ts-types="./lib/comrak_wasm.d.ts" -import { markdown_to_html, type Options } from "./lib/comrak_wasm.js"; +import { markdown_to_html } from "./lib/comrak_wasm.js"; /** * Options for the {@linkcode markdownToHTML} function. @@ -21,9 +21,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Hello www.github.com.\n", { extension: { autolink: true } }); - * // "

Hello www.github.com.

\n" + * const html = markdownToHTML("Hello www.github.com.\n", { extension: { autolink: true } }); + * assert.equal(html, "

Hello www.github.com.

\n"); * ``` * * @default {false} @@ -37,9 +38,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Term\n\n: Definition", { extension: { descriptionLists: true } }); - * // "
Term
\n
\n

Definition

\n
\n
\n" + * const html = markdownToHTML("Term\n\n: Definition", { extension: { descriptionLists: true } }); + * assert.equal(html, "
Term
\n
\n

Definition

\n
\n
\n"); * ``` * * @default {false} @@ -54,9 +56,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Hi[^x].\n\n[^x]: A greeting.\n", { extension: { footnotes: true } }); - * // "

Hi1.

\n
\n
    \n
  1. \n

    A greeting.

    \n
  2. \n
\n
\n" + * const html = markdownToHTML("Hi[^x].\n\n[^x]: A greeting.\n", { extension: { footnotes: true } }); + * assert.equal(html, "

Hi1.

\n
\n
    \n
  1. \n

    A greeting.

    \n
  2. \n
\n
\n"); * ``` * * @default {false} @@ -72,9 +75,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("---\nlayout: post\n---\nText\n", { extension: { frontMatterDelimiter: "---" } }); - * // "

Text

\n" + * const html = markdownToHTML("---\nlayout: post\n---\nText\n", { extension: { frontMatterDelimiter: "---" } }); + * assert.equal(html, "

Text

\n"); * ``` * * @default {null} @@ -85,9 +89,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("# README\n", { extension: { headerIDs: "user-content-" } }); - * // "

README

\n" + * const html = markdownToHTML("# README\n", { extension: { headerIDs: "user-content-" } }); + * assert.equal(html, "

README

\n"); * ``` * * @default {null} @@ -100,9 +105,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Hello ~world~ there.\n", { extension: { strikethrough: true } }); - * // "

Hello world there.

\n" + * const html = markdownToHTML("Hello ~world~ there.\n", { extension: { strikethrough: true } }); + * assert.equal(html, "

Hello world there.

\n"); * ``` * * @default {false} @@ -113,9 +119,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("e = mc^2^.\n", { extension: { superscript: true } }); - * // "

e = mc2.

\n" + * const html = markdownToHTML("e = mc^2^.\n", { extension: { superscript: true } }); + * assert.equal(html, "

e = mc2.

\n"); * ``` * * @default {false} @@ -128,10 +135,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("| a | b |\n|---|---|\n| c | d |\n", { extension: { table: true } }); - * // "\n\n\n\n\n\n\n" + - * // "\n\n\n\n\n\n
ab
cd
\n" + * const html = markdownToHTML("| a | b |\n|---|---|\n| c | d |\n", { extension: { table: true } }); + * assert.equal(html, "\n\n\n\n\n\n\n\n\n\n\n\n\n
ab
cd
\n"); * ``` * * @default {false} @@ -144,9 +151,10 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Hello .\n\n<xmp>", { extension: { tagfilter: true } }); - * // "<p>Hello &lt;xmp>.</p>\n&lt;xmp>\n" + * const html = markdownToHTML("Hello <xmp>.\n\n<xmp>", { extension: { tagfilter: true } }); + * assert.equal(html, "<p>Hello &lt;xmp>.</p>\n&lt;xmp>\n"); * ``` * * @default {false} @@ -161,15 +169,164 @@ export interface ComrakExtensionOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("* [x] Done\n* [ ] Not done\n", { extension: { tasklist: true } }); - * // "<ul>\n<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Done</li>\n\ - * // <li><input type=\"checkbox\" disabled=\"\" /> Not done</li>\n</ul>\n" + * const html = markdownToHTML("* [x] Done\n* [ ] Not done\n", { extension: { tasklist: true } }); + * assert.equal(html, "<ul>\n<li><input type=\"checkbox\" disabled=\"\" checked=\"\" /> Done</li>\n<li><input type=\"checkbox\" disabled=\"\" /> Not done</li>\n</ul>\n"); * ``` * * @default {false} */ tasklist?: boolean; + + /** Enables the multiline block quote extension. + /** + * Enables the multiline block quote extension. + * + * Place `>>>` before and after text to make it into a block quote. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML(">>>\nparagraph\n>>>", { extension: { multilineBlockQuotes: true } }); + * assert.equal(html, "<blockquote>\n<p>paragraph</p>\n</blockquote>\n"); + * ``` + * + * @default {false} + */ + multilineBlockQuotes?: boolean; + + /** Enables GitHub style alerts. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("> [!note]\n> Something of note", { extension: { alerts: true } }); + * assert.equal(html, "<div class=\"markdown-alert markdown-alert-note\">\n<p class=\"markdown-alert-title\">Note</p>\n<p>Something of note</p>\n</div>\n"); + * ``` + * + * @default {false} + */ + alerts?: boolean; + + /** Enables math using dollar syntax. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("$1 + 2$ and $$x = y$$", { extension: { mathDollars: true } }); + * assert.equal(html, "<p><span data-math-style=\"inline\">1 + 2</span> and <span data-math-style=\"display\">x = y</span></p>\n"); + * ``` + * + * @default {false} + */ + mathDollars?: boolean; + + /** Enables math using code syntax. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("$`1 + 2`$", { extension: { mathCode: true } }); + * assert.equal(html, "<p><code data-math-style=\"inline\">1 + 2</code></p>\n"); + * ``` + * + * @default {false} + */ + mathCode?: boolean; + + /** Enables wikilinks extension. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * // Title after pipe: [[url|title]] + * const html1 = markdownToHTML("[[url|link label]]", { extension: { wikilinksTitle: "after" } }); + * assert.equal(html1, "<p><a href=\"url\" data-wikilink=\"true\">link label</a></p>\n"); + * + * // Title before pipe: [[title|url]] + * const html2 = markdownToHTML("[[link label|url]]", { extension: { wikilinksTitle: "before" } }); + * assert.equal(html2, "<p><a href=\"url\" data-wikilink=\"true\">link label</a></p>\n"); + * ``` + * + * @default {undefined} + */ + wikilinksTitle?: "before" | "after"; + + /** Enables underlines using double underscores. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("__underlined text__", { extension: { underline: true } }); + * assert.equal(html, "<p><u>underlined text</u></p>\n"); + * ``` + * + * @default {false} + */ + underline?: boolean; + + /** Enables subscript text using single tildes. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("H~2~O", { extension: { subscript: true } }); + * assert.equal(html, "<p>H<sub>2</sub>O</p>\n"); + * ``` + * + * @default {false} + */ + subscript?: boolean; + + /** Enables spoilers using double vertical bars. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("Darth Vader is ||Luke's father||", { extension: { spoiler: true } }); + * assert.equal(html, "<p>Darth Vader is <span class=\"spoiler\">Luke's father</span></p>\n"); + * ``` + * + * @default {false} + */ + spoiler?: boolean; + + /** Requires at least one space after a `>` character to generate a blockquote. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML(">implying implications", { extension: { greentext: true } }); + * assert.equal(html, "<p>&gt;implying implications</p>\n"); + * ``` + * + * @default {false} + */ + greentext?: boolean; + + /** Recognizes many emphasis that appear in CJK contexts. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("**この文は重要です。**但这句话并不重要。", { extension: { cjkFriendlyEmphasis: true } }); + * assert.equal(html, "<p><strong>この文は重要です。</strong>但这句话并不重要。</p>\n"); + * ``` + * + * @default {false} + */ + cjkFriendlyEmphasis?: boolean; } /** Options for parser functions. */ @@ -179,12 +336,13 @@ export interface ComrakParseOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("\`\`\`\nfn hello();\n\`\`\`\n"); - * // "<pre><code>fn hello();\n</code></pre>\n" + * let html = markdownToHTML("\`\`\`\nfn hello();\n\`\`\`\n"); + * assert.equal(html, "<pre><code>fn hello();\n</code></pre>\n"); * - * markdownToHTML("\`\`\`\nfn hello();\n\`\`\`\n", { parse: { defaultInfoString: "rust" } }); - * // "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n" + * html = markdownToHTML("\`\`\`\nfn hello();\n\`\`\`\n", { parse: { defaultInfoString: "rust" } }); + * assert.equal(html, "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n"); * ``` * * @default {null} @@ -194,14 +352,17 @@ export interface ComrakParseOptions { /** Punctuation (quotes, full-stops and hyphens) are converted into ‘smart’ * punctuation. * + * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("'Hello,' \"world\" ..."); - * // "<p>'Hello,' &quot;world&quot; ...</p>\n" + * let html = markdownToHTML("'Hello,' \"world\" ..."); + * assert.equal(html, "<p>'Hello,' \&quot;world\&quot; ...</p>\n"); * - * markdownToHTML("'Hello,' \"world\" ...", { parse: { smart: true } }); - * // "<p>‘Hello,’ “world” …</p>\n" + * html = markdownToHTML("'Hello,' \"world\" ...", { parse: { smart: true } }); + * assert.equal(html, "<p>'Hello,' "world" …</p>\n"); + * ``` * ``` * * @default {false} @@ -215,6 +376,21 @@ export interface ComrakParseOptions { * @default {false} */ relaxedTasklistMatching?: boolean; + + /** Relax parsing of autolinks, allow links to be detected inside brackets + * and allow all url schemes. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("[https://foo.com]", { extension: { autolink: true }, parse: { relaxedAutolinks: true } }); + * assert.equal(html, "<p>[<a href=\"https://foo.com\">https://foo.com</a>]</p>\n"); + * ``` + * + * @default {false} + */ + relaxedAutolinks?: boolean; } /** @@ -225,12 +401,13 @@ export interface ComrakRenderOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("<i>italic text</i>"); - * // "<p><!-- raw HTML omitted -->italic text<!-- raw HTML omitted --></p>\n" + * let html = markdownToHTML("<i>italic text</i>"); + * assert.equal(html, "<p><!-- raw HTML omitted -->italic text<!-- raw HTML omitted --></p>\n"); * - * markdownToHTML("<i>italic text</i>", { render: { escape: true } }); - * // "<p>&lt;i&gt;italic text&lt;/i&gt;</p>\n" + * html = markdownToHTML("<i>italic text</i>", { render: { escape: true } }); + * assert.equal(html, "<p>&lt;i&gt;italic text&lt;/i&gt;</p>\n"); * ``` * * @default {false} @@ -242,12 +419,13 @@ export interface ComrakRenderOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("\`\`\`rust\nfn hello();\n\`\`\`\n"); - * // "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n" + * let html = markdownToHTML("\`\`\`rust\nfn hello();\n\`\`\`\n"); + * assert.equal(html, "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n"); * - * markdownToHTML("\`\`\`rust\nfn hello();\n\`\`\`\n", { render: { githubPreLang: true } }); - * // "<pre lang=\"rust\"><code>fn hello();\n</code></pre>\n" + * html = markdownToHTML("\`\`\`rust\nfn hello();\n\`\`\`\n", { render: { githubPreLang: true } }); + * assert.equal(html, "<pre lang=\"rust\"><code>fn hello();\n</code></pre>\n"); * ``` * * @default {false} @@ -259,12 +437,13 @@ export interface ComrakRenderOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("Hello.\nWorld.\n"); - * // "<p>Hello.\nWorld.</p>\n" + * let html = markdownToHTML("Hello.\nWorld.\n"); + * assert.equal(html, "<p>Hello.\nWorld.</p>\n"); * - * markdownToHTML("Hello.\nWorld.\n", { render: { hardbreaks: true } }); - * // "<p>Hello.<br />\nWorld.</p>\n" + * html = markdownToHTML("Hello.\nWorld.\n", { render: { hardbreaks: true } }); + * assert.equal(html, "<p>Hello.<br />\nWorld.</p>\n"); * ``` * * @default {false} @@ -311,12 +490,13 @@ export interface ComrakRenderOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("\`\`\`rust extra info\nfn hello();\n\`\`\`\n"); - * // "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n" + * let html = markdownToHTML("\`\`\`rust extra info\nfn hello();\n\`\`\`\n"); + * assert.equal(html, "<pre><code class=\"language-rust\">fn hello();\n</code></pre>\n"); * - * markdownToHTML("\`\`\`rust extra info\nfn hello();\n\`\`\`\n", { render: { fullInfoString: true } }); - * // "<pre><code class=\"language-rust\" data-meta="extra info">fn hello();\n</code></pre>\n" + * html = markdownToHTML("\`\`\`rust extra info\nfn hello();\n\`\`\`\n", { render: { fullInfoString: true } }); + * assert.equal(html, "<pre><code class=\"language-rust\" data-meta="extra info">fn hello();\n</code></pre>\n"); * ``` * * @default {false} @@ -327,14 +507,131 @@ export interface ComrakRenderOptions { * * ```ts * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; * - * markdownToHTML("* Item\n* Item\n", { render: { listStyle: "star" } }); - * // "<ul>\n<li>Item</li>\n<li>Item</li>\n</ul>\n" + * const html = markdownToHTML("* Item\n* Item\n", { render: { listStyle: "star" } }); + * assert.equal(html, "<ul>\n<li>Item</li>\n<li>Item</li>\n</ul>\n"); * ``` * * @default {"dash"} */ listStyle?: "dash" | "plus" | "star"; + + /** Include source position attributes in HTML and XML output. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("Hello *world*!", { render: { sourcepos: true } }); + * assert.equal(html, "<p data-sourcepos=\"1:1-1:14\">Hello <em data-sourcepos=\"1:7-1:13\">world</em>!</p>\n"); + * ``` + * + * @default {false} + */ + sourcepos?: boolean; + + /** Wrap escaped characters in a `<span>` to allow any post-processing to recognize them. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("Notify user \\@example", { render: { escapedCharSpans: true } }); + * assert.equal(html, "<p>Notify user <span data-escaped-char>@</span>example</p>\n"); + * ``` + * + * @default {false} + */ + escapedCharSpans?: boolean; + + /** Ignore setext headings in input. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("setext heading\n---", { render: { ignoreSetext: true } }); + * assert.equal(html, "<p>setext heading</p>\n<hr />\n"); + * ``` + * + * @default {false} + */ + ignoreSetext?: boolean; + + /** Ignore empty links in input. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("[]()", { render: { ignoreEmptyLinks: true } }); + * assert.equal(html, "<p>[]()</p>\n"); + * ``` + * + * @default {false} + */ + ignoreEmptyLinks?: boolean; + + /** Enables GFM quirks in HTML output which break CommonMark compatibility. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("****abcd****", { render: { gfmQuirks: true } }); + * assert.equal(html, "<p><strong>abcd</strong></p>\n"); + * ``` + * + * @default {false} + */ + gfmQuirks?: boolean; + + /** Prefer fenced code blocks when outputting CommonMark. + * + * @default {false} + */ + preferFenced?: boolean; + + /** Render the image as a figure element with the title as its caption. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("![image](https://example.com/image.png \"this is an image\")", { render: { figureWithCaption: true } }); + * assert.equal(html, "<p><figure><img src=\"https://example.com/image.png\" alt=\"image\" title=\"this is an image\" /><figcaption>this is an image</figcaption></figure></p>\n"); + * ``` + * + * @default {false} + */ + figureWithCaption?: boolean; + + /** Add classes to the output of the tasklist extension. + * + * ```ts + * import { markdownToHTML } from "@nick/comrak"; + * import assert from "node:assert"; + * + * const html = markdownToHTML("- [ ] Foo", { extension: { tasklist: true }, render: { tasklistClasses: true } }); + * assert.equal(html, "<ul class=\"contains-task-list\">\n<li class=\"task-list-item\"><input type=\"checkbox\" class=\"task-list-item-checkbox\" disabled=\"\" /> Foo</li>\n</ul>\n"); + * ``` + * + * @default {false} + */ + tasklistClasses?: boolean; + + /** Render ordered list with a minimum marker width. + * + * @default {0} + */ + olWidth?: number; + + /** Minimise escapes used in CommonMark output. + * + * @default {false} + */ + experimentalMinimizeCommonmark?: boolean; } /** @@ -355,29 +652,5 @@ export function markdownToHTML( markdown: string, options: ComrakOptions = {}, ): string { - const { extension = {}, parse = {}, render = {} } = options; - - const opts = { - extension_autolink: extension.autolink ?? false, - extension_description_lists: extension.descriptionLists ?? false, - extension_footnotes: extension.footnotes ?? false, - extension_front_matter_delimiter: extension.frontMatterDelimiter ?? "---", - extension_header_ids: extension.headerIDs ?? "", - extension_strikethrough: extension.strikethrough ?? false, - extension_superscript: extension.superscript ?? false, - extension_table: extension.table ?? false, - extension_tagfilter: extension.tagfilter ?? false, - extension_tasklist: extension.tasklist ?? false, - parse_default_info_string: parse.defaultInfoString ?? "", - parse_smart: parse.smart ?? false, - parse_relaxed_tasklist_matching: parse.relaxedTasklistMatching ?? false, - render_escape: render.escape ?? false, - render_github_pre_lang: render.githubPreLang ?? false, - render_hardbreaks: render.hardbreaks ?? false, - render_unsafe: render.unsafe ?? false, - render_width: render.width ?? 0, - render_full_info_string: render.fullInfoString ?? false, - render_list_style: render.listStyle ?? "dash", - } satisfies Options; - return markdown_to_html(markdown, opts); + return markdown_to_html(markdown, options); } diff --git a/src/lib.rs b/src/lib.rs index f0facab..9123202 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,122 +44,211 @@ pub enum ListStyleType { Star = 42, } +#[derive(Deserialize, Debug, Clone, Copy)] +#[serde(rename_all = "lowercase")] +pub enum WikiLinksTitle { + Before, + After, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", default)] +struct ExtensionOptions { + autolink: bool, + description_lists: bool, + footnotes: bool, + front_matter_delimiter: Option<String>, + #[serde(rename = "headerIDs")] + header_ids: Option<String>, + strikethrough: bool, + superscript: bool, + table: bool, + tagfilter: bool, + tasklist: bool, + multiline_block_quotes: bool, + alerts: bool, + math_dollars: bool, + math_code: bool, + wikilinks_title: Option<WikiLinksTitle>, + underline: bool, + subscript: bool, + spoiler: bool, + greentext: bool, + cjk_friendly_emphasis: bool, +} + #[derive(Deserialize)] -struct FlatComrakOptions { - extension_autolink: bool, - extension_description_lists: bool, - extension_footnotes: bool, - #[serde(default)] - extension_front_matter_delimiter: Option<String>, - #[serde(default)] - extension_header_ids: Option<String>, - extension_strikethrough: bool, - extension_superscript: bool, - extension_table: bool, - extension_tagfilter: bool, - extension_tasklist: bool, - #[serde(default)] - parse_default_info_string: Option<String>, - parse_smart: bool, - parse_relaxed_tasklist_matching: bool, - render_escape: bool, - render_full_info_string: bool, - render_github_pre_lang: bool, - render_hardbreaks: bool, +#[serde(rename_all = "camelCase", default)] +struct ParseOptions { + default_info_string: Option<String>, + smart: bool, + relaxed_tasklist_matching: bool, + relaxed_autolinks: bool, +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase", default)] +struct RenderOptions { + escape: bool, + full_info_string: bool, + github_pre_lang: bool, + hardbreaks: bool, #[serde(default, with = "ListStyleType")] - render_list_style: ComrakListStyleType, - render_unsafe: bool, - render_width: usize, + list_style: ComrakListStyleType, + #[serde(rename = "unsafe")] + unsafe_: bool, + width: usize, + sourcepos: bool, + escaped_char_spans: bool, + ignore_setext: bool, + ignore_empty_links: bool, + gfm_quirks: bool, + prefer_fenced: bool, + figure_with_caption: bool, + tasklist_classes: bool, + ol_width: usize, + experimental_minimize_commonmark: bool, +} + +#[derive(Deserialize, Default)] +#[serde(rename_all = "camelCase", default)] +struct Options { + extension: ExtensionOptions, + parse: ParseOptions, + render: RenderOptions, +} + +impl Default for ExtensionOptions { + fn default() -> Self { + Self { + autolink: true, + description_lists: true, + footnotes: true, + front_matter_delimiter: None, + header_ids: None, + strikethrough: true, + superscript: true, + table: true, + tagfilter: true, + tasklist: true, + multiline_block_quotes: false, + alerts: false, + math_dollars: false, + math_code: false, + wikilinks_title: None, + underline: false, + subscript: false, + spoiler: false, + greentext: false, + cjk_friendly_emphasis: false, + } + } } -impl Default for FlatComrakOptions { +impl Default for ParseOptions { fn default() -> Self { Self { - extension_autolink: true, - extension_description_lists: true, - extension_footnotes: true, - extension_front_matter_delimiter: None, - extension_header_ids: None, - extension_strikethrough: true, - extension_superscript: true, - extension_table: true, - extension_tagfilter: true, - extension_tasklist: true, - parse_relaxed_tasklist_matching: false, - parse_default_info_string: None, - parse_smart: true, - render_escape: true, - render_github_pre_lang: true, - render_hardbreaks: false, - render_unsafe: false, - render_width: 80, - render_list_style: Default::default(), - render_full_info_string: false, + default_info_string: None, + smart: true, + relaxed_tasklist_matching: false, + relaxed_autolinks: false, } } } -#[wasm_bindgen(typescript_custom_section)] -pub const OPTIONS_TYPE: &'static str = r#" -export interface Options { - extension_autolink: boolean; - extension_description_lists: boolean; - extension_footnotes: boolean; - extension_front_matter_delimiter?: string; - extension_header_ids?: string; - extension_strikethrough: boolean; - extension_superscript: boolean; - extension_table: boolean; - extension_tagfilter: boolean; - extension_tasklist: boolean; - parse_default_info_string?: string; - parse_smart: boolean; - parse_relaxed_tasklist_matching?: boolean; - render_escape: boolean; - render_full_info_string?: boolean; - render_github_pre_lang: boolean; - render_hardbreaks: boolean; - render_list_style?: "dash" | "plus" | "star"; - render_unsafe: boolean; - render_width: number; +impl Default for RenderOptions { + fn default() -> Self { + Self { + escape: true, + full_info_string: false, + github_pre_lang: true, + hardbreaks: false, + list_style: Default::default(), + unsafe_: false, + width: 80, + sourcepos: false, + escaped_char_spans: false, + ignore_setext: false, + ignore_empty_links: false, + gfm_quirks: false, + prefer_fenced: false, + figure_with_caption: false, + tasklist_classes: false, + ol_width: 0, + experimental_minimize_commonmark: false, + } + } } -"#; #[wasm_bindgen] pub fn markdown_to_html( md: &str, - #[wasm_bindgen(unchecked_param_type = "Options")] opts: JsValue, + #[wasm_bindgen(unchecked_param_type = "ComrakOptions")] opts: JsValue, ) -> Result<String, JsValue> { - let opts: FlatComrakOptions = + let opts: Options = from_value(opts.clone()).map_err(|err| TypeError::new(&err.to_string()))?; - let opts = ComrakOptions { + + let (wikilinks_title_after_pipe, wikilinks_title_before_pipe) = match opts.extension.wikilinks_title { + Some(WikiLinksTitle::After) => (true, false), + Some(WikiLinksTitle::Before) => (false, true), + None => (false, false), + }; + + let comrak_opts = ComrakOptions { extension: ComrakExtensionOptions { - autolink: opts.extension_autolink, - description_lists: opts.extension_description_lists, - footnotes: opts.extension_footnotes, - front_matter_delimiter: opts.extension_front_matter_delimiter, - header_ids: opts.extension_header_ids, - strikethrough: opts.extension_strikethrough, - superscript: opts.extension_superscript, - table: opts.extension_table, - tagfilter: opts.extension_tagfilter, - tasklist: opts.extension_tasklist, + autolink: opts.extension.autolink, + description_lists: opts.extension.description_lists, + footnotes: opts.extension.footnotes, + front_matter_delimiter: opts.extension.front_matter_delimiter, + header_ids: opts.extension.header_ids, + strikethrough: opts.extension.strikethrough, + superscript: opts.extension.superscript, + table: opts.extension.table, + tagfilter: opts.extension.tagfilter, + tasklist: opts.extension.tasklist, + multiline_block_quotes: opts.extension.multiline_block_quotes, + alerts: opts.extension.alerts, + math_dollars: opts.extension.math_dollars, + math_code: opts.extension.math_code, + wikilinks_title_after_pipe, + wikilinks_title_before_pipe, + underline: opts.extension.underline, + subscript: opts.extension.subscript, + spoiler: opts.extension.spoiler, + greentext: opts.extension.greentext, + cjk_friendly_emphasis: opts.extension.cjk_friendly_emphasis, + image_url_rewriter: None, + link_url_rewriter: None, + #[cfg(feature = "shortcodes")] + shortcodes: false, }, parse: ComrakParseOptions { - default_info_string: opts.parse_default_info_string, - smart: opts.parse_smart, - relaxed_tasklist_matching: opts.parse_relaxed_tasklist_matching, + default_info_string: opts.parse.default_info_string, + smart: opts.parse.smart, + relaxed_tasklist_matching: opts.parse.relaxed_tasklist_matching, + relaxed_autolinks: opts.parse.relaxed_autolinks, + broken_link_callback: None, }, render: ComrakRenderOptions { - escape: opts.render_escape, - github_pre_lang: opts.render_github_pre_lang, - hardbreaks: opts.render_hardbreaks, - unsafe_: opts.render_unsafe, - width: opts.render_width, - full_info_string: opts.render_full_info_string, - list_style: opts.render_list_style, + escape: opts.render.escape, + github_pre_lang: opts.render.github_pre_lang, + hardbreaks: opts.render.hardbreaks, + unsafe_: opts.render.unsafe_, + width: opts.render.width, + full_info_string: opts.render.full_info_string, + list_style: opts.render.list_style, + sourcepos: opts.render.sourcepos, + escaped_char_spans: opts.render.escaped_char_spans, + ignore_setext: opts.render.ignore_setext, + ignore_empty_links: opts.render.ignore_empty_links, + gfm_quirks: opts.render.gfm_quirks, + prefer_fenced: opts.render.prefer_fenced, + figure_with_caption: opts.render.figure_with_caption, + tasklist_classes: opts.render.tasklist_classes, + ol_width: opts.render.ol_width, + experimental_minimize_commonmark: opts.render.experimental_minimize_commonmark, }, }; - let html = comrak::markdown_to_html(md, &opts); + let html = comrak::markdown_to_html(md, &comrak_opts); Ok(html) }