From c2487692e6d51da16194cd38ba2b42f095ee7594 Mon Sep 17 00:00:00 2001 From: clover caruso Date: Tue, 17 Feb 2026 00:42:05 -0800 Subject: [PATCH] chore: fork markdown-it to fix a bug --- ARCHITECTURE.md | 11 +- Cargo.lock | 11 +- Cargo.toml | 6 +- crates/markdown-it/Cargo.toml | 35 + crates/markdown-it/LICENSE | 28 + crates/markdown-it/README.md | 32 + crates/markdown-it/examples/ferris/README.md | 21 + .../markdown-it/examples/ferris/block_rule.rs | 72 + .../markdown-it/examples/ferris/core_rule.rs | 64 + .../examples/ferris/inline_rule.rs | 61 + crates/markdown-it/examples/ferris/main.rs | 31 + crates/markdown-it/src/common/mod.rs | 11 + crates/markdown-it/src/common/ruler.rs | 393 + crates/markdown-it/src/common/sourcemap.rs | 143 + crates/markdown-it/src/common/typekey.rs | 81 + crates/markdown-it/src/common/utils.rs | 467 + .../src/examples/ferris/block_rule.rs | 7 + .../src/examples/ferris/core_rule.rs | 7 + .../src/examples/ferris/inline_rule.rs | 7 + crates/markdown-it/src/examples/ferris/mod.rs | 15 + crates/markdown-it/src/examples/mod.rs | 4 + crates/markdown-it/src/examples/testreadme.rs | 2 + .../src/generics/inline/code_pair.rs | 142 + .../src/generics/inline/emph_pair.rs | 390 + .../src/generics/inline/full_link.rs | 440 + crates/markdown-it/src/generics/inline/mod.rs | 4 + crates/markdown-it/src/generics/mod.rs | 12 + crates/markdown-it/src/lib.rs | 29 + .../src/parser/block/builtin/block_parser.rs | 23 + .../src/parser/block/builtin/mod.rs | 9 + crates/markdown-it/src/parser/block/mod.rs | 127 + crates/markdown-it/src/parser/block/rule.rs | 13 + crates/markdown-it/src/parser/block/state.rs | 272 + crates/markdown-it/src/parser/core/mod.rs | 6 + crates/markdown-it/src/parser/core/root.rs | 21 + crates/markdown-it/src/parser/core/rule.rs | 55 + crates/markdown-it/src/parser/extset.rs | 232 + .../parser/inline/builtin/inline_parser.rs | 80 + .../src/parser/inline/builtin/mod.rs | 12 + .../src/parser/inline/builtin/skip_text.rs | 140 + crates/markdown-it/src/parser/inline/mod.rs | 162 + crates/markdown-it/src/parser/inline/rule.rs | 15 + crates/markdown-it/src/parser/inline/state.rs | 244 + crates/markdown-it/src/parser/linkfmt.rs | 100 + crates/markdown-it/src/parser/main.rs | 90 + crates/markdown-it/src/parser/mod.rs | 45 + crates/markdown-it/src/parser/node.rs | 229 + crates/markdown-it/src/parser/renderer.rs | 154 + .../src/plugins/cmark/block/blockquote.rs | 168 + .../src/plugins/cmark/block/code.rs | 69 + .../src/plugins/cmark/block/fence.rs | 164 + .../src/plugins/cmark/block/heading.rs | 87 + .../markdown-it/src/plugins/cmark/block/hr.rs | 55 + .../src/plugins/cmark/block/lheading.rs | 104 + .../src/plugins/cmark/block/list.rs | 370 + .../src/plugins/cmark/block/mod.rs | 10 + .../src/plugins/cmark/block/paragraph.rs | 68 + .../src/plugins/cmark/block/reference.rs | 374 + .../src/plugins/cmark/inline/autolink.rs | 87 + .../src/plugins/cmark/inline/backticks.rs | 28 + .../src/plugins/cmark/inline/emphasis.rs | 40 + .../src/plugins/cmark/inline/entity.rs | 85 + .../src/plugins/cmark/inline/escape.rs | 57 + .../src/plugins/cmark/inline/image.rs | 34 + .../src/plugins/cmark/inline/link.rs | 35 + .../src/plugins/cmark/inline/mod.rs | 9 + .../src/plugins/cmark/inline/newline.rs | 81 + crates/markdown-it/src/plugins/cmark/mod.rs | 34 + .../src/plugins/extra/beautify_links.rs | 39 + .../src/plugins/extra/heading_anchors.rs | 69 + .../markdown-it/src/plugins/extra/linkify.rs | 135 + crates/markdown-it/src/plugins/extra/mod.rs | 44 + .../src/plugins/extra/smartquotes.rs | 567 + .../src/plugins/extra/strikethrough.rs | 20 + .../markdown-it/src/plugins/extra/syntect.rs | 74 + .../markdown-it/src/plugins/extra/tables.rs | 456 + .../src/plugins/extra/typographer.rs | 124 + .../src/plugins/html/html_block.rs | 151 + .../src/plugins/html/html_inline.rs | 50 + crates/markdown-it/src/plugins/html/mod.rs | 29 + .../src/plugins/html/utils/blocks.rs | 68 + .../markdown-it/src/plugins/html/utils/mod.rs | 2 + .../src/plugins/html/utils/regexps.rs | 46 + crates/markdown-it/src/plugins/mod.rs | 20 + crates/markdown-it/src/plugins/sourcepos.rs | 52 + crates/markdown-it/tests/commonmark.rs | 6557 +++++++++++ crates/markdown-it/tests/extras.rs | 202 + crates/markdown-it/tests/fixtures/README.md | 28 + .../tests/fixtures/commonmark/bad.txt | 0 .../tests/fixtures/commonmark/good.txt | 7834 +++++++++++++ .../tests/fixtures/commonmark/spec.txt | 9756 +++++++++++++++++ crates/markdown-it/tests/fixtures/deno.lock | 72 + .../fixtures/markdown-it/markdown-it-rs.txt | 6 + .../fixtures/markdown-it/smartquotes.txt | 192 + .../tests/fixtures/markdown-it/tables.txt | 808 ++ .../markdown-it/typographer-extra.txt | 14 + .../fixtures/markdown-it/typographer.txt | 110 + .../markdown-it/tests/fixtures/package.json | 6 + crates/markdown-it/tests/fixtures/testgen.js | 99 + crates/markdown-it/tests/linkify.rs | 170 + .../tests/markdown-it-smartquotes.rs | 214 + .../tests/markdown-it-typographer.rs | 157 + crates/markdown-it/tests/markdown-it.rs | 828 ++ crates/markdown-it/tests/pathological.rs | 140 + crates/markdown-it/tests/sourcemaps.rs | 376 + src/lib.rs | 14 + 106 files changed, 36030 insertions(+), 13 deletions(-) create mode 100644 crates/markdown-it/Cargo.toml create mode 100644 crates/markdown-it/LICENSE create mode 100644 crates/markdown-it/README.md create mode 100644 crates/markdown-it/examples/ferris/README.md create mode 100644 crates/markdown-it/examples/ferris/block_rule.rs create mode 100644 crates/markdown-it/examples/ferris/core_rule.rs create mode 100644 crates/markdown-it/examples/ferris/inline_rule.rs create mode 100644 crates/markdown-it/examples/ferris/main.rs create mode 100644 crates/markdown-it/src/common/mod.rs create mode 100644 crates/markdown-it/src/common/ruler.rs create mode 100644 crates/markdown-it/src/common/sourcemap.rs create mode 100644 crates/markdown-it/src/common/typekey.rs create mode 100644 crates/markdown-it/src/common/utils.rs create mode 100644 crates/markdown-it/src/examples/ferris/block_rule.rs create mode 100644 crates/markdown-it/src/examples/ferris/core_rule.rs create mode 100644 crates/markdown-it/src/examples/ferris/inline_rule.rs create mode 100644 crates/markdown-it/src/examples/ferris/mod.rs create mode 100644 crates/markdown-it/src/examples/mod.rs create mode 100644 crates/markdown-it/src/examples/testreadme.rs create mode 100644 crates/markdown-it/src/generics/inline/code_pair.rs create mode 100644 crates/markdown-it/src/generics/inline/emph_pair.rs create mode 100644 crates/markdown-it/src/generics/inline/full_link.rs create mode 100644 crates/markdown-it/src/generics/inline/mod.rs create mode 100644 crates/markdown-it/src/generics/mod.rs create mode 100644 crates/markdown-it/src/lib.rs create mode 100644 crates/markdown-it/src/parser/block/builtin/block_parser.rs create mode 100644 crates/markdown-it/src/parser/block/builtin/mod.rs create mode 100644 crates/markdown-it/src/parser/block/mod.rs create mode 100644 crates/markdown-it/src/parser/block/rule.rs create mode 100644 crates/markdown-it/src/parser/block/state.rs create mode 100644 crates/markdown-it/src/parser/core/mod.rs create mode 100644 crates/markdown-it/src/parser/core/root.rs create mode 100644 crates/markdown-it/src/parser/core/rule.rs create mode 100644 crates/markdown-it/src/parser/extset.rs create mode 100644 crates/markdown-it/src/parser/inline/builtin/inline_parser.rs create mode 100644 crates/markdown-it/src/parser/inline/builtin/mod.rs create mode 100644 crates/markdown-it/src/parser/inline/builtin/skip_text.rs create mode 100644 crates/markdown-it/src/parser/inline/mod.rs create mode 100644 crates/markdown-it/src/parser/inline/rule.rs create mode 100644 crates/markdown-it/src/parser/inline/state.rs create mode 100644 crates/markdown-it/src/parser/linkfmt.rs create mode 100644 crates/markdown-it/src/parser/main.rs create mode 100644 crates/markdown-it/src/parser/mod.rs create mode 100644 crates/markdown-it/src/parser/node.rs create mode 100644 crates/markdown-it/src/parser/renderer.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/blockquote.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/code.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/fence.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/heading.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/hr.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/lheading.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/list.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/mod.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/paragraph.rs create mode 100644 crates/markdown-it/src/plugins/cmark/block/reference.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/autolink.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/backticks.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/emphasis.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/entity.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/escape.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/image.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/link.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/mod.rs create mode 100644 crates/markdown-it/src/plugins/cmark/inline/newline.rs create mode 100644 crates/markdown-it/src/plugins/cmark/mod.rs create mode 100644 crates/markdown-it/src/plugins/extra/beautify_links.rs create mode 100644 crates/markdown-it/src/plugins/extra/heading_anchors.rs create mode 100644 crates/markdown-it/src/plugins/extra/linkify.rs create mode 100644 crates/markdown-it/src/plugins/extra/mod.rs create mode 100644 crates/markdown-it/src/plugins/extra/smartquotes.rs create mode 100644 crates/markdown-it/src/plugins/extra/strikethrough.rs create mode 100644 crates/markdown-it/src/plugins/extra/syntect.rs create mode 100644 crates/markdown-it/src/plugins/extra/tables.rs create mode 100644 crates/markdown-it/src/plugins/extra/typographer.rs create mode 100644 crates/markdown-it/src/plugins/html/html_block.rs create mode 100644 crates/markdown-it/src/plugins/html/html_inline.rs create mode 100644 crates/markdown-it/src/plugins/html/mod.rs create mode 100644 crates/markdown-it/src/plugins/html/utils/blocks.rs create mode 100644 crates/markdown-it/src/plugins/html/utils/mod.rs create mode 100644 crates/markdown-it/src/plugins/html/utils/regexps.rs create mode 100644 crates/markdown-it/src/plugins/mod.rs create mode 100644 crates/markdown-it/src/plugins/sourcepos.rs create mode 100644 crates/markdown-it/tests/commonmark.rs create mode 100644 crates/markdown-it/tests/extras.rs create mode 100644 crates/markdown-it/tests/fixtures/README.md create mode 100644 crates/markdown-it/tests/fixtures/commonmark/bad.txt create mode 100644 crates/markdown-it/tests/fixtures/commonmark/good.txt create mode 100644 crates/markdown-it/tests/fixtures/commonmark/spec.txt create mode 100644 crates/markdown-it/tests/fixtures/deno.lock create mode 100644 crates/markdown-it/tests/fixtures/markdown-it/markdown-it-rs.txt create mode 100644 crates/markdown-it/tests/fixtures/markdown-it/smartquotes.txt create mode 100644 crates/markdown-it/tests/fixtures/markdown-it/tables.txt create mode 100644 crates/markdown-it/tests/fixtures/markdown-it/typographer-extra.txt create mode 100644 crates/markdown-it/tests/fixtures/markdown-it/typographer.txt create mode 100644 crates/markdown-it/tests/fixtures/package.json create mode 100644 crates/markdown-it/tests/fixtures/testgen.js create mode 100644 crates/markdown-it/tests/linkify.rs create mode 100644 crates/markdown-it/tests/markdown-it-smartquotes.rs create mode 100644 crates/markdown-it/tests/markdown-it-typographer.rs create mode 100644 crates/markdown-it/tests/markdown-it.rs create mode 100644 crates/markdown-it/tests/pathological.rs create mode 100644 crates/markdown-it/tests/sourcemaps.rs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index c780f326f73b383b9984d4332bdb7ee55ccd9437..cc8847517cc7b26ee4a49a33577102544c6fe4c0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,16 +1,23 @@ # architecture +markodown is implemented in rust using a fork of the markdown-it-rust create. we +fork it because they are unmaintained and we have hit bugs. the rust codebase is +compiled to wasm and then shipped as an NPM/JSR package for use in the web +ecosystem. + ## `src/`: rust code the rust code in `src` implements most of markodown. it exports a cli for -testing. most of the tests are on the rust code +testing. most of the tests are on the rust code. here are the relevant files - `src/lib.rs` - main library, primary function `transform` -- `src/main.rs` - main cli + - `src/wasm.rs` - binding for WASM +- `src/main.rs` - testing cli - `src/typescript.rs` - wrappers around oxc parser to expose parsers for different subsets of the ast, including a much more garbage-forgiving expression parser. - `src/marko.rs` - general marko tag parser + - `src/marko_ast.rs` - ast structure for the marko parser - `src/plugin/` - `markdown-it` plugin and all rules markdown-it does not support failiable plugins, so instead errors are lowered diff --git a/Cargo.lock b/Cargo.lock index ef94b89219890540ff2b8952482c7fc3a6119547..d91b69225b6a6b44ce44a46eed57b0a72cae354a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -82,12 +82,6 @@ dependencies = [ "object", ] -[[package]] -name = "argparse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8ebf5827e4ac4fd5946560e6a99776ea73b596d80898f357007317a7141e47" - [[package]] name = "autocfg" version = "1.5.0" @@ -379,11 +373,8 @@ dependencies = [ [[package]] name = "markdown-it" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f99c010929c8217b2dc0940954267a2e15a15f17cb309cd1f299e21933f84fac" +version = "0.6.1-patched" dependencies = [ - "argparse", "const_format", "derivative", "derive_more", diff --git a/Cargo.toml b/Cargo.toml index d26ec9f4c5d407f6875d7aae41ef3e1cc1ac9ddf..eb11d960ddee71bbb4a6727ad090a7831caeeccb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ crate-type = ["cdylib", "rlib"] [dependencies] serde = { version = "1.0", features = ["derive"] } clap = { version = "4.5.57", features = ["derive"] } -markdown-it = { version = "0.6.1", default-features = false, features = ["linkify"] } +markdown-it = { path = "crates/markdown-it", default-features = false, features = ["linkify"] } oxc_allocator = "0.96.0" oxc_ast = "0.96.0" oxc_diagnostics = "0.96.0" @@ -18,3 +18,7 @@ serde_json = "1.0" serde_yml = "0.0.12" wasm-bindgen = { version = "0.2" } serde-wasm-bindgen = "0.6.5" + +[workspace] +members = [".", "crates/markdown-it"] +resolver = "2" diff --git a/crates/markdown-it/Cargo.toml b/crates/markdown-it/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..c1d28951914b9322092af74994dc4e9f2999f17d --- /dev/null +++ b/crates/markdown-it/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "markdown-it" +version = "0.6.1-patched" +authors = ["Alex Kocharin "] +description = "Rust port of popular markdown-it.js library." +homepage = "https://github.com/markdown-it-rust/markdown-it" +repository = "https://github.com/markdown-it-rust/markdown-it" +readme = "README.md" +keywords = ["markdown", "commonmark"] +license = "MIT" +categories = ["text-processing", "parsing"] +edition = "2021" + +[lib] +name = "markdown_it" +path = "src/lib.rs" + +[features] +default = ["linkify"] +syntect = [] + +[dependencies] +const_format = ">= 0.1.0, < 0.3" +derivative = ">= 1.0.2, < 3" +derive_more = ">= 0.99.0, < 1" +downcast-rs = ">= 1.0.2, < 2" +entities = ">= 0.1.0, < 2" +html-escape = ">= 0.1.0, < 0.3" +linkify = { version = ">= 0.5.0, < 0.11", optional = true } +mdurl = ">= 0.3.1, < 0.4" +once_cell = ">= 1.0.1, < 2" +readonly = ">= 0.2.0, < 0.3" +regex = ">= 1.0.0, < 2" +stacker = ">= 0.1.2, < 0.2" +unicode-general-category = ">= 0.1.0, < 0.7" diff --git a/crates/markdown-it/LICENSE b/crates/markdown-it/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..db822a38828c222915d939fc5bdd1f5dd051dd97 --- /dev/null +++ b/crates/markdown-it/LICENSE @@ -0,0 +1,28 @@ +The MIT License + +Rust port https://github.com/rlidwka/markdown-it.rs: +Copyright (c) 2022 Alex Kocharin. + +Original library https://github.com/markdown-it/markdown-it: +Copyright (c) 2014 Vitaly Puzrin, Alex Kocharin. + +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: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/crates/markdown-it/README.md b/crates/markdown-it/README.md new file mode 100644 index 0000000000000000000000000000000000000000..98b85da29825b255757ba9b8c0abedac74dc7e10 --- /dev/null +++ b/crates/markdown-it/README.md @@ -0,0 +1,32 @@ +# markdown-it + +This directory holds a fork of the unmaintained markdown-it-rust package. +Markodown forks this package to make bug fixes to its core CommonMark +implementation. + +Rust port of popular +[markdown-it.js](https://github.com/markdown-it/markdown-it) library. + +### Features + +- 100% CommonMark compatibility +- AST +- Source maps (full support, not just on block tags like cmark) +- Ability to write your own syntax of arbitrary complexity + - to prove this point, CommonMark syntax itself is written as a plugin + +### Usage + +```rust +let parser = &mut markdown_it::MarkdownIt::new(); +markdown_it::plugins::cmark::add(parser); +markdown_it::plugins::extra::add(parser); + +let ast = parser.parse("Hello **world**!"); +let html = ast.render(); + +print!("{html}"); +// prints "

Hello world!

" +``` + +For a guide on how to extend it, see `examples` folder. diff --git a/crates/markdown-it/examples/ferris/README.md b/crates/markdown-it/examples/ferris/README.md new file mode 100644 index 0000000000000000000000000000000000000000..cfcfe8ab616491ad8e115df33319fb1740c10a67 --- /dev/null +++ b/crates/markdown-it/examples/ferris/README.md @@ -0,0 +1,21 @@ +This is an example of how you can make your own plugins in markdown-it. + +### What is it + +There are 3 different plugins here: + +- inline rule - turns `(\/)` into `🦀` in *inline context* (i.e. inside other text) + +- block rule - turns `(\/)-------(\/)` into + [ferris.svg](https://upload.wikimedia.org/wikipedia/commons/0/0f/Original_Ferris.svg) + in *block context* (i.e. it has to occupy the entire line) + + - core rule - counts the number of nodes created by the above two plugins and writes + that number at the end of the document + +It represents three stages of markdown processing (block elements, inline elements +and AST post-processing). + +### How to use + +`cargo run --example ferris` diff --git a/crates/markdown-it/examples/ferris/block_rule.rs b/crates/markdown-it/examples/ferris/block_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..f96cec5d461ccdc96b066ddd70334e4baa5daba8 --- /dev/null +++ b/crates/markdown-it/examples/ferris/block_rule.rs @@ -0,0 +1,72 @@ +// Replaces `(\/)-------(\/)` with a nice picture. + +use markdown_it::parser::block::{BlockRule, BlockState}; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; + +const CRAB_CLAW : &str = r#"(\/)"#; +const CRAB_URL : &str = "https://upload.wikimedia.org/wikipedia/commons/0/0f/Original_Ferris.svg"; + +#[derive(Debug)] +// This is a structure that represents your custom Node in AST. +pub struct BlockFerris; + +impl NodeValue for BlockFerris { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + // build attributes for `div` + let mut attrs_div = node.attrs.clone(); + attrs_div.push(("class", "ferris-block".into())); + + // build attributes for `img` + let attrs_img = vec![("src", CRAB_URL.into())]; + + fmt.cr(); // linebreak, multiples get merged + fmt.open("div", &attrs_div); // opening tag, `
` + fmt.self_close("img", &attrs_img); // `` + fmt.close("div"); // closing tag, `
` + fmt.cr(); + } +} + +// This is an extension for the block subparser. +struct FerrisBlockScanner; + +impl BlockRule for FerrisBlockScanner { + // This is a custom function that will be invoked on every line + // in a block context. + // + // It should get a line number `state.line` and report if your + // custom structure appears there. + // + // If custom structure is found, it: + // - creates a new `Node` in AST + // - increments `state.line` to a position after this node + // - returns true + // + // In "silent mode" (when `silent=true`) you aren't allowed to + // create any nodes, should only increment `state.line`. + // + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + // get contents of a line number `state.line` and check it + let line = state.get_line(state.line).trim(); + if !line.starts_with(CRAB_CLAW) { return None; } + if !line.ends_with(CRAB_CLAW) { return None; } + + // require any number of `-` in between, but no less than 4 + if line.len() < CRAB_CLAW.len() * 2 + 4 { return None; } + + // and make sure no other characters are present there + let dashes = &line[CRAB_CLAW.len()..line.len()-CRAB_CLAW.len()]; + if dashes.chars().any(|c| c != '-') { return None; } + + // return new node and number of lines it occupies + Some(( + Node::new(BlockFerris), + 1, + )) + } +} + +pub fn add(md: &mut MarkdownIt) { + // insert this rule into block subparser + md.block.add_rule::(); +} diff --git a/crates/markdown-it/examples/ferris/core_rule.rs b/crates/markdown-it/examples/ferris/core_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..2b56877de0f1c876865f7f5fc42f3b02f1a14011 --- /dev/null +++ b/crates/markdown-it/examples/ferris/core_rule.rs @@ -0,0 +1,64 @@ +// Counts the number of crabs lurking around. + +use super::block_rule::BlockFerris; +use super::inline_rule::InlineFerris; +use markdown_it::parser::core::CoreRule; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +// This is a structure that represents your custom Node in AST, +// it has one single argument - crab counter. +pub struct FerrisCounter(usize); + +// This defines how your custom node should be rendered. +impl NodeValue for FerrisCounter { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + // `node.attrs` are custom attributes added by other plugins + // (for example, source mapping information) + let mut attrs = node.attrs.clone(); + + // add a custom class attribute + attrs.push(("class", "ferris-counter".into())); + + fmt.cr(); // linebreak, multiples get merged + fmt.open("footer", &attrs); + #[allow(clippy::useless_format)] // for simplicity's sake + fmt.text(&match self.0 { + 0 => format!("No crabs around here."), + 1 => format!("There is a crab lurking in this document."), + _ => format!("There are {} crabs lurking in this document.", self.0), + }); + fmt.close("footer"); + fmt.cr(); + } +} + +// This is an extension for the markdown parser. +struct FerrisCounterRule; + +impl CoreRule for FerrisCounterRule { + // This is a custom function that will be invoked once per document. + // + // It has `root` node of the AST as an argument and may modify its + // contents as you like. + // + fn run(root: &mut Node, _: &MarkdownIt) { + let mut counter = 0; + + // walk through AST recursively and count the number of two + // custom nodes added by other two rules + root.walk(|node, _| { + if node.is::() || node.is::() { + counter += 1; + } + }); + + // append a counter to the root as a custom node + root.children.push(Node::new(FerrisCounter(counter))) + } +} + +pub fn add(md: &mut MarkdownIt) { + // insert this rule into parser + md.add_rule::(); +} diff --git a/crates/markdown-it/examples/ferris/inline_rule.rs b/crates/markdown-it/examples/ferris/inline_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..8305afe93f476f3c18d789c22ef30fe5bf445327 --- /dev/null +++ b/crates/markdown-it/examples/ferris/inline_rule.rs @@ -0,0 +1,61 @@ +// Replaces `(\/)` with `🦀`. + +use markdown_it::parser::inline::{InlineRule, InlineState}; +use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; + +const CRAB_CLAW : &str = r#"(\/)"#; + +#[derive(Debug)] +// This is a structure that represents your custom Node in AST. +pub struct InlineFerris; + +// This defines how your custom node should be rendered. +impl NodeValue for InlineFerris { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + // `node.attrs` are custom attributes added by other plugins + // (for example, source mapping information) + let mut attrs = node.attrs.clone(); + + // add a custom class attribute + attrs.push(("class", "ferris-inline".into())); + + fmt.open("span", &attrs); + fmt.text("🦀"); + fmt.close("span"); + } +} + +// This is an extension for the inline subparser. +struct FerrisInlineScanner; + +impl InlineRule for FerrisInlineScanner { + // This is a character that starts your custom structure + // (other characters may get skipped over). + const MARKER: char = '('; + + // This is a custom function that will be invoked on every character + // in an inline context. + // + // It should look for `state.src` exactly at position `state.pos` + // and report if your custom structure appears there. + // + // If custom structure is found, it: + // - creates a new `Node` in AST + // - returns length of it + // + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let input = &state.src[state.pos..state.pos_max]; // look for stuff at state.pos + if !input.starts_with(CRAB_CLAW) { return None; } // return None if it's not found + + // return new node and length of this structure + Some(( + Node::new(InlineFerris), + CRAB_CLAW.len(), + )) + } +} + +pub fn add(md: &mut MarkdownIt) { + // insert this rule into inline subparser + md.inline.add_rule::(); +} diff --git a/crates/markdown-it/examples/ferris/main.rs b/crates/markdown-it/examples/ferris/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..811f225667b980ffa4182d8f87a0ec3389b7b4da --- /dev/null +++ b/crates/markdown-it/examples/ferris/main.rs @@ -0,0 +1,31 @@ +// Adds three custom plugins and runs them. +mod block_rule; +mod core_rule; +mod inline_rule; + +fn main() { + // create markdown parser + let md = &mut markdown_it::MarkdownIt::new(); + + // add commonmark syntax, you almost always want to do that + markdown_it::plugins::cmark::add(md); + + // add custom three rules described above + inline_rule::add(md); + block_rule::add(md); + core_rule::add(md); + + // and now you can use it + let html = md.parse(r#" +(\/) hello world (\/) +(\/)-------------(\/) + "#).render(); + + print!("{html}"); + + assert_eq!(html.trim(), r#" +

🦀 hello world 🦀

+
+
There are 3 crabs lurking in this document.
+ "#.trim()); +} diff --git a/crates/markdown-it/src/common/mod.rs b/crates/markdown-it/src/common/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..96ebc156fc99bccceb7933cb1712b301bc7478dc --- /dev/null +++ b/crates/markdown-it/src/common/mod.rs @@ -0,0 +1,11 @@ +//! Self-contained modules used for miscellaneous purposes. +//! +//! These are all candidates for being separated into different crates, +//! tell me if functionality they provide is useful enough to do that. + +pub mod ruler; +pub mod sourcemap; +pub mod utils; + +mod typekey; +pub use typekey::TypeKey; diff --git a/crates/markdown-it/src/common/ruler.rs b/crates/markdown-it/src/common/ruler.rs new file mode 100644 index 0000000000000000000000000000000000000000..b332dbdd389d5cabff76769e4bfe58e18270abb7 --- /dev/null +++ b/crates/markdown-it/src/common/ruler.rs @@ -0,0 +1,393 @@ +//! Plugin manager with dependency resolution. + +use derivative::Derivative; +use once_cell::sync::OnceCell; +use std::collections::{HashMap, HashSet}; +use std::fmt::Debug; +use std::hash::Hash; +use std::slice::Iter; + +/// +/// Ruler allows you to implement a plugin system with dependency management and ensure that +/// your dependencies are called in the correct order. +/// +/// You can use it like this: +/// ``` +/// use markdown_it::common::ruler::Ruler; +/// +/// // this example prints "[ hello, world! ]", +/// // where each token is printed by separate closure +/// let mut chain = Ruler::<&str, fn (&mut String)>::new(); +/// +/// // define rules printing "hello" and "world" +/// chain.add("hello", |s| s.push_str("hello")); +/// chain.add("world", |s| s.push_str("world")); +/// +/// // open bracket should be before "hello", and closing one after "world" +/// chain.add("open_bracket", |s| s.push_str("[ ")).before("hello"); +/// chain.add("close_bracket", |s| s.push_str(" ]")).after("world"); +/// +/// // between "hello" and "world" we shall have a comma +/// chain.add("comma", |s| s.push_str(", ")).after("hello").before("world"); +/// +/// // after "world" we should have "!" as a first rule, but ensure "world" exists first +/// chain.add("bang", |s| s.push_str("!")).require("world").after("world").before_all(); +/// +/// // now we run this chain +/// let mut result = String::new(); +/// for f in chain.iter() { f(&mut result); } +/// assert_eq!(result, "[ hello, world! ]"); +/// ``` +/// +/// This data structure contains any number of elements (M, T), where T is any type and +/// M (mark) is its identifier. +/// +/// - `M` is used for ordering and dependency checking, it must implement `Eq + Copy + Hash + Debug` +/// . Common choices for `M` are `u32`, `&'static str`, or a special `Symbol` type +/// designed for this purpose. +/// +/// - `T` is any user-defined type. It's usually a function or boxed trait. +/// +pub struct Ruler { + deps: Vec>, + compiled: OnceCell<(Vec, Vec)>, +} + +impl Ruler { + pub fn new() -> Self { + Self::default() + } +} + +impl Ruler { + /// Add a new rule identified by `mark` with payload `value`. + pub fn add(&mut self, mark: M, value: T) -> &mut RuleItem { + self.compiled = OnceCell::new(); + let dep = RuleItem::new(mark, value); + self.deps.push(dep); + self.deps.last_mut().unwrap() + } + + /// Remove all rules identified by `mark`. + pub fn remove(&mut self, mark: M) { + self.deps.retain(|dep| !dep.marks.contains(&mark)); + } + + /// Check if there are any rules identified by `mark`. + pub fn contains(&mut self, mark: M) -> bool { + self.deps.iter().any(|dep| dep.marks.contains(&mark)) + } + + /// Ordered iteration through rules. + #[inline] + pub fn iter(&self) -> Iter { + self.compiled.get_or_init(|| self.compile()).1.iter() + } + + fn compile(&self) -> (Vec, Vec) { + // ID -> [RuleItem index] + let mut idhash = HashMap::>::new(); + + // RuleItem index -> [RuleItem index] - dependency graph, None if already inserted + let mut deps_graph = vec![HashSet::new(); self.deps.len()]; + + // additional level of indirection that takes into account item priority + let mut deps_order = vec![]; + let mut beforeall_len = 0; + let mut afterall_len = 0; + + // compiled result + let mut result = vec![]; + let mut result_idx = vec![]; + + // track which rules have been added already + let mut deps_inserted = vec![false; self.deps.len()]; + let mut deps_remaining = self.deps.len(); + + for (idx, dep) in self.deps.iter().enumerate() { + match dep.prio { + RuleItemPriority::Normal => { + deps_order.insert(deps_order.len() - afterall_len, idx); + } + RuleItemPriority::BeforeAll => { + deps_order.insert(beforeall_len, idx); + beforeall_len += 1; + } + RuleItemPriority::AfterAll => { + deps_order.insert(deps_order.len(), idx); + afterall_len += 1; + } + } + for mark in &dep.marks { + idhash.entry(*mark).or_default().push(idx); + } + } + + // build dependency graph, replacing all after's with before's, + // i.e. B.after(A) -> A.before(B) + for idx in deps_order.iter().copied() { + let dep = self.deps.get(idx).unwrap(); + for constraint in &dep.cons { + match constraint { + RuleItemConstraint::Before(v) => { + for depidx in idhash.entry(*v).or_default().iter() { + deps_graph.get_mut(*depidx).unwrap().insert(idx); + } + } + RuleItemConstraint::After(v) => { + for depidx in idhash.entry(*v).or_default().iter() { + deps_graph.get_mut(idx).unwrap().insert(*depidx); + } + } + RuleItemConstraint::Require(v) => { + assert!( + idhash.contains_key(v), + "missing dependency: {:?} requires {:?}", dep.marks.first().unwrap(), v + ); + } + } + } + } + + // now go through the deps and push whatever doesn't have any + 'outer: while deps_remaining > 0 { + for idx in deps_order.iter().copied() { + let inserted = deps_inserted.get_mut(idx).unwrap(); + if *inserted { continue; } + + let dlist = deps_graph.get(idx).unwrap(); + if dlist.is_empty() { + let dep = self.deps.get(idx).unwrap(); + result.push(dep.value.clone()); + result_idx.push(idx); + *inserted = true; + deps_remaining -= 1; + for d in deps_graph.iter_mut() { + d.remove(&idx); + } + continue 'outer; + } + } + + #[cfg(debug_assertions)] { + // check cycles in dependency graph; + // this is very suboptimal, but only used to generate a nice panic message. + // in release mode we'll just simply panic + for idx in deps_order.iter().copied() { + let mut seen = HashMap::new(); + let mut vec = vec![idx]; + while let Some(didx) = vec.pop() { + let dlist = deps_graph.get(didx).unwrap(); + for x in dlist.iter() { + if seen.contains_key(x) { continue; } + vec.push(*x); + seen.insert(*x, didx); + if *x == idx { + let mut backtrack = vec![]; + let mut curr = idx; + while !backtrack.contains(&curr) { + backtrack.push(curr); + curr = *seen.get(&curr).unwrap(); + } + backtrack.push(curr); + let path = backtrack.iter() + .rev() + .map(|x| format!("{:?}", self.deps.get(*x).unwrap().marks.first().unwrap())) + .collect::>() + .join(" < "); + panic!("cyclic dependency: {}", path); + } + } + } + } + } + + // if you see this in debug mode, report it as a bug + panic!("cyclic dependency: (use debug mode for more details)"); + } + + (result_idx, result) + } +} + +impl Debug for Ruler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let vec: Vec<(usize, M)> = self.compiled.get_or_init(|| self.compile()).0 + .iter() + .map(|idx| (*idx, *self.deps.get(*idx).unwrap().marks.first().unwrap())) + .collect(); + + f.debug_struct("Ruler") + .field("deps", &self.deps) + .field("compiled", &vec) + .finish() + } +} + +impl Default for Ruler { + fn default() -> Self { + Self { + deps: Vec::new(), + compiled: OnceCell::new(), + } + } +} + +/// +/// Result of [Ruler::add](Ruler::add), allows to customize position of each rule. +/// +#[derive(Derivative)] +#[derivative(Debug)] +pub struct RuleItem { + marks: Vec, + #[derivative(Debug="ignore")] + value: T, + prio: RuleItemPriority, + cons: Vec>, +} + +impl RuleItem { + fn new(mark: M, value: T) -> Self { + Self { + marks: vec![mark], + value, + prio: RuleItemPriority::Normal, + cons: vec![], + } + } +} + +impl RuleItem { + /// Make sure this rule will be inserted before any rule defined by `mark` (if such rule exists). + /// ``` + /// use markdown_it::common::ruler::Ruler; + /// let mut chain = Ruler::<&str, fn (&mut String)>::new(); + /// + /// chain.add("a", |s| s.push_str("bar")); + /// chain.add("b", |s| s.push_str("foo")).before("a"); + /// + /// let mut result = String::new(); + /// for f in chain.iter() { f(&mut result); } + /// assert_eq!(result, "foobar"); + /// ``` + pub fn before(&mut self, mark: M) -> &mut Self { + self.cons.push(RuleItemConstraint::Before(mark)); + self + } + + /// Make sure this rule will be inserted after any rule defined by `mark` (if such rule exists). + /// Similar to [RuleItem::before](RuleItem::before). + pub fn after(&mut self, mark: M) -> &mut Self { + self.cons.push(RuleItemConstraint::After(mark)); + self + } + + /// This rule will be inserted as early as possible, while still taking into account dependencies, + /// i.e. `.after(X).before_all()` causes this to be first rule after X. + /// ``` + /// use markdown_it::common::ruler::Ruler; + /// let mut chain = Ruler::<&str, fn (&mut String)>::new(); + /// + /// chain.add("a", |s| s.push_str("A")); + /// chain.add("c", |s| s.push_str("C")).after("a"); + /// chain.add("b", |s| s.push_str("B")).after("a").before_all(); + /// + /// let mut result = String::new(); + /// for f in chain.iter() { f(&mut result); } + /// // without before_all order will be ACB + /// assert_eq!(result, "ABC"); + /// ``` + pub fn before_all(&mut self) -> &mut Self { + self.prio = RuleItemPriority::BeforeAll; + self + } + + /// This rule will be inserted as late as possible, while still taking into account dependencies, + /// i.e. `.before(X).after_all()` causes this to be last rule before X. + /// Similar to [RuleItem::before_all](RuleItem::before_all). + pub fn after_all(&mut self) -> &mut Self { + self.prio = RuleItemPriority::AfterAll; + self + } + + /// Add another auxiliary identifier to this rule. It can be used to group together multiple + /// rules with similar functionality. + /// ``` + /// use markdown_it::common::ruler::Ruler; + /// let mut chain = Ruler::<&str, fn (&mut String)>::new(); + /// + /// chain.add("b", |s| s.push_str("B")).alias("BorC"); + /// chain.add("c", |s| s.push_str("C")).alias("BorC"); + /// chain.add("a", |s| s.push_str("A")).before("BorC"); + /// + /// let mut result = String::new(); + /// for f in chain.iter() { f(&mut result); } + /// assert_eq!(result, "ABC"); + /// ``` + pub fn alias(&mut self, mark: M) -> &mut Self { + self.marks.push(mark); + self + } + + /// Require another rule identified by `mark`, panic if not found. + pub fn require(&mut self, mark: M) -> &mut Self { + self.cons.push(RuleItemConstraint::Require(mark)); + self + } +} + +#[derive(Debug)] +enum RuleItemConstraint { + Before(M), + After(M), + Require(M), +} + +#[derive(Debug)] +enum RuleItemPriority { + Normal, + BeforeAll, + AfterAll, +} + + +#[cfg(test)] +mod tests { + use super::Ruler; + + #[test] + #[should_panic(expected=r#"cyclic dependency: "A" < "B" < "C" < "D" < "E" < "F" < "A""#)] + #[cfg(debug_assertions)] + fn cyclic_dependency_debug() { + let mut r = Ruler::new(); + r.add("%", ()).after("D"); + r.add("A", ()).after("B"); + r.add("E", ()).after("F"); + r.add("C", ()).after("D"); + r.add("B", ()).after("C"); + r.add("D", ()).after("E"); + r.add("F", ()).after("A"); + r.compile(); + } + + #[test] + #[should_panic(expected=r#"cyclic dependency"#)] + fn cyclic_dependency() { + let mut r = Ruler::new(); + r.add("A", ()).after("B"); + r.add("B", ()).after("C"); + r.add("C", ()).after("A"); + r.compile(); + } + + + #[test] + #[should_panic(expected=r#"missing dependency: "C" requires "Z"#)] + fn missing_require() { + let mut r = Ruler::new(); + r.add("A", ()); + r.add("B", ()).require("A"); + r.add("C", ()).require("Z"); + r.compile(); + } +} diff --git a/crates/markdown-it/src/common/sourcemap.rs b/crates/markdown-it/src/common/sourcemap.rs new file mode 100644 index 0000000000000000000000000000000000000000..18a45c673167f7be02a90801fc1c0a39b66dd316 --- /dev/null +++ b/crates/markdown-it/src/common/sourcemap.rs @@ -0,0 +1,143 @@ +//! Tools to work with source positions and mapping. + +#[derive(Debug)] +/// Holds source code, allows to calculate `line:column` from byte offset. +pub struct SourceWithLineStarts { + src: String, + marks: Vec, +} + +impl SourceWithLineStarts { + pub fn new(src: &str) -> Self { + let mut iterator = src.char_indices().peekable(); + let mut line = 1; + let mut column = 0; + let mut marks = vec![CharMappingMark { offset: 0, line, column }]; + + loop { + match iterator.next() { + Some((_, '\r')) if matches!(iterator.peek(), Some((_, '\n'))) => { + // ignore \r followed by \n + column += 1; + } + Some((offset, '\r' | '\n')) => { + // \r or \n are linebreaks + line += 1; + column = 0; + marks.push(CharMappingMark { offset: offset + 1, line, column }); + } + Some((offset, _)) => { + // any other character, just increase position + if column % 16 == 0 && column > 0 { + marks.push(CharMappingMark { offset, line, column }); + } + column += 1; + }, + None => break, + } + } + + Self { src: src.to_owned(), marks } + } + + fn get_position(&self, byte_offset: usize) -> (u32, u32) { + let byte_offset = byte_offset + 1; // include current char + let found = match self.marks.binary_search_by(|mark| mark.offset.cmp(&byte_offset)) { + Ok(x) => x, + Err(x) => x - 1, + }; + let mark = &self.marks[found]; + let line = mark.line; + let mut column = mark.column; + for (offset, _) in self.src[mark.offset..].char_indices() { + if mark.offset + offset >= byte_offset { break; } + column += 1; + } + (line, column) + } +} + +#[derive(Debug)] +struct CharMappingMark { + offset: usize, + line: u32, + column: u32, +} + +#[derive(Default, Clone, Copy)] +/// Positions of the start and the end of an AST node. +pub struct SourcePos { + byte_offset: (usize, usize), +} + +impl SourcePos { + /// Create positions from byte offsets: + /// - start - offset of the first char of the node + /// - end - offset of the first char after the node + pub fn new(start: usize, end: usize) -> Self { + SourcePos { + byte_offset: (start, end), + } + } + + pub fn get_byte_offsets(&self) -> (usize, usize) { + self.byte_offset + } + + /// Returns (line_start, column_start, line_end, column_end) from given positions + pub fn get_positions(&self, map: &SourceWithLineStarts) -> ((u32, u32), (u32, u32)) { + let start = map.get_position(self.byte_offset.0); + let end_off = if self.byte_offset.1 > 0 { self.byte_offset.1 - 1 } else { self.byte_offset.1 }; + let end = map.get_position(end_off); + (start, end) + } +} + +impl std::fmt::Debug for SourcePos { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.byte_offset.fmt(f) + } +} + +#[cfg(test)] +mod tests { + use super::SourceWithLineStarts; + use super::SourcePos; + + #[test] + fn no_linebreaks() { + let map = SourceWithLineStarts::new("qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM"); + for i in 0..20 { + assert_eq!(SourcePos::new(i, 0).get_positions(&map).0, (1, i as u32 + 1)); + } + } + + #[test] + fn unicode() { + let map = SourceWithLineStarts::new("!ΑαΒβΓγΔδΕεΖζΗηΘθΙιΚκΛλΜμΝνΞξΟοΠπΡρΣσςΤτΥυΦφΧχΨψΩω"); + assert_eq!(SourcePos::new(0, 0).get_positions(&map).0, (1, 1)); + for i in 1..20 { + assert_eq!(SourcePos::new(i, 0).get_positions(&map).0, (1, ((i - 1) / 2) as u32 + 2)); + } + } + + #[test] + fn many_linebreaks() { + let map = SourceWithLineStarts::new("\n\n\n\n\n\n123"); + for i in 0..6 { + assert_eq!(SourcePos::new(i, 0).get_positions(&map).0, (i as u32 + 2, 0)); + } + assert_eq!(SourcePos::new(7, 0).get_positions(&map).0, (7, 2)); + assert_eq!(SourcePos::new(8, 0).get_positions(&map).0, (7, 3)); + } + + #[test] + fn after_end() { + let map = SourceWithLineStarts::new("123"); + assert_eq!(SourcePos::new(100, 0).get_positions(&map).0, (1, 3)); + let map = SourceWithLineStarts::new("123\n"); + assert_eq!(SourcePos::new(100, 0).get_positions(&map).0, (2, 0)); + let map = SourceWithLineStarts::new("123\n456"); + assert_eq!(SourcePos::new(100, 0).get_positions(&map).0, (2, 3)); + } +} diff --git a/crates/markdown-it/src/common/typekey.rs b/crates/markdown-it/src/common/typekey.rs new file mode 100644 index 0000000000000000000000000000000000000000..3424405a8d3224cce252d090cc31ebb32156e3b3 --- /dev/null +++ b/crates/markdown-it/src/common/typekey.rs @@ -0,0 +1,81 @@ +use std::any::{self, TypeId}; +use std::fmt::{self, Debug}; +use std::hash::{Hash, Hasher}; + +#[readonly::make] +#[derive(Clone, Copy)] +/// [std::any::TypeId] and [std::any::type_name] fused into one struct. +/// +/// It acts as TypeId when hashed or compared, and it acts as type_name when printed. +/// Used to improve debuggability of type ids in hashmaps in particular. +/// ``` +/// # use markdown_it::common::TypeKey; +/// struct A; +/// struct B; +/// +/// let mut set = std::collections::HashSet::new(); +/// +/// set.insert(TypeKey::of::()); +/// set.insert(TypeKey::of::()); +/// +/// assert!(set.contains(&TypeKey::of::())); +/// dbg!(set); +/// ``` +pub struct TypeKey { + /// type id (read only) + pub id: TypeId, + /// type name (read only) + pub name: &'static str, +} + +impl TypeKey { + #[must_use] + /// Similar to [TypeId::of](std::any::TypeId::of), returns `TypeKey` + /// of the type this generic function has been instantiated with. + pub fn of() -> Self { + Self { id: TypeId::of::(), name: any::type_name::() } + } +} + +impl Hash for TypeKey { + fn hash(&self, state: &mut H) { + self.id.hash(state); + } +} + +impl PartialEq for TypeKey { + fn eq(&self, other: &Self) -> bool { + self.id == other.id + } +} + +impl Eq for TypeKey {} + +impl Debug for TypeKey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.name) + } +} + +#[cfg(test)] +mod tests { + use super::TypeKey; + + #[test] + fn typekey_eq() { + struct A; + struct B; + assert_eq!(TypeKey { id: std::any::TypeId::of::(), name: "foo" }, + TypeKey { id: std::any::TypeId::of::(), name: "bar" }); + assert_ne!(TypeKey { id: std::any::TypeId::of::(), name: "foo" }, + TypeKey { id: std::any::TypeId::of::(), name: "foo" }); + } + + #[test] + fn typekey_of() { + struct A; + struct B; + assert_eq!(TypeKey::of::(), TypeKey::of::()); + assert_ne!(TypeKey::of::(), TypeKey::of::()); + } +} diff --git a/crates/markdown-it/src/common/utils.rs b/crates/markdown-it/src/common/utils.rs new file mode 100644 index 0000000000000000000000000000000000000000..f12054bbba0181cb77e77ad73aff43f5664d3874 --- /dev/null +++ b/crates/markdown-it/src/common/utils.rs @@ -0,0 +1,467 @@ +//! Random assortment of functions that's used internally to write plugins. + +use entities; +use once_cell::sync::Lazy; +use regex::Regex; +use std::borrow::Cow; +use std::collections::HashMap; + +const UNESCAPE_MD_RE : &str = r##"\\([!"#$%&'()*+,\-./:;<=>?@\[\\\]^_`{|}~])"##; +const ENTITY_RE : &str = r##"&([A-Za-z#][A-Za-z0-9]{1,31});"##; + +static DIGITAL_ENTITY_TEST_RE : Lazy = Lazy::new(|| + Regex::new(r#"(?i)^&#(x[a-f0-9]{1,8}|[0-9]{1,8});$"#).unwrap() +); +static UNESCAPE_ALL_RE : Lazy = Lazy::new(|| + Regex::new(&format!("{UNESCAPE_MD_RE}|{ENTITY_RE}")).unwrap() +); + +#[allow(clippy::manual_range_contains)] +/// Return true if a `code` you got from `&#xHHHH;` entity is a valid charcode. +/// +/// It returns false for surrogates and non-printables, so it's a subset of `char::from_u32`. +/// For example, it returns false for 0xFDD0, which is a valid character, but not safe to +/// render on the screen due to turning you into stone, as per +/// ``` +/// # use markdown_it::common::utils::is_valid_entity_code; +/// assert_eq!(is_valid_entity_code(1), false); +/// assert_eq!(is_valid_entity_code(32), true); +/// ``` +pub fn is_valid_entity_code(code: u32) -> bool { + // broken sequence + if code >= 0xD800 && code <= 0xDFFF { return false; } + // never used + if code >= 0xFDD0 && code <= 0xFDEF { return false; } + if (code & 0xFFFF) == 0xFFFF || (code & 0xFFFF) == 0xFFFE { return false; } + // control codes + if code <= 0x08 { return false; } + if code == 0x0B { return false; } + if code >= 0x0E && code <= 0x1F { return false; } + if code >= 0x7F && code <= 0x9F { return false; } + // out of range + if code > 0x10FFFF { return false; } + true +} + +/// Check if "&xxxx;" string is a valid HTML entity, return character it represents. +/// ``` +/// # use markdown_it::common::utils::get_entity_from_str; +/// assert_eq!(get_entity_from_str("&"), Some("&")); +/// assert_eq!(get_entity_from_str("&xxx;"), None); +/// ``` +pub fn get_entity_from_str(str: &str) -> Option<&'static str> { + pub static ENTITIES_HASH : Lazy> = Lazy::new(|| { + let mut mapping = HashMap::new(); + for e in &entities::ENTITIES { + if e.entity.ends_with(';') { + mapping.insert(e.entity, e.characters); + } + } + mapping + }); + + ENTITIES_HASH.get(str).copied() +} + +#[allow(clippy::from_str_radix_10)] +fn replace_entity_pattern(str: &str) -> Option { + if let Some(entity) = get_entity_from_str(str) { + Some((*entity).to_owned()) + } else if let Some(captures) = DIGITAL_ENTITY_TEST_RE.captures(str) { + let str = captures.get(1).unwrap().as_str(); + let code = if str.starts_with('x') || str.starts_with('X') { + u32::from_str_radix(&str[1..], 16).unwrap() + } else { + u32::from_str_radix(str, 10).unwrap() + }; + + if is_valid_entity_code(code) { + Some(char::from_u32(code).unwrap().into()) + } else { + None + } + } else { + None + } +} + +/// Unescape both entities (`" -> "`) and backslash escapes (`\" -> "`). +/// ``` +/// # use markdown_it::common::utils::unescape_all; +/// assert_eq!(unescape_all("&"), "&"); +/// assert_eq!(unescape_all("\\&"), "&"); +/// ``` +pub fn unescape_all(str: &str) -> Cow { + if !str.contains('\\') && !str.contains('&') { return Cow::Borrowed(str); } + + UNESCAPE_ALL_RE.replace_all(str, |captures: ®ex::Captures| { + let s = captures.get(0).unwrap().as_str(); + if let Some(m) = captures.get(1) { + // \" -> " + m.as_str().to_owned() + } else if let Some(replacement) = replace_entity_pattern(s) { + // " -> " + replacement + } else { + s.to_owned() + } + }) +} + +/// Escape `" < > &` with corresponding HTML entities; +/// ``` +/// # use markdown_it::common::utils::escape_html; +/// assert_eq!(escape_html("&\""), "&""); +/// ``` +pub fn escape_html(str: &str) -> Cow { + html_escape::encode_double_quoted_attribute(str) +} + +/// Unicode case folding + space normalization, used for for reference labels. +/// +/// So that strings equal according to commonmark standard are converted to +/// the same string (lowercase/uppercase differences and spacing go away). +/// ``` +/// # use markdown_it::common::utils::normalize_reference; +/// assert_eq!(normalize_reference("hello"), normalize_reference("HELLO")); +/// assert_eq!(normalize_reference("a b"), normalize_reference("a b")); +/// ``` +pub fn normalize_reference(str: &str) -> String { + static SPACE_RE : Lazy = Lazy::new(|| Regex::new(r"\s+").unwrap()); + + // Trim and collapse whitespace + // + let str = SPACE_RE.replace_all(str.trim(), " "); + + // .toLowerCase().toUpperCase() should get rid of all differences + // between letter variants. + // + // Simple .toLowerCase() doesn't normalize 125 code points correctly, + // and .toUpperCase doesn't normalize 6 of them (list of exceptions: + // İ, Ď´, ẞ, Ω, K, Å - those are already uppercased, but have differently + // uppercased versions). + // + // Here's an example showing how it happens. Lets take greek letter omega: + // uppercase U+0398 (Θ), U+03f4 (Ď´) and lowercase U+03b8 (θ), U+03d1 (ϑ) + // + // Unicode entries: + // 0398;GREEK CAPITAL LETTER THETA;Lu;0;L;;;;;N;;;;03B8; + // 03B8;GREEK SMALL LETTER THETA;Ll;0;L;;;;;N;;;0398;;0398 + // 03D1;GREEK THETA SYMBOL;Ll;0;L; 03B8;;;;N;GREEK SMALL LETTER SCRIPT THETA;;0398;;0398 + // 03F4;GREEK CAPITAL THETA SYMBOL;Lu;0;L; 0398;;;;N;;;;03B8; + // + // Case-insensitive comparison should treat all of them as equivalent. + // + // But .toLowerCase() doesn't change ϑ (it's already lowercase), + // and .toUpperCase() doesn't change Ď´ (already uppercase). + // + // Applying first lower then upper case normalizes any character: + // '\u0398\u03f4\u03b8\u03d1'.toLowerCase().toUpperCase() === '\u0398\u0398\u0398\u0398' + // + // Note: this is equivalent to unicode case folding; unicode normalization + // is a different step that is not required here. + // + // Final result should be uppercased, because it's later stored in an object + // (this avoid a conflict with Object.prototype members, + // most notably, `__proto__`) + // + str.to_lowercase().to_uppercase() +} + +/// Count number of characters since last occurrence of `char`. +/// +/// Finds last occurrence of `char` in `source`, returns number of characters from +/// that last occurrence. If char is not found, return number of characters total. +/// ``` +/// # use markdown_it::common::utils::rfind_and_count; +/// assert_eq!(rfind_and_count("abcde", 'e'), 0); +/// assert_eq!(rfind_and_count("abcde", 'b'), 3); +/// assert_eq!(rfind_and_count("abcde", 'z'), 5); +/// ``` +pub fn rfind_and_count(source: &str, char: char) -> usize { + let mut result = 0; + for c in source.chars().rev() { + if c == char { break; } + result += 1; + } + result +} + +/// Calculate number of spaces from `pos` to first non-space character or EOL. +/// +/// Tabs are expanded to variable number of spaces with tabstop = 4. +/// Returns relative indent and offset of first non-space character. +/// ``` +/// # use markdown_it::common::utils::find_indent_of; +/// assert_eq!(find_indent_of("\tfoo", 0), (4, 1)); +/// ``` +pub fn find_indent_of(line: &str, mut pos: usize) -> (usize, usize) { + let mut chars = line[pos..].chars(); + let mut indent = 0; + + loop { + match chars.next() { + Some('\t') => { + let bs_count = rfind_and_count(&line[..pos], '\t'); + indent += 4 - bs_count % 4; + pos += 1; + } + Some(' ') => { + indent += 1; + pos += 1; + } + _ => return ( indent, pos ), + } + } +} + +/// Returns trailing whitespace with total length of `indent`. +/// +/// Input: a string of characters (presumed whitespaces, can be anything), where each one of +/// them contributes 1 to indent (except for tabs, whose width may vary with tabstop = 4). +/// +/// If an indent would split a tab, that tab is replaced with 4 spaces. +/// +/// Example: cut_right_whitespace_with_tabstops("\t\t", 6) would return " \t" (two preceding +/// spaces) because first tab gets expanded to 6 spaces. +/// ``` +/// # use markdown_it::common::utils::cut_right_whitespace_with_tabstops; +/// assert_eq!(cut_right_whitespace_with_tabstops("\t\t", 6), " \t"); +/// ``` +pub fn cut_right_whitespace_with_tabstops(source: &str, indent: i32) -> Cow { + let (num_spaces, start) = calc_right_whitespace_with_tabstops(source, indent); + + if num_spaces > 0 { + let mut result = " ".repeat(num_spaces); + result += &source[start..]; + Cow::Owned(result) + } else { + Cow::Borrowed(&source[start..]) + } +} + +/// Calculate trailing whitespace with total length of `indent`. +/// +/// See [cut_right_whitespace_with_tabstops](cut_right_whitespace_with_tabstops) +/// for algorithm and details. +/// +/// Returns number of spaces + number of bytes to cut from the end. +/// ``` +/// # use markdown_it::common::utils::calc_right_whitespace_with_tabstops; +/// assert_eq!(calc_right_whitespace_with_tabstops("\t\t", 6), (2, 1)); +/// ``` +pub fn calc_right_whitespace_with_tabstops(source: &str, mut indent: i32) -> (usize, usize) { + let mut start = source.len(); + let mut chars = source.char_indices().rev(); + + while indent > 0 { + match chars.next() { + Some((pos, '\t')) => { + // previous tab is guaranteed to finish at 0 modulo 4, + // so we can finish counting there + let indent_from_start = rfind_and_count(&source[..pos], '\t'); + let tab_width = 4 - indent_from_start as i32 % 4; + + if indent < tab_width { + return ( indent as usize, start ); + } + + indent -= tab_width; + start = pos; + } + Some((pos, _)) => { + indent -= 1; + start = pos; + } + None => { + start = 0; + break; + } + } + } + + ( 0, start ) +} + +/// Checks whether a given character should count as punctuation +/// +/// used to determine word boundaries, made to match the implementation of +/// `isPunctChar` from the JS library. +/// This is currently implemented as a `match`, but might be simplified as a +/// regex if benchmarking shows this to be beneficient. +pub fn is_punct_char(ch: char) -> bool { + use unicode_general_category::get_general_category; + use unicode_general_category::GeneralCategory::*; + + match get_general_category(ch) { + // P + ConnectorPunctuation | DashPunctuation | OpenPunctuation | ClosePunctuation | + InitialPunctuation | FinalPunctuation | OtherPunctuation => true, + + // L + UppercaseLetter | LowercaseLetter | TitlecaseLetter | ModifierLetter | OtherLetter | + // M + NonspacingMark | SpacingMark | EnclosingMark | + // N + DecimalNumber | LetterNumber | OtherNumber | + // S + MathSymbol | CurrencySymbol | ModifierSymbol | OtherSymbol | + // Z + SpaceSeparator | LineSeparator | ParagraphSeparator | + // C + Control | Format | Surrogate | PrivateUse | Unassigned => false + } +} + +#[cfg(test)] +mod tests { + use super::cut_right_whitespace_with_tabstops as cut_ws; + use super::rfind_and_count; + use super::find_indent_of; + use super::replace_entity_pattern; + use super::unescape_all; + + #[test] + fn rfind_and_count_test() { + assert_eq!(rfind_and_count("", 'b'), 0); + assert_eq!(rfind_and_count("abcde", 'e'), 0); + assert_eq!(rfind_and_count("abcde", 'b'), 3); + assert_eq!(rfind_and_count("abcde", 'z'), 5); + assert_eq!(rfind_and_count("abcεπ", 'b'), 3); + } + + #[test] + fn find_indent_of_simple_test() { + assert_eq!(find_indent_of("a", 0), (0, 0)); + assert_eq!(find_indent_of(" a", 0), (1, 1)); + assert_eq!(find_indent_of(" a", 0), (3, 3)); + assert_eq!(find_indent_of(" ", 0), (4, 4)); + assert_eq!(find_indent_of("\ta", 0), (4, 1)); + assert_eq!(find_indent_of(" \ta", 0), (4, 2)); + assert_eq!(find_indent_of(" \ta", 0), (4, 3)); + assert_eq!(find_indent_of(" \ta", 0), (4, 4)); + assert_eq!(find_indent_of(" \ta", 0), (8, 5)); + } + + #[test] + fn find_indent_of_with_offset() { + assert_eq!(find_indent_of(" a", 2), (1, 3)); + assert_eq!(find_indent_of(" a", 2), (2, 4)); + assert_eq!(find_indent_of(" \ta", 2), (2, 3)); + assert_eq!(find_indent_of(" \ta", 2), (2, 4)); + assert_eq!(find_indent_of(" \ta", 2), (6, 5)); + assert_eq!(find_indent_of(" \ta", 2), (6, 6)); + } + + #[test] + fn find_indent_of_tabs_test() { + assert_eq!(find_indent_of(" \t \ta", 1), (7, 5)); + assert_eq!(find_indent_of(" \t \ta", 2), (6, 5)); + assert_eq!(find_indent_of(" \t \ta", 3), (4, 5)); + assert_eq!(find_indent_of(" \t \ta", 4), (3, 5)); + } + + #[test] + fn cut_ws_simple() { + assert_eq!(cut_ws("abc", -1), ""); + assert_eq!(cut_ws("abc", 0), ""); + assert_eq!(cut_ws("abc", 1), "c"); + assert_eq!(cut_ws("abc", 2), "bc"); + assert_eq!(cut_ws("abc", 3), "abc"); + assert_eq!(cut_ws("abc", 4), "abc"); + } + + #[test] + fn cut_ws_unicode() { + assert_eq!(cut_ws("ιβγδ", 1), "δ"); + assert_eq!(cut_ws("ιβγδ ", 3), "γδ "); + } + + #[test] + fn cut_ws_expands_partial_tabs() { + assert_eq!(cut_ws("\t", 1), " "); + assert_eq!(cut_ws("\t", 2), " "); + assert_eq!(cut_ws("\t", 3), " "); + assert_eq!(cut_ws("\t\t\t", 5), " \t"); + assert_eq!(cut_ws("\t\t\t", 7), " \t"); + } + + #[test] + fn cut_ws_retains_full_tabs() { + assert_eq!(cut_ws("\t\t\t", 4), "\t"); + assert_eq!(cut_ws("\t\t\t", 8), "\t\t"); + } + + #[test] + fn cut_ws_proper_tabstops() { + assert_eq!(cut_ws("a\t", 1), " "); + assert_eq!(cut_ws("a\t", 2), " "); + assert_eq!(cut_ws("a\t", 3), "\t"); + assert_eq!(cut_ws("ab\t", 3), "b\t"); + assert_eq!(cut_ws("abc\t", 3), "bc\t"); + } + + #[test] + fn cut_ws_proper_tabstops_nested() { + assert_eq!(cut_ws("a\tb\t", 2), " "); + assert_eq!(cut_ws("a\tb\t", 3), "\t"); + assert_eq!(cut_ws("a\tb\t", 4), "b\t"); + assert_eq!(cut_ws("a\tb\t", 5), " b\t"); + assert_eq!(cut_ws("a\tb\t", 6), " b\t"); + assert_eq!(cut_ws("a\tb\t", 7), "\tb\t"); + assert_eq!(cut_ws("a\tb\t", 8), "a\tb\t"); + } + + #[test] + fn cut_ws_different_tabstops_nested() { + assert_eq!(cut_ws("abc\tde\tf\tg", 3), " g"); + assert_eq!(cut_ws("abc\tde\tf\tg", 4), "\tg"); + assert_eq!(cut_ws("abc\tde\tf\tg", 5), "f\tg"); + assert_eq!(cut_ws("abc\tde\tf\tg", 6), " f\tg"); + assert_eq!(cut_ws("abc\tde\tf\tg", 7), "\tf\tg"); + assert_eq!(cut_ws("abc\tde\tf\tg", 9), "de\tf\tg"); + assert_eq!(cut_ws("abc\tde\tf\tg", 10), "\tde\tf\tg"); + } + + #[test] + fn test_replace_entity_pattern() { + assert_eq!(replace_entity_pattern("&"), Some("&".into())); + assert_eq!(replace_entity_pattern("€"), Some("€".into())); + assert_eq!(replace_entity_pattern("—"), Some("—".into())); + assert_eq!(replace_entity_pattern("—"), Some("—".into())); + assert_eq!(replace_entity_pattern(" "), Some(" ".into())); + assert_eq!(replace_entity_pattern("?"), Some("?".into())); + assert_eq!(replace_entity_pattern("&ffff;"), None); + assert_eq!(replace_entity_pattern("F;"), None); + assert_eq!(replace_entity_pattern("&#xGG;"), None); + } + + #[test] + fn test_unescape_all_simple() { + assert_eq!(unescape_all("&"), "&"); + assert_eq!(unescape_all("\\&"), "&"); + } + + #[test] + fn test_unescape_all_xss() { + assert_eq!( + unescape_all(r#"javascript:alert(1)"#), + r#"javascript:alert(1)"#); + + assert_eq!( + unescape_all(r#"Javascript:alert(1)"#), + r#"Javascript:alert(1)"#); + + assert_eq!( + unescape_all(r#"&#74;avascript:alert(1)"#), + r#"Javascript:alert(1)"#); + + assert_eq!( + unescape_all(r#"\Javascript:alert(1)"#), + r#"Javascript:alert(1)"#); + + assert_eq!( + unescape_all(r#""><script>alert("xss")</script>"#), + r#"">"#); + } +} diff --git a/crates/markdown-it/src/examples/ferris/block_rule.rs b/crates/markdown-it/src/examples/ferris/block_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..b7133fd6bafbce0ec632fb0b3a03a87749c24b54 --- /dev/null +++ b/crates/markdown-it/src/examples/ferris/block_rule.rs @@ -0,0 +1,7 @@ +//! Replaces `(\/)-------(\/)` with a nice picture. +//! +//! ```rust +//! # const IGNORE : &str = stringify! { +#![doc=include_str!("../../../examples/ferris/block_rule.rs")] +//! # }; +//! ``` diff --git a/crates/markdown-it/src/examples/ferris/core_rule.rs b/crates/markdown-it/src/examples/ferris/core_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..6435fc216eb6477d4648f03e83df09cb5f806323 --- /dev/null +++ b/crates/markdown-it/src/examples/ferris/core_rule.rs @@ -0,0 +1,7 @@ +//! Counts the number of crabs lurking around. +//! +//! ```rust +//! # const IGNORE : &str = stringify! { +#![doc=include_str!("../../../examples/ferris/core_rule.rs")] +//! # }; +//! ``` diff --git a/crates/markdown-it/src/examples/ferris/inline_rule.rs b/crates/markdown-it/src/examples/ferris/inline_rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..c6f0c66255474faffe4d6a97725cdfc4e5551d4b --- /dev/null +++ b/crates/markdown-it/src/examples/ferris/inline_rule.rs @@ -0,0 +1,7 @@ +//! Replaces `(\/)` with `🦀`. +//! +//! ```rust +//! # const IGNORE : &str = stringify! { +#![doc=include_str!("../../../examples/ferris/inline_rule.rs")] +//! # }; +//! ``` diff --git a/crates/markdown-it/src/examples/ferris/mod.rs b/crates/markdown-it/src/examples/ferris/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..7b40ef62fa879486106ae33c6e23261a0ca0757c --- /dev/null +++ b/crates/markdown-it/src/examples/ferris/mod.rs @@ -0,0 +1,15 @@ +#![doc=include_str!("../../../examples/ferris/README.md")] +//! +//! ### Implementation +//! +//! See [core_rule], [block_rule] and [inline_rule] below for implementations. +//! +//! ```rust +//! # const IGNORE : &str = stringify! { +#![doc=include_str!("../../../examples/ferris/main.rs")] +//! # }; +//! ``` + +pub mod block_rule; +pub mod core_rule; +pub mod inline_rule; diff --git a/crates/markdown-it/src/examples/mod.rs b/crates/markdown-it/src/examples/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..ce13ce77c51b87a05e01e326a013d4a132341a4d --- /dev/null +++ b/crates/markdown-it/src/examples/mod.rs @@ -0,0 +1,4 @@ +//! Copied content from `examples/` folder (docs only). + +pub mod ferris; +mod testreadme; diff --git a/crates/markdown-it/src/examples/testreadme.rs b/crates/markdown-it/src/examples/testreadme.rs new file mode 100644 index 0000000000000000000000000000000000000000..f3e768e5d4c9a0a60e80274a20347a71883cf8bf --- /dev/null +++ b/crates/markdown-it/src/examples/testreadme.rs @@ -0,0 +1,2 @@ +// This file is used only to test examples in README. +#![doc=include_str!("../../README.md")] diff --git a/crates/markdown-it/src/generics/inline/code_pair.rs b/crates/markdown-it/src/generics/inline/code_pair.rs new file mode 100644 index 0000000000000000000000000000000000000000..299c533270dc6ea2843cccbf81e3f7883a6eb3f0 --- /dev/null +++ b/crates/markdown-it/src/generics/inline/code_pair.rs @@ -0,0 +1,142 @@ +//! Structure similar to `` `code span` `` with configurable markers of variable length. +//! +//! It allows you to define a custom structure with variable number of markers +//! (e.g. with `%` defined as a marker, user can write `%foo%` or `%%%foo%%%` +//! resulting in the same node). +//! +//! You add a custom structure by using [add_with] function, which takes following arguments: +//! - `MARKER` - marker character +//! - `md` - parser instance +//! - `f` - function that should return your custom [Node] +//! +//! Here is an example of a rule turning `%foo%` into `🦀foo🦀`: +//! +//! ```rust +//! use markdown_it::generics::inline::code_pair; +//! use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; +//! +//! #[derive(Debug)] +//! struct Ferris; +//! impl NodeValue for Ferris { +//! fn render(&self, node: &Node, fmt: &mut dyn Renderer) { +//! fmt.text("🦀"); +//! fmt.contents(&node.children); +//! fmt.text("🦀"); +//! } +//! } +//! +//! let md = &mut MarkdownIt::new(); +//! code_pair::add_with::<'%'>(md, |_| Node::new(Ferris)); +//! let html = md.parse("hello %world%").render(); +//! assert_eq!(html.trim(), "hello 🦀world🦀"); +//! ``` +//! +//! This generic structure follows exact rules of code span in CommonMark: +//! +//! 1. Literal marker character sequence can be used inside of structure if its length +//! doesn't match length of the opening/closing sequence (e.g. with `%` defined +//! as a marker, `%%foo%bar%%` gets parsed as `Node("foo%bar")`). +//! +//! 2. Single space inside is trimmed to allow you to write `% %%foo %` to be parsed as +//! `Node("%%foo")`. +//! +//! If you define two structures with the same marker, only the first one will work. +//! +use crate::parser::extset::{InlineRootExt, MarkdownItExt}; +use crate::parser::inline::{InlineRule, InlineState, Text}; +use crate::{MarkdownIt, Node}; + +#[derive(Debug, Default)] +struct CodePairCache { + scanned: bool, + max: Vec, +} +impl InlineRootExt for CodePairCache {} + +#[derive(Debug)] +struct CodePairConfig(fn (usize) -> Node); +impl MarkdownItExt for CodePairConfig {} + +pub fn add_with(md: &mut MarkdownIt, f: fn (length: usize) -> Node) { + md.ext.insert(CodePairConfig::(f)); + + md.inline.add_rule::>(); +} + +#[doc(hidden)] +pub struct CodePairScanner; +impl InlineRule for CodePairScanner { + const MARKER: char = MARKER; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != MARKER { return None; } + if state.trailing_text_get().ends_with(MARKER) { return None; } + + let mut pos = state.pos + 1; + + // scan marker length + while Some(MARKER) == chars.next() { + pos += 1; + } + + // backtick length => last seen position + let backticks = state.inline_ext.get_or_insert_default::>(); + let opener_len = pos - state.pos; + + if backticks.scanned && backticks.max.get(opener_len).copied().unwrap_or(0) <= state.pos { + // performance note: adding entire sequence into pending is 5x faster, + // but it will interfere with other rules working on the same char; + // and it is extremely rare that user would put a thousand "`" in text + return None; + } + + let mut match_start; + let mut match_end = pos; + + // Nothing found in the cache, scan until the end of the line (or until marker is found) + while let Some(p) = state.src[match_end..state.pos_max].find(MARKER) { + match_start = match_end + p; + + // scan marker length + match_end = match_start + 1; + chars = state.src[match_end..state.pos_max].chars(); + + while Some(MARKER) == chars.next() { + match_end += 1; + } + + let closer_len = match_end - match_start; + + if closer_len == opener_len { + // Found matching closer length. + let mut content = state.src[pos..match_start].to_owned().replace('\n', " "); + if content.starts_with(' ') && content.ends_with(' ') && content.len() > 2 { + content[1..content.len() - 1].to_owned().clone_into(&mut content); + pos += 1; + match_start -= 1; + } + + let f = state.md.ext.get::>().unwrap().0; + let mut node = f(opener_len); + + let mut inner_node = Node::new(Text { content }); + inner_node.srcmap = state.get_map(pos, match_start); + node.children.push(inner_node); + + return Some((node, match_end - state.pos)); + } + + // Some different length found, put it in cache as upper limit of where closer can be found + let backticks = state.inline_ext.get_mut::>().unwrap(); + while backticks.max.len() <= closer_len { backticks.max.push(0); } + backticks.max[closer_len] = match_start; + } + + // Scanned through the end, didn't find anything + let backticks = state.inline_ext.get_mut::>().unwrap(); + backticks.scanned = true; + + None + } +} diff --git a/crates/markdown-it/src/generics/inline/emph_pair.rs b/crates/markdown-it/src/generics/inline/emph_pair.rs new file mode 100644 index 0000000000000000000000000000000000000000..acb1fb2527e0f9720cf206878e8154a80ec87a04 --- /dev/null +++ b/crates/markdown-it/src/generics/inline/emph_pair.rs @@ -0,0 +1,390 @@ +//! Structure similar to `*emphasis*` with configurable markers of fixed length. +//! +//! There are many structures in various markdown flavors that +//! can be implemented with this, namely: +//! +//! - `*emphasis*` or `_emphasis_` -> `emphasis` +//! - `**strong**` or `__strong__` -> `strong` +//! - `~~strikethrough~~` -> `strikethrough` +//! - `==marked==` -> `marked` +//! - `++inserted++` -> `inserted` +//! - `~subscript~` -> `subscript` +//! - `^superscript^` -> `superscript` +//! +//! You add a custom structure by using [add_with] function, which takes following arguments: +//! - `MARKER` - marker character +//! - `LENGTH` - length of the opening/closing marker (can be 1, 2 or 3) +//! - `CAN_SPLIT_WORD` - whether this structure can be found in the middle of the word +//! (for example, note the difference between `foo*bar*baz` and `foo_bar_baz` +//! in CommonMark - first one is an emphasis, second one isn't) +//! - `md` - parser instance +//! - `f` - function that should return your custom [Node] +//! +//! Here is an example of implementing superscript in your custom code: +//! +//! ```rust +//! use markdown_it::generics::inline::emph_pair; +//! use markdown_it::{MarkdownIt, Node, NodeValue, Renderer}; +//! +//! #[derive(Debug)] +//! struct Superscript; +//! impl NodeValue for Superscript { +//! fn render(&self, node: &Node, fmt: &mut dyn Renderer) { +//! fmt.open("sup", &node.attrs); +//! fmt.contents(&node.children); +//! fmt.close("sup"); +//! } +//! } +//! +//! let md = &mut MarkdownIt::new(); +//! emph_pair::add_with::<'^', 1, true>(md, || Node::new(Superscript)); +//! +//! let html = md.parse("e^iπ^+1=0").render(); +//! assert_eq!(html.trim(), "eiπ+1=0"); +//! ``` +//! +//! Note that these structures have lower priority than the rest of the rules, +//! e.g. `` *foo`bar*baz` `` is parsed as `*foobar*baz`. +//! +use std::cmp::min; + +use crate::common::sourcemap::SourcePos; +use crate::parser::core::CoreRule; +use crate::parser::extset::{MarkdownItExt, NodeExt}; +use crate::parser::inline::builtin::InlineParserRule; +use crate::parser::inline::{InlineRule, InlineState, Text}; +use crate::{MarkdownIt, Node, NodeValue}; + +#[derive(Debug, Default)] +struct PairConfig { + inserted: bool, + fns: [Option Node>; 3], +} +impl MarkdownItExt for PairConfig {} + +#[derive(Debug, Default)] +struct OpenersBottom([usize; 6]); +impl NodeExt for OpenersBottom {} + +#[derive(Debug, Clone)] +#[doc(hidden)] +pub struct EmphMarker { + // Starting marker + pub marker: char, + + // Total length of these series of delimiters. + pub length: usize, + + // Remaining length that's not already matched to other delimiters. + pub remaining: usize, + + // Boolean flags that determine if this delimiter could open or close + // an emphasis. + pub open: bool, + pub close: bool, + + // Inline position (offset into the stripped inline text) where this + // delimiter starts. Stored so that `run` can compute the correct + // inline-coordinate token length when the delimiter spans multiple + // block-quote continuation lines (where srcmap offsets include the + // stripped `> ` bytes and therefore exceed the inline position). + pub inline_pos: usize, +} + +// this node is supposed to be replaced by actual emph or text node +impl NodeValue for EmphMarker {} + +pub fn add_with( + md: &mut MarkdownIt, + f: fn() -> Node, +) { + let pair_config = md.ext.get_or_insert_default::>(); + pair_config.fns[LENGTH as usize - 1] = Some(f); + + if !pair_config.inserted { + pair_config.inserted = true; + md.inline + .add_rule::>(); + } + + if !md.has_rule::() { + md.add_rule::() + .before_all() + .after::(); + } +} + +#[doc(hidden)] +pub struct EmphPairScanner; +impl InlineRule + for EmphPairScanner +{ + const MARKER: char = MARKER; + + // this rule works on a closing marker, so for technical reasons any rules trying to skip it + // should see just plain text + fn check(_: &mut InlineState) -> Option { + None + } + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != MARKER { + return None; + } + + let scanned = state.scan_delims(state.pos, CAN_SPLIT_WORD); + let inline_closer_start = state.pos; + let mut node = Node::new(EmphMarker { + marker: MARKER, + length: scanned.length, + remaining: scanned.length, + open: scanned.can_open, + close: scanned.can_close, + inline_pos: state.pos, + }); + node.srcmap = state.get_map(state.pos, state.pos + scanned.length); + let (node, opener_inline_pos) = scan_and_match_delimiters::(state, node); + // backtrack to keep correct source maps + state.pos += scanned.length; + // Compute token_len in inline coordinates. Using srcmap byte offsets + // (map.1 - map.0) is incorrect when the token spans multiple blockquote + // continuation lines because the srcmap reflects the original file bytes + // (which include the stripped `> ` prefixes) rather than the shorter + // inline text. When a match was found we use the opener's stored inline + // position; when no match was found the token is only the scanned + // delimiter itself. + let token_len = match opener_inline_pos { + Some(oip) => state.pos - oip, + None => scanned.length, + }; + // Sanity: keep state.pos valid (should never underflow, but guard anyway) + debug_assert!( + state.pos >= token_len, + "emph backtrack underflow: pos={} token_len={} closer_start={}", + state.pos, + token_len, + inline_closer_start + ); + state.pos -= token_len; + Some((node, token_len)) + } +} + +/// Assuming last token is a closing delimiter we just inserted, +/// try to find opener(s). If any are found, move stuff to nested emph node. +/// +/// Returns `(node, opener_inline_pos)`. `opener_inline_pos` is `Some` when a +/// match was found; the value is the `inline_pos` field of the matched opener. +fn scan_and_match_delimiters( + state: &mut InlineState, + mut closer_token: Node, +) -> (Node, Option) { + if state.node.children.is_empty() { + return (closer_token, None); + } // must have at least opener and closer + + let mut closer = closer_token.cast_mut::().unwrap().clone(); + if !closer.close { + return (closer_token, None); + } + + // Previously calculated lower bounds (previous fails) + // for each marker, each delimiter length modulo 3, + // and for whether this closer can be an opener; + // https://github.com/commonmark/cmark/commit/34250e12ccebdc6372b8b49c44fab57c72443460 + let openers_for_marker = state + .node + .ext + .get_or_insert_default::>(); + let openers_parameter = (closer.open as usize) * 3 + closer.length % 3; + + let min_opener_idx = openers_for_marker.0[openers_parameter]; + + let mut idx = state.node.children.len() - 1; + let mut new_min_opener_idx = idx; + let mut matched_opener_inline_pos: Option = None; + while idx > min_opener_idx { + idx -= 1; + + let Some(opener) = state.node.children[idx].cast::() else { + continue; + }; + + let mut opener = opener.clone(); + if opener.open && opener.marker == closer.marker && !is_odd_match(&opener, &closer) { + while closer.remaining > 0 && opener.remaining > 0 { + let max_marker_len = min(3, min(opener.remaining, closer.remaining)); + let mut matched_rule = None; + let fns = &state.md.ext.get::>().unwrap().fns; + for marker_len in (1..=max_marker_len).rev() { + if let Some(f) = fns[marker_len - 1] { + matched_rule = Some((marker_len, f)); + break; + } + } + + // If matched_fn isn't found, it can only mean that function is defined for larger marker + // than we have (e.g. function defined for **, we have *). + // Treat this as "marker not found". + if matched_rule.is_none() { + break; + } + + let (marker_len, marker_fn) = matched_rule.unwrap(); + + closer.remaining -= marker_len; + opener.remaining -= marker_len; + + let mut new_token = marker_fn(); + new_token.children = state.node.children.split_off(idx + 1); + + // cut marker_len chars from start, i.e. "12345" -> "345" + let mut end_map_pos = 0; + if let Some(map) = closer_token.srcmap { + let (start, end) = map.get_byte_offsets(); + closer_token.srcmap = Some(SourcePos::new(start + marker_len, end)); + end_map_pos = start + marker_len; + } + + // cut marker_len chars from end, i.e. "12345" -> "123" + let mut start_map_pos = 0; + let opener_token = state.node.children.last_mut().unwrap(); + if let Some(map) = opener_token.srcmap { + let (start, end) = map.get_byte_offsets(); + opener_token.srcmap = Some(SourcePos::new(start, end - marker_len)); + start_map_pos = end - marker_len; + } + + new_token.srcmap = state.get_map(start_map_pos, end_map_pos); + + // remove empty node as a small optimization so we can do less work later + if opener.remaining == 0 { + state.node.children.pop(); + } + + new_min_opener_idx = 0; + if matched_opener_inline_pos.is_none() { + matched_opener_inline_pos = Some(opener.inline_pos); + } + state.node.children.push(new_token); + } + } + + if opener.remaining > 0 { + state.node.children[idx].replace(opener); + } // otherwise node was already deleted + } + + if new_min_opener_idx != 0 { + // If match for this delimiter run failed, we want to set lower bound for + // future lookups. This is required to make sure algorithm has linear + // complexity. + // + // See details here: + // https://github.com/commonmark/cmark/issues/178#issuecomment-270417442 + // + let openers_for_marker = state + .node + .ext + .get_or_insert_default::>(); + openers_for_marker.0[openers_parameter] = new_min_opener_idx; + } + + // remove empty node as a small optimization so we can do less work later + if closer.remaining > 0 { + closer_token.replace(closer); + (closer_token, matched_opener_inline_pos) + } else { + ( + state.node.children.pop().unwrap(), + matched_opener_inline_pos, + ) + } +} + +fn is_odd_match(opener: &EmphMarker, closer: &EmphMarker) -> bool { + // from spec: + // + // If one of the delimiters can both open and close emphasis, then the + // sum of the lengths of the delimiter runs containing the opening and + // closing delimiters must not be a multiple of 3 unless both lengths + // are multiples of 3. + // + #[allow(clippy::collapsible_if)] + if opener.close || closer.open { + if (opener.length + closer.length) % 3 == 0 { + if opener.length % 3 != 0 || closer.length % 3 != 0 { + return true; + } + } + } + + false +} + +#[doc(hidden)] +pub struct FragmentsJoin; +impl CoreRule for FragmentsJoin { + fn run(node: &mut Node, _: &MarkdownIt) { + node.walk_mut(|node, _| fragments_join(node)); + } +} + +/// Clean up tokens after emphasis and strikethrough postprocessing: +/// merge adjacent text nodes into one and re-calculate all token levels +/// +/// This is necessary because initially emphasis delimiter markers (*, _, ~) +/// are treated as their own separate text tokens. Then emphasis rule either +/// leaves them as text (needed to merge with adjacent text) or turns them +/// into opening/closing tags (which messes up levels inside). +/// +fn fragments_join(node: &mut Node) { + // replace all emph markers with text tokens + for token in node.children.iter_mut() { + if let Some(data) = token.cast::() { + let content = data.marker.to_string().repeat(data.remaining); + token.replace(Text { content }); + } + } + + // collapse adjacent text tokens + for idx in 1..node.children.len() { + let (tokens1, tokens2) = node.children.split_at_mut(idx); + + let token1 = tokens1.last_mut().unwrap(); + let Some(t1_data) = token1.cast_mut::() else { + continue; + }; + + let token2 = tokens2.first_mut().unwrap(); + let Some(t2_data) = token2.cast_mut::() else { + continue; + }; + + // concat contents + let t2_content = std::mem::take(&mut t2_data.content); + t1_data.content += &t2_content; + + // adjust source maps + if let Some(map1) = token1.srcmap { + if let Some(map2) = token2.srcmap { + token1.srcmap = Some(SourcePos::new( + map1.get_byte_offsets().0, + map2.get_byte_offsets().1, + )); + } + } + + node.children.swap(idx - 1, idx); + } + + // remove all empty tokens + node.children.retain(|token| { + if let Some(data) = token.cast::() { + !data.content.is_empty() + } else { + true + } + }); +} diff --git a/crates/markdown-it/src/generics/inline/full_link.rs b/crates/markdown-it/src/generics/inline/full_link.rs new file mode 100644 index 0000000000000000000000000000000000000000..d1f10ba767846a4ca9487b8f68763b4d383e7b0a --- /dev/null +++ b/crates/markdown-it/src/generics/inline/full_link.rs @@ -0,0 +1,440 @@ +//! Structure similar to `[link]( "stuff")` with configurable prefix. +//! +//! There are two structures in CommonMark that match this syntax: +//! - links - `[text]( "title")` +//! - images - `![alt]( "title")` +//! +//! You can add custom rules like `~[foo]( "baz")`. Let us know if +//! you come up with fun use case to add as an example! +//! +//! Add a custom structure by using [add_prefix] function, which takes following arguments: +//! - `PREFIX` - marker character before label (`!` in case of images) +//! - `ENABLE_NESTED` - allow nested links inside +//! - `md` - parser instance +//! - `f` - function that should return your custom [Node] given href and title +//! +use std::collections::HashMap; + +use crate::common::utils::unescape_all; +use crate::parser::extset::{InlineRootExt, MarkdownItExt}; +use crate::parser::inline::{InlineRule, InlineState}; +use crate::plugins::cmark::block::reference::ReferenceMap; +use crate::{MarkdownIt, Node}; + +#[derive(Debug)] +struct LinkCfg(fn (Option, Option) -> Node); +impl MarkdownItExt for LinkCfg {} + +/// adds custom rule with no prefix +pub fn add( + md: &mut MarkdownIt, + f: fn (url: Option, title: Option) -> Node +) { + md.ext.insert(LinkCfg::<'\0'>(f)); + md.inline.add_rule::>(); + if !md.inline.has_rule::() { + md.inline.add_rule::(); + } +} + +/// adds custom rule with given `PREFIX` character +pub fn add_prefix( + md: &mut MarkdownIt, + f: fn (url: Option, title: Option) -> Node +) { + md.ext.insert(LinkCfg::(f)); + md.inline.add_rule::>(); + if !md.inline.has_rule::() { + md.inline.add_rule::(); + } +} + +#[doc(hidden)] +pub struct LinkScanner; +impl InlineRule for LinkScanner { + const MARKER: char = '['; + + fn check(state: &mut InlineState) -> Option { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '[' { return None; } + rule_check(state, ENABLE_NESTED, 0) + } + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '[' { return None; } + let f = state.md.ext.get::>().unwrap().0; + rule_run(state, ENABLE_NESTED, 0, f) + } +} + +#[doc(hidden)] +pub struct LinkPrefixScanner; +impl InlineRule for LinkPrefixScanner { + const MARKER: char = PREFIX; + + fn check(state: &mut InlineState) -> Option { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next() != Some(PREFIX) { return None; } + if chars.next() != Some('[') { return None; } + rule_check(state, ENABLE_NESTED, 1) + } + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next() != Some(PREFIX) { return None; } + if chars.next() != Some('[') { return None; } + let f = state.md.ext.get::>().unwrap().0; + rule_run(state, ENABLE_NESTED, 1, f) + } +} + +#[doc(hidden)] +/// this rule makes sure that parser is stopped on "]" character, +/// but it actually doesn't do anything +pub struct LinkScannerEnd; +impl InlineRule for LinkScannerEnd { + const MARKER: char = ']'; + + fn check(_: &mut InlineState) -> Option { None } + fn run(_: &mut InlineState) -> Option<(Node, usize)> { None } +} + +fn rule_check(state: &mut InlineState, enable_nested: bool, offset: usize) -> Option { + if let Some(result) = parse_link(state, state.pos + offset, enable_nested) { + Some(result.end - state.pos) + } else { + None + } +} + +fn rule_run( + state: &mut InlineState, + enable_nested: bool, + offset: usize, + f: fn (Option, Option) -> Node +) -> Option<(Node, usize)> { + let start = state.pos; + let result = parse_link(state, state.pos + offset, enable_nested)?; + + // + // We found the end of the link, and know for a fact it's a valid link; + // so all that's left to do is to call tokenizer. + // + let old_node = std::mem::replace(&mut state.node, f(result.href, result.title)); + let max = state.pos_max; + + state.link_level += 1; + state.pos = result.label_start; + state.pos_max = result.label_end; + state.md.inline.tokenize(state); + state.pos = start; + state.pos_max = max; + state.link_level -= 1; + + let node = std::mem::replace(&mut state.node, old_node); + Some((node, result.end - state.pos)) +} + +#[derive(Debug, Default)] +struct LinkLabelScanCache(HashMap<(usize, bool), Option>); +impl InlineRootExt for LinkLabelScanCache {} + + +// Parse link label +// +// this function assumes that first character ("[") already matches; +// returns the end of the label +fn parse_link_label(state: &mut InlineState, start: usize, enable_nested: bool) -> Option { + let cache = state.inline_ext.get_or_insert_default::(); + if let Some(&cached) = cache.0.get(&(start, enable_nested)) { + return cached; + } + + let old_pos = state.pos; + let mut found = false; + let mut label_end = None; + let mut level = 1; + + state.pos = start + 1; + + while let Some(ch) = state.src[state.pos..state.pos_max].chars().next() { + if ch == ']' { + level -= 1; + if level == 0 { + found = true; + break; + } + } + + let prev_pos = state.pos; + state.md.inline.skip_token(state); + if ch == '[' { + if prev_pos == state.pos - 1 { + // increase level if we find text `[`, which is not a part of any token + level += 1; + + let cache = state.inline_ext.get_or_insert_default::(); + if let Some(&cached) = cache.0.get(&(prev_pos, enable_nested)) { + // maybe cache appeared as a result of skip_token + if let Some(cached_pos) = cached { + state.pos = cached_pos; + } else { + break; + } + } + + } else if !enable_nested { + break; + } + } + } + + if found { + label_end = Some(state.pos); + } + + // restore old state + state.pos = old_pos; + + let cache = state.inline_ext.get_or_insert_default::(); + cache.0.insert((start, enable_nested), label_end); + + label_end +} + + +pub struct ParseLinkFragmentResult { + /// end position + pub pos: usize, + /// number of linebreaks inside + pub lines: usize, + /// parsed result + pub str: String, +} + + +/// Helper function used to parse `` part of the links with optional brackets. +pub fn parse_link_destination(str: &str, start: usize, max: usize) -> Option { + let mut chars = str[start..max].chars().peekable(); + let mut pos = start; + + if let Some('<') = chars.peek() { + chars.next(); // skip '<' + pos += 1; + loop { + match chars.next() { + Some('\n' | '<') | None => return None, + Some('>') => { + return Some(ParseLinkFragmentResult { + pos: pos + 1, + lines: 0, + str: unescape_all(&str[start + 1..pos]).into_owned(), + }); + } + Some('\\') => { + match chars.next() { + None => return None, + Some(x) => pos += 1 + x.len_utf8(), + } + } + Some(x) => { + pos += x.len_utf8(); + } + } + } + } else { + let mut level : u32 = 0; + loop { + match chars.next() { + // space + ascii control characters + Some('\0'..=' ' | '\x7f') | None => break, + Some('\\') => { + match chars.next() { + Some(' ') | None => break, + Some(x) => pos += 1 + x.len_utf8(), + } + } + Some('(') => { + level += 1; + if level > 32 { return None; } + pos += 1; + } + Some(')') => { + if level == 0 { break; } + level -= 1; + pos += 1; + } + Some(x) => { + pos += x.len_utf8(); + } + } + } + + if level != 0 { return None; } + + Some(ParseLinkFragmentResult { + pos, + lines: 0, + str: unescape_all(&str[start..pos]).into_owned(), + }) + } +} + + +/// Helper function used to parse `"title"` part of the links (with `'title'` or `(title)` alternative syntax). +pub fn parse_link_title(str: &str, start: usize, max: usize) -> Option { + let mut chars = str[start..max].chars(); + let mut pos = start + 1; + let mut lines = 0; + + let marker = match chars.next() { + Some('"') => '"', + Some('\'') => '\'', + Some('(') => ')', + None | Some(_) => return None, + }; + + loop { + match chars.next() { + Some(ch) if ch == marker => { + return Some(ParseLinkFragmentResult { + pos: pos + 1, + lines, + str: unescape_all(&str[start + 1..pos]).into_owned(), + }); + } + Some('(') if marker == ')' => { + return None; + } + Some('\n') => { + pos += 1; + lines += 1; + } + Some('\\') => { + match chars.next() { + None => return None, + Some(x) => pos += 1 + x.len_utf8(), + } + } + Some(x) => { + pos += x.len_utf8(); + } + None => { + return None; + } + } + } +} + +struct ParseLinkResult { + pub label_start: usize, + pub label_end: usize, + pub href: Option, + pub title: Option, + pub end: usize, +} + +// Parses [link]( "stuff") +// +// this function assumes that first character ("[") already matches +// +fn parse_link(state: &mut InlineState, pos: usize, enable_nested: bool) -> Option { + let label_end = parse_link_label(state, pos, enable_nested)?; + let label_start = pos + 1; + let mut pos = label_end + 1; + let mut chars = state.src[pos..state.pos_max].chars(); + let mut href = None; + let mut title = None; + + if let Some('(') = chars.next() { + // + // Inline link + // + + // [link]( "title" ) + // ^^ skipping these spaces + pos += 1; + while let Some(' ' | '\t' | '\n') = chars.next() { + pos += 1; + } + + // [link]( "title" ) + // ^^^^^^ parsing link destination + if let Some(res) = parse_link_destination(&state.src, pos, state.pos_max) { + let href_candidate = state.md.link_formatter.normalize_link(&res.str); + if state.md.link_formatter.validate_link(&href_candidate).is_some() { + pos = res.pos; + href = Some(href_candidate); + } + + // [link]( "title" ) + // ^^ skipping these spaces + let mut chars = state.src[pos..state.pos_max].chars(); + while let Some(' ' | '\t' | '\n') = chars.next() { + pos += 1; + } + + if let Some(res) = parse_link_title(&state.src, pos, state.pos_max) { + title = Some(res.str); + pos = res.pos; + + // [link]( "title" ) + // ^^ skipping these spaces + let mut chars = state.src[pos..state.pos_max].chars(); + while let Some(' ' | '\t' | '\n') = chars.next() { + pos += 1; + } + } + } + + if let Some(')') = state.src[pos..state.pos_max].chars().next() { + return Some(ParseLinkResult { + label_start, + label_end, + href, + title, + end: pos + 1, + }) + } + } + + // + // Link reference + // + // TODO: check if I have any references? + pos = label_end + 1; + let mut maybe_label = None; + + match state.src[pos..state.pos_max].chars().next() { + Some('[') => { + if let Some(x) = parse_link_label(state, pos, false) { + maybe_label = Some(&state.src[pos + 1..x]); + pos = x + 1; + } else { + pos = label_end + 1; + } + } + _ => pos = label_end + 1, + } + + let references = state.root_ext.get::()?; + + // covers label === '' and label === undefined + // (collapsed reference link and shortcut reference link respectively) + let label = if matches!(maybe_label, None | Some("")) { + &state.src[label_start..label_end] + } else { + maybe_label.unwrap() + }; + + let (destination, title) = references.get(label)?; + + Some(ParseLinkResult { + label_start, + label_end, + href: Some(destination.to_owned()), + title: title.map(|s| s.to_owned()), + end: pos, + }) +} diff --git a/crates/markdown-it/src/generics/inline/mod.rs b/crates/markdown-it/src/generics/inline/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..2b45b08102efd82a1b71f1a1060e7dc145742961 --- /dev/null +++ b/crates/markdown-it/src/generics/inline/mod.rs @@ -0,0 +1,4 @@ +//! Generic inline-level structures. +pub mod code_pair; +pub mod emph_pair; +pub mod full_link; diff --git a/crates/markdown-it/src/generics/mod.rs b/crates/markdown-it/src/generics/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..22dadb8ec0c853fda30d520de9d75c553fc39e02 --- /dev/null +++ b/crates/markdown-it/src/generics/mod.rs @@ -0,0 +1,12 @@ +//! Use these to build your own markdown syntax. +//! +//! Some markdown structures are very similar under the hood, for example: +//! - `*emphasis*`, `^supertext^` and `~~strikethrough~~` +//! - `[link]()` and `![image]()` +//! +//! In order to reuse the code between all those, a notion of generic +//! markdown structures was created. If you want to use syntax like +//! `=this=` or `++that++`, you only need to specify a character marker +//! and a renderer function, these rules will figure out the rest. +//! +pub mod inline; diff --git a/crates/markdown-it/src/lib.rs b/crates/markdown-it/src/lib.rs new file mode 100644 index 0000000000000000000000000000000000000000..710a91daf8b8e9467c80340442ea1391c6702398 --- /dev/null +++ b/crates/markdown-it/src/lib.rs @@ -0,0 +1,29 @@ +// for bragging rights +#![forbid(unsafe_code)] +// +// useful asserts that's off by default +#![warn(clippy::manual_assert)] +#![warn(clippy::semicolon_if_nothing_returned)] +// +// these are often intentionally not collapsed for readability +#![allow(clippy::collapsible_else_if)] +#![allow(clippy::collapsible_if)] +#![allow(clippy::collapsible_match)] +// +// these are intentional in bevy systems: nobody is directly calling those, +// so extra arguments don't decrease readability +#![allow(clippy::too_many_arguments)] +#![allow(clippy::type_complexity)] +// +// just a style choice that clippy has no business complaining about +#![allow(clippy::uninlined_format_args)] + +pub mod common; +pub mod examples; +pub mod generics; +pub mod parser; +pub mod plugins; + +pub use parser::main::MarkdownIt; +pub use parser::node::{Node, NodeValue}; +pub use parser::renderer::Renderer; diff --git a/crates/markdown-it/src/parser/block/builtin/block_parser.rs b/crates/markdown-it/src/parser/block/builtin/block_parser.rs new file mode 100644 index 0000000000000000000000000000000000000000..368a7780696a8bdd3c1a7e2d3f63056342af4771 --- /dev/null +++ b/crates/markdown-it/src/parser/block/builtin/block_parser.rs @@ -0,0 +1,23 @@ +use crate::parser::core::{CoreRule, Root}; +use crate::{MarkdownIt, Node}; + +pub fn add(md: &mut MarkdownIt) { + md.add_rule::() + .before_all(); +} + +pub struct BlockParserRule; +impl CoreRule for BlockParserRule { + fn run(root: &mut Node, md: &MarkdownIt) { + let mut node = std::mem::take(root); + let data = node.cast_mut::().unwrap(); + let source = std::mem::take(&mut data.content); + let mut ext = std::mem::take(&mut data.ext); + + node = md.block.parse(source.as_str(), node, md, &mut ext); + let data = node.cast_mut::().unwrap(); + data.content = source; + data.ext = ext; + *root = node; + } +} diff --git a/crates/markdown-it/src/parser/block/builtin/mod.rs b/crates/markdown-it/src/parser/block/builtin/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..0355ca95e1741de9ee0114d58d41bd8871376213 --- /dev/null +++ b/crates/markdown-it/src/parser/block/builtin/mod.rs @@ -0,0 +1,9 @@ +use crate::MarkdownIt; + +pub(super) mod block_parser; + +pub use block_parser::BlockParserRule; + +pub fn add(md: &mut MarkdownIt) { + block_parser::add(md); +} diff --git a/crates/markdown-it/src/parser/block/mod.rs b/crates/markdown-it/src/parser/block/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..6132435bfe8ee9f6cca1c56e529b2bbfaed9df9c --- /dev/null +++ b/crates/markdown-it/src/parser/block/mod.rs @@ -0,0 +1,127 @@ +//! Block rule chain +mod state; +pub use state::*; + +mod rule; +pub use rule::*; + +#[doc(hidden)] +pub mod builtin; + +use crate::common::ruler::Ruler; +use crate::common::TypeKey; +use crate::parser::extset::RootExtSet; +use crate::parser::inline::InlineRoot; +use crate::parser::node::NodeEmpty; +use crate::{MarkdownIt, Node}; + +type RuleFns = ( + fn (&mut BlockState) -> Option<()>, + fn (&mut BlockState) -> Option<(Node, usize)>, +); + +#[derive(Debug, Default)] +/// Block-level tokenizer. +pub struct BlockParser { + ruler: Ruler, +} + +impl BlockParser { + pub fn new() -> Self { + Self::default() + } + + /// Generate tokens for input range + /// + pub fn tokenize(&self, state: &mut BlockState) { + stacker::maybe_grow(64*1024, 1024*1024, || { + let mut has_empty_lines = false; + + while state.line < state.line_max { + state.line = state.skip_empty_lines(state.line); + if state.line >= state.line_max { break; } + + // Termination condition for nested calls. + // Nested calls currently used for blockquotes & lists + if state.line_indent(state.line) < 0 { break; } + + // If nesting level exceeded - skip tail to the end. That's not ordinary + // situation and we should not care about content. + if state.level >= state.md.max_nesting { + state.line = state.line_max; + break; + } + + // Try all possible rules. + // On success, rule should: + // + // - update `state.line` + // - update `state.tokens` + // - return true + let mut ok = None; + + for rule in self.ruler.iter() { + ok = rule.1(state); + if ok.is_some() { + break; + } + } + + if let Some((mut node, len)) = ok { + state.line += len; + if !node.is::() { + node.srcmap = state.get_map(state.line - len, state.line - 1); + state.node.children.push(node); + } + } else { + // this can only happen if user disables paragraph rule + // push text as is, this behavior can change in the future; + // users should always have some kind of default block rule + let mut content = state.get_line(state.line).to_owned(); + content.push('\n'); + let node = Node::new(InlineRoot::new( + content, + vec![(0, state.line_offsets[state.line].first_nonspace)], + )); + state.node.children.push(node); + state.line += 1; + } + + // set state.tight if we had an empty line before current tag + // i.e. latest empty line should not count + state.tight = !has_empty_lines; + + // paragraph might "eat" one newline after it in nested lists + if state.is_empty(state.line - 1) { + has_empty_lines = true; + } + + if state.line < state.line_max && state.is_empty(state.line) { + has_empty_lines = true; + state.line += 1; + } + } + }); + } + + /// Process input string and push block tokens into `out_tokens` + /// + pub fn parse(&self, src: &str, node: Node, md: &MarkdownIt, root_ext: &mut RootExtSet) -> Node { + let mut state = BlockState::new(src, md, root_ext, node); + self.tokenize(&mut state); + state.node + } + + pub fn add_rule(&mut self) -> RuleBuilder { + let item = self.ruler.add(TypeKey::of::(), (T::check, T::run)); + RuleBuilder::new(item) + } + + pub fn has_rule(&mut self) -> bool { + self.ruler.contains(TypeKey::of::()) + } + + pub fn remove_rule(&mut self) { + self.ruler.remove(TypeKey::of::()); + } +} diff --git a/crates/markdown-it/src/parser/block/rule.rs b/crates/markdown-it/src/parser/block/rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..ceae5f69643318231400dbf98978aa3eaf3c2101 --- /dev/null +++ b/crates/markdown-it/src/parser/block/rule.rs @@ -0,0 +1,13 @@ +use crate::parser::core::rule_builder; +use crate::Node; + +/// Each member of block rule chain must implement this trait +pub trait BlockRule : 'static { + fn check(state: &mut super::BlockState) -> Option<()> { + Self::run(state).map(|_| ()) + } + + fn run(state: &mut super::BlockState) -> Option<(Node, usize)>; +} + +rule_builder!(BlockRule); diff --git a/crates/markdown-it/src/parser/block/state.rs b/crates/markdown-it/src/parser/block/state.rs new file mode 100644 index 0000000000000000000000000000000000000000..c8d67aa249223be2c13d6d65839a9ff93c21c31b --- /dev/null +++ b/crates/markdown-it/src/parser/block/state.rs @@ -0,0 +1,272 @@ +// Parser state class +// +use crate::common::sourcemap::SourcePos; +use crate::common::utils::calc_right_whitespace_with_tabstops; +use crate::parser::extset::RootExtSet; +use crate::{MarkdownIt, Node}; + +#[derive(Debug)] +#[readonly::make] +/// Sandbox object containing data required to parse block structures. +pub struct BlockState<'a, 'b> where 'b: 'a { + /// Markdown source. + #[readonly] + pub src: &'b str, + + /// Link to parser instance. + #[readonly] + pub md: &'a MarkdownIt, + + pub root_ext: &'b mut RootExtSet, + + /// Current node, your rule is supposed to add children to it. + pub node: Node, + + pub line_offsets: Vec, + + /// Current block content indent (for example, if we are + /// inside a list, it would be positioned after list marker). + pub blk_indent: usize, + + /// Current line in src. + pub line: usize, + + /// Maximum allowed line in src. + pub line_max: usize, + + /// True if there are no empty lines between paragraphs, used to + /// toggle loose/tight mode for lists. + pub tight: bool, + + /// indent of the current list block. + pub list_indent: Option, + + pub level: u32, +} + +/// Holds start/end/etc. positions for a specific source text line. +#[derive(Debug, Clone)] +pub struct LineOffset { + /// `line_start` is the actual start of the line. + /// + /// # const IGNORE : &str = stringify! { + /// " > blockquote\r\n" + /// ^-- it will always point here (must not be modified by rules) + /// # }; + pub line_start: usize, + + /// `line_end` is first newline character after the line, + /// or position after string length if there aren't any newlines left. + /// + /// # const IGNORE : &str = stringify! { + /// " > blockquote\r\n" + /// ^-- it will point here + /// # }; + pub line_end: usize, + + /// `first_nonspace` is the byte offset of the first non-space character in + /// the current line. + /// + /// # const IGNORE : &str = stringify! { + /// " > blockquote\r\n" + /// ^-- it will point here when paragraph is parsed + /// ^----- it is initially pointed here + /// # }; + /// + /// It will be modified by rules (list and blockquote), chars before it + /// must be treated as whitespaces. + /// + pub first_nonspace: usize, + + /// `indent_nonspace` is the indent (amount of virtual spaces from start) + /// of first non-space character in the current line, taking into account + /// tab expansion. + /// + /// For example, in case of ` \t foo`, indent is 5 (tab ends at multiple of 4, + /// then one space after it). Only tabs and spaces are counted for it, + /// so no funny unicode business (if cmark supported unicode spaces, they'd + /// be counted as 1 each regardless of utf8 width). + /// + /// You should compare `indent_nonspace` with `state.blkindent` when determining + /// real indent after taking into account lists. + /// + /// Most block rules in commonmark are indented 0..=3, and >=4 is code block. + /// Special value of ident_nonspace=-1 is used by this library as a sign + /// that this rule can only be a paragraph continuation (used in blockquotes), + /// so you must take into account that any math can end up negative. + /// + pub indent_nonspace: i32, +} + +impl<'a, 'b> BlockState<'a, 'b> { + pub fn new(src: &'b str, md: &'a MarkdownIt, root_ext: &'b mut RootExtSet, node: Node) -> Self { + let mut result = Self { + src, + md, + root_ext, + node, + line_offsets: Vec::new(), + blk_indent: 0, + line: 0, + line_max: 0, + tight: false, + list_indent: None, + level: 0, + }; + + result.generate_caches(); + result + } + + fn generate_caches(&mut self) { + // Create caches + // Generate markers. + let mut chars = self.src.chars().peekable(); + let mut indent_found = false; + let mut indent = 0; + let mut offset = 0; + let mut start = 0; + let mut pos = 0; + + loop { + match chars.next() { + Some(ch @ (' ' | '\t')) if !indent_found => { + indent += 1; + offset += if ch == '\t' { 4 - offset % 4 } else { 1 }; + pos += 1; + } + ch @ (Some('\n' | '\r') | None) => { + self.line_offsets.push(LineOffset { + line_start: start, + line_end: pos, + first_nonspace: start + indent, + indent_nonspace: offset, + }); + + if ch == Some('\r') && chars.peek() == Some(&'\n') { + // treat CR+LF as one linebreak + chars.next(); + pos += 1; + } + + indent_found = false; + indent = 0; + offset = 0; + start = pos + 1; + pos += 1; + + if ch.is_none() || chars.peek().is_none() { + break; + } + } + Some(ch) => { + indent_found = true; + pos += ch.len_utf8(); + } + } + } + + self.line_max = self.line_offsets.len(); + } + + #[must_use] + pub fn test_rules_at_line(&mut self) -> bool { + for rule in self.md.block.ruler.iter() { + if rule.0(self).is_some() { + return true; + } + } + false + } + + #[must_use] + #[inline] + pub fn is_empty(&self, line: usize) -> bool { + if let Some(offsets) = self.line_offsets.get(line) { + offsets.first_nonspace >= offsets.line_end + } else { + false + } + } + + pub fn skip_empty_lines(&self, from: usize) -> usize { + let mut line = from; + while line != self.line_max && self.is_empty(line) { + line += 1; + } + line + } + + /// return line indent of specific line, taking into account blockquotes and lists; + /// it may be negative if a text has less indentation than current list item + #[must_use] + #[inline] + pub fn line_indent(&self, line: usize) -> i32 { + if line < self.line_max { + self.line_offsets[line].indent_nonspace - self.blk_indent as i32 + } else { + 0 + } + } + + /// return a single line, trimming initial spaces + #[must_use] + #[inline] + pub fn get_line(&self, line: usize) -> &str { + if line < self.line_max { + let pos = self.line_offsets[line].first_nonspace; + let max = self.line_offsets[line].line_end; + &self.src[pos..max] + } else { + "" + } + } + + /// Cut a range of lines begin..end (not including end) from the source without preceding indent. + /// Returns a string (lines) plus a mapping (start of each line in result -> start of each line in source). + pub fn get_lines(&self, begin: usize, end: usize, indent: usize, keep_last_lf: bool) -> (String, Vec<(usize, usize)>) { + debug_assert!(begin <= end); + + let mut line = begin; + let mut result = String::new(); + let mut mapping = Vec::new(); + + while line < end { + let offsets = &self.line_offsets[line]; + let last = offsets.line_end; + let add_last_lf = line + 1 < end || keep_last_lf; + + let (num_spaces, first) = calc_right_whitespace_with_tabstops( + &self.src[offsets.line_start..offsets.first_nonspace], + offsets.indent_nonspace - indent as i32 + ); + + mapping.push(( result.len(), offsets.line_start+first )); + result += &" ".repeat(num_spaces); + result += &self.src[offsets.line_start+first..last]; + if add_last_lf { result.push('\n'); } + line += 1; + } + + ( result, mapping ) + } + + #[must_use] + #[inline] + pub fn get_map(&self, start_line: usize, end_line: usize) -> Option { + debug_assert!(start_line <= end_line); + + Some(SourcePos::new( + self.line_offsets[start_line].first_nonspace, + self.line_offsets[end_line].line_end + )) + } + + #[must_use] + #[inline] + pub fn get_map_from_offsets(&self, start_pos: usize, end_pos: usize) -> Option { + debug_assert!(start_pos <= end_pos); + + Some(SourcePos::new(start_pos, end_pos)) + } +} diff --git a/crates/markdown-it/src/parser/core/mod.rs b/crates/markdown-it/src/parser/core/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..010aa0062045fae75d9fb6b7e4204ffcca5f4f61 --- /dev/null +++ b/crates/markdown-it/src/parser/core/mod.rs @@ -0,0 +1,6 @@ +//! Core rule chain +mod rule; +pub use rule::*; + +mod root; +pub use root::*; diff --git a/crates/markdown-it/src/parser/core/root.rs b/crates/markdown-it/src/parser/core/root.rs new file mode 100644 index 0000000000000000000000000000000000000000..22e12c0e9ec0f25f0970b2508235207044290a4d --- /dev/null +++ b/crates/markdown-it/src/parser/core/root.rs @@ -0,0 +1,21 @@ +use crate::parser::extset::RootExtSet; +use crate::{Node, NodeValue, Renderer}; + +#[derive(Debug)] +/// Root node of the AST. +pub struct Root { + pub content: String, + pub ext: RootExtSet, +} + +impl Root { + pub fn new(content: String) -> Self { + Self { content, ext: RootExtSet::new() } + } +} + +impl NodeValue for Root { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.contents(&node.children); + } +} diff --git a/crates/markdown-it/src/parser/core/rule.rs b/crates/markdown-it/src/parser/core/rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..73471d48cfa0e1c882ce915887030851900401d7 --- /dev/null +++ b/crates/markdown-it/src/parser/core/rule.rs @@ -0,0 +1,55 @@ +use crate::{MarkdownIt, Node}; + +/// Each member of core rule chain must implement this trait +pub trait CoreRule : 'static { + fn run(root: &mut Node, md: &MarkdownIt); +} + +macro_rules! rule_builder { + ($var: ident) => { + /// Adjust positioning of a newly added rule in the chain. + pub struct RuleBuilder<'a, T> { + item: &'a mut crate::common::ruler::RuleItem + } + + impl<'a, T> RuleBuilder<'a, T> { + pub(crate) fn new(item: &'a mut crate::common::ruler::RuleItem) -> Self { + Self { item } + } + + pub fn before(self) -> Self { + self.item.before(crate::common::TypeKey::of::()); + self + } + + pub fn after(self) -> Self { + self.item.after(crate::common::TypeKey::of::()); + self + } + + pub fn before_all(self) -> Self { + self.item.before_all(); + self + } + + pub fn after_all(self) -> Self { + self.item.after_all(); + self + } + + pub fn alias(self) -> Self { + self.item.alias(crate::common::TypeKey::of::()); + self + } + + pub fn require(self) -> Self { + self.item.require(crate::common::TypeKey::of::()); + self + } + } + }; +} + +rule_builder!(CoreRule); + +pub(crate) use rule_builder; diff --git a/crates/markdown-it/src/parser/extset.rs b/crates/markdown-it/src/parser/extset.rs new file mode 100644 index 0000000000000000000000000000000000000000..07532f73f7aa4e2643dfa7dcf1eb984618bf3fc1 --- /dev/null +++ b/crates/markdown-it/src/parser/extset.rs @@ -0,0 +1,232 @@ +//! Extension sets +//! +//! These things allow you to put custom data inside internal markdown-it structures. +//! +use downcast_rs::{impl_downcast, Downcast}; +use std::fmt::Debug; + +/// Extension set member for the entire parser (only writable at init). +pub trait MarkdownItExt : Debug + Downcast + Send + Sync {} +impl_downcast!(MarkdownItExt); +extension_set!(MarkdownItExtSet, MarkdownItExt); + +/// Extension set member for an arbitrary AST node. +pub trait NodeExt : Debug + Downcast + Send + Sync {} +impl_downcast!(NodeExt); +extension_set!(NodeExtSet, NodeExt); + +/// Extension set member for an inline context. +pub trait InlineRootExt : Debug + Downcast + Send + Sync {} +impl_downcast!(InlineRootExt); +extension_set!(InlineRootExtSet, InlineRootExt); + +/// Extension set member for a block context. +pub trait RootExt : Debug + Downcast + Send + Sync {} +impl_downcast!(RootExt); +extension_set!(RootExtSet, RootExt); + +/// Extension set member for a renderer context. +pub trait RenderExt : Debug + Downcast + Send + Sync {} +impl_downcast!(RenderExt); +extension_set!(RenderExtSet, RenderExt); + +// see https://github.com/malobre/erased_set for inspiration and API +// see https://lucumr.pocoo.org/2022/1/7/as-any-hack/ for additional impl details +macro_rules! extension_set { + ($name: ident, $trait: ident) => { + #[derive(Debug, Default)] + pub struct $name(::std::collections::HashMap>); + + impl $name { + #[must_use] + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + #[must_use] + pub fn len(&self) -> usize { + self.0.len() + } + + pub fn clear(&mut self) { + self.0.clear(); + } + + #[must_use] + pub fn contains(&self) -> bool { + let key = crate::common::TypeKey::of::(); + self.0.contains_key(&key) + } + + #[must_use] + pub fn get(&self) -> Option<&T> { + let key = crate::common::TypeKey::of::(); + let result = self.0.get(&key)?; + result.downcast_ref::() + } + + #[must_use] + pub fn get_mut(&mut self) -> Option<&mut T> { + let key = crate::common::TypeKey::of::(); + let result = self.0.get_mut(&key)?; + result.downcast_mut::() + } + + pub fn get_or_insert(&mut self, value: T) -> &mut T { + let key = crate::common::TypeKey::of::(); + let result = self.0.entry(key).or_insert_with(|| Box::new(value)); + result.downcast_mut::().unwrap() + } + + pub fn get_or_insert_with(&mut self, f: impl FnOnce() -> T) -> &mut T { + let key = crate::common::TypeKey::of::(); + let result = self.0.entry(key).or_insert_with(|| Box::new(f())); + result.downcast_mut::().unwrap() + } + + pub fn get_or_insert_default(&mut self) -> &mut T { + let key = crate::common::TypeKey::of::(); + let result = self.0.entry(key).or_insert_with(|| Box::::default()); + result.downcast_mut::().unwrap() + } + + pub fn insert(&mut self, value: T) -> Option { + let key = crate::common::TypeKey::of::(); + let result = self.0.insert(key, Box::new(value))?; + Some(*result.downcast::().unwrap()) + } + + pub fn remove(&mut self) -> Option { + let key = crate::common::TypeKey::of::(); + let result = self.0.remove(&key)?; + Some(*result.downcast::().unwrap()) + } + } + } +} + +pub(crate) use extension_set; + +#[cfg(test)] +mod tests { + use super::extension_set; + use downcast_rs::{Downcast, impl_downcast}; + use std::fmt::Debug; + + pub trait TestExt : Debug + Downcast + Send + Sync {} + impl_downcast!(TestExt); + + extension_set!(TestExtSet, TestExt); + + impl TestExt for T {} + + #[test] + fn empty_set() { + let set = TestExtSet::new(); + assert_eq!(set.len(), 0); + assert!(set.is_empty()); + } + + #[test] + fn insert_elements() { + let mut set = TestExtSet::new(); + set.insert(42u8); + assert_eq!(set.len(), 1); + assert!(!set.is_empty()); + set.insert(42u16); + assert_eq!(set.len(), 2); + assert!(!set.is_empty()); + } + + #[test] + fn contains() { + let mut set = TestExtSet::new(); + set.insert(42u8); + assert!(!set.contains::()); + set.insert(42u16); + assert!(set.contains::()); + set.remove::(); + assert!(!set.contains::()); + } + + #[test] + fn get() { + let mut set = TestExtSet::new(); + set.insert(42u8); + assert_eq!(set.get::(), None); + set.insert(42u16); + set.insert(123u16); + assert_eq!(set.get::(), Some(&123u16)); + } + + #[test] + fn get_mut() { + let mut set = TestExtSet::new(); + set.insert(42u16); + *set.get_mut::().unwrap() = 123u16; + assert_eq!(set.get::(), Some(&123u16)); + } + + #[test] + fn or_insert() { + let mut set = TestExtSet::new(); + set.insert(123u8); + assert_eq!(set.get_or_insert(0u8), &mut 123u8); + assert_eq!(set.get_or_insert_default::(), &mut 123u8); + assert_eq!(set.get_or_insert_with(|| 0u8), &mut 123u8); + set.clear(); + assert_eq!(set.get_or_insert(10u8), &mut 10u8); + set.clear(); + assert_eq!(set.get_or_insert_with(|| 20u8), &mut 20u8); + set.clear(); + assert_eq!(set.get_or_insert_default::(), &mut 0u8); + } + + #[test] + fn different_types_stored_once() { + let mut set = TestExtSet::new(); + set.insert("foo"); + set.insert("bar"); + set.insert("quux"); + assert_eq!(set.len(), 1); + } + + #[test] + fn zero_sized_types() { + #[derive(Debug, PartialEq, Eq)] + struct A; + #[derive(Debug, PartialEq, Eq)] + struct B; + let mut set = TestExtSet::new(); + set.insert(A); + set.insert(B); + assert_eq!(set.len(), 2); + assert_eq!(set.get::(), Some(&A)); + } + + #[test] + fn clear() { + let mut set = TestExtSet::new(); + set.insert(42u8); + set.insert(42u16); + assert_eq!(set.len(), 2); + set.clear(); + assert_eq!(set.len(), 0); + } + + #[test] + fn debug() { + let mut set = TestExtSet::new(); + set.insert(42); + set.insert("test"); + let str = format!("{:?}", set); + // there are no guarantees about field order, so check both + assert!(str == "TestExtSet({i32: 42, &str: \"test\"})" || + str == "TestExtSet({&str: \"test\", i32: 42})"); + } +} diff --git a/crates/markdown-it/src/parser/inline/builtin/inline_parser.rs b/crates/markdown-it/src/parser/inline/builtin/inline_parser.rs new file mode 100644 index 0000000000000000000000000000000000000000..39f9af8b04155dec3e16f2d375de35ed638b80af --- /dev/null +++ b/crates/markdown-it/src/parser/inline/builtin/inline_parser.rs @@ -0,0 +1,80 @@ +use crate::parser::block::builtin::BlockParserRule; +use crate::parser::core::{CoreRule, Root}; +use crate::parser::extset::{InlineRootExtSet, RootExtSet}; +use crate::{MarkdownIt, Node, NodeValue}; + +#[derive(Debug)] +/// Temporary node which gets replaced with inline nodes when +/// [InlineParser](crate::parser::inline::InlineParser) is called. +pub struct InlineRoot { + pub content: String, + pub mapping: Vec<(usize, usize)>, + pub ext: InlineRootExtSet, +} + +impl InlineRoot { + pub fn new(content: String, mapping: Vec<(usize, usize)>) -> Self { + Self { content, mapping, ext: InlineRootExtSet::new() } + } +} + +// this token is supposed to be replaced by one or many actual tokens by inline rule +impl NodeValue for InlineRoot {} + +pub fn add(md: &mut MarkdownIt) { + md.add_rule::() + .after::() + .before_all(); +} + +pub struct InlineParserRule; +impl CoreRule for InlineParserRule { + fn run(root: &mut Node, md: &MarkdownIt) { + fn walk_recursive(node: &mut Node, md: &MarkdownIt, root_ext: &mut RootExtSet) { + let mut idx = 0; + while idx < node.children.len() { + let child = &mut node.children[idx]; + if let Some(data) = child.cast_mut::() { + let content = std::mem::take(&mut data.content); + let mapping = std::mem::take(&mut data.mapping); + let mut inline_ext = std::mem::take(&mut data.ext); + + let mut root = std::mem::take(child); + root.ext = std::mem::take(&mut node.ext); + root.children = Vec::new(); + root = md.inline.parse(content, mapping, root, md, root_ext, &mut inline_ext); + + let len = root.children.len(); + node.children.splice(idx..=idx, std::mem::take(&mut root.children)); + node.ext = std::mem::take(&mut root.ext); + idx += len; + } else { + stacker::maybe_grow(64*1024, 1024*1024, || { + walk_recursive(child, md, root_ext); + }); + idx += 1; + } + } + } + + let data = root.cast_mut::().unwrap(); + let mut root_ext = std::mem::take(&mut data.ext); + + // this is invalid if input only contains reference; + // so if user disables block parser, he must insert smth like this instead + /*if root.children.is_empty() { + // block parser disabled, parse as if input was one big inline block + let data = root.cast_mut::().unwrap(); + let node = Node::new(InlineRoot { + content: data.content.clone(), + mapping: vec![(0, 0)], + }); + root.children.push(node); + }*/ + + walk_recursive(root, md, &mut root_ext); + + let data = root.cast_mut::().unwrap(); + data.ext = root_ext; + } +} diff --git a/crates/markdown-it/src/parser/inline/builtin/mod.rs b/crates/markdown-it/src/parser/inline/builtin/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..8e96c7fac79462079828a0185e8c95ebb21c6bc6 --- /dev/null +++ b/crates/markdown-it/src/parser/inline/builtin/mod.rs @@ -0,0 +1,12 @@ +use crate::MarkdownIt; + +pub(super) mod inline_parser; +pub(super) mod skip_text; + +pub use inline_parser::InlineParserRule; +pub use skip_text::TextScanner; + +pub fn add(md: &mut MarkdownIt) { + skip_text::add(md); + inline_parser::add(md); +} diff --git a/crates/markdown-it/src/parser/inline/builtin/skip_text.rs b/crates/markdown-it/src/parser/inline/builtin/skip_text.rs new file mode 100644 index 0000000000000000000000000000000000000000..81f9f6b32927cbca1f29a8391e745b661d50b45a --- /dev/null +++ b/crates/markdown-it/src/parser/inline/builtin/skip_text.rs @@ -0,0 +1,140 @@ +//! Skip text characters for text token, place those to pending buffer +//! and increment current pos +//! +use regex::{self, Regex}; + +use crate::parser::inline::{InlineRule, InlineState}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +/// Plain text AST node. +pub struct Text { + pub content: String +} + +impl NodeValue for Text { + fn render(&self, _: &Node, fmt: &mut dyn Renderer) { + fmt.text(&self.content); + } +} + +#[derive(Debug)] +/// Escaped text AST node (backslash escapes and entities). +pub struct TextSpecial { + pub content: String, + pub markup: String, + pub info: &'static str, +} + +impl NodeValue for TextSpecial { + fn render(&self, _: &Node, fmt: &mut dyn Renderer) { + fmt.text(&self.content); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.inline.add_rule::() + .before_all(); +} + +#[derive(Debug)] +pub(crate) enum TextScannerImpl { + SkipPunct, + SkipRegex(Regex), +} + +/// Rule to skip pure text +/// '{}$%@~+=:' reserved for extensions +/// +/// !, ", #, $, %, &, ', (, ), *, +, ,, -, ., /, :, ;, <, =, >, ?, @, [, \, ], ^, _, `, {, |, }, or ~ +/// +/// !!!! Don't confuse with "Markdown ASCII Punctuation" chars +/// http://spec.commonmark.org/0.15/#ascii-punctuation-character +/// +pub struct TextScanner; + +impl TextScanner { + fn choose_text_impl(charmap: Vec) -> TextScannerImpl { + let mut can_use_punct = true; + for ch in charmap.iter() { + match ch { + '\n' | '!' | '#' | '$' | '%' | '&' | '*' | '+' | '-' | + ':' | '<' | '=' | '>' | '@' | '[' | '\\' | ']' | '^' | + '_' | '`' | '{' | '}' | '~' => {}, + _ => { + can_use_punct = false; + break; + } + } + } + + if can_use_punct { + TextScannerImpl::SkipPunct + } else { + TextScannerImpl::SkipRegex( + Regex::new( + // [] panics on "unclosed character class", but it cannot happen here + // (we'd use punct rule instead) + &format!("^[^{}]+", charmap.into_iter().map( + |c| regex::escape(&c.to_string()) + ).collect::()) + ).unwrap() + ) + } + } + + fn find_text_length(state: &mut InlineState) -> usize { + let text_impl = state.md.inline.text_impl.get_or_init( + || Self::choose_text_impl(state.md.inline.text_charmap.keys().copied().collect()) + ); + + let mut len = 0; + + match text_impl { + TextScannerImpl::SkipPunct => { + let mut chars = state.src[state.pos..state.pos_max].chars(); + + loop { + match chars.next() { + Some( + '\n' | '!' | '#' | '$' | '%' | '&' | '*' | '+' | '-' | + ':' | '<' | '=' | '>' | '@' | '[' | '\\' | ']' | '^' | + '_' | '`' | '{' | '}' | '~' + ) => { + break; + } + Some(chr) => { + len += chr.len_utf8(); + } + None => { break; } + } + } + } + TextScannerImpl::SkipRegex(re) => { + if let Some(capture) = re.find(&state.src[state.pos..state.pos_max]) { + len = capture.end(); + } + } + } + + len + } +} + +impl InlineRule for TextScanner { + const MARKER: char = '\0'; + + fn check(state: &mut InlineState) -> Option { + let len = Self::find_text_length(state); + if len == 0 { return None; } + Some(len) + } + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let len = Self::find_text_length(state); + if len == 0 { return None; } + state.trailing_text_push(state.pos, state.pos + len); + state.pos += len; + Some((Node::default(), 0)) + } +} diff --git a/crates/markdown-it/src/parser/inline/mod.rs b/crates/markdown-it/src/parser/inline/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..90cef66524e9dc95fe7564515feb6b75fccac82c --- /dev/null +++ b/crates/markdown-it/src/parser/inline/mod.rs @@ -0,0 +1,162 @@ +//! Inline rule chain +use once_cell::sync::OnceCell; +use std::collections::HashMap; + +mod state; +pub use state::*; + +mod rule; +pub use rule::*; + +#[doc(hidden)] +pub mod builtin; + +pub use builtin::inline_parser::InlineRoot; +pub use builtin::skip_text::{Text, TextSpecial}; +use builtin::skip_text::TextScannerImpl; + +use crate::{MarkdownIt, Node}; +use crate::common::TypeKey; +use crate::common::ruler::Ruler; +use crate::parser::extset::{InlineRootExtSet, RootExtSet}; + +use super::node::NodeEmpty; + +type RuleFns = ( + fn (&mut InlineState) -> Option, + fn (&mut InlineState) -> Option<(Node, usize)>, +); + +#[derive(Debug, Default)] +/// Inline-level tokenizer. +pub struct InlineParser { + ruler: Ruler, + text_charmap: HashMap>, + text_impl: OnceCell, +} + +impl InlineParser { + pub fn new() -> Self { + Self::default() + } + + /// Skip single token by running all rules in validation mode; + /// returns `true` if any rule reported success + /// + pub fn skip_token(&self, state: &mut InlineState) { + stacker::maybe_grow(64*1024, 1024*1024, || { + let mut ok = None; + + if state.level < state.md.max_nesting { + for rule in self.ruler.iter() { + ok = rule.0(state); + if ok.is_some() { + break; + } + } + } else { + // Too much nesting, just skip until the end of the paragraph. + // + // NOTE: this will cause links to behave incorrectly in the following case, + // when an amount of `[` is exactly equal to `maxNesting + 1`: + // + // [[[[[[[[[[[[[[[[[[[[[foo]() + // + // TODO: remove this workaround when CM standard will allow nested links + // (we can replace it by preventing links from being parsed in + // validation mode) + // + state.pos = state.pos_max; + } + + if let Some(len) = ok { + state.pos += len; + } else { + let ch = state.src[state.pos..state.pos_max].chars().next().unwrap(); + state.pos += ch.len_utf8(); + } + }); + } + + /// Generate tokens for input range + /// + pub fn tokenize(&self, state: &mut InlineState) { + stacker::maybe_grow(64*1024, 1024*1024, || { + let end = state.pos_max; + + while state.pos < end { + // Try all possible rules. + // On success, rule should: + // + // - update `state.pos` + // - update `state.tokens` + // - return true + let mut ok = None; + + if state.level < state.md.max_nesting { + for rule in self.ruler.iter() { + ok = rule.1(state); + if ok.is_some() { + break; + } + } + } + + if let Some((mut node, len)) = ok { + state.pos += len; + if !node.is::() { + node.srcmap = state.get_map(state.pos - len, state.pos); + state.node.children.push(node); + if state.pos >= end { break; } + } + continue; + } + + let ch = state.src[state.pos..state.pos_max].chars().next().unwrap(); + let len = ch.len_utf8(); + state.trailing_text_push(state.pos, state.pos + len); + state.pos += len; + } + }); + } + + /// Process input string and push inline tokens into `out_tokens` + /// + pub fn parse( + &self, + src: String, + srcmap: Vec<(usize, usize)>, + node: Node, + md: &MarkdownIt, + root_ext: &mut RootExtSet, + inline_ext: &mut InlineRootExtSet, + ) -> Node { + let mut state = InlineState::new(src, srcmap, md, root_ext, inline_ext, node); + self.tokenize(&mut state); + state.node + } + + pub fn add_rule(&mut self) -> RuleBuilder { + if T::MARKER != '\0' { + let charvec = self.text_charmap.entry(T::MARKER).or_default(); + charvec.push(TypeKey::of::()); + } + + let item = self.ruler.add(TypeKey::of::(), (T::check, T::run)); + RuleBuilder::new(item) + } + + pub fn has_rule(&mut self) -> bool { + self.ruler.contains(TypeKey::of::()) + } + + pub fn remove_rule(&mut self) { + if T::MARKER != '\0' { + let mut charvec = self.text_charmap.remove(&T::MARKER).unwrap_or_default(); + charvec.retain(|x| *x != TypeKey::of::()); + self.text_charmap.insert(T::MARKER, charvec); + } + + self.ruler.remove(TypeKey::of::()); + } +} diff --git a/crates/markdown-it/src/parser/inline/rule.rs b/crates/markdown-it/src/parser/inline/rule.rs new file mode 100644 index 0000000000000000000000000000000000000000..a2e3843378af3cdf7459e204524639ee184788d5 --- /dev/null +++ b/crates/markdown-it/src/parser/inline/rule.rs @@ -0,0 +1,15 @@ +use crate::parser::core::rule_builder; +use crate::Node; + +/// Each member of inline rule chain must implement this trait +pub trait InlineRule : 'static { + const MARKER: char; + + fn check(state: &mut super::InlineState) -> Option { + Self::run(state).map(|(_node, len)| len) + } + + fn run(state: &mut super::InlineState) -> Option<(Node, usize)>; +} + +rule_builder!(InlineRule); diff --git a/crates/markdown-it/src/parser/inline/state.rs b/crates/markdown-it/src/parser/inline/state.rs new file mode 100644 index 0000000000000000000000000000000000000000..4b0b34d41bb9c4e8aad464f29d0287ae2de0e2d3 --- /dev/null +++ b/crates/markdown-it/src/parser/inline/state.rs @@ -0,0 +1,244 @@ +// Inline parser state +// +use crate::common::sourcemap::SourcePos; +use crate::common::utils::is_punct_char; +use crate::parser::extset::{InlineRootExtSet, RootExtSet}; +use crate::parser::inline::Text; +use crate::{MarkdownIt, Node}; + +#[derive(Debug, Clone, Copy)] +/// Information about emphasis delimiter run returned from [InlineState::scan_delims]. +pub struct DelimiterRun { + /// Starting marker character. + pub marker: char, + + /// Boolean flag that determines if this delimiter could open an emphasis. + pub can_open: bool, + + /// Boolean flag that determines if this delimiter could open an emphasis. + pub can_close: bool, + + /// Total length of scanned delimiters. + pub length: usize, +} + +#[derive(Debug)] +#[readonly::make] +/// Sandbox object containing data required to parse inline structures. +pub struct InlineState<'a, 'b> where 'b: 'a { + /// Markdown source. + #[readonly] + pub src: String, + + /// Link to parser instance. + #[readonly] + pub md: &'a MarkdownIt, + + /// Current node, your rule is supposed to add children to it. + pub node: Node, + + /// For each line, it holds offset of the start of the line in original + /// markdown source and offset of the start of the line in `src`. + pub srcmap: Vec<(usize, usize)>, + pub root_ext: &'b mut RootExtSet, + pub inline_ext: &'b mut InlineRootExtSet, + + /// Current byte offset in `src`, it must respect char boundaries. + pub pos: usize, + + /// Maximum allowed byte offset in `src`, it must respect char boundaries. + pub pos_max: usize, + + /// Counter used to disable inline linkifier execution + /// inside raw html and markdown links. + pub link_level: i32, + + /// Counter used to prevent recursion by image and link rules. + pub level: u32, +} + +impl<'a, 'b> InlineState<'a, 'b> { + pub fn new( + src: String, + srcmap: Vec<(usize, usize)>, + md: &'a MarkdownIt, + root_ext: &'b mut RootExtSet, + inline_ext: &'b mut InlineRootExtSet, + node: Node, + ) -> Self { + let mut result = Self { + pos: 0, + pos_max: src.len(), + src, + srcmap, + root_ext, + inline_ext, + md, + node, + link_level: 0, + level: 0, + }; + + result.trim_src(); + result + } + + fn trim_src(&mut self) { + let mut chars = self.src.as_bytes().iter(); + while let Some(b' ' | b'\t') = chars.next_back() { + self.pos_max -= 1; + } + while let Some(b' ' | b'\t') = chars.next() { + self.pos += 1; + } + } + + pub fn trailing_text_push(&mut self, start: usize, end: usize) { + if let Some(text) = self.node.children.last_mut() + .and_then(|t| t.cast_mut::()) { + text.content.push_str(&self.src[start..end]); + + if let Some(map) = self.node.children.last_mut().unwrap().srcmap { + let (map_start, _) = map.get_byte_offsets(); + let map_end = self.get_source_pos_for(end); + self.node.children.last_mut().unwrap().srcmap = Some(SourcePos::new(map_start, map_end)); + } + } else { + let mut node = Node::new(Text { content: self.src[start..end].to_owned() }); + node.srcmap = self.get_map(start, end); + self.node.children.push(node); + } + } + + pub fn trailing_text_pop(&mut self, count: usize) { + if count == 0 { return; } + + let mut node = self.node.children.pop().unwrap(); + let text = node.cast_mut::().unwrap(); + if text.content.len() == count { + // do nothing, just remove the node + drop(node); + } else { + // modify the token and reinsert it later + text.content.truncate(text.content.len() - count); + if let Some(map) = node.srcmap { + let (map_start, map_end) = map.get_byte_offsets(); + let map_end = self.get_source_pos_for(map_end - count); + node.srcmap = Some(SourcePos::new(map_start, map_end)); + } + self.node.children.push(node); + } + } + + #[must_use] + pub fn trailing_text_get(&self) -> &str { + if let Some(text) = self.node.children.last() + .and_then(|t| t.cast::()) { + text.content.as_str() + } else { + "" + } + } + + /// Scan a sequence of emphasis-like markers, and determine whether + /// it can start an emphasis sequence or end an emphasis sequence. + /// + /// - start - position to scan from (it should point at a valid marker); + /// - can_split_word - determine if these markers can be found inside a word + /// + #[must_use] + pub fn scan_delims(&self, start: usize, can_split_word: bool) -> DelimiterRun { + let mut left_flanking = true; + let mut right_flanking = true; + + let last_char = if start > 0 { + self.src[..start].chars().next_back().unwrap() + } else { + // treat beginning of the line as a whitespace + ' ' + }; + + let mut chars = self.src[start..self.pos_max].chars(); + let marker = chars.next().unwrap(); + let next_char; + let mut count = 1; + + loop { + match chars.next() { + None => { + next_char = ' '; + break; + } + Some(x) => { + if x != marker { + // treat end of the line as a whitespace + next_char = x; + break; + } + } + } + count += 1; + } + + let is_last_punct_char = last_char.is_ascii_punctuation() || is_punct_char(last_char); + let is_next_punct_char = next_char.is_ascii_punctuation() || is_punct_char(next_char); + + let is_last_whitespace = last_char.is_whitespace(); + let is_next_whitespace = next_char.is_whitespace(); + + #[allow(clippy::collapsible_if)] + if is_next_whitespace { + left_flanking = false; + } else if is_next_punct_char { + if !(is_last_whitespace || is_last_punct_char) { + left_flanking = false; + } + } + + #[allow(clippy::collapsible_if)] + if is_last_whitespace { + right_flanking = false; + } else if is_last_punct_char { + if !(is_next_whitespace || is_next_punct_char) { + right_flanking = false; + } + } + + let can_open; + let can_close; + + if !can_split_word { + can_open = left_flanking && (!right_flanking || is_last_punct_char); + can_close = right_flanking && (!left_flanking || is_next_punct_char); + } else { + can_open = left_flanking; + can_close = right_flanking; + } + + DelimiterRun { + marker, + can_open, + can_close, + length: count + } + } + + #[must_use] + fn get_source_pos_for(&self, pos: usize) -> usize { + let line = match self.srcmap.binary_search_by(|x| x.0.cmp(&pos)) { + Ok(x) => x, + Err(x) => x - 1, + }; + self.srcmap[line].1 + (pos - self.srcmap[line].0) + } + + #[must_use] + pub fn get_map(&self, start_pos: usize, end_pos: usize) -> Option { + debug_assert!(start_pos <= end_pos); + + Some(SourcePos::new( + self.get_source_pos_for(start_pos), + self.get_source_pos_for(end_pos) + )) + } +} diff --git a/crates/markdown-it/src/parser/linkfmt.rs b/crates/markdown-it/src/parser/linkfmt.rs new file mode 100644 index 0000000000000000000000000000000000000000..cb0782a93c6034d02eec564d90d571b3ffd77e0e --- /dev/null +++ b/crates/markdown-it/src/parser/linkfmt.rs @@ -0,0 +1,100 @@ +//! Link validator and formatter + +use once_cell::sync::Lazy; +use regex::Regex; +use std::fmt::Debug; + +pub trait LinkFormatter : Debug + Send + Sync { + /// Validate link url, return `Some(())` if it is allowed + /// and `None` if it is a security risk. + fn validate_link(&self, url: &str) -> Option<()>; + + /// Encode link url to a machine-readable format, + /// which includes url-encoding, punycode, etc. + fn normalize_link(&self, url: &str) -> String; + + /// Decode link url to a human-readable format. + fn normalize_link_text(&self, url: &str) -> String; +} + +/// Default link validator and formatter for markdown-it. +/// +/// This validator can prohibit more than really needed to prevent XSS. It's a +/// tradeoff to keep code simple and to be secure by default. +/// +/// If you need different setup - override validator method as you wish. Or +/// replace it with dummy function and use external sanitizer. +/// +#[derive(Default, Debug)] +pub struct MDLinkFormatter; + +impl MDLinkFormatter { + pub fn new() -> Self { + Self + } +} + +impl LinkFormatter for MDLinkFormatter { + fn validate_link(&self, url: &str) -> Option<()> { + // url should be normalized at this point, and existing entities are decoded + static BAD_PROTO_RE : Lazy = Lazy::new(|| + Regex::new(r#"(?i)^(vbscript|javascript|file|data):"#).unwrap() + ); + + static GOOD_DATA_RE : Lazy = Lazy::new(|| + Regex::new(r#"(?i)^data:image/(gif|png|jpeg|webp);"#).unwrap() + ); + + if !BAD_PROTO_RE.is_match(url) || GOOD_DATA_RE.is_match(url) { + Some(()) + } else { + None + } + } + + fn normalize_link(&self, url: &str) -> String { + mdurl::urlencode::encode(url, mdurl::urlencode::ENCODE_DEFAULT_CHARS, true).into() + } + + fn normalize_link_text(&self, url: &str) -> String { + url.to_owned() + } +} + + +#[cfg(test)] +mod tests { + use super::LinkFormatter; + use super::MDLinkFormatter; + + #[test] + fn should_allow_normal_urls() { + let fmt = MDLinkFormatter::new(); + assert!(fmt.validate_link("http://example.org").is_some()); + assert!(fmt.validate_link("HTTPS://example.org").is_some()); + } + + #[test] + fn should_allow_plain_text() { + let fmt = MDLinkFormatter::new(); + assert!(fmt.validate_link("javascript").is_some()); + assert!(fmt.validate_link("/javascript:link").is_some()); + } + + #[test] + fn should_not_allow_some_protocols() { + let fmt = MDLinkFormatter::new(); + assert!(fmt.validate_link("javascript:alert(1)").is_none()); + assert!(fmt.validate_link("JAVASCRIPT:alert(1)").is_none()); + assert!(fmt.validate_link("vbscript:alert(1)").is_none()); + assert!(fmt.validate_link("VbScript:alert(1)").is_none()); + assert!(fmt.validate_link("file:///123").is_none()); + } + + #[test] + fn should_not_allow_data_url_except_whitelisted() { + let fmt = MDLinkFormatter::new(); + assert!(fmt.validate_link("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7").is_some()); + assert!(fmt.validate_link("data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4K").is_none()); + } +} diff --git a/crates/markdown-it/src/parser/main.rs b/crates/markdown-it/src/parser/main.rs new file mode 100644 index 0000000000000000000000000000000000000000..5032b2526d996517ef8cf68df70bc33b5683f2f7 --- /dev/null +++ b/crates/markdown-it/src/parser/main.rs @@ -0,0 +1,90 @@ +use derivative::Derivative; + +use crate::common::ruler::Ruler; +use crate::common::sourcemap::SourcePos; +use crate::common::TypeKey; +use crate::parser::block::{self, BlockParser}; +use crate::parser::core::{Root, *}; +use crate::parser::extset::MarkdownItExtSet; +use crate::parser::inline::{self, InlineParser}; +use crate::parser::linkfmt::{LinkFormatter, MDLinkFormatter}; +use crate::Node; + +type RuleFn = fn (&mut Node, &MarkdownIt); + +#[derive(Derivative)] +#[derivative(Debug)] +/// Main parser struct, created once and reused for parsing multiple documents. +pub struct MarkdownIt { + /// Block-level tokenizer. + pub block: BlockParser, + + /// Inline-level tokenizer. + pub inline: InlineParser, + + /// Link validator and formatter. + pub link_formatter: Box, + + /// Storage for custom data used in plugins. + pub ext: MarkdownItExtSet, + + /// Maximum depth of the generated AST, exists to prevent recursion + /// (if markdown source reaches this depth, deeply nested structures + /// will be parsed as plain text). + /// TODO: doesn't work + #[doc(hidden)] + pub max_nesting: u32, + + /// Maximum allowed indentation for syntax blocks + /// default i32::MAX, indented code blocks will set this to 4 + pub max_indent: i32, + + ruler: Ruler, +} + +impl MarkdownIt { + pub fn new() -> Self { + Self::default() + } + + pub fn parse(&self, src: &str) -> Node { + let mut node = Node::new(Root::new(src.to_owned())); + node.srcmap = Some(SourcePos::new(0, src.len())); + + for rule in self.ruler.iter() { + rule(&mut node, self); + debug_assert!(node.is::(), "root node of the AST must always be Root"); + } + node + } + + pub fn add_rule(&mut self) -> RuleBuilder { + let item = self.ruler.add(TypeKey::of::(), T::run); + RuleBuilder::new(item) + } + + pub fn has_rule(&mut self) -> bool { + self.ruler.contains(TypeKey::of::()) + } + + pub fn remove_rule(&mut self) { + self.ruler.remove(TypeKey::of::()); + } +} + +impl Default for MarkdownIt { + fn default() -> Self { + let mut md = Self { + block: BlockParser::new(), + inline: InlineParser::new(), + link_formatter: Box::new(MDLinkFormatter::new()), + ext: MarkdownItExtSet::new(), + max_nesting: 100, + ruler: Ruler::new(), + max_indent: i32::MAX, + }; + block::builtin::add(&mut md); + inline::builtin::add(&mut md); + md + } +} diff --git a/crates/markdown-it/src/parser/mod.rs b/crates/markdown-it/src/parser/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..da04fad1d0de2129e634a658ab0327d9c14cdac5 --- /dev/null +++ b/crates/markdown-it/src/parser/mod.rs @@ -0,0 +1,45 @@ +//! Parser itself + stuff that allows you to extend it. +//! +//! In order to understand how this parser works, you need to understand the concept +//! of Rule Chains. "Rule Chain" is an ordered set of functions that get executed +//! sequentially. This is an example of a Rule Chain: +//! +//! ```rust +//! let rules : Vec = vec![ +//! |s| { s.push_str("hello"); }, +//! |s| { s.push(','); }, +//! |s| { s.push(' '); }, +//! |s| { s.push_str("world"); }, +//! |s| { s.push('!'); }, +//! ]; +//! dbg!(rules.iter().fold(String::new(), |mut s, f| { f(&mut s); s })); +//! ``` +//! +//! The example above builds a string using 5 independent functions. You can extend +//! it by pushing your own function in that vector that manipulate the state (String) +//! in any way you like. +//! +//! MarkdownIt parser consists of three Rule Chains: +//! - [inline] (where functions get executed on every character) +//! - [block] (where functions get executed on every line) +//! - [core] (where functions get executed once per document) +//! +//! You can extend each one of these chains by using +//! [md.inline.add_rule](inline::InlineParser::add_rule), +//! [md.block.add_rule](block::BlockParser::add_rule) or +//! [md.add_rule](crate::MarkdownIt::add_rule) respectively. +//! +//! These are examples of the rules in each chain (view source to see implementation): +//! - [inline rule](crate::plugins::cmark::inline::autolink) - autolink +//! - [block rule](crate::plugins::cmark::block::hr) - thematic break +//! - [core rule](crate::plugins::sourcepos) - source mapping +//! +pub mod block; +pub mod core; +pub mod extset; +pub mod inline; +pub mod linkfmt; + +pub(super) mod main; +pub(super) mod node; +pub(super) mod renderer; diff --git a/crates/markdown-it/src/parser/node.rs b/crates/markdown-it/src/parser/node.rs new file mode 100644 index 0000000000000000000000000000000000000000..43e0b0ae03a9e44610c63584aaf485159c2bc226 --- /dev/null +++ b/crates/markdown-it/src/parser/node.rs @@ -0,0 +1,229 @@ +use downcast_rs::{impl_downcast, Downcast}; +use std::any::TypeId; +use std::fmt::Debug; + +use crate::common::sourcemap::SourcePos; +use crate::common::TypeKey; +use crate::parser::extset::NodeExtSet; +use crate::parser::inline::Text; +use crate::parser::renderer::HTMLRenderer; +use crate::plugins::cmark::inline::newline::Softbreak; +use crate::Renderer; + +/// Single node in the CommonMark AST. +#[derive(Debug)] +#[readonly::make] +pub struct Node { + /// Array of child nodes. + pub children: Vec, + + /// Source mapping info. + pub srcmap: Option, + + /// Custom data specific to this token. + pub ext: NodeExtSet, + + /// Additional attributes to be added to resulting html. + pub attrs: Vec<(&'static str, String)>, + + /// Type name, used for debugging. + #[readonly] + pub node_type: TypeKey, + + /// Storage for arbitrary token-specific data. + #[readonly] + pub node_value: Box, +} + +impl Node { + /// Create a new [Node](Node) with a custom value. + pub fn new(value: T) -> Self { + Self { + children: Vec::new(), + srcmap: None, + attrs: Vec::new(), + ext: NodeExtSet::new(), + node_type: TypeKey::of::(), + node_value: Box::new(value), + } + } + + /// Return std::any::type_name() of node value. + pub fn name(&self) -> &'static str { + self.node_type.name + } + + /// Check that this node value is of given type. + pub fn is(&self) -> bool { + self.node_type.id == TypeId::of::() + } + + /// Downcast node value to specific type. + pub fn cast(&self) -> Option<&T> { + if self.node_type.id == TypeId::of::() { + Some(self.node_value.downcast_ref::().unwrap()) + // performance note: `node_type.id` improves walk speed by a LOT by removing indirection + // (~5% of overall program speed), so having type id duplicated in Node is very beneficial; + // we can also remove extra check with downcast_unchecked, but it doesn't do much + //Some(unsafe { &*(&*self.node_value as *const dyn NodeValue as *const T) }) + } else { + None + } + } + + /// Downcast node value to specific type. + pub fn cast_mut(&mut self) -> Option<&mut T> { + if self.node_type.id == TypeId::of::() { + Some(self.node_value.downcast_mut::().unwrap()) + // performance note: see above + //Some(unsafe { &mut *(&mut *self.node_value as *mut dyn NodeValue as *mut T) }) + } else { + None + } + } + + /// Render this node to HTML. + pub fn render(&self) -> String { + let mut fmt = HTMLRenderer::::new(); + fmt.render(self); + fmt.into() + } + + /// Render this node to XHTML, it adds slash to self-closing tags like this: ``. + /// + /// This mode exists for compatibility with CommonMark tests. + pub fn xrender(&self) -> String { + let mut fmt = HTMLRenderer::::new(); + fmt.render(self); + fmt.into() + } + + /// Replace custom value with another value (this is roughly equivalent + /// to replacing the entire node and copying children and sourcemaps). + pub fn replace(&mut self, value: T) { + self.node_type = TypeKey::of::(); + self.node_value = Box::new(value); + } + + /// Execute function `f` recursively on every member of AST tree + /// (using preorder deep-first search). + pub fn walk<'a>(&'a self, mut f: impl FnMut(&'a Node, u32)) { + // performance note: this is faster than emulating recursion using vec stack + fn walk_recursive<'b>(node: &'b Node, depth: u32, f: &mut impl FnMut(&'b Node, u32)) { + f(node, depth); + for n in node.children.iter() { + stacker::maybe_grow(64*1024, 1024*1024, || { + walk_recursive(n, depth + 1, f); + }); + } + } + + walk_recursive(self, 0, &mut f); + } + + /// Execute function `f` recursively on every member of AST tree + /// (using preorder deep-first search). + pub fn walk_mut(&mut self, mut f: impl FnMut(&mut Node, u32)) { + // performance note: this is faster than emulating recursion using vec stack + fn walk_recursive(node: &mut Node, depth: u32, f: &mut impl FnMut(&mut Node, u32)) { + f(node, depth); + for n in node.children.iter_mut() { + stacker::maybe_grow(64*1024, 1024*1024, || { + walk_recursive(n, depth + 1, f); + }); + } + } + + walk_recursive(self, 0, &mut f); + } + + /// Execute function `f` recursively on every member of AST tree + /// (using postorder deep-first search). + pub fn walk_post(&self, mut f: impl FnMut(&Node, u32)) { + fn walk_recursive(node: &Node, depth: u32, f: &mut impl FnMut(&Node, u32)) { + for n in node.children.iter() { + stacker::maybe_grow(64*1024, 1024*1024, || { + walk_recursive(n, depth + 1, f); + }); + } + f(node, depth); + } + + walk_recursive(self, 0, &mut f); + } + + /// Execute function `f` recursively on every member of AST tree + /// (using postorder deep-first search). + pub fn walk_post_mut(&mut self, mut f: impl FnMut(&mut Node, u32)) { + fn walk_recursive(node: &mut Node, depth: u32, f: &mut impl FnMut(&mut Node, u32)) { + for n in node.children.iter_mut() { + stacker::maybe_grow(64*1024, 1024*1024, || { + walk_recursive(n, depth + 1, f); + }); + } + f(node, depth); + } + + walk_recursive(self, 0, &mut f); + } + + /// Walk recursively through child nodes and collect all text nodes + /// into a single string. + pub fn collect_text(&self) -> String { + let mut result = String::new(); + + self.walk(|node, _| { + if let Some(text) = node.cast::() { + result.push_str(text.content.as_str()); + } else if node.is::() { + result.push('\n'); + } + }); + + result + } +} + +impl Drop for Node { + fn drop(&mut self) { + self.walk_post_mut(|node, _| { + drop(std::mem::take(&mut node.children)); + }); + } +} + +#[derive(Debug)] +#[doc(hidden)] +pub struct NodeEmpty; +impl NodeValue for NodeEmpty {} + +impl Default for Node { + /// Create empty Node. Empty node should only be used as placeholder for functions like + /// std::mem::take, and it cannot be rendered. + fn default() -> Self { + Node::new(NodeEmpty) + } +} + +/// Contents of the specific AST node. +pub trait NodeValue : Debug + Downcast { + /// Output HTML corresponding to this node using Renderer API. + /// + /// Example implementation looks like this: + /// ```rust + /// # const IGNORE : &str = stringify! { + /// fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + /// fmt.open("div", &[]); + /// fmt.contents(&node.children); + /// fmt.close("div"); + /// fmt.cr(); + /// } + /// # }; + /// ``` + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let _ = fmt; + unimplemented!("{} doesn't implement render", node.name()); + } +} + +impl_downcast!(NodeValue); diff --git a/crates/markdown-it/src/parser/renderer.rs b/crates/markdown-it/src/parser/renderer.rs new file mode 100644 index 0000000000000000000000000000000000000000..30d09b026b51d949f88a9902f6f611a931d62c04 --- /dev/null +++ b/crates/markdown-it/src/parser/renderer.rs @@ -0,0 +1,154 @@ +use std::collections::HashMap; +use std::fmt::Debug; + +use crate::common::utils::escape_html; +use crate::parser::extset::RenderExtSet; +use crate::Node; + +/// Each node outputs its HTML using this API. +/// +/// Renderer is a struct that walks through AST and collects HTML from each node +/// into internal buffer. +pub trait Renderer { + /// Write opening html tag with attributes, e.g. ``. + fn open(&mut self, tag: &str, attrs: &[(&str, String)]); + /// Write closing html tag, e.g. ``. + fn close(&mut self, tag: &str); + /// Write self-closing html tag with attributes, e.g. ``. + fn self_close(&mut self, tag: &str, attrs: &[(&str, String)]); + /// Loop through child nodes and render each one. + fn contents(&mut self, nodes: &[Node]); + /// Write line break (`\n`). Default renderer ignores it if last char in the buffer is `\n` already. + fn cr(&mut self); + /// Write plain text with escaping, `
` -> `<div>`. + fn text(&mut self, text: &str); + /// Write plain text without escaping, `
` -> `
`. + fn text_raw(&mut self, text: &str); + /// Extension set to store custom stuff. + fn ext(&mut self) -> &mut RenderExtSet; +} + +#[derive(Debug, Default)] +/// Default HTML/XHTML renderer. +pub(crate) struct HTMLRenderer { + result: String, + ext: RenderExtSet, +} + +impl HTMLRenderer { + pub fn new() -> Self { + Self { + result: String::new(), + ext: RenderExtSet::new(), + } + } + + pub fn render(&mut self, node: &Node) { + node.node_value.render(node, self); + } + + fn make_attr(&mut self, name: &str, value: &str) { + self.result.push(' '); + self.result.push_str(&escape_html(name)); + self.result.push('='); + self.result.push('"'); + self.result.push_str(&escape_html(value)); + self.result.push('"'); + } + + fn make_attrs(&mut self, attrs: &[(&str, String)]) { + let mut attr_hash = HashMap::new(); + let mut attr_order = Vec::with_capacity(attrs.len()); + + for (name, value) in attrs { + let entry = attr_hash.entry(*name).or_insert(Vec::new()); + entry.push(value.as_str()); + attr_order.push(*name); + } + + for name in attr_order { + let Some(value) = attr_hash.remove(name) else { continue; }; + + if name == "class" { + self.make_attr(name, &value.join(" ")); + } else if name == "style" { + self.make_attr(name, &value.join(";")); + } else { + for v in value { + self.make_attr(name, v); + } + } + } + } +} + +impl From> for String { + fn from(f: HTMLRenderer) -> Self { + #[cold] + fn replace_null(input: String) -> String { + input.replace('\0', "\u{FFFD}") + } + + if f.result.contains('\0') { + // U+0000 must be replaced with U+FFFD as per commonmark spec, + // we do it at the very end in order to avoid messing with byte offsets + // for source maps (since "\0".len() != "\u{FFFD}".len()) + replace_null(f.result) + } else { + f.result + } + } +} + +impl Renderer for HTMLRenderer { + fn open(&mut self, tag: &str, attrs: &[(&str, String)]) { + self.result.push('<'); + self.result.push_str(tag); + self.make_attrs(attrs); + self.result.push('>'); + } + + fn close(&mut self, tag: &str) { + self.result.push('<'); + self.result.push('/'); + self.result.push_str(tag); + self.result.push('>'); + } + + fn self_close(&mut self, tag: &str, attrs: &[(&str, String)]) { + self.result.push('<'); + self.result.push_str(tag); + self.make_attrs(attrs); + if XHTML { + self.result.push(' '); + self.result.push('/'); + } + self.result.push('>'); + } + + fn contents(&mut self, nodes: &[Node]) { + for node in nodes.iter() { + self.render(node); + } + } + + fn cr(&mut self) { + // only push '\n' if last character isn't it + match self.result.as_bytes().last() { + Some(b'\n') | None => {} + Some(_) => self.result.push('\n') + } + } + + fn text(&mut self, text: &str) { + self.result.push_str(&escape_html(text)); + } + + fn text_raw(&mut self, text: &str) { + self.result.push_str(text); + } + + fn ext(&mut self) -> &mut RenderExtSet { + &mut self.ext + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/blockquote.rs b/crates/markdown-it/src/plugins/cmark/block/blockquote.rs new file mode 100644 index 0000000000000000000000000000000000000000..7974f4bcb79efcd862f736cff4d803b07059bcfc --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/blockquote.rs @@ -0,0 +1,168 @@ +//! Block quotes +//! +//! `> looks like this` +//! +//! +use crate::common::utils::find_indent_of; +use crate::parser::block::{BlockRule, BlockState}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Blockquote; + +impl NodeValue for Blockquote { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("blockquote", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("blockquote"); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); +} + +#[doc(hidden)] +pub struct BlockquoteScanner; +impl BlockRule for BlockquoteScanner { + fn check(state: &mut BlockState) -> Option<()> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + // check the block quote marker + let Some('>') = state.get_line(state.line).chars().next() else { return None; }; + + Some(()) + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + Self::check(state)?; + + let mut old_line_offsets = Vec::new(); + let start_line = state.line; + let mut next_line = state.line; + let mut last_line_empty = false; + + // Search the end of the block + // + // Block ends with either: + // 1. an empty line outside: + // ``` + // > test + // + // ``` + // 2. an empty line inside: + // ``` + // > + // test + // ``` + // 3. another tag: + // ``` + // > test + // - - - + // ``` + while next_line < state.line_max { + // check if it's outdented, i.e. it's inside list item and indented + // less than said list item: + // + // ``` + // 1. anything + // > current blockquote + // 2. checking this line + // ``` + let is_outdented = state.line_indent(next_line) < 0; + let line = state.get_line(next_line).to_owned(); + let mut chars = line.chars(); + + match chars.next() { + None => { + // Case 1: line is not inside the blockquote, and this line is empty. + break; + } + Some('>') if !is_outdented => { + // This line is inside the blockquote. + + // set offset past spaces and ">" + let offsets = &state.line_offsets[next_line]; + let pos_after_marker = offsets.first_nonspace + 1; + + old_line_offsets.push(state.line_offsets[next_line].clone()); + + let ( mut indent_after_marker, first_nonspace ) = find_indent_of( + &state.src[offsets.line_start..offsets.line_end], + pos_after_marker - offsets.line_start); + + last_line_empty = first_nonspace == offsets.line_end - offsets.line_start; + + // skip one optional space after '>' + if matches!(chars.next(), Some(' ' | '\t')) { + indent_after_marker -= 1; + } + + state.line_offsets[next_line].indent_nonspace = indent_after_marker as i32; + state.line_offsets[next_line].first_nonspace = first_nonspace + state.line_offsets[next_line].line_start; + next_line += 1; + continue; + } + _ => {} + } + + // Case 2: line is not inside the blockquote, and the last line was empty. + if last_line_empty { break; } + + // Case 3: another tag found. + state.line = next_line; + + if state.test_rules_at_line() { + // Quirk to enforce "hard termination mode" for paragraphs; + // normally if you call `nodeize(state, startLine, nextLine)`, + // paragraphs will look below nextLine for paragraph continuation, + // but if blockquote is terminated by another tag, they shouldn't + //state.line_max = next_line; + + if state.blk_indent != 0 { + // state.blkIndent was non-zero, we now set it to zero, + // so we need to re-calculate all offsets to appear as + // if indent wasn't changed + old_line_offsets.push(state.line_offsets[next_line].clone()); + state.line_offsets[next_line].indent_nonspace -= state.blk_indent as i32; + } + + break; + } + + old_line_offsets.push(state.line_offsets[next_line].clone()); + + // A negative indentation means that this is a paragraph continuation + // + state.line_offsets[next_line].indent_nonspace = -1; + next_line += 1; + } + + let old_indent = state.blk_indent; + state.blk_indent = 0; + + let old_node = std::mem::replace(&mut state.node, Node::new(Blockquote)); + let old_line_max = state.line_max; + state.line = start_line; + state.line_max = next_line; + state.md.block.tokenize(state); + next_line = state.line; + state.line = start_line; + state.line_max = old_line_max; + + // Restore original tShift; this might not be necessary since the parser + // has already been here, but just to make sure we can do that. + for (idx, line_offset) in old_line_offsets.iter_mut().enumerate() { + std::mem::swap(&mut state.line_offsets[idx + start_line], line_offset); + } + state.blk_indent = old_indent; + + let node = std::mem::replace(&mut state.node, old_node); + Some((node, next_line - start_line)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/code.rs b/crates/markdown-it/src/plugins/cmark/block/code.rs new file mode 100644 index 0000000000000000000000000000000000000000..806a0c0a401eee98df857412529fc12763145695 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/code.rs @@ -0,0 +1,69 @@ +//! Indented code block +//! +//! Parses anything indented with 4 spaces. +//! +//! +use crate::parser::block::{BlockRule, BlockState}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +const CODE_INDENT: i32 = 4; + +#[derive(Debug)] +pub struct CodeBlock { + pub content: String, +} + +impl NodeValue for CodeBlock { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("pre", &[]); + fmt.open("code", &node.attrs); + fmt.text(&self.content); + fmt.close("code"); + fmt.close("pre"); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); + md.max_indent = CODE_INDENT; +} + +#[doc(hidden)] +pub struct CodeScanner; +impl BlockRule for CodeScanner { + fn check(_: &mut BlockState) -> Option<()> { + None + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + if state.line_indent(state.line) < CODE_INDENT { return None; } + + let mut next_line = state.line + 1; + let mut last = next_line; + + while next_line < state.line_max { + if state.is_empty(next_line) { + next_line += 1; + continue; + } + + if state.line_indent(next_line) >= CODE_INDENT { + next_line += 1; + last = next_line; + continue; + } + + break; + } + + let (mut content, _mapping) = state.get_lines(state.line, last, CODE_INDENT as usize + state.blk_indent, false); + content += "\n"; + + let node = Node::new(CodeBlock { content }); + //node.srcmap = state.get_map_from_offsets(mapping[0].1, state.line_offsets[last - 1].line_end); + + Some((node, last - state.line)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/fence.rs b/crates/markdown-it/src/plugins/cmark/block/fence.rs new file mode 100644 index 0000000000000000000000000000000000000000..837dd2e167bec76d0c3c14f23413d4ce21a63eb9 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/fence.rs @@ -0,0 +1,164 @@ +//! Code fence +//! +//! ` ```lang ` or `~~~lang` +//! +//! +use crate::common::utils::unescape_all; +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::extset::MarkdownItExt; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct CodeFence { + pub info: String, + pub marker: char, + pub marker_len: usize, + pub content: String, + pub lang_prefix: &'static str, +} + +impl NodeValue for CodeFence { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let info = unescape_all(&self.info); + let mut split = info.split_whitespace(); + let lang_name = split.next().unwrap_or(""); + let mut attrs = node.attrs.clone(); + let class; + + if !lang_name.is_empty() { + class = format!("{}{}", self.lang_prefix, lang_name); + attrs.push(("class", class)); + } + + fmt.cr(); + fmt.open("pre", &[]); + fmt.open("code", &attrs); + fmt.text(&self.content); + fmt.close("code"); + fmt.close("pre"); + fmt.cr(); + } +} + +#[derive(Debug, Clone, Copy)] +struct FenceSettings(&'static str); +impl MarkdownItExt for FenceSettings {} + +impl Default for FenceSettings { + fn default() -> Self { + Self("language-") + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); +} + +pub fn set_lang_prefix(md: &mut MarkdownIt, lang_prefix: &'static str) { + md.ext.insert(FenceSettings(lang_prefix)); +} + +#[doc(hidden)] +pub struct FenceScanner; + +impl FenceScanner { + fn get_header<'a>(state: &'a mut BlockState) -> Option<(char, usize, &'a str)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let line = state.get_line(state.line); + let mut chars = line.chars(); + + let marker = chars.next()?; + if marker != '~' && marker != '`' { return None; } + + // scan marker length + let mut len = 1; + while Some(marker) == chars.next() { len += 1; } + + if len < 3 { return None; } + + let params = &line[len..]; + + if marker == '`' && params.contains(marker) { return None; } + + Some((marker, len, params)) + } +} + +impl BlockRule for FenceScanner { + fn check(state: &mut BlockState) -> Option<()> { + Self::get_header(state).map(|_| ()) + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let (marker, len, params) = Self::get_header(state)?; + let params = params.to_owned(); + + let mut next_line = state.line; + let mut have_end_marker = false; + + // search end of block + 'outer: loop { + next_line += 1; + if next_line >= state.line_max { + // unclosed block should be autoclosed by end of document. + // also block seems to be autoclosed by end of parent + break; + } + + let line = state.get_line(next_line); + + if !line.is_empty() && state.line_indent(next_line) < 0 { + // non-empty line with negative indent should stop the list: + // - ``` + // test + break; + } + + let mut chars = line.chars().peekable(); + + if Some(marker) != chars.next() { continue; } + + if state.line_indent(next_line) >= state.md.max_indent { + continue; + } + + // scan marker length + let mut len_end = 1; + while Some(&marker) == chars.peek() { + chars.next(); + len_end += 1; + } + + // closing code fence must be at least as long as the opening one + if len_end < len { continue; } + + // make sure tail has spaces only + loop { + match chars.next() { + Some(' ' | '\t') => {}, + Some(_) => continue 'outer, + None => { + have_end_marker = true; + break 'outer; + } + } + } + } + + // If a fence has heading spaces, they should be removed from its inner block + let indent = state.line_offsets[state.line].indent_nonspace; + let (content, _) = state.get_lines(state.line + 1, next_line, indent as usize, true); + + let lang_prefix = state.md.ext.get::().copied().unwrap_or_default().0; + let node = Node::new(CodeFence { + info: params, + marker, + marker_len: len, + content, + lang_prefix, + }); + Some((node, next_line - state.line + if have_end_marker { 1 } else { 0 })) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/heading.rs b/crates/markdown-it/src/plugins/cmark/block/heading.rs new file mode 100644 index 0000000000000000000000000000000000000000..76f7318ba000eb80e35ee513395fad53b832693a --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/heading.rs @@ -0,0 +1,87 @@ +//! ATX heading +//! +//! `# h1`, `## h2`, etc. +//! +//! +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::inline::InlineRoot; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct ATXHeading { + pub level: u8, +} + +impl NodeValue for ATXHeading { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + static TAG : [&str; 6] = [ "h1", "h2", "h3", "h4", "h5", "h6" ]; + debug_assert!(self.level >= 1 && self.level <= 6); + + fmt.cr(); + fmt.open(TAG[self.level as usize - 1], &node.attrs); + fmt.contents(&node.children); + fmt.close(TAG[self.level as usize - 1]); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); +} + +#[doc(hidden)] +pub struct HeadingScanner; +impl BlockRule for HeadingScanner { + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let line = state.get_line(state.line); + let Some('#') = line.chars().next() else { return None; }; + + let text_pos; + + // count heading level + let mut level = 0u8; + let mut chars = line.char_indices(); + loop { + match chars.next() { + Some((_, '#')) => { + level += 1; + if level > 6 { return None; } + } + Some((x, ' ' | '\t')) => { + text_pos = x; + break; + } + None => { + text_pos = level as usize; + break; + } + Some(_) => return None, + } + } + + // Let's cut tails like ' ### ' from the end of string + + let mut chars_back = chars.rev().peekable(); + while let Some((_, ' ' | '\t')) = chars_back.peek() { chars_back.next(); } + while let Some((_, '#')) = chars_back.peek() { chars_back.next(); } + + let text_max = match chars_back.next() { + // ## foo ## + Some((last_pos, ' ' | '\t')) => last_pos + 1, + // ## foo## + Some(_) => line.len(), + // ## ## (already consumed the space) + None => text_pos, + }; + + let content = line[text_pos..text_max].to_owned(); + let mapping = vec![(0, state.line_offsets[state.line].first_nonspace + text_pos)]; + + let mut node = Node::new(ATXHeading { level }); + node.children.push(Node::new(InlineRoot::new(content, mapping))); + Some((node, 1)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/hr.rs b/crates/markdown-it/src/plugins/cmark/block/hr.rs new file mode 100644 index 0000000000000000000000000000000000000000..26a3c0914907daaf75daa2d3cb3970a68ee36c4c --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/hr.rs @@ -0,0 +1,55 @@ +//! Thematic breaks +//! +//! `***`, `---`, `___` +//! +//! +use crate::parser::block::{BlockRule, BlockState}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct ThematicBreak { + pub marker: char, + pub marker_len: usize, +} + +impl NodeValue for ThematicBreak { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.self_close("hr", &node.attrs); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); +} + +#[doc(hidden)] +pub struct HrScanner; +impl BlockRule for HrScanner { + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let mut chars = state.get_line(state.line).chars(); + + // Check hr marker + let marker = chars.next()?; + if marker != '*' && marker != '-' && marker != '_' { return None; } + + // markers can be mixed with spaces, but there should be at least 3 of them + let mut cnt = 1; + for ch in chars { + if ch == marker { + cnt += 1; + } else if ch != ' ' && ch != '\t' { + return None; + } + } + + if cnt < 3 { return None; } + + let node = Node::new(ThematicBreak { marker, marker_len: cnt }); + Some((node, 1)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/lheading.rs b/crates/markdown-it/src/plugins/cmark/block/lheading.rs new file mode 100644 index 0000000000000000000000000000000000000000..e9cbea314d8c03ef541f81f221cccc7b9bd7a695 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/lheading.rs @@ -0,0 +1,104 @@ +//! Setext headings +//! +//! Paragraph underlined with `===` or `---`. +//! +//! +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::inline::InlineRoot; +use crate::plugins::cmark::block::paragraph::ParagraphScanner; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct SetextHeader { + pub level: u8, + pub marker: char, +} + +impl NodeValue for SetextHeader { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + static TAG : [&str; 2] = [ "h1", "h2" ]; + debug_assert!(self.level >= 1 && self.level <= 2); + + fmt.cr(); + fmt.open(TAG[self.level as usize - 1], &node.attrs); + fmt.contents(&node.children); + fmt.close(TAG[self.level as usize - 1]); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::() + .before::() + .after_all(); +} + +#[doc(hidden)] +pub struct LHeadingScanner; +impl BlockRule for LHeadingScanner { + fn check(_: &mut BlockState) -> Option<()> { + None // can't interrupt any tags + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let start_line = state.line; + let mut next_line = start_line; + let mut level = 0; + + 'outer: loop { + next_line += 1; + + if next_line >= state.line_max || state.is_empty(next_line) { break; } + + // this may be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if state.line_indent(next_line) >= state.md.max_indent { continue; } + + // + // Check for underline in setext header + // + if state.line_indent(next_line) >= 0 { + let mut chars = state.get_line(next_line).chars().peekable(); + if let Some(marker @ ('-' | '=')) = chars.next() { + while Some(&marker) == chars.peek() { chars.next(); } + while let Some(' ' | '\t') = chars.peek() { chars.next(); } + if chars.next().is_none() { + level = if marker == '=' { 1 } else { 2 }; + break 'outer; + } + } + } + + // quirk for blockquotes, this line should already be checked by that rule + if state.line_offsets[next_line].indent_nonspace < 0 { continue; } + + // Some tags can terminate paragraph without empty line. + let old_state_line = state.line; + state.line = next_line; + if state.test_rules_at_line() { + state.line = old_state_line; + break 'outer; + } + state.line = old_state_line; + } + + + if level == 0 { + // Didn't find valid underline + return None; + } + + let (content, mapping) = state.get_lines(start_line, next_line, state.blk_indent, false); + + let mut node = Node::new(SetextHeader { + level, + marker: if level == 2 { '-' } else { '=' } + }); + node.children.push(Node::new(InlineRoot::new(content, mapping))); + + Some((node, next_line + 1 - start_line)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/list.rs b/crates/markdown-it/src/plugins/cmark/block/list.rs new file mode 100644 index 0000000000000000000000000000000000000000..1b5733acd2c60b411d28ec6fd3e428ce24815ff5 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/list.rs @@ -0,0 +1,370 @@ +//! Ordered and bullet lists +//! +//! This plugin parses both kinds of lists (bullet and ordered) as well as list items. +//! +//! looks like `1. this` or `- this` +//! +//! - +//! - +use crate::common::utils::find_indent_of; +use crate::parser::block::{BlockRule, BlockState}; +use crate::plugins::cmark::block::hr::HrScanner; +use crate::plugins::cmark::block::paragraph::Paragraph; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct OrderedList { + pub start: u32, + pub marker: char, +} + +impl NodeValue for OrderedList { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + let start; + if self.start != 1 { + start = self.start.to_string(); + attrs.push(("start", start)); + } + fmt.cr(); + fmt.open("ol", &attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("ol"); + fmt.cr(); + } +} + +#[derive(Debug)] +pub struct BulletList { + pub marker: char, +} + +impl NodeValue for BulletList { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("ul", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("ul"); + fmt.cr(); + } +} + +#[derive(Debug)] +pub struct ListItem; + +impl NodeValue for ListItem { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.open("li", &node.attrs); + fmt.contents(&node.children); + fmt.close("li"); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::() + .after::(); +} + +#[doc(hidden)] +pub struct ListScanner; + +impl ListScanner { + // Search `[-+*][\n ]`, returns next pos after marker on success + // or -1 on fail. + fn skip_bullet_list_marker(src: &str) -> Option { + let mut chars = src.chars(); + + let Some('*' | '-' | '+') = chars.next() else { return None; }; + + match chars.next() { + Some(' ' | '\t') | None => Some(1), + Some(_) => None, // " -test " - is not a list item + } + } + + // Search `\d+[.)][\n ]`, returns next pos after marker on success + // or -1 on fail. + fn skip_ordered_list_marker(src: &str) -> Option { + let mut chars = src.chars(); + let Some('0'..='9') = chars.next() else { return None; }; + + let mut pos = 1; + loop { + pos += 1; + match chars.next() { + Some('0'..='9') => { + // List marker should have no more than 9 digits + // (prevents integer overflow in browsers) + if pos >= 10 { return None; } + } + Some(')' | '.') => { + // found valid marker + break; + } + Some(_) | None => { return None; } + } + } + + match chars.next() { + Some(' ' | '\t') | None => Some(pos), + Some(_) => None, // " 1.test " - is not a list item + } + } + + fn mark_tight_paragraphs(nodes: &mut Vec) { + let mut idx = 0; + while idx < nodes.len() { + if nodes[idx].is::() { + let children = std::mem::take(&mut nodes[idx].children); + let len = children.len(); + nodes.splice(idx..idx+1, children); + idx += len; + } else { + idx += 1; + } + } + } + + fn find_marker(state: &mut BlockState, silent: bool) -> Option<(usize, Option, char)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + // Special case: + // - item 1 + // - item 2 + // - item 3 + // - item 4 + // - this one is a paragraph continuation + if let Some(list_indent) = state.list_indent { + let indent_nonspace = state.line_offsets[state.line].indent_nonspace; + if indent_nonspace - list_indent as i32 >= state.md.max_indent && + indent_nonspace < state.blk_indent as i32 { + return None; + } + } + + let mut is_terminating_paragraph = false; + + // limit conditions when list can interrupt + // a paragraph (validation mode only) + if silent { + // Next list item should still terminate previous list item; + // + // This code can fail if plugins use blkIndent as well as lists, + // but I hope the spec gets fixed long before that happens. + // + if state.line_indent(state.line) >= 0 { + is_terminating_paragraph = true; + } + } + + let current_line = state.get_line(state.line); + + let marker_value; + let pos_after_marker; + + // Detect list type and position after marker + if let Some(p) = Self::skip_ordered_list_marker(current_line) { + pos_after_marker = p; + let int = str::parse(¤t_line[..pos_after_marker - 1]).unwrap(); + marker_value = Some(int); + + // If we're starting a new ordered list right after + // a paragraph, it should start with 1. + if is_terminating_paragraph && int != 1 { return None; } + + } else if let Some(p) = Self::skip_bullet_list_marker(current_line) { + pos_after_marker = p; + marker_value = None; + } else { + return None; + } + + // If we're starting a new unordered list right after + // a paragraph, first line should not be empty. + if is_terminating_paragraph { + let mut chars = current_line[pos_after_marker..].chars(); + loop { + match chars.next() { + Some(' ' | '\t') => {}, + Some(_) => break, + None => return None, + } + } + } + + // We should terminate list on style change. Remember first one to compare. + let marker_char = current_line[..pos_after_marker].chars().next_back().unwrap(); + + Some((pos_after_marker, marker_value, marker_char)) + } +} + +impl BlockRule for ListScanner { + fn check(state: &mut BlockState) -> Option<()> { + if state.node.is::() || state.node.is::() { return None; } + + Self::find_marker(state, true).map(|_| ()) + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let (mut pos_after_marker, marker_value, marker_char) = Self::find_marker(state, false)?; + + let new_node = if let Some(int) = marker_value { + Node::new(OrderedList { + start: int, + marker: marker_char + }) + } else { + Node::new(BulletList { + marker: marker_char + }) + }; + + let old_node = std::mem::replace(&mut state.node, new_node); + + // + // Iterate list items + // + + let start_line = state.line; + let mut next_line = state.line; + let mut prev_empty_end = false; + let mut tight = true; + let mut current_line; + + while next_line < state.line_max { + let offsets = &state.line_offsets[next_line]; + let initial = offsets.indent_nonspace as usize + pos_after_marker; + + let ( mut indent_after_marker, first_nonspace ) = find_indent_of( + &state.src[offsets.line_start..offsets.line_end], + pos_after_marker + offsets.first_nonspace - offsets.line_start); + + let reached_end_of_line = first_nonspace == offsets.line_end - offsets.line_start; + let indent_nonspace = initial + indent_after_marker; + + #[allow(clippy::if_same_then_else)] + if reached_end_of_line { + // trimming space in "- \n 3" case, indent is 1 here + indent_after_marker = 1; + } else if indent_after_marker as i32 > state.md.max_indent { + // If we have more than the max indent, the indent is 1 + // (the rest is just indented code block) + indent_after_marker = 1; + } + + // " - test" + // ^^^^^ - calculating total length of this thing + let indent = initial + indent_after_marker; + + // Run subparser & write tokens + let old_node = std::mem::replace(&mut state.node, Node::new(ListItem)); + + // change current state, then restore it after parser subcall + let old_tight = state.tight; + let old_lineoffset = offsets.clone(); + + // - example list + // ^ listIndent position will be here + // ^ blkIndent position will be here + // + let old_list_indent = state.list_indent; + state.list_indent = Some(state.blk_indent as u32); + state.blk_indent = indent; + + state.tight = true; + state.line_offsets[next_line].first_nonspace = first_nonspace + state.line_offsets[next_line].line_start; + state.line_offsets[next_line].indent_nonspace = indent_nonspace as i32; + + if reached_end_of_line && state.is_empty(next_line + 1) { + // workaround for this case + // (list item is empty, list terminates before "foo"): + // ~~~~~~~~ + // - + // + // foo + // ~~~~~~~~ + state.line = if state.line + 2 < state.line_max { + state.line + 2 + } else { + state.line_max + } + } else { + state.line = next_line; + state.md.block.tokenize(state); + } + + // If any of list item is tight, mark list as tight + if !state.tight || prev_empty_end { + tight = false; + } + + // Item become loose if finish with empty line, + // but we should filter last element, because it means list finish + prev_empty_end = (state.line - next_line) > 1 && state.is_empty(state.line - 1); + + state.blk_indent = state.list_indent.unwrap() as usize; + state.list_indent = old_list_indent; + state.line_offsets[next_line] = old_lineoffset; + state.tight = old_tight; + + let end_line = state.line; + let mut node = std::mem::replace(&mut state.node, old_node); + node.srcmap = state.get_map(next_line, end_line - 1); + state.node.children.push(node); + next_line = state.line; + + if next_line >= state.line_max { break; } + + // + // Try to check if list is terminated or continued. + // + if state.line_indent(next_line) < 0 { break; } + + if state.line_indent(next_line) >= state.md.max_indent { break; } + + // fail if terminating block found + if state.test_rules_at_line() { break; } + + current_line = state.get_line(state.line).to_owned(); + + // fail if list has another type + #[allow(clippy::collapsible_else_if)] + if marker_value.is_some() { + if let Some(p) = Self::skip_ordered_list_marker(¤t_line) { + pos_after_marker = p; + } else { + break; + } + } else { + if let Some(p) = Self::skip_bullet_list_marker(¤t_line) { + pos_after_marker = p; + } else { + break; + } + } + + let next_marker_char = current_line[..pos_after_marker].chars().next_back().unwrap(); + if next_marker_char != marker_char { break; } + } + + // mark paragraphs tight if needed + if tight { + for child in state.node.children.iter_mut() { + debug_assert!(child.is::()); + Self::mark_tight_paragraphs(&mut child.children); + } + } + + // Finalize list + state.line = start_line; + let node = std::mem::replace(&mut state.node, old_node); + Some((node, next_line - state.line)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/mod.rs b/crates/markdown-it/src/plugins/cmark/block/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..467f2ca9d3b926b04685a0010fd285b9c769c61e --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/mod.rs @@ -0,0 +1,10 @@ +//! Block-level CommonMark structures. +pub mod blockquote; +pub mod code; +pub mod fence; +pub mod heading; +pub mod hr; +pub mod lheading; +pub mod list; +pub mod paragraph; +pub mod reference; diff --git a/crates/markdown-it/src/plugins/cmark/block/paragraph.rs b/crates/markdown-it/src/plugins/cmark/block/paragraph.rs new file mode 100644 index 0000000000000000000000000000000000000000..5fde9d381ffbdc92178376717fcddbecd34c984e --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/paragraph.rs @@ -0,0 +1,68 @@ +//! Paragraph +//! +//! This is the default rule if nothing else matches. +//! +//! +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::inline::InlineRoot; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::() + .after_all(); +} + +#[derive(Debug)] +pub struct Paragraph; + +impl NodeValue for Paragraph { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("p", &node.attrs); + fmt.contents(&node.children); + fmt.close("p"); + fmt.cr(); + } +} + +#[doc(hidden)] +pub struct ParagraphScanner; +impl BlockRule for ParagraphScanner { + fn check(_: &mut BlockState) -> Option<()> { + None // can't interrupt anything + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let start_line = state.line; + let mut next_line = start_line; + + // jump line-by-line until empty one or EOF + 'outer: loop { + next_line += 1; + + if next_line >= state.line_max || state.is_empty(next_line) { break; } + + // this may be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if state.line_indent(next_line) >= state.md.max_indent { continue; } + + // quirk for blockquotes, this line should already be checked by that rule + if state.line_offsets[next_line].indent_nonspace < 0 { continue; } + + // Some tags can terminate paragraph without empty line. + let old_state_line = state.line; + state.line = next_line; + if state.test_rules_at_line() { + state.line = old_state_line; + break 'outer; + } + state.line = old_state_line; + } + + let (content, mapping) = state.get_lines(start_line, next_line, state.blk_indent, false); + + let mut node = Node::new(Paragraph); + node.children.push(Node::new(InlineRoot::new(content, mapping))); + Some((node, next_line - start_line)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/block/reference.rs b/crates/markdown-it/src/plugins/cmark/block/reference.rs new file mode 100644 index 0000000000000000000000000000000000000000..5a2770b96c5df47c15d9f67bb2a754eaeaf8d788 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/block/reference.rs @@ -0,0 +1,374 @@ +//! Link reference definition +//! +//! `[label]: /url "title"` +//! +//! +//! +//! This plugin parses markdown link references. Check documentation on [ReferenceMap] +//! to see how you can use and/or extend it if you have external source for references. +//! +use derivative::Derivative; +use derive_more::{Deref, DerefMut}; +use downcast_rs::{impl_downcast, Downcast}; +use std::collections::HashMap; +use std::fmt::Debug; + +use crate::common::utils::normalize_reference; +use crate::generics::inline::full_link; +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::extset::RootExt; +use crate::{MarkdownIt, Node, NodeValue}; + +/// Storage for parsed references +/// +/// if you have some external source for your link references, you can add them like this: +/// +/// ```rust +/// use markdown_it::parser::block::builtin::BlockParserRule; +/// use markdown_it::parser::core::{CoreRule, Root}; +/// use markdown_it::plugins::cmark::block::reference::{ReferenceMap, DefaultReferenceMap, CustomReferenceMap}; +/// use markdown_it::{MarkdownIt, Node}; +/// +/// let md = &mut MarkdownIt::new(); +/// markdown_it::plugins::cmark::add(md); +/// +/// #[derive(Debug, Default)] +/// struct RefMapOverride(DefaultReferenceMap); +/// impl CustomReferenceMap for RefMapOverride { +/// fn get(&self, label: &str) -> Option<(&str, Option<&str>)> { +/// // override a specific link +/// if label == "rust" { +/// return Some(( +/// "https://www.rust-lang.org/", +/// Some("The Rust Language"), +/// )); +/// } +/// +/// self.0.get(label) +/// } +/// +/// fn insert(&mut self, label: String, destination: String, title: Option) -> bool { +/// self.0.insert(label, destination, title) +/// } +/// } +/// +/// struct AddCustomReferences; +/// impl CoreRule for AddCustomReferences { +/// fn run(root: &mut Node, _: &MarkdownIt) { +/// let data = root.cast_mut::().unwrap(); +/// data.ext.insert(ReferenceMap::new(RefMapOverride::default())); +/// } +/// } +/// +/// md.add_rule::() +/// .before::(); +/// +/// let html = md.parse("[rust]").render(); +/// assert_eq!( +/// html.trim(), +/// r#"

rust

"# +/// ); +/// ``` +/// +/// You can also view all references that user created by adding the following rule: +/// +/// ```rust +/// use markdown_it::parser::core::{CoreRule, Root}; +/// use markdown_it::plugins::cmark::block::reference::{ReferenceMap, DefaultReferenceMap}; +/// use markdown_it::{MarkdownIt, Node}; +/// +/// let md = &mut MarkdownIt::new(); +/// markdown_it::plugins::cmark::add(md); +/// +/// let ast = md.parse("[hello]: world"); +/// let root = ast.node_value.downcast_ref::().unwrap(); +/// let refmap = root.ext.get::() +/// .map(|m| m.downcast_ref::().expect("expect references to be handled by default map")); +/// +/// let mut labels = vec![]; +/// if let Some(refmap) = refmap { +/// for (label, _dest, _title) in refmap.iter() { +/// labels.push(label); +/// } +/// } +/// +/// assert_eq!(labels, ["hello"]); +/// ``` +/// +#[derive(Debug, Deref, DerefMut)] +#[deref(forward)] +#[deref_mut(forward)] +pub struct ReferenceMap(Box); + +impl ReferenceMap { + pub fn new(custom_map: impl CustomReferenceMap + 'static) -> Self { + Self(Box::new(custom_map)) + } +} + +impl Default for ReferenceMap { + fn default() -> Self { + Self::new(DefaultReferenceMap::new()) + } +} + +impl RootExt for ReferenceMap {} + +pub trait CustomReferenceMap : Debug + Downcast + Send + Sync { + /// Insert new element to the reference map. You may return false if it's not a valid label to stop parsing. + fn insert(&mut self, label: String, destination: String, title: Option) -> bool; + + /// Get an element referenced by `label` from the map, returns destination and optional title. + fn get(&self, label: &str) -> Option<(&str, Option<&str>)>; +} + +impl_downcast!(CustomReferenceMap); + +#[derive(Default, Debug)] +pub struct DefaultReferenceMap(HashMap); + +impl DefaultReferenceMap { + pub fn new() -> Self { + Self::default() + } + + pub fn iter(&self) -> impl Iterator)> { + Box::new(self.0.iter().map(|(a, b)| { + (a.label.as_str(), b.destination.as_str(), b.title.as_deref()) + })) + } +} + +impl CustomReferenceMap for DefaultReferenceMap { + fn insert(&mut self, label: String, destination: String, title: Option) -> bool { + let Some(key) = ReferenceMapKey::new(label) else { return false; }; + self.0.entry(key) + .or_insert(ReferenceMapEntry::new(destination, title)); + true + } + + fn get(&self, label: &str) -> Option<(&str, Option<&str>)> { + let key = ReferenceMapKey::new(label.to_owned())?; + self.0.get(&key) + .map(|r| (r.destination.as_str(), r.title.as_deref())) + } +} + +#[derive(Derivative)] +#[derivative(Debug, Default, Hash, PartialEq, Eq)] +/// Reference label +struct ReferenceMapKey { + #[derivative(PartialEq = "ignore")] + #[derivative(Hash = "ignore")] + pub label: String, + normalized: String, +} + +impl ReferenceMapKey { + pub fn new(label: String) -> Option { + let normalized = normalize_reference(&label); + + if normalized.is_empty() { + // CommonMark 0.20 disallows empty labels + return None; + } + + Some(Self { label, normalized }) + } +} + +#[derive(Debug, Default)] +/// Reference value +struct ReferenceMapEntry { + pub destination: String, + pub title: Option, +} + +impl ReferenceMapEntry { + pub fn new(destination: String, title: Option) -> Self { + Self { destination, title } + } +} + +/// Add plugin that parses markdown link references +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::(); +} + +#[derive(Debug)] +pub struct Definition { + pub label: String, + pub destination: String, + pub title: Option, +} +impl NodeValue for Definition { + fn render(&self, _: &Node, _: &mut dyn crate::Renderer) {} +} + +#[doc(hidden)] +pub struct ReferenceScanner; +impl BlockRule for ReferenceScanner { + fn check(_: &mut BlockState) -> Option<()> { + None // can't interrupt anything + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let mut chars = state.get_line(state.line).chars(); + + let Some('[') = chars.next() else { return None; }; + + // Simple check to quickly interrupt scan on [link](url) at the start of line. + // Can be useful on practice: https://github.com/markdown-it/markdown-it/issues/54 + loop { + match chars.next() { + Some('\\') => { chars.next(); }, + Some(']') => { + if let Some(':') = chars.next() { + break; + } else { + return None; + } + } + Some(_) => {}, + None => break, + } + } + + let start_line = state.line; + let mut next_line = start_line; + + // jump line-by-line until empty one or EOF + 'outer: loop { + next_line += 1; + + if next_line >= state.line_max || state.is_empty(next_line) { break; } + + // this may be a code block normally, but after paragraph + // it's considered a lazy continuation regardless of what's there + if state.line_indent(next_line) >= state.md.max_indent { continue; } + + // quirk for blockquotes, this line should already be checked by that rule + if state.line_offsets[next_line].indent_nonspace < 0 { continue; } + + // Some tags can terminate paragraph without empty line. + let old_state_line = state.line; + state.line = next_line; + if state.test_rules_at_line() { + state.line = old_state_line; + break 'outer; + } + state.line = old_state_line; + } + + let (str_before_trim, _) = state.get_lines(start_line, next_line, state.blk_indent, false); + let str = str_before_trim.trim(); + let mut chars = str.char_indices(); + chars.next(); // skip '[' + let label_end; + let mut lines = 0; + + loop { + match chars.next() { + Some((_, '[')) => return None, + Some((p, ']')) => { + label_end = p; + break; + } + Some((_, '\n')) => lines += 1, + Some((_, '\\')) => { + if let Some((_, '\n')) = chars.next() { + lines += 1; + } + } + Some(_) => {}, + None => return None, + } + } + + let Some((_, ':')) = chars.next() else { return None; }; + + // [label]: destination 'title' + // ^^^ skip optional whitespace here + let mut pos = label_end + 2; + while let Some((_, ch @ (' ' | '\t' | '\n'))) = chars.next() { + if ch == '\n' { lines += 1; } + pos += 1; + } + + // [label]: destination 'title' + // ^^^^^^^^^^^ parse this + let href; + if let Some(res) = full_link::parse_link_destination(str, pos, str.len()) { + if pos == res.pos { return None; } + href = state.md.link_formatter.normalize_link(&res.str); + state.md.link_formatter.validate_link(&href)?; + pos = res.pos; + lines += res.lines; + } else { + return None; + } + + // save cursor state, we could require to rollback later + let dest_end_pos = pos; + let dest_end_lines = lines; + + // [label]: destination 'title' + // ^^^ skipping those spaces + let start = pos; + let mut chars = str[pos..].chars(); + while let Some(ch @ (' ' | '\t' | '\n')) = chars.next() { + if ch == '\n' { lines += 1; } + pos += 1; + } + + // [label]: destination 'title' + // ^^^^^^^ parse this + let mut title = None; + if pos != start { + if let Some(res) = full_link::parse_link_title(str, pos, str.len()) { + title = Some(res.str); + pos = res.pos; + lines += res.lines; + } else { + pos = dest_end_pos; + lines = dest_end_lines; + } + } + + // skip trailing spaces until the rest of the line + let mut chars = str[pos..].chars(); + loop { + match chars.next() { + Some(' ' | '\t') => pos += 1, + Some('\n') | None => break, + Some(_) if title.is_some() => { + // garbage at the end of the line after title, + // but it could still be a valid reference if we roll back + title = None; + pos = dest_end_pos; + lines = dest_end_lines; + chars = str[pos..].chars(); + } + Some(_) => { + // garbage at the end of the line + return None; + } + } + } + + let references = state.root_ext.get_or_insert_default::(); + if !references.insert(str[1..label_end].to_owned(), href.clone(), title.clone()) { return None; } + + Some((Node::new( + Definition { + label: str[1..label_end].to_owned(), + destination: href, + title + }), + lines + 1 + )) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/autolink.rs b/crates/markdown-it/src/plugins/cmark/inline/autolink.rs new file mode 100644 index 0000000000000000000000000000000000000000..a997830192d72fffaf47d1daac98001f774bd954 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/autolink.rs @@ -0,0 +1,87 @@ +//! Autolinks +//! +//! `` +//! +//! +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Autolink { + pub url: String, +} + +impl NodeValue for Autolink { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + attrs.push(("href", self.url.clone())); + + fmt.open("a", &attrs); + fmt.contents(&node.children); + fmt.close("a"); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.inline.add_rule::(); +} + +static AUTOLINK_RE : Lazy = Lazy::new(|| { + Regex::new(r"^([a-zA-Z][a-zA-Z0-9+.\-]{1,31}):([^<>\x00-\x20]*)$").unwrap() +}); + +static EMAIL_RE : Lazy = Lazy::new(|| { + Regex::new(r"^([a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)$").unwrap() +}); + +#[doc(hidden)] +pub struct AutolinkScanner; +impl InlineRule for AutolinkScanner { + const MARKER: char = '<'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '<' { return None; } + + let mut pos = state.pos + 2; + + loop { + match chars.next() { + Some('<') | None => return None, + Some('>') => break, + Some(x) => pos += x.len_utf8(), + } + } + + let url = &state.src[state.pos+1..pos-1]; + let is_autolink = AUTOLINK_RE.is_match(url); + let is_email = EMAIL_RE.is_match(url); + + if !is_autolink && !is_email { return None; } + + let full_url = if is_autolink { + state.md.link_formatter.normalize_link(url) + } else { + state.md.link_formatter.normalize_link(&("mailto:".to_owned() + url)) + }; + + state.md.link_formatter.validate_link(&full_url)?; + + let content = state.md.link_formatter.normalize_link_text(url); + + let mut inner_node = Node::new(TextSpecial { + content: content.clone(), + markup: content, + info: "autolink", + }); + inner_node.srcmap = state.get_map(state.pos + 1, pos - 1); + + let mut node = Node::new(Autolink { url: full_url }); + node.children.push(inner_node); + + Some((node, pos - state.pos)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/backticks.rs b/crates/markdown-it/src/plugins/cmark/inline/backticks.rs new file mode 100644 index 0000000000000000000000000000000000000000..b69b862292deb05b77676619a9e93027ca41fe2c --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/backticks.rs @@ -0,0 +1,28 @@ +//! Code spans +//! +//! `` `looks like this` `` +//! +//! +use crate::generics::inline::code_pair; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct CodeInline { + pub marker: char, + pub marker_len: usize, +} + +impl NodeValue for CodeInline { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.open("code", &node.attrs); + fmt.contents(&node.children); + fmt.close("code"); + } +} + +pub fn add(md: &mut MarkdownIt) { + code_pair::add_with::<'`'>(md, |len| Node::new(CodeInline { + marker: '`', + marker_len: len, + })); +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/emphasis.rs b/crates/markdown-it/src/plugins/cmark/inline/emphasis.rs new file mode 100644 index 0000000000000000000000000000000000000000..726c4f80d7812f6a2a025b4c99e8c8a2e0e0229b --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/emphasis.rs @@ -0,0 +1,40 @@ +//! Emphasis and strong emphasis +//! +//! looks like `*this*` or `__that__` +//! +//! +use crate::generics::inline::emph_pair; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Em { + pub marker: char +} + +impl NodeValue for Em { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.open("em", &node.attrs); + fmt.contents(&node.children); + fmt.close("em"); + } +} + +#[derive(Debug)] +pub struct Strong { + pub marker: char +} + +impl NodeValue for Strong { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.open("strong", &node.attrs); + fmt.contents(&node.children); + fmt.close("strong"); + } +} + +pub fn add(md: &mut MarkdownIt) { + emph_pair::add_with::<'*', 1, true> (md, || Node::new(Em { marker: '*' })); + emph_pair::add_with::<'_', 1, false> (md, || Node::new(Em { marker: '_' })); + emph_pair::add_with::<'*', 2, true> (md, || Node::new(Strong { marker: '*' })); + emph_pair::add_with::<'_', 2, false> (md, || Node::new(Strong { marker: '_' })); +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/entity.rs b/crates/markdown-it/src/plugins/cmark/inline/entity.rs new file mode 100644 index 0000000000000000000000000000000000000000..f4b341f927af58751845870cf0f560c33aab0972 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/entity.rs @@ -0,0 +1,85 @@ +//! Entity and numeric character references +//! +//! `{`, `¯`, `"` +//! +//! +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::common::utils::{get_entity_from_str, is_valid_entity_code}; +use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; +use crate::{MarkdownIt, Node}; + +pub fn add(md: &mut MarkdownIt) { + md.inline.add_rule::(); +} + +static DIGITAL_RE : Lazy = Lazy::new(|| { + Regex::new("(?i)^&#((?:x[a-f0-9]{1,6}|[0-9]{1,7}));").unwrap() +}); + +static NAMED_RE : Lazy = Lazy::new(|| { + Regex::new("(?i)^&([a-z][a-z0-9]{1,31});").unwrap() +}); + +#[doc(hidden)] +pub struct EntityScanner; + +impl EntityScanner { + fn parse_digital_entity(state: &mut InlineState) -> Option<(Node, usize)> { + let capture = DIGITAL_RE.captures(&state.src[state.pos..])?; + let entity_len = capture[0].len(); + let entity = &capture[1]; + #[allow(clippy::from_str_radix_10)] + let code = if entity.starts_with('x') || entity.starts_with('X') { + u32::from_str_radix(&entity[1..], 16).unwrap() + } else { + u32::from_str_radix(entity, 10).unwrap() + }; + + let content_str = if is_valid_entity_code(code) { + char::from_u32(code).unwrap().into() + } else { + '\u{FFFD}'.into() + }; + + let markup_str = capture[0].to_owned(); + + let node = Node::new(TextSpecial { + content: content_str, + markup: markup_str, + info: "entity", + }); + Some((node, entity_len)) + } + + fn parse_named_entity(state: &mut InlineState) -> Option<(Node, usize)> { + let capture = NAMED_RE.captures(&state.src[state.pos..])?; + let str = get_entity_from_str(&capture[0])?; + let entity_len = capture[0].len(); + let markup_str = capture[0].to_owned(); + let content_str = (*str).to_owned(); + + let node = Node::new(TextSpecial { + content: content_str, + markup: markup_str, + info: "entity", + }); + Some((node, entity_len)) + } +} + +impl InlineRule for EntityScanner { + const MARKER: char = '&'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '&' { return None; } + + if let Some('#') = chars.next() { + Self::parse_digital_entity(state) + } else { + Self::parse_named_entity(state) + } + } +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/escape.rs b/crates/markdown-it/src/plugins/cmark/inline/escape.rs new file mode 100644 index 0000000000000000000000000000000000000000..8755f4f49ea1911d9cbe1bc0094ab0c775ca9198 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/escape.rs @@ -0,0 +1,57 @@ +//! Backslash escapes +//! +//! Allows escapes like `\*hello*`, also processes hard breaks at the end +//! of the line. +//! +//! +use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; +use crate::plugins::cmark::inline::newline::Hardbreak; +use crate::{MarkdownIt, Node}; + +pub fn add(md: &mut MarkdownIt) { + md.inline.add_rule::(); +} + +#[doc(hidden)] +pub struct EscapeScanner; +impl InlineRule for EscapeScanner { + const MARKER: char = '\\'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '\\' { return None; } + + match chars.next() { + Some('\n') => { + // skip leading whitespaces from next line + let mut len = 2; + while let Some(' ' | '\t') = chars.next() { + len += 1; + } + Some((Node::new(Hardbreak), len)) + } + Some(chr) => { + let start = state.pos; + let end = state.pos + 1 + chr.len_utf8(); + + let mut orig_str = "\\".to_owned(); + orig_str.push(chr); + + let content_str = match chr { + '\\' | '!' | '"' | '#' | '$' | '%' | '&' | '\'' | '(' | ')' | + '*' | '+' | ',' | '.' | '/' | ':' | ';' | '<' | '=' | '>' | '?' | + '@' | '[' | ']' | '^' | '_' | '`' | '{' | '|' | '}' | '~' | '-' => chr.into(), + _ => orig_str.clone() + }; + + let node = Node::new(TextSpecial { + content: content_str, + markup: orig_str, + info: "escape", + }); + Some((node, end - start)) + } + None => None + } + } +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/image.rs b/crates/markdown-it/src/plugins/cmark/inline/image.rs new file mode 100644 index 0000000000000000000000000000000000000000..f8db475d160b0d0c367b95497bac86f09d5477a4 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/image.rs @@ -0,0 +1,34 @@ +//! Images +//! +//! `![image]( "title")` +//! +//! +use crate::generics::inline::full_link; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Image { + pub url: String, + pub title: Option, +} + +impl NodeValue for Image { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + attrs.push(("src", self.url.clone())); + attrs.push(("alt", node.collect_text())); + + if let Some(title) = &self.title { + attrs.push(("title", title.clone())); + } + + fmt.self_close("img", &attrs); + } +} + +pub fn add(md: &mut MarkdownIt) { + full_link::add_prefix::<'!', true>(md, |href, title| Node::new(Image { + url: href.unwrap_or_default(), + title, + })); +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/link.rs b/crates/markdown-it/src/plugins/cmark/inline/link.rs new file mode 100644 index 0000000000000000000000000000000000000000..0342f2c22208780f93204b8b6bb69f638afe43e4 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/link.rs @@ -0,0 +1,35 @@ +//! Links +//! +//! `![link]( "stuff")` +//! +//! +use crate::generics::inline::full_link; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Link { + pub url: String, + pub title: Option, +} + +impl NodeValue for Link { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + attrs.push(("href", self.url.clone())); + + if let Some(title) = &self.title { + attrs.push(("title", title.clone())); + } + + fmt.open("a", &attrs); + fmt.contents(&node.children); + fmt.close("a"); + } +} + +pub fn add(md: &mut MarkdownIt) { + full_link::add::(md, |href, title| Node::new(Link { + url: href.unwrap_or_default(), + title, + })); +} diff --git a/crates/markdown-it/src/plugins/cmark/inline/mod.rs b/crates/markdown-it/src/plugins/cmark/inline/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..8a956436cce2c253f0bcd13dc90c3b41f8d306f2 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/mod.rs @@ -0,0 +1,9 @@ +//! Inline-level CommonMark structures. +pub mod autolink; +pub mod backticks; +pub mod emphasis; +pub mod entity; +pub mod escape; +pub mod image; +pub mod link; +pub mod newline; diff --git a/crates/markdown-it/src/plugins/cmark/inline/newline.rs b/crates/markdown-it/src/plugins/cmark/inline/newline.rs new file mode 100644 index 0000000000000000000000000000000000000000..119c14a4a06abe0d086aff171af3804fe86453ee --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/inline/newline.rs @@ -0,0 +1,81 @@ +//! Line breaks +//! +//! Processes EOL (`\n`, soft and hard breaks). +//! +//! - +//! - +use crate::parser::inline::{InlineRule, InlineState}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Hardbreak; + +impl NodeValue for Hardbreak { + fn render(&self, _: &Node, fmt: &mut dyn Renderer) { + fmt.self_close("br", &[]); + fmt.cr(); + } +} + +#[derive(Debug)] +pub struct Softbreak; + +impl NodeValue for Softbreak { + fn render(&self, _: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.inline.add_rule::(); +} + +#[doc(hidden)] +pub struct NewlineScanner; +impl InlineRule for NewlineScanner { + const MARKER: char = '\n'; + + fn check(state: &mut InlineState) -> Option { + // check rule is required because run() modifies trailing text + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != '\n' { return None; } + Some(1) + } + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + + if chars.next().unwrap() != '\n' { return None; } + + let mut pos = state.pos; + pos += 1; + + // skip leading whitespaces from next line + while let Some(' ' | '\t') = chars.next() { + pos += 1; + } + + // ' \n' -> hardbreak + let mut tail_size = 0; + let trailing_text = state.trailing_text_get(); + + for ch in trailing_text.chars().rev() { + if ch == ' ' { + tail_size += 1; + } else { + break; + } + } + + state.trailing_text_pop(tail_size); + + let node = if tail_size >= 2 { + Node::new(Hardbreak) + } else { + Node::new(Softbreak) + }; + + state.pos -= tail_size; // backtrack to include tail in source maps + Some((node, pos - state.pos)) + } +} diff --git a/crates/markdown-it/src/plugins/cmark/mod.rs b/crates/markdown-it/src/plugins/cmark/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..334b7d09e181fe12214f6b7680bf29fa2e474686 --- /dev/null +++ b/crates/markdown-it/src/plugins/cmark/mod.rs @@ -0,0 +1,34 @@ +//! Basic markdown syntax, you probably want to add this. +//! +//! This is full implementation of [CommonMark](https://spec.commonmark.org/0.30/) +//! standard (with the exception of HTML inlines/blocks which are moved to separate +//! [plugin](crate::plugins::html) for security reasons). +//! +//! [cmark::add](self::add) function adds all features at once. If you only want +//! to enable some of it (e.g. disable images), you can add each syntax one by one +//! by invoking `add` function of the respective module. +pub mod block; +pub mod inline; + +use crate::MarkdownIt; + +pub fn add(md: &mut MarkdownIt) { + inline::newline::add(md); + inline::escape::add(md); + inline::backticks::add(md); + inline::emphasis::add(md); + inline::link::add(md); + inline::image::add(md); + inline::autolink::add(md); + inline::entity::add(md); + + block::code::add(md); + block::fence::add(md); + block::blockquote::add(md); + block::hr::add(md); + block::list::add(md); + block::reference::add(md); + block::heading::add(md); + block::lheading::add(md); + block::paragraph::add(md); +} diff --git a/crates/markdown-it/src/plugins/extra/beautify_links.rs b/crates/markdown-it/src/plugins/extra/beautify_links.rs new file mode 100644 index 0000000000000000000000000000000000000000..d60bc1241d925af7eec1e2ec56a3bf188aaab220 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/beautify_links.rs @@ -0,0 +1,39 @@ +//! Pretty-print all urls and fit them into N characters + +use crate::parser::linkfmt::{LinkFormatter, MDLinkFormatter}; +use crate::MarkdownIt; + +#[derive(Debug)] +struct LinkBeautifier { + max_length: usize, + parent: Box, +} + +impl LinkFormatter for LinkBeautifier { + fn validate_link(&self, url: &str) -> Option<()> { + self.parent.as_ref().validate_link(url) + } + + fn normalize_link(&self, url: &str) -> String { + mdurl::format_url_for_computers(url) + } + + fn normalize_link_text(&self, url: &str) -> String { + mdurl::format_url_for_humans(url, self.max_length) + } +} + + +/// Add beautifier plugin, limiting urls to default 50 characters +pub fn add(md: &mut MarkdownIt) { + add_with_char_limit(md, 50); +} + +/// Add beautifier plugin, limiting urls to `max_length` characters +pub fn add_with_char_limit(md: &mut MarkdownIt, max_length: usize) { + let parent = std::mem::replace(&mut md.link_formatter, Box::new(MDLinkFormatter::new())); + md.link_formatter = Box::new(LinkBeautifier { + max_length, + parent, + }); +} diff --git a/crates/markdown-it/src/plugins/extra/heading_anchors.rs b/crates/markdown-it/src/plugins/extra/heading_anchors.rs new file mode 100644 index 0000000000000000000000000000000000000000..1062b10125bcefcf43cc3dbaa02d9a54dd788c82 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/heading_anchors.rs @@ -0,0 +1,69 @@ +//! Add id attribute (slug) to headings. +//! +//! ```rust +//! // it is recommended to use 3rd party slug implementation +//! //let slugify_fn = |s: &str| slug::slugify(s); +//! let slugify_fn = markdown_it::plugins::extra::heading_anchors::simple_slugify_fn; +//! +//! let md = &mut markdown_it::MarkdownIt::new(); +//! markdown_it::plugins::cmark::add(md); +//! markdown_it::plugins::extra::heading_anchors::add(md, slugify_fn); +//! +//! assert_eq!( +//! md.parse("## An example heading").render(), +//! "

An example heading

\n", +//! ); +//! ``` +use std::fmt::Debug; + +use crate::parser::core::CoreRule; +use crate::parser::extset::MarkdownItExt; +use crate::plugins::cmark::block::heading::ATXHeading; +use crate::plugins::cmark::block::lheading::SetextHeader; +use crate::{MarkdownIt, Node}; + +pub fn add(md: &mut MarkdownIt, slugify: fn (&str) -> String) { + md.ext.insert(SlugifyFunction(slugify)); + md.add_rule::(); +} + +/// Simple built-in slugify function. It is added for testing and demonstration +/// purposes only, you should be using `slug`/`slugify` crate instead or your own impl. +pub fn simple_slugify_fn(s: &str) -> String { + s.chars().map(|x| { + if x.is_alphanumeric() { + x.to_ascii_lowercase() + } else { + '-' + } + }).collect() +} + +#[derive(Clone, Copy)] +struct SlugifyFunction(fn (&str) -> String); +impl MarkdownItExt for SlugifyFunction {} + +impl Default for SlugifyFunction { + fn default() -> Self { + Self(simple_slugify_fn) + } +} + +impl Debug for SlugifyFunction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SlugifyFunction").finish() + } +} + +pub struct AddHeadingAnchors; +impl CoreRule for AddHeadingAnchors { + fn run(root: &mut Node, md: &MarkdownIt) { + let slugify = md.ext.get::().copied().unwrap_or_default().0; + + root.walk_mut(|node, _| { + if node.is::() || node.is::() { + node.attrs.push(("id", slugify(&node.collect_text()))); + } + }); + } +} diff --git a/crates/markdown-it/src/plugins/extra/linkify.rs b/crates/markdown-it/src/plugins/extra/linkify.rs new file mode 100644 index 0000000000000000000000000000000000000000..40d2e5e36a55a0fd14b63f778229503d1e334a70 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/linkify.rs @@ -0,0 +1,135 @@ +//! Find urls and emails, and turn them into links + +use linkify::{LinkFinder, LinkKind}; +use once_cell::sync::Lazy; +use regex::Regex; +use std::cmp::Ordering; + +use crate::parser::core::{CoreRule, Root}; +use crate::parser::extset::RootExt; +use crate::parser::inline::builtin::InlineParserRule; +use crate::parser::inline::{InlineRule, InlineState, TextSpecial}; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +static SCHEME_RE : Lazy = Lazy::new(|| { + Regex::new(r"(?i)(?:^|[^a-z0-9.+-])([a-z][a-z0-9.+-]*)$").unwrap() +}); + +#[derive(Debug)] +pub struct Linkified { + pub url: String, +} + +impl NodeValue for Linkified { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let mut attrs = node.attrs.clone(); + attrs.push(("href", self.url.clone())); + + fmt.open("a", &attrs); + fmt.contents(&node.children); + fmt.close("a"); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.add_rule::() + .before::(); + + md.inline.add_rule::(); +} + +type LinkifyState = Vec; +impl RootExt for LinkifyState {} + +#[derive(Debug, Clone, Copy)] +struct LinkifyPosition { + start: usize, + end: usize, + //email: bool, +} + +#[doc(hidden)] +pub struct LinkifyPrescan; +impl CoreRule for LinkifyPrescan { + fn run(root: &mut Node, _: &MarkdownIt) { + let root_data = root.cast_mut::().unwrap(); + let source = root_data.content.as_str(); + let finder = LinkFinder::new(); + let positions = finder.links(source).filter_map(|link| { + if *link.kind() == LinkKind::Url { + Some(LinkifyPosition { + start: link.start(), + end: link.end(), + //email: *link.kind() == LinkKind::Email, + }) + } else { + None + } + }).collect::>(); + root_data.ext.insert(positions); + } +} + +#[doc(hidden)] +pub struct LinkifyScanner; +impl InlineRule for LinkifyScanner { + const MARKER: char = ':'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let mut chars = state.src[state.pos..state.pos_max].chars(); + if chars.next().unwrap() != ':' { return None; } + if state.link_level > 0 { return None; } + + let trailing = state.trailing_text_get(); + if !SCHEME_RE.is_match(trailing) { return None; } + + let map = state.get_map(state.pos, state.pos_max)?; + let (start, _) = map.get_byte_offsets(); + + let positions = state.root_ext.get::().unwrap(); + + let found_idx = positions.binary_search_by(|x| { + if x.start >= start { + Ordering::Greater + } else if x.end <= start { + Ordering::Less + } else { + Ordering::Equal + } + }).ok()?; + + let found = positions[found_idx]; + let proto_size = start - found.start; + if proto_size > trailing.len() { return None; } + + debug_assert_eq!( + &trailing[trailing.len()-proto_size..], + &state.src[state.pos-proto_size..state.pos] + ); + + let url_start = state.pos - proto_size; + let url_end = state.pos - proto_size + found.end - found.start; + if url_end > state.pos_max { return None; } + + let url = &state.src[url_start..url_end]; + let full_url = state.md.link_formatter.normalize_link(url); + + state.md.link_formatter.validate_link(&full_url)?; + + let content = state.md.link_formatter.normalize_link_text(url); + + let mut inner_node = Node::new(TextSpecial { + content: content.clone(), + markup: content, + info: "autolink", + }); + inner_node.srcmap = state.get_map(url_start, url_end); + + let mut node = Node::new(Linkified { url: full_url }); + node.children.push(inner_node); + + state.trailing_text_pop(proto_size); + state.pos -= proto_size; + Some((node, url_end - url_start)) + } +} diff --git a/crates/markdown-it/src/plugins/extra/mod.rs b/crates/markdown-it/src/plugins/extra/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..9be23833be2fd21e2b42ded8fcaea969c42ea88f --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/mod.rs @@ -0,0 +1,44 @@ +//! Frequently used markdown extensions and stuff from GFM. +//! +//! - strikethrough (~~xxx~~~) +//! - tables +//! - linkify (convert http://example.com to a link) +//! - beautify links (cut "http://" from links and shorten paths) +//! - smartquotes and typographer +//! - code block highlighting using `syntect` +//! +//! ```rust +//! let md = &mut markdown_it::MarkdownIt::new(); +//! markdown_it::plugins::cmark::add(md); +//! markdown_it::plugins::extra::add(md); +//! +//! let html = md.parse("hello ~~world~~").render(); +//! assert_eq!(html.trim(), r#"

hello world

"#); +//! +//! let html = md.parse(r#"Markdown done "The Right Way(TM)""#).render(); +//! assert_eq!(html.trim(), r#"

Markdown done “The Right Way™”

"#); +//! ``` +pub mod beautify_links; +pub mod heading_anchors; +#[cfg(feature = "linkify")] +pub mod linkify; +pub mod smartquotes; +pub mod strikethrough; +#[cfg(feature = "syntect")] +pub mod syntect; +pub mod tables; +pub mod typographer; + +use crate::MarkdownIt; + +pub fn add(md: &mut MarkdownIt) { + strikethrough::add(md); + beautify_links::add(md); + #[cfg(feature = "linkify")] + linkify::add(md); + tables::add(md); + #[cfg(feature = "syntect")] + syntect::add(md); + typographer::add(md); + smartquotes::add(md); +} diff --git a/crates/markdown-it/src/plugins/extra/smartquotes.rs b/crates/markdown-it/src/plugins/extra/smartquotes.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a7bc0708db9dd2679d68ee0ea83e6102057660b --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/smartquotes.rs @@ -0,0 +1,567 @@ +//! Replaces `"` and `'` quotes with "nicer" ones like `‘`, `’`, `“`, `”`, or +//! with `’` for words like "isn't". +//! +//! This currently only supports single character quotes, which is a limitation +//! of the Rust implementation due to the use of `const` generics. +//! +//! ## Implementation notes +//! +//! The main obstacle to implementing this was the fact that the document is +//! necessarily represented as a tree of nodes. +//! Each node is thus necessarily referenced by its parents, which means that an +//! any given moment we cannot hold a mutable reference to a node if any other +//! part of the code holds a reference to the document. At least that's my +//! understanding of the problem. +//! The smartquotes algorithm from the JS library makes heavy use of iteration +//! backwards and forwards through a flat list of tokens. This isn't really +//! possible in the Rust implementation. Building a flat representation of all +//! `Node` objects is easy, but holding that list precludes us from executing a +//! `root.walk_mut` call at the same time. +//! On top of that, while the smartquotes algorithm iterates linearly over all +//! nodes/tokens, looking at a specific token with index `j` can trigger +//! replacements in any of the tokens with `0` to `j - 1`. +//! +//! The solution proposed here is to first compute all the replacement +//! operations on a read-only flat view of the document, and _then_ to perform +//! all replacements in a single call to `root.walk_mut`. +use std::collections::HashMap; + +use crate::common::utils::is_punct_char; +use crate::parser::core::CoreRule; +use crate::parser::inline::Text; +use crate::plugins::cmark::block::paragraph::Paragraph; +use crate::plugins::cmark::inline::newline::{Hardbreak, Softbreak}; +use crate::plugins::html::html_inline::HtmlInline; +use crate::{MarkdownIt, Node}; + +const APOSTROPHE: char = '\u{2019}'; +const SINGLE_QUOTE: char = '\''; +const DOUBLE_QUOTE: char = '"'; +const SPACE: char = ' '; + +/// Add smartquotes with the "classic" quote set of `‘`, `’`, `“`, and `”`. +pub fn add(md: &mut MarkdownIt) { + add_with::<'‘', '’', '“', '”'>(md); +} + +pub fn add_with< + const OPEN_SINGLE_QUOTE: char, + const CLOSE_SINGLE_QUOTE: char, + const OPEN_DOUBLE_QUOTE: char, + const CLOSE_DOUBLE_QUOTE: char, +>( + md: &mut MarkdownIt, +) { + md.add_rule::>(); +} + +/// Simplified Node type that only holds the info we need +/// +/// To replace quotes, we'll be iterating forward and backward over the nodes in +/// our document tree. The `Node` class doesn't provide a mechanism to do this +/// efficiently, and in any case we only care about certain parts of the +/// information. This struct will be used to build a flat view of the document; +/// the `Irrelevant` variant serves as a "filler" so that the indexes of the +/// entries line up correctly with the order we see during tree traversal. +enum FlatToken<'a> { + LineBreak, + Text { + content: &'a str, + nesting_level: u32, + }, + HtmlInline { + content: &'a str, + }, + Irrelevant, +} + +/// A simple enum to distinguish single and double quotes +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +enum QuoteType { + Single, + Double, +} + +/// Holds information about quotes we have encountered thus far. +/// +/// These quotes may or may not be used to close a pair further down the line. +/// The different fields thus hold all the information we need to a) decide +/// whether or not to match them up with another quote we encounter, and b) to +/// perform the correct replacement, should be indeed use this quote to close a +/// pair. +struct QuoteMarker { + /// The iteration index of the node in which this quote was found. + /// + /// This is the index at which this quote's `Node` appears in a pre-order + /// depth-first walk of the document tree. Since we can only _modify_ nodes + /// during a walk, we rely on this index to tell us which nodes to modify. + walk_index: usize, + /// The position of the quote within node's `content` + quote_position: usize, + /// Whether this is a single or a double quote + quote_type: QuoteType, + /// Nesting level of the containing token + /// + /// This is the nesting of the containing `Node` within the document tree. + /// It is used to decide which quotes can be matched up. + level: u32, +} + +/// Description of a single quote replacement to be executed +/// +/// As described above, we have to compute the replacements in a first step that +/// treats the entire document tree read-only. Only then can we perform the +/// actual replacements. This `struct` holds the information we need to perform +/// the replacement of a single quote character during a `walk_mut`. +struct ReplacementOp { + walk_index: usize, + quote_position: usize, + quote: char, +} + +pub struct SmartQuotesRule< + const OPEN_SINGLE_QUOTE: char, + const CLOSE_SINGLE_QUOTE: char, + const OPEN_DOUBLE_QUOTE: char, + const CLOSE_DOUBLE_QUOTE: char, +>; + +impl< + const OPEN_SINGLE_QUOTE: char, + const CLOSE_SINGLE_QUOTE: char, + const OPEN_DOUBLE_QUOTE: char, + const CLOSE_DOUBLE_QUOTE: char, + > CoreRule + for SmartQuotesRule< + OPEN_SINGLE_QUOTE, + CLOSE_SINGLE_QUOTE, + OPEN_DOUBLE_QUOTE, + CLOSE_DOUBLE_QUOTE, + > +{ + fn run(root: &mut Node, _: &MarkdownIt) { + let text_tokens = all_text_tokens(root); + + let replacement_ops = Self::compute_replacements(text_tokens); + + // now that we know what we want to replace where, we go over the nodes a _third_ time to do all the actual replacements. + let mut current_index: usize = 0; + + root.walk_mut(|node, _| { + if let Some(current_replacements) = replacement_ops.get(¤t_index) { + let text_node = node.cast_mut::() + .expect("Expected to find a text node at this index because we constructed our replacements HashMap accordingly."); + text_node.content = execute_replacements(current_replacements, &text_node.content); + }; + current_index += 1; + }); + } +} + +impl< + const OPEN_SINGLE_QUOTE: char, + const CLOSE_SINGLE_QUOTE: char, + const OPEN_DOUBLE_QUOTE: char, + const CLOSE_DOUBLE_QUOTE: char, + > + SmartQuotesRule +{ + /// Walk the list of tokens to figure out what needs replacing where. to do + /// this, we need to search back and forth over the nodes to find matching + /// quotes across nodes. The borrow checker won't let us handle the entire + /// set of nodes as mutable at the same time however, so all we do here is + /// figure out what we _want_ to replace in which node. + fn compute_replacements(text_tokens: Vec) -> HashMap> { + let mut quote_stack: Vec = Vec::new(); + let mut replacement_ops: HashMap> = HashMap::new(); + for (walk_index, token) in text_tokens.iter().enumerate() { + if let FlatToken::Text { + content, + nesting_level, + } = token + { + for op in Self::replace_smartquotes( + content, + walk_index, + *nesting_level, + &text_tokens, + &mut quote_stack, + ) { + replacement_ops + .entry(op.walk_index) + .or_default() + .insert(op.quote_position, op.quote); + } + } + } + replacement_ops + } + + /// Compute quote replacements found by looking at a single text block + fn replace_smartquotes( + content: &str, + walk_index: usize, + level: u32, + text_tokens: &[FlatToken], + quote_stack: &mut Vec, + ) -> Vec { + truncate_stack(quote_stack, level); + + let mut result: Vec<_> = Vec::new(); + for (quote_position, quote_type) in find_quotes(content) { + let last_char = find_last_char_before(text_tokens, walk_index, quote_position); + let next_char = find_first_char_after(text_tokens, walk_index, quote_position); + + let (can_open, can_close): (bool, bool) = + can_open_or_close("e_type, last_char, next_char); + + if !can_open && !can_close { + // if this is a single quote then we're in the middle of a word and + // assume it to be an apostrophe + if quote_type == QuoteType::Single { + result.push(ReplacementOp { + walk_index, + quote_position, + quote: APOSTROPHE, + }); + } + // in any case, we're done with this quote and continue searching + // for more quotes in this text block + continue; + } + + if can_close { + if let Some((opening_op, closing_op, new_stack_len)) = + Self::try_close(quote_stack, walk_index, level, quote_type, quote_position) + { + quote_stack.truncate(new_stack_len); + result.push(opening_op); + result.push(closing_op); + continue; + } + } + + if can_open { + quote_stack.push(QuoteMarker { + walk_index, + quote_position, + quote_type, + level, + }); + } else if can_close && quote_type == QuoteType::Single { + result.push(ReplacementOp { + walk_index, + quote_position, + quote: APOSTROPHE, + }); + } + } + result + } + + /// Try to find a matching opening quote to the given one. + /// + /// If a match is found, returns `Some` with two `ReplacementOp`s to be + /// added to the result, and with the resulting length of the `quote_stack`. + fn try_close( + quote_stack: &[QuoteMarker], + walk_index: usize, + level: u32, + quote_type: QuoteType, + quote_position: usize, + ) -> Option<(ReplacementOp, ReplacementOp, usize)> { + for (j, other_item) in quote_stack.iter().enumerate().rev() { + if other_item.level < level { + return None; + } + if other_item.quote_type == quote_type && other_item.level == level { + return Some(( + ReplacementOp { + walk_index: other_item.walk_index, + quote_position: other_item.quote_position, + quote: if quote_type == QuoteType::Single { + OPEN_SINGLE_QUOTE + } else { + OPEN_DOUBLE_QUOTE + }, + }, + ReplacementOp { + walk_index, + quote_position, + quote: if quote_type == QuoteType::Single { + CLOSE_SINGLE_QUOTE + } else { + CLOSE_DOUBLE_QUOTE + }, + }, + j, + )); + } + } + None + } +} + +/// Produces a simplified flat list of all tokens, with the necessary +/// information to do smart quote replacement. +/// +/// This handles inline html and inline code like JS version seems to do. +/// This list is a work-around for the fact that we can't build a flat list of +/// all nodes for iteration back and forth, and at the same time do a mutable +/// walk on the document tree. +/// +/// Returns a `Vec>` where `<'a>` is the same lifetime as `root`. +/// This simply reflects the fact that the `content: &str` entries of the +/// `FlatToken` structs reference the same memory as `root`'s children. +/// Every entry in the `Vec` will produce an entry in the result, meaning that +/// the index of a token in the resulting `Vec` will be the same as the index it +/// would get during a `root.walk` call. +fn all_text_tokens(root: &Node) -> Vec { + let mut result = Vec::new(); + let mut walk_index = 0; + root.walk(|node, nesting_level| { + if let Some(text_node) = node.cast::() { + result.push(FlatToken::Text { + content: &text_node.content, + nesting_level, + }); + } else if let Some(html_node) = node.cast::() { + result.push(FlatToken::HtmlInline { + content: &html_node.content, + }); + } else if node.is::() || node.is::() || node.is::() { + result.push(FlatToken::LineBreak); + } else { + result.push(FlatToken::Irrelevant); + } + walk_index += 1; + }); + result +} + +/// Checks whether we can open or close a pair of quotes, given the quote type +/// and the type of characters before and after the quote +fn can_open_or_close(quote_type: &QuoteType, last_char: char, next_char: char) -> (bool, bool) { + // special case: 1"" -> count first quote as an inch + // We handle this before doing anything else to simplify the conditions + // below. + let is_double = *quote_type == QuoteType::Double; + let next_is_double = next_char == DOUBLE_QUOTE; + let last_is_digit = last_char.is_ascii_digit(); + if next_is_double && is_double && last_is_digit { + return (false, false); + } + + // using `is_ascii_punctuation` here matches the JS version exactly, but + // that also means we might inherit that implementation's shortcomings + // by ignoring unicode punctuation. `is_punct_char` however should + // compensate for this. + let is_last_punctuation = last_char.is_ascii_punctuation() || is_punct_char(last_char); + let is_next_punctuation = next_char.is_ascii_punctuation() || is_punct_char(next_char); + + // Yet again we rely on rust's built-in character handling. The definition + // of `is_whitespace` according to the unicode proplist.txt shows that the + // difference to the JS version. + // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt + // + // Recognized as whitespace by Rust, but not by JS: + // 0x85, 0x28, 0x29 + let is_last_whitespace = last_char.is_whitespace(); + let is_next_whitespace = next_char.is_whitespace(); + + let can_open = + !is_next_whitespace && (!is_next_punctuation || is_last_whitespace || is_last_punctuation); + let can_close = + !is_last_whitespace && (!is_last_punctuation || is_next_whitespace || is_next_punctuation); + + if can_open && can_close { + // Replace quotes in the middle of punctuation sequence, but not + // in the middle of the words, i.e.: + // + // 1. foo " bar " baz - not replaced + // 2. foo-"-bar-"-baz - replaced + // 3. foo"bar"baz - not replaced + return (is_last_punctuation, is_next_punctuation); + } + + (can_open, can_close) +} + +/// Executes a set of character replacements on a string +fn execute_replacements(replacement_ops: &HashMap, content: &str) -> String { + content + .chars() + .enumerate() + .map(|(i, c)| *replacement_ops.get(&i).unwrap_or(&c)) + .collect() +} + +/// Truncates the stack of quotes following the JS implementation. +/// +/// This _might_ be simplified by removing the `rev` call and using +/// `Vec::take_while` instead, but I'm not 100% sure yet that the levels on the +/// stack are really monotonously increasing, so I'm leaving it as is for now. +fn truncate_stack(quote_stack: &mut Vec, level: u32) { + let stack_len = quote_stack + .iter() + .rev() + .skip_while(|qm| qm.level > level) + .count(); + quote_stack.truncate(stack_len); +} + +/// Finds all single or double quotes in a string, together with their positions +/// +/// This might be replaced with a regex search, but not sure that's really worth +/// it, given that we only check for two fixed characters. +fn find_quotes(content: &str) -> impl Iterator + '_ { + content.chars().enumerate().filter_map(|(p, c)| { + if c == SINGLE_QUOTE || c == DOUBLE_QUOTE { + Some(( + p, + if c == SINGLE_QUOTE { + QuoteType::Single + } else { + QuoteType::Double + }, + )) + } else { + None + } + }) +} + +/// Finds the next relevant character after a given position +/// +/// This is the mirror image of `find_last_char_before`. +/// +/// The position given is that of a quote we found. It is identified by its +/// token/node index and the position of the quote inside that token. The full +/// sequence of the text tokens is searched forwards from that point and the +/// first character is returned. +/// +/// If a line break or the end of the document is encountered during search, +/// space (0x20) is returned. +/// +/// This function is a bit simpler than `find_last_char_before` because Vec +/// conveniently returns None for out-of-range indexes at the top end, while not +/// allowing to index with negative index. +fn find_first_char_after( + text_tokens: &[FlatToken], + token_index: usize, + quote_position: usize, +) -> char { + for (idx_t, text_token) in text_tokens.iter().enumerate().skip(token_index) { + let token = match text_token { + FlatToken::LineBreak => return SPACE, + FlatToken::Text { + content, + nesting_level: _, + } => content, + FlatToken::HtmlInline { + content, + } => content, + FlatToken::Irrelevant => continue, + }; + let start_index = if idx_t == token_index { + quote_position + 1 + } else { + 0 + }; + if let Some(c) = token.chars().nth(start_index) { + return c; + } + } + // this will be hit if we start searching at the last position of the last + // text token + SPACE +} + +/// Finds the last relevant character before a given position +/// +/// The position given is that of a quote we found. It is identified by its +/// token/node index and the position of the quote inside that token. The full +/// sequence of the text tokens is searched backwards from that point and the +/// first character is returned. +/// +/// If a line break or the beginning of the document is encountered during +/// search, space (0x20) is returned. +fn find_last_char_before( + text_tokens: &[FlatToken], + token_index: usize, + quote_position: usize, +) -> char { + for idx_t in (0..=token_index).rev() { + let token = match &text_tokens[idx_t] { + FlatToken::LineBreak => return SPACE, + FlatToken::Text { + content, + nesting_level: _, + } => content, + FlatToken::HtmlInline { + content, + } => content, + FlatToken::Irrelevant => continue, + }; + + // this is _not_ the first index we want to look at, but rather the + // index just _after_ that. The reason is simply that this is `usize` + // and we want to first check if it's possible to still subtract 1 from + // it without panicking. + let start_index: usize = if idx_t == token_index { + quote_position + } else { + token.chars().count() + }; + // means we can't go any further left -> try the next token (i.e. the + // one preceding this one) + if start_index == 0 { + continue; + } + // unwrapping is safe here, we built our index to match the length of + // the string, or (in the case of the token containing the quote itself) + // it should be indexing a _prefix_ of the string. + return token.chars().nth(start_index - 1).unwrap(); + } + // this will be hit if we find a quote in the first position of the first token + SPACE +} + + +#[cfg(test)] +mod tests { + #[test] + fn smartquotes_basics() { + let md = &mut crate::MarkdownIt::new(); + crate::plugins::cmark::add(md); + crate::plugins::extra::smartquotes::add(md); + let html = md.parse(r#"'hello' "world""#).render(); + assert_eq!(html.trim(), r#"

‘hello’ “world”

"#); + } + + #[test] + fn smartquotes_shouldnt_affect_html() { + let md = &mut crate::MarkdownIt::new(); + crate::plugins::cmark::add(md); + crate::plugins::html::html_inline::add(md); + crate::plugins::extra::smartquotes::add(md); + let html = md.parse(r#""#).render(); + assert_eq!(html.trim(), r#"

"#); + } + + #[test] + fn smartquotes_should_work_with_typographer() { + // regression test for https://github.com/rlidwka/markdown-it.rs/issues/26 + let md = &mut crate::MarkdownIt::new(); + crate::plugins::cmark::add(md); + crate::plugins::html::html_inline::add(md); + crate::plugins::extra::typographer::add(md); + crate::plugins::extra::smartquotes::add(md); + let html = md.parse("\"**...**\"").render(); + assert_eq!(html.trim(), "

“…”

"); + } +} diff --git a/crates/markdown-it/src/plugins/extra/strikethrough.rs b/crates/markdown-it/src/plugins/extra/strikethrough.rs new file mode 100644 index 0000000000000000000000000000000000000000..c3190ef514e4c1b9c1f29d3982dcbd9d588879a1 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/strikethrough.rs @@ -0,0 +1,20 @@ +//! Strikethrough syntax (like `~~this~~`) +use crate::generics::inline::emph_pair; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Strikethrough { + pub marker: char +} + +impl NodeValue for Strikethrough { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.open("s", &node.attrs); + fmt.contents(&node.children); + fmt.close("s"); + } +} + +pub fn add(md: &mut MarkdownIt) { + emph_pair::add_with::<'~', 2, true>(md, || Node::new(Strikethrough { marker: '~' })); +} diff --git a/crates/markdown-it/src/plugins/extra/syntect.rs b/crates/markdown-it/src/plugins/extra/syntect.rs new file mode 100644 index 0000000000000000000000000000000000000000..50144917e881e38d1a9dfa30f366b9e0fd28fb54 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/syntect.rs @@ -0,0 +1,74 @@ +//! Syntax highlighting for code blocks +use syntect::highlighting::ThemeSet; +use syntect::html::highlighted_html_for_string; +use syntect::parsing::SyntaxSet; + +use crate::parser::core::CoreRule; +use crate::parser::extset::MarkdownItExt; +use crate::plugins::cmark::block::code::CodeBlock; +use crate::plugins::cmark::block::fence::CodeFence; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct SyntectSnippet { + pub html: String, +} + +impl NodeValue for SyntectSnippet { + fn render(&self, _: &Node, fmt: &mut dyn Renderer) { + fmt.text_raw(&self.html); + } +} + +#[derive(Debug, Clone, Copy)] +struct SyntectSettings(&'static str); +impl MarkdownItExt for SyntectSettings {} + +impl Default for SyntectSettings { + fn default() -> Self { + Self("InspiredGitHub") + } +} + +pub fn add(md: &mut MarkdownIt) { + md.add_rule::(); +} + +pub fn set_theme(md: &mut MarkdownIt, theme: &'static str) { + md.ext.insert(SyntectSettings(theme)); +} + +pub struct SyntectRule; +impl CoreRule for SyntectRule { + fn run(root: &mut Node, md: &MarkdownIt) { + let ss = SyntaxSet::load_defaults_newlines(); + let ts = ThemeSet::load_defaults(); + let theme = &ts.themes[md.ext.get::().copied().unwrap_or_default().0]; + + root.walk_mut(|node, _| { + let mut content = None; + let mut language = None; + + if let Some(data) = node.cast::() { + content = Some(&data.content); + } else if let Some(data) = node.cast::() { + language = Some(data.info.clone()); + content = Some(&data.content); + } + + if let Some(content) = content { + let mut syntax = None; + if let Some(language) = language { + syntax = ss.find_syntax_by_token(&language); + } + let syntax = syntax.unwrap_or_else(|| ss.find_syntax_plain_text()); + + let html = highlighted_html_for_string(content, &ss, syntax, theme); + + if let Ok(html) = html { + node.replace(SyntectSnippet { html }); + } + } + }); + } +} diff --git a/crates/markdown-it/src/plugins/extra/tables.rs b/crates/markdown-it/src/plugins/extra/tables.rs new file mode 100644 index 0000000000000000000000000000000000000000..4eb3c54b9c82070807168f2ebe515ad6445595e1 --- /dev/null +++ b/crates/markdown-it/src/plugins/extra/tables.rs @@ -0,0 +1,456 @@ +//! GFM tables +//! +//! +use crate::common::sourcemap::SourcePos; +use crate::parser::block::{BlockRule, BlockState}; +use crate::parser::extset::RenderExt; +use crate::parser::inline::InlineRoot; +use crate::plugins::cmark::block::heading::HeadingScanner; +use crate::plugins::cmark::block::list::ListScanner; +use crate::{MarkdownIt, Node, NodeValue, Renderer}; + +#[derive(Debug)] +pub struct Table { + pub alignments: Vec, +} + +impl NodeValue for Table { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let old_context = fmt.ext().remove::(); + fmt.ext().insert(TableRenderContext { head: false, alignments: self.alignments.clone(), index: 0 }); + + fmt.cr(); + fmt.open("table", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("table"); + fmt.cr(); + + old_context.map(|ctx| fmt.ext().insert(ctx)); + } +} + +#[derive(Debug, Default)] +pub struct TableRenderContext { + pub head: bool, + pub index: usize, + pub alignments: Vec, +} + +impl RenderExt for TableRenderContext {} + +#[derive(Debug)] +pub struct TableHead; + +impl NodeValue for TableHead { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let ctx = fmt.ext().get_or_insert_default::(); + ctx.head = true; + + fmt.cr(); + fmt.open("thead", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("thead"); + fmt.cr(); + + let ctx = fmt.ext().get_or_insert_default::(); + ctx.head = false; + } +} + +#[derive(Debug)] +pub struct TableBody; + +impl NodeValue for TableBody { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + fmt.cr(); + fmt.open("tbody", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("tbody"); + fmt.cr(); + } +} + +#[derive(Debug)] +pub struct TableRow; + +impl NodeValue for TableRow { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let ctx = fmt.ext().get_or_insert_default::(); + ctx.index = 0; + + fmt.cr(); + fmt.open("tr", &node.attrs); + fmt.cr(); + fmt.contents(&node.children); + fmt.cr(); + fmt.close("tr"); + fmt.cr(); + } +} + +#[derive(Debug)] +pub struct TableCell; + +impl NodeValue for TableCell { + fn render(&self, node: &Node, fmt: &mut dyn Renderer) { + let ctx = fmt.ext().get_or_insert_default::(); + let tag = if ctx.head { "th" } else { "td" }; + + let mut attrs = node.attrs.clone(); + + match ctx.alignments.get(ctx.index).copied().unwrap_or_default() { + ColumnAlignment::None => (), + ColumnAlignment::Left => attrs.push(("style", "text-align:left".to_owned())), + ColumnAlignment::Right => attrs.push(("style", "text-align:right".to_owned())), + ColumnAlignment::Center => attrs.push(("style", "text-align:center".to_owned())), + } + + ctx.index += 1; + + fmt.open(tag, &attrs); + fmt.contents(&node.children); + fmt.close(tag); + fmt.cr(); + } +} + +pub fn add(md: &mut MarkdownIt) { + md.block.add_rule::() + .before::() + .before::(); +} + +#[doc(hidden)] +pub struct TableScanner; + +#[derive(Debug)] +struct RowContent { + str: String, + srcmap: Vec<(usize, usize)>, +} + +#[derive(Debug, Clone, Copy)] +pub enum ColumnAlignment { + None, + Left, + Right, + Center, +} + +impl Default for ColumnAlignment { + fn default() -> Self { Self::None } +} + +impl TableScanner { + fn scan_row(line: &str) -> Vec { + let mut result = Vec::new(); + let mut str = String::new(); + let mut srcmap = vec![(0, 0)]; + let mut is_escaped = false; + let mut is_leading = true; + + for (pos, ch) in line.char_indices() { + match ch { + ' ' | '\t' if is_leading => { + srcmap[0].1 += 1; + } + '|' => { + is_leading = false; + if is_escaped { + str.push_str(&line[srcmap.last().unwrap().1..pos-1]); + srcmap.push((str.len(), pos)); + } else { + str.push_str(&line[srcmap.last().unwrap().1..pos]); + result.push(RowContent { + str: std::mem::take(&mut str), + srcmap: std::mem::take(&mut srcmap), + }); + srcmap = vec![(0, pos + 1)]; + is_escaped = false; + is_leading = true; + } + } + '\\' => { + is_leading = false; + is_escaped = true; + } + _ => { + is_leading = false; + is_escaped = false; + } + } + } + + str.push_str(&line[srcmap.last().unwrap().1..]); + result.push(RowContent { + str, + srcmap, + }); + + // trim trailing spaces + for content in result.iter_mut() { + while content.str.ends_with([ ' ', '\t' ]) { + content.str.pop(); + } + } + + // remove last cell if empty + if let Some(RowContent { str, srcmap: _ }) = result.last() { + if str.is_empty() { result.pop(); } + } + + // remove first cell if empty + if let Some(RowContent { str, srcmap: _ }) = result.first() { + if str.is_empty() { result.remove(0); } + } + + result + } + + fn scan_alignment_row(line: &str) -> Option> { + // quick check second line, only allow :-| and spaces + // (this is for performance only) + let mut has_delimiter = false; + for ch in line.chars() { + match ch { + '|'| ':' => { has_delimiter = true }, + '-' | ' ' | '\t' => (), + _ => return None, + } + } + if !has_delimiter { return None; } + + // if first character is '-', then second character must not be a space + // (due to parsing ambiguity with list) + if line.starts_with("- ") { return None; } + + let mut result = Vec::new(); + + for RowContent { str, srcmap: _ } in Self::scan_row(line) { + let mut alignment : u8 = 0; + let mut cell = str.as_str(); + + if cell.starts_with(':') { + alignment |= 1; + cell = &cell[1..]; + } + + if cell.ends_with(':') { + alignment |= 2; + cell = &cell[..cell.len()-1]; + } + + // only allow '-----' in the remainder + if cell.is_empty() || cell.contains(|c| c != '-') { + return None; + } + + result.push(match alignment { + 0 => ColumnAlignment::None, + 1 => ColumnAlignment::Left, + 2 => ColumnAlignment::Right, + 3 => ColumnAlignment::Center, + _ => unreachable!(), + }); + } + + Some(result) + } + + fn scan_header(state: &BlockState) -> Option<(Vec, Vec)> { + // should have at least two lines + if state.line + 2 > state.line_max { return None; } + + if state.line_indent(state.line) >= state.md.max_indent { return None; } + + let next_line = state.line + 1; + if state.line_indent(next_line) < 0 { return None; } + + if state.line_indent(next_line) >= state.md.max_indent { return None; } + + let alignments = Self::scan_alignment_row(state.get_line(next_line))?; + let header_row = Self::scan_row(state.get_line(state.line)); + + // header row must match the delimiter row in the number of cells + if header_row.len() != alignments.len() { + return None; + } + + // table without any columns is not a table, see markdown-it#724 + if header_row.is_empty() { + return None; + } + + Some(( header_row, alignments )) + } +} + +impl BlockRule for TableScanner { + fn check(state: &mut BlockState) -> Option<()> { + if state.node.is::() { return None; } + + Self::scan_header(state).map(|_| ()) + } + + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let ( header_row, alignments ) = Self::scan_header(state)?; + let table_cell_count = header_row.len(); + let mut table_node = Node::new(Table { alignments }); + + let mut thead_node = Node::new(TableHead); + thead_node.srcmap = state.get_map(state.line, state.line + 1); + + let mut row_node = Node::new(TableRow); + row_node.srcmap = state.get_map(state.line, state.line); + + fn add_cell(row_node: &mut Node, cell: String, srcmap: Vec<(usize, usize)>) { + let mut cell_node = Node::new(TableCell); + let (start, _) = row_node.srcmap.unwrap().get_byte_offsets(); + cell_node.srcmap = Some(SourcePos::new( + start + srcmap.first().unwrap().1, + start + srcmap.last().unwrap().1 + cell.len() - srcmap.last().unwrap().0, + )); + if !cell.is_empty() { + let mapping = srcmap.into_iter().map(|(dstpos, srcpos)| (dstpos, srcpos + start)).collect(); + cell_node.children.push(Node::new(InlineRoot::new(cell, mapping))); + } + row_node.children.push(cell_node); + } + + for RowContent { str: cell, srcmap } in header_row { + add_cell(&mut row_node, cell, srcmap); + } + + thead_node.children.push(row_node); + table_node.children.push(thead_node); + + let tbody_node = Node::new(TableBody); + let old_node = std::mem::replace(&mut state.node, tbody_node); + + // + // Iterate table rows + // + + let start_line = state.line; + state.line += 2; + + while state.line < state.line_max { + // + // Try to check if table is terminated or continued. + // + if state.line_indent(state.line) < 0 { break; } + + if state.line_indent(state.line) >= state.md.max_indent { break; } + + // stop if the line is empty + if state.is_empty(state.line) { break; } + + // fail if terminating block found + if state.test_rules_at_line() { break; } + + let mut row_node = Node::new(TableRow); + row_node.srcmap = state.get_map(state.line, state.line); + let line = state.get_line(state.line); + + let mut body_row = Self::scan_row(line); + let mut end_of_line = RowContent { str: String::new(), srcmap: vec![(0, line.len())] }; + + for index in 0..table_cell_count { + let RowContent { str: cell, srcmap } = body_row.get_mut(index).unwrap_or(&mut end_of_line); + add_cell(&mut row_node, cell.clone(), srcmap.clone()); + } + + state.node.children.push(row_node); + state.line += 1; + } + + let mut tbody_node = std::mem::replace(&mut state.node, old_node); + + if !tbody_node.children.is_empty() { + tbody_node.srcmap = state.get_map(start_line + 2, state.line - 1); + table_node.children.push(tbody_node); + } + + let line_count = state.line - start_line; + state.line = start_line; + Some((table_node, line_count)) + } +} + + +#[cfg(test)] +mod tests { + use super::TableScanner; + + #[test] + fn should_split_cells() { + assert_eq!(TableScanner::scan_row("").len(), 0); + assert_eq!(TableScanner::scan_row("a").len(), 1); + assert_eq!(TableScanner::scan_row("a | b").len(), 2); + assert_eq!(TableScanner::scan_row("a | b | c").len(), 3); + } + + #[test] + fn should_ignore_leading_trailing_empty_cells() { + assert_eq!(TableScanner::scan_row("foo | bar").len(), 2); + assert_eq!(TableScanner::scan_row("foo | bar |").len(), 2); + assert_eq!(TableScanner::scan_row("| foo | bar").len(), 2); + assert_eq!(TableScanner::scan_row("| foo | bar |").len(), 2); + assert_eq!(TableScanner::scan_row("| | foo | bar | |").len(), 4); + assert_eq!(TableScanner::scan_row("|").len(), 0); + assert_eq!(TableScanner::scan_row("||").len(), 1); + } + + #[test] + fn should_trim_cell_content() { + assert_eq!(TableScanner::scan_row("|foo|")[0].str, "foo"); + assert_eq!(TableScanner::scan_row("| foo |")[0].str, "foo"); + assert_eq!(TableScanner::scan_row("|\tfoo\t|")[0].str, "foo"); + assert_eq!(TableScanner::scan_row("| \t foo \t |")[0].str, "foo"); + } + + #[test] + fn should_process_backslash_escapes() { + assert_eq!(TableScanner::scan_row(r#"| foo\bar |"#)[0].str, r#"foo\bar"#); + assert_eq!(TableScanner::scan_row(r#"| foo\|bar |"#)[0].str, r#"foo|bar"#); + assert_eq!(TableScanner::scan_row(r#"| foo\\|bar |"#)[0].str, r#"foo\|bar"#); + assert_eq!(TableScanner::scan_row(r#"| foo\\\|bar |"#)[0].str, r#"foo\\|bar"#); + assert_eq!(TableScanner::scan_row(r#"| foo\\\\|bar |"#)[0].str, r#"foo\\\|bar"#); + } + + #[test] + fn should_trim_cell_content_srcmaps() { + let row = TableScanner::scan_row("| foo | \tbar\t |"); + assert_eq!(row[0].str, "foo"); + assert_eq!(row[0].srcmap, vec![(0, 2)]); + assert_eq!(row[1].str, "bar"); + assert_eq!(row[1].srcmap, vec![(0, 9)]); + } + + #[test] + fn should_process_backslash_escapes_srcmaps() { + let row = TableScanner::scan_row(r#"| foo\\|bar\\\|baz\ |"#); + assert_eq!(row[0].str, r#"foo\|bar\\|baz\"#); + assert_eq!(row[0].srcmap, vec![(0, 3), (4, 8), (10, 15)]); + } + + #[test] + fn require_pipe_or_colon_in_align_row() { + let md = &mut crate::MarkdownIt::new(); + crate::plugins::extra::tables::add(md); + let html = md.parse("foo\n---\nbar").render(); + assert_eq!(html.trim(), "foo\n---\nbar"); + let html = md.parse("|foo\n---\nbar").render(); + assert_eq!(html.trim(), "|foo\n---\nbar"); + let html = md.parse("foo\n|---\nbar").render(); + assert!(html.trim().starts_with("Hello world!.. This is the Right Way™ to markdown!!!

"#); +//! ``` +//! In summary, these are the replacements that will be made when using this: +//! +//! ## Typography +//! +//! - Repeated dots (`...`) to ellipsis (`…`) +//! except `?...` and `!...` which become `?..` and `!..` respectively +//! - `+-` to `±` +//! - Don't repeat `?` and `!` more than 3 times: `???` +//! - De-duplicate commas +//! - em and en dashes: `--` to `–` and `---` to `—` +//! +//! ## Common symbols (case insensitive) +//! +//! - Copyright: `(c)` to `©` +//! - Reserved: `(r)` to `®` +//! - Trademark: `(tm)` to `™` + +use once_cell::sync::Lazy; +use regex::Regex; +use std::borrow::Cow; + +use crate::parser::core::CoreRule; +use crate::parser::inline::Text; +use crate::{MarkdownIt, Node}; + +static REPLACEMENTS: Lazy> = Lazy::new(|| { + Box::new([ + (Regex::new(r"\+-").unwrap(), "±"), + (Regex::new(r"\.{2,}").unwrap(), "…"), + (Regex::new(r"([?!])…").unwrap(), "$1.."), + (Regex::new(r"([?!]){4,}").unwrap(), "$1$1$1"), + (Regex::new(r",{2,}").unwrap(), ","), + // These look a little different from the JS implementation because the + // regex crate doesn't support look-behind and look-ahead patterns + ( + Regex::new(r"(?m)(?P
^|[^-])(?P---)(?P[^-]|$)").unwrap(),
+            "$pre\u{2014}$post",
+        ),
+        (
+            Regex::new(r"(?m)(?P
^|\s)(?P--)(?P\s|$)").unwrap(),
+            "$pre\u{2013}$post",
+        ),
+        (
+            Regex::new(r"(?m)(?P
^|[^-\s])(?P--)(?P[^-\s]|$)").unwrap(),
+            "$pre\u{2013}$post",
+        ),
+    ])
+});
+static SCOPED_RE: Lazy = Lazy::new(|| Regex::new(r"(?i)\((c|tm|r)\)").unwrap());
+static RARE_RE: Lazy = Lazy::new(|| Regex::new(r"\+-|\.\.|\?\?\?\?|!!!!|,,|--").unwrap());
+
+fn replace_abbreviation(input: &str) -> &'static str {
+    match input.to_lowercase().as_str() {
+        "(c)" => "Š",
+        "(r)" => "ÂŽ",
+        "(tm)" => "™",
+        _ => unreachable!("Got invalid abbreviation '{}'", input),
+    }
+}
+
+pub fn add(md: &mut MarkdownIt) {
+    md.add_rule::();
+}
+
+pub struct TypographerRule;
+
+impl CoreRule for TypographerRule {
+    fn run(root: &mut Node, _: &MarkdownIt) {
+        root.walk_mut(|node, _| {
+            let Some(text_node) = node.cast_mut::() else { return; };
+
+            if SCOPED_RE.is_match(&text_node.content) {
+                text_node.content = SCOPED_RE
+                    .replace_all(&text_node.content, |caps: ®ex::Captures| {
+                        replace_abbreviation(caps.get(0).unwrap().as_str())
+                    })
+                    .to_string();
+            }
+            if RARE_RE.is_match(&text_node.content) {
+                let mut result = Cow::Borrowed(text_node.content.as_str());
+
+                for (pattern, replacement) in REPLACEMENTS.iter() {
+                    if let Cow::Owned(s) = pattern.replace_all(&result, *replacement) {
+                        result = Cow::Owned(s);
+
+                        // This is a bit unfortunate but since we can't use
+                        // look-ahead and look-behind patterns in the dash
+                        // replacements, the preceding and following
+                        // characters (pre and post in the patterns) become
+                        // part of the match. So a string like "bla-- --foo"
+                        // would create two *overlapping* matches, "a-- "
+                        // and " --f". But replace_all only replaces
+                        // non-overlapping matches. So we can't do this in
+                        // one single replacement. My only consolation here
+                        // is that this won't happen very often in practice,
+                        // and that it cost us "only" one extra call.
+                        if let Cow::Owned(s) = pattern.replace_all(&result, *replacement) {
+                            result = Cow::Owned(s);
+                        }
+                    }
+                }
+
+                if let Cow::Owned(s) = result {
+                    text_node.content = s;
+                }
+            }
+        });
+    }
+}
diff --git a/crates/markdown-it/src/plugins/html/html_block.rs b/crates/markdown-it/src/plugins/html/html_block.rs
new file mode 100644
index 0000000000000000000000000000000000000000..fae4fd1106e5cc08a135bc9dc2c22bbe7a8a9c60
--- /dev/null
+++ b/crates/markdown-it/src/plugins/html/html_block.rs
@@ -0,0 +1,151 @@
+//! HTML block syntax from CommonMark
+//!
+//! 
+use once_cell::sync::Lazy;
+use regex::Regex;
+
+use super::utils::blocks::*;
+use super::utils::regexps::*;
+use crate::parser::block::{BlockRule, BlockState};
+use crate::{MarkdownIt, Node, NodeValue, Renderer};
+
+#[derive(Debug)]
+pub struct HtmlBlock {
+    pub content: String,
+}
+
+impl NodeValue for HtmlBlock {
+    fn render(&self, _: &Node, fmt: &mut dyn Renderer) {
+        fmt.cr();
+        fmt.text_raw(&self.content);
+        fmt.cr();
+    }
+}
+
+pub fn add(md: &mut MarkdownIt) {
+    md.block.add_rule::();
+}
+
+struct HTMLSequence {
+    open: Regex,
+    close: Regex,
+    can_terminate_paragraph: bool,
+}
+
+impl HTMLSequence {
+    pub fn new(open: Regex, close: Regex, can_terminate_paragraph: bool) -> Self {
+        Self { open, close, can_terminate_paragraph }
+    }
+}
+
+// An array of opening and corresponding closing sequences for html tags,
+// last argument defines whether it can terminate a paragraph or not
+//
+static HTML_SEQUENCES : Lazy<[HTMLSequence; 7]> = Lazy::new(|| {
+    let block_names = HTML_BLOCKS.join("|");
+    let open_close_tag_re = HTML_OPEN_CLOSE_TAG_RE.as_str();
+
+    [
+        HTMLSequence::new(
+            Regex::new(r#"(?i)^<(script|pre|style|textarea)(\s|>|$)"#).unwrap(),
+            Regex::new(r#"(?i)"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(r#"^"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(r#"^<\?"#).unwrap(),
+            Regex::new(r#"\?>"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(r#"^"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(r#"^"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(&format!("(?i)^|$)")).unwrap(),
+            Regex::new(r#"^$"#).unwrap(),
+            true
+        ),
+
+        HTMLSequence::new(
+            Regex::new(&format!("{open_close_tag_re}\\s*$")).unwrap(),
+            Regex::new(r#"^$"#).unwrap(),
+            false
+        ),
+    ]
+});
+
+#[doc(hidden)]
+pub struct HtmlBlockScanner;
+
+impl HtmlBlockScanner {
+    fn get_sequence(state: &mut BlockState) -> Option<&'static HTMLSequence> {
+
+        if state.line_indent(state.line) >= state.md.max_indent { return None; }
+
+        let line_text = state.get_line(state.line);
+        let Some('<') = line_text.chars().next() else { return None; };
+
+        let mut sequence = None;
+        for seq in HTML_SEQUENCES.iter() {
+            if seq.open.is_match(line_text) {
+                sequence = Some(seq);
+                break;
+            }
+        }
+
+        sequence
+    }
+}
+
+impl BlockRule for HtmlBlockScanner {
+    fn check(state: &mut BlockState) -> Option<()> {
+        let sequence = Self::get_sequence(state)?;
+        if !sequence.can_terminate_paragraph { return None; }
+        Some(())
+    }
+
+    fn run(state: &mut BlockState) -> Option<(Node, usize)> {
+        let sequence = Self::get_sequence(state)?;
+
+        let line_text = state.get_line(state.line);
+        let start_line = state.line;
+        let mut next_line = state.line + 1;
+
+        // If we are here - we detected HTML block.
+        // Let's roll down till block end.
+        if !sequence.close.is_match(line_text) {
+            while next_line < state.line_max {
+                if state.line_indent(next_line) < 0 { break; }
+
+                let line_text = state.get_line(next_line);
+
+                if sequence.close.is_match(line_text) {
+                    if !line_text.is_empty() { next_line += 1; }
+                    break;
+                }
+
+                next_line += 1;
+            }
+        }
+
+        let (content, _) = state.get_lines(start_line, next_line, state.blk_indent, true);
+        let node = Node::new(HtmlBlock { content });
+        Some((node, next_line - state.line))
+    }
+}
diff --git a/crates/markdown-it/src/plugins/html/html_inline.rs b/crates/markdown-it/src/plugins/html/html_inline.rs
new file mode 100644
index 0000000000000000000000000000000000000000..ec74f06850ccc2b211273207cfea2503d1d5c2f2
--- /dev/null
+++ b/crates/markdown-it/src/plugins/html/html_inline.rs
@@ -0,0 +1,50 @@
+//! HTML inline syntax from CommonMark
+//!
+//! 
+use super::utils::regexps::*;
+use crate::parser::inline::{InlineRule, InlineState};
+use crate::{MarkdownIt, Node, NodeValue, Renderer};
+
+#[derive(Debug)]
+pub struct HtmlInline {
+    pub content: String,
+}
+
+impl NodeValue for HtmlInline {
+    fn render(&self, _: &Node, fmt: &mut dyn Renderer) {
+        fmt.text_raw(&self.content);
+    }
+}
+
+pub fn add(md: &mut MarkdownIt) {
+    md.inline.add_rule::();
+}
+
+#[doc(hidden)]
+pub struct HtmlInlineScanner;
+impl InlineRule for HtmlInlineScanner {
+    const MARKER: char = '<';
+
+    fn run(state: &mut InlineState) -> Option<(Node, usize)> {
+        // Check start
+        let mut chars = state.src[state.pos..state.pos_max].chars();
+        if chars.next().unwrap() != '<' { return None; }
+
+        // Quick fail on second char
+        let Some('!' | '?' | '/' | 'A'..='Z' | 'a'..='z') = chars.next() else { return None; };
+
+        let capture = HTML_TAG_RE.captures(&state.src[state.pos..state.pos_max])?.get(0).unwrap().as_str();
+        let capture_len = capture.len();
+
+        let content = capture.to_owned();
+
+        if HTML_LINK_OPEN.is_match(&content) {
+            state.link_level += 1;
+        } else if HTML_LINK_CLOSE.is_match(&content) {
+            state.link_level -= 1;
+        }
+
+        let node = Node::new(HtmlInline { content });
+        Some((node, capture_len))
+    }
+}
diff --git a/crates/markdown-it/src/plugins/html/mod.rs b/crates/markdown-it/src/plugins/html/mod.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4106eaf82555fb6cc8f1c9ac9eb6eaa7651735f4
--- /dev/null
+++ b/crates/markdown-it/src/plugins/html/mod.rs
@@ -0,0 +1,29 @@
+//! Raw html syntax (block and inline), part of CommonMark standard.
+//!
+//! This feature is separated from cmark because it is unsafe to enable by
+//! default (due to lack of any kind of html sanitization).
+//!
+//! You can enable it if you're:
+//!  - looking for strict CommonMark compatibility
+//!  - only have trusted input (i.e. writing markdown yourself)
+//!  - or took some care to sanitize html yourself
+//!
+//! ```rust
+//! let md = &mut markdown_it::MarkdownIt::new();
+//! markdown_it::plugins::cmark::add(md);
+//! markdown_it::plugins::html::add(md);
+//!
+//! let html = md.parse("hello
world").render(); +//! assert_eq!(html.trim(), r#"

hello
world

"#); +//! ``` + +pub mod html_block; +pub mod html_inline; +mod utils; + +use crate::MarkdownIt; + +pub fn add(md: &mut MarkdownIt) { + html_inline::add(md); + html_block::add(md); +} diff --git a/crates/markdown-it/src/plugins/html/utils/blocks.rs b/crates/markdown-it/src/plugins/html/utils/blocks.rs new file mode 100644 index 0000000000000000000000000000000000000000..8f98534401aba91538ffb41ee013748585cbaab2 --- /dev/null +++ b/crates/markdown-it/src/plugins/html/utils/blocks.rs @@ -0,0 +1,68 @@ +//! List of valid html blocks names, according to commonmark spec +//! http://jgm.github.io/CommonMark/spec.html#html-blocks +//! + +pub const HTML_BLOCKS: [&str; 62] = [ + "address", + "article", + "aside", + "base", + "basefont", + "blockquote", + "body", + "caption", + "center", + "col", + "colgroup", + "dd", + "details", + "dialog", + "dir", + "div", + "dl", + "dt", + "fieldset", + "figcaption", + "figure", + "footer", + "form", + "frame", + "frameset", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "head", + "header", + "hr", + "html", + "iframe", + "legend", + "li", + "link", + "main", + "menu", + "menuitem", + "nav", + "noframes", + "ol", + "optgroup", + "option", + "p", + "param", + "section", + "source", + "summary", + "table", + "tbody", + "td", + "tfoot", + "th", + "thead", + "title", + "tr", + "track", + "ul", +]; diff --git a/crates/markdown-it/src/plugins/html/utils/mod.rs b/crates/markdown-it/src/plugins/html/utils/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..5ce2de6a45a8d04b01875f297b77025bf0b7662e --- /dev/null +++ b/crates/markdown-it/src/plugins/html/utils/mod.rs @@ -0,0 +1,2 @@ +pub mod blocks; +pub mod regexps; diff --git a/crates/markdown-it/src/plugins/html/utils/regexps.rs b/crates/markdown-it/src/plugins/html/utils/regexps.rs new file mode 100644 index 0000000000000000000000000000000000000000..ab5411d2eae8815dc109a32d68f3210191054ec6 --- /dev/null +++ b/crates/markdown-it/src/plugins/html/utils/regexps.rs @@ -0,0 +1,46 @@ +//! Regexps to match html elements +//! +#![allow(non_upper_case_globals)] +use const_format::formatcp; +use once_cell::sync::Lazy; +use regex::Regex; + +const attr_name : &str = r#"[a-zA-Z_:][a-zA-Z0-9:._-]*"#; + +const unquoted : &str = r#"[^"'=<>`\x00-\x20]+"#; +const single_quoted : &str = r#"'[^']*'"#; +const double_quoted : &str = r#""[^"]*""#; + +const attr_value : &str = formatcp!("(?:{unquoted}|{single_quoted}|{double_quoted})"); + +const attribute : &str = formatcp!("(?:\\s+{attr_name}(?:\\s*=\\s*{attr_value})?)"); + +const open_tag : &str = formatcp!("<[A-Za-z][A-Za-z0-9\\-]*{attribute}*\\s*/?>"); + +const close_tag : &str = r#""#; +const comment : &str = r#"|"#; +const processing : &str = r#"<[?][\s\S]*?[?]>"#; +const declaration : &str = r#"]*>"#; +const cdata : &str = r#""#; + +#[allow(clippy::double_parens)] +pub static HTML_TAG_RE : Lazy = Lazy::new(|| { + Regex::new( + formatcp!("^(?:{open_tag}|{close_tag}|{comment}|{processing}|{declaration}|{cdata})") + ).unwrap() +}); + +#[allow(clippy::double_parens)] +pub static HTML_OPEN_CLOSE_TAG_RE : Lazy = Lazy::new(|| { + Regex::new( + formatcp!("^(?:{open_tag}|{close_tag})") + ).unwrap() +}); + +pub static HTML_LINK_OPEN : Lazy = Lazy::new(|| { + Regex::new(r#"^\s]"#).unwrap() +}); + +pub static HTML_LINK_CLOSE : Lazy = Lazy::new(|| { + Regex::new(r#"^"#).unwrap() +}); diff --git a/crates/markdown-it/src/plugins/mod.rs b/crates/markdown-it/src/plugins/mod.rs new file mode 100644 index 0000000000000000000000000000000000000000..997555013ffd1664224f8aefa27ffd00260d5a03 --- /dev/null +++ b/crates/markdown-it/src/plugins/mod.rs @@ -0,0 +1,20 @@ +//! Ready-to-use plugins. Everything, including basic markdown syntax, is a plugin. +//! +//! This library is made to be as extensible as possible. In order to ensure that +//! you can write your own markdown syntax of any arbitrary complexity, +//! CommonMark syntax itself is made into a plugin (`cmark`), which you can use +//! as an example of how to write your own. +//! +//! Add each plugin you need by invoking `add` function like this: +//! ```rust +//! let md = &mut markdown_it::MarkdownIt::new(); +//! markdown_it::plugins::cmark::add(md); +//! markdown_it::plugins::extra::add(md); +//! markdown_it::plugins::html::add(md); +//! markdown_it::plugins::sourcepos::add(md); +//! // ... +//! ``` +pub mod cmark; +pub mod extra; +pub mod html; +pub mod sourcepos; diff --git a/crates/markdown-it/src/plugins/sourcepos.rs b/crates/markdown-it/src/plugins/sourcepos.rs new file mode 100644 index 0000000000000000000000000000000000000000..801a10508ca0b69743ec4b1a9790c24f979895fd --- /dev/null +++ b/crates/markdown-it/src/plugins/sourcepos.rs @@ -0,0 +1,52 @@ +//! Add source mapping to resulting HTML, looks like this: ``. +//! ```rust +//! let md = &mut markdown_it::MarkdownIt::new(); +//! markdown_it::plugins::cmark::add(md); +//! markdown_it::plugins::sourcepos::add(md); +//! +//! let html = md.parse("# hello").render(); +//! assert_eq!(html.trim(), r#"

hello

"#); +//! ``` +use crate::common::sourcemap::SourceWithLineStarts; +use crate::parser::block::builtin::BlockParserRule; +use crate::parser::core::{CoreRule, Root}; +use crate::parser::inline::builtin::InlineParserRule; +use crate::{MarkdownIt, Node}; + +pub fn add(md: &mut MarkdownIt) { + md.add_rule::() + .after::() + .after::(); +} + +#[doc(hidden)] +pub struct SyntaxPosRule; +impl CoreRule for SyntaxPosRule { + fn run(root: &mut Node, _: &MarkdownIt) { + let source = root.cast::().unwrap().content.as_str(); + let mapping = SourceWithLineStarts::new(source); + + root.walk_mut(|node, _| { + if let Some(map) = node.srcmap { + let ((startline, startcol), (endline, endcol)) = map.get_positions(&mapping); + node.attrs.push(("data-sourcepos", format!("{}:{}-{}:{}", startline, startcol, endline, endcol))); + } + }); + } +} + + +#[cfg(test)] +mod tests { + #[test] + fn header_test() { + // same as doctest, keep in sync! + // used for code coverage and quicker rust-analyzer hints + let md = &mut crate::MarkdownIt::new(); + crate::plugins::cmark::add(md); + crate::plugins::sourcepos::add(md); + + let html = md.parse("# hello").render(); + assert_eq!(html.trim(), r#"

hello

"#); + } +} diff --git a/crates/markdown-it/tests/commonmark.rs b/crates/markdown-it/tests/commonmark.rs new file mode 100644 index 0000000000000000000000000000000000000000..289077e49692ad6f69a3866bf9ad6a5f573712df --- /dev/null +++ b/crates/markdown-it/tests/commonmark.rs @@ -0,0 +1,6557 @@ + +fn run(input: &str, output: &str) { + let output = if output.is_empty() { "".to_owned() } else { output.to_owned() + "\n" }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + + // make sure we have sourcemaps for everything + node.walk(|node, _| assert!(node.srcmap.is_some())); + + let result = node.xrender(); + assert_eq!(result, output); + + // make sure it doesn't crash without trailing \n + let _ = md.parse(input.trim_end()); +} + +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/commonmark/good.txt +#[rustfmt::skip] +mod fixtures_commonmark_good_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn src_line_355() { + let input = "\tfoo\tbaz\t\tbim"; + let output = "
foo\tbaz\t\tbim
+
"; + run(input, output); +} + +#[test] +fn src_line_362() { + let input = " \tfoo\tbaz\t\tbim"; + let output = "
foo\tbaz\t\tbim
+
"; + run(input, output); +} + +#[test] +fn src_line_369() { + let input = " a\ta + ὐ\ta"; + let output = "
a\ta
+ὐ\ta
+
"; + run(input, output); +} + +#[test] +fn src_line_382() { + let input = " - foo + +\tbar"; + let output = r#"
    +
  • +

    foo

    +

    bar

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_395() { + let input = "- foo + +\t\tbar"; + let output = r#"
    +
  • +

    foo

    +
      bar
    +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_418() { + let input = ">\t\tfoo"; + let output = r#"
+
  foo
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_427() { + let input = "-\t\tfoo"; + let output = r#"
    +
  • +
      foo
    +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_439() { + let input = " foo +\tbar"; + let output = r#"
foo
+bar
+
"#; + run(input, output); +} + +#[test] +fn src_line_448() { + let input = " - foo + - bar +\t - baz"; + let output = r#"
    +
  • foo +
      +
    • bar +
        +
      • baz
      • +
      +
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_466() { + let input = "#\tFoo"; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_472() { + let input = "*\t*\t*\t"; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_489() { + let input = r#"\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~"#; + let output = r#"

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

"#; + run(input, output); +} + +#[test] +fn src_line_499() { + let input = "\\\t\\A\\a\\ \\3\\φ\\«"; + let output = "

\\\t\\A\\a\\ \\3\\φ\\«

"; + run(input, output); +} + +#[test] +fn src_line_509() { + let input = r#"\*not emphasized* +\
not a tag +\[not a link](/foo) +\`not code` +1\. not a list +\* not a list +\# not a heading +\[foo]: /url "not a reference" +\ö not a character entity"#; + let output = r#"

*not emphasized* +<br/> not a tag +[not a link](/foo) +`not code` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +&ouml; not a character entity

"#; + run(input, output); +} + +#[test] +fn src_line_534() { + let input = r#"\\*emphasis*"#; + let output = r#"

\emphasis

"#; + run(input, output); +} + +#[test] +fn src_line_543() { + let input = r#"foo\ +bar"#; + let output = r#"

foo
+bar

"#; + run(input, output); +} + +#[test] +fn src_line_555() { + let input = r#"`` \[\` ``"#; + let output = r#"

\[\`

"#; + run(input, output); +} + +#[test] +fn src_line_562() { + let input = r#" \[\]"#; + let output = r#"
\[\]
+
"#; + run(input, output); +} + +#[test] +fn src_line_570() { + let input = r#"~~~ +\[\] +~~~"#; + let output = r#"
\[\]
+
"#; + run(input, output); +} + +#[test] +fn src_line_580() { + let input = r#""#; + let output = r#"

http://example.com?find=\*

"#; + run(input, output); +} + +#[test] +fn src_line_587() { + let input = r#""#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_597() { + let input = r#"[foo](/bar\* "ti\*tle")"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_604() { + let input = r#"[foo] + +[foo]: /bar\* "ti\*tle""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_613() { + let input = r#"``` foo\+bar +foo +```"#; + let output = r#"
foo
+
"#; + run(input, output); +} + +#[test] +fn src_line_649() { + let input = r#"  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸"#; + let output = r#"

  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸

"#; + run(input, output); +} + +#[test] +fn src_line_668() { + let input = r#"# Ӓ Ϡ �"#; + let output = r#"

# Ӓ Ϡ �

"#; + run(input, output); +} + +#[test] +fn src_line_681() { + let input = r#"" ആ ಫ"#; + let output = r#"

" ആ ಫ

"#; + run(input, output); +} + +#[test] +fn src_line_690() { + let input = r#"  &x; &#; &#x; +� +&#abcdef0; +&ThisIsNotDefined; &hi?;"#; + let output = r#"

&nbsp &x; &#; &#x; +&#87654321; +&#abcdef0; +&ThisIsNotDefined; &hi?;

"#; + run(input, output); +} + +#[test] +fn src_line_707() { + let input = r#"©"#; + let output = r#"

&copy

"#; + run(input, output); +} + +#[test] +fn src_line_717() { + let input = r#"&MadeUpEntity;"#; + let output = r#"

&MadeUpEntity;

"#; + run(input, output); +} + +#[test] +fn src_line_728() { + let input = r#""#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_735() { + let input = r#"[foo](/föö "föö")"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_742() { + let input = r#"[foo] + +[foo]: /föö "föö""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_751() { + let input = r#"``` föö +foo +```"#; + let output = r#"
foo
+
"#; + run(input, output); +} + +#[test] +fn src_line_764() { + let input = r#"`föö`"#; + let output = r#"

f&ouml;&ouml;

"#; + run(input, output); +} + +#[test] +fn src_line_771() { + let input = r#" föfö"#; + let output = r#"
f&ouml;f&ouml;
+
"#; + run(input, output); +} + +#[test] +fn src_line_783() { + let input = r#"*foo* +*foo*"#; + let output = r#"

*foo* +foo

"#; + run(input, output); +} + +#[test] +fn src_line_791() { + let input = r#"* foo + +* foo"#; + let output = r#"

* foo

+
    +
  • foo
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_802() { + let input = r#"foo bar"#; + let output = r#"

foo + +bar

"#; + run(input, output); +} + +#[test] +fn src_line_810() { + let input = r#" foo"#; + let output = "

\tfoo

"; + run(input, output); +} + +#[test] +fn src_line_817() { + let input = r#"[a](url "tit")"#; + let output = r#"

[a](url "tit")

"#; + run(input, output); +} + +#[test] +fn src_line_840() { + let input = r#"- `one +- two`"#; + let output = r#"
    +
  • `one
  • +
  • two`
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_879() { + let input = r#"*** +--- +___"#; + let output = r#"
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_892() { + let input = r#"+++"#; + let output = r#"

+++

"#; + run(input, output); +} + +#[test] +fn src_line_899() { + let input = r#"==="#; + let output = r#"

===

"#; + run(input, output); +} + +#[test] +fn src_line_908() { + let input = r#"-- +** +__"#; + let output = r#"

-- +** +__

"#; + run(input, output); +} + +#[test] +fn src_line_921() { + let input = r#" *** + *** + ***"#; + let output = r#"
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_934() { + let input = r#" ***"#; + let output = r#"
***
+
"#; + run(input, output); +} + +#[test] +fn src_line_942() { + let input = r#"Foo + ***"#; + let output = r#"

Foo +***

"#; + run(input, output); +} + +#[test] +fn src_line_953() { + let input = r#"_____________________________________"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_962() { + let input = r#" - - -"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_969() { + let input = r#" ** * ** * ** * **"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_976() { + let input = r#"- - - -"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_985() { + let input = "- - - - \x20"; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_994() { + let input = r#"_ _ _ _ a + +a------ + +---a---"#; + let output = r#"

_ _ _ _ a

+

a------

+

---a---

"#; + run(input, output); +} + +#[test] +fn src_line_1010() { + let input = r#" *-*"#; + let output = r#"

-

"#; + run(input, output); +} + +#[test] +fn src_line_1019() { + let input = r#"- foo +*** +- bar"#; + let output = r#"
    +
  • foo
  • +
+
+
    +
  • bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_1036() { + let input = r#"Foo +*** +bar"#; + let output = r#"

Foo

+
+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_1053() { + let input = r#"Foo +--- +bar"#; + let output = r#"

Foo

+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_1066() { + let input = r#"* Foo +* * * +* Bar"#; + let output = r#"
    +
  • Foo
  • +
+
+
    +
  • Bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_1083() { + let input = r#"- Foo +- * * *"#; + let output = r#"
    +
  • Foo
  • +
  • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_1112() { + let input = r#"# foo +## foo +### foo +#### foo +##### foo +###### foo"#; + let output = r#"

foo

+

foo

+

foo

+

foo

+
foo
+
foo
"#; + run(input, output); +} + +#[test] +fn src_line_1131() { + let input = r#"####### foo"#; + let output = r#"

####### foo

"#; + run(input, output); +} + +#[test] +fn src_line_1146() { + let input = r#"#5 bolt + +#hashtag"#; + let output = r#"

#5 bolt

+

#hashtag

"#; + run(input, output); +} + +#[test] +fn src_line_1158() { + let input = r#"\## foo"#; + let output = r#"

## foo

"#; + run(input, output); +} + +#[test] +fn src_line_1167() { + let input = r#"# foo *bar* \*baz\*"#; + let output = r#"

foo bar *baz*

"#; + run(input, output); +} + +#[test] +fn src_line_1176() { + let input = "# foo \x20"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_1185() { + let input = r#" ### foo + ## foo + # foo"#; + let output = r#"

foo

+

foo

+

foo

"#; + run(input, output); +} + +#[test] +fn src_line_1198() { + let input = r#" # foo"#; + let output = r#"
# foo
+
"#; + run(input, output); +} + +#[test] +fn src_line_1206() { + let input = r#"foo + # bar"#; + let output = r#"

foo +# bar

"#; + run(input, output); +} + +#[test] +fn src_line_1217() { + let input = r#"## foo ## + ### bar ###"#; + let output = r#"

foo

+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_1228() { + let input = r#"# foo ################################## +##### foo ##"#; + let output = r#"

foo

+
foo
"#; + run(input, output); +} + +#[test] +fn src_line_1239() { + let input = "### foo ### \x20"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_1250() { + let input = r#"### foo ### b"#; + let output = r#"

foo ### b

"#; + run(input, output); +} + +#[test] +fn src_line_1259() { + let input = r#"# foo#"#; + let output = r#"

foo#

"#; + run(input, output); +} + +#[test] +fn src_line_1269() { + let input = r#"### foo \### +## foo #\## +# foo \#"#; + let output = r#"

foo ###

+

foo ###

+

foo #

"#; + run(input, output); +} + +#[test] +fn src_line_1283() { + let input = r#"**** +## foo +****"#; + let output = r#"
+

foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_1294() { + let input = r#"Foo bar +# baz +Bar foo"#; + let output = r#"

Foo bar

+

baz

+

Bar foo

"#; + run(input, output); +} + +#[test] +fn src_line_1307() { + let input = "##\x20 +# +### ###"; + let output = r#"

+

+

"#; + run(input, output); +} + +#[test] +fn src_line_1350() { + let input = r#"Foo *bar* +========= + +Foo *bar* +---------"#; + let output = r#"

Foo bar

+

Foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_1364() { + let input = r#"Foo *bar +baz* +===="#; + let output = r#"

Foo bar +baz

"#; + run(input, output); +} + +#[test] +fn src_line_1378() { + let input = " Foo *bar +baz*\t +===="; + let output = r#"

Foo bar +baz

"#; + run(input, output); +} + +#[test] +fn src_line_1390() { + let input = r#"Foo +------------------------- + +Foo +="#; + let output = r#"

Foo

+

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_1405() { + let input = r#" Foo +--- + + Foo +----- + + Foo + ==="#; + let output = r#"

Foo

+

Foo

+

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_1423() { + let input = r#" Foo + --- + + Foo +---"#; + let output = r#"
Foo
+---
+
+Foo
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_1442() { + let input = "Foo + ---- \x20"; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_1452() { + let input = r#"Foo + ---"#; + let output = r#"

Foo +---

"#; + run(input, output); +} + +#[test] +fn src_line_1463() { + let input = r#"Foo += = + +Foo +--- -"#; + let output = r#"

Foo += =

+

Foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_1479() { + let input = "Foo \x20 +-----"; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_1489() { + let input = r#"Foo\ +----"#; + let output = r#"

Foo\

"#; + run(input, output); +} + +#[test] +fn src_line_1500() { + let input = r#"`Foo +---- +` + +"#; + let output = r#"

`Foo

+

`

+

<a title="a lot

+

of dashes"/>

"#; + run(input, output); +} + +#[test] +fn src_line_1519() { + let input = r#"> Foo +---"#; + let output = r#"
+

Foo

+
+
"#; + run(input, output); +} + +#[test] +fn src_line_1530() { + let input = r#"> foo +bar +==="#; + let output = r#"
+

foo +bar +===

+
"#; + run(input, output); +} + +#[test] +fn src_line_1543() { + let input = r#"- Foo +---"#; + let output = r#"
    +
  • Foo
  • +
+
"#; + run(input, output); +} + +#[test] +fn src_line_1558() { + let input = r#"Foo +Bar +---"#; + let output = r#"

Foo +Bar

"#; + run(input, output); +} + +#[test] +fn src_line_1571() { + let input = r#"--- +Foo +--- +Bar +--- +Baz"#; + let output = r#"
+

Foo

+

Bar

+

Baz

"#; + run(input, output); +} + +#[test] +fn src_line_1588() { + let input = r#" +===="#; + let output = r#"

====

"#; + run(input, output); +} + +#[test] +fn src_line_1600() { + let input = r#"--- +---"#; + let output = r#"
+
"#; + run(input, output); +} + +#[test] +fn src_line_1609() { + let input = r#"- foo +-----"#; + let output = r#"
    +
  • foo
  • +
+
"#; + run(input, output); +} + +#[test] +fn src_line_1620() { + let input = r#" foo +---"#; + let output = r#"
foo
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_1630() { + let input = r#"> foo +-----"#; + let output = r#"
+

foo

+
+
"#; + run(input, output); +} + +#[test] +fn src_line_1644() { + let input = r#"\> foo +------"#; + let output = r#"

> foo

"#; + run(input, output); +} + +#[test] +fn src_line_1675() { + let input = r#"Foo + +bar +--- +baz"#; + let output = r#"

Foo

+

bar

+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_1691() { + let input = r#"Foo +bar + +--- + +baz"#; + let output = r#"

Foo +bar

+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_1709() { + let input = r#"Foo +bar +* * * +baz"#; + let output = r#"

Foo +bar

+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_1724() { + let input = r#"Foo +bar +\--- +baz"#; + let output = r#"

Foo +bar +--- +baz

"#; + run(input, output); +} + +#[test] +fn src_line_1752() { + let input = r#" a simple + indented code block"#; + let output = r#"
a simple
+  indented code block
+
"#; + run(input, output); +} + +#[test] +fn src_line_1766() { + let input = r#" - foo + + bar"#; + let output = r#"
    +
  • +

    foo

    +

    bar

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_1780() { + let input = r#"1. foo + + - bar"#; + let output = r#"
    +
  1. +

    foo

    +
      +
    • bar
    • +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_1800() { + let input = r#"
+ *hi* + + - one"#; + let output = r#"
<a/>
+*hi*
+
+- one
+
"#; + run(input, output); +} + +#[test] +fn src_line_1816() { + let input = " chunk1 + + chunk2 + \x20 +\x20 +\x20 + chunk3"; + let output = r#"
chunk1
+
+chunk2
+
+
+
+chunk3
+
"#; + run(input, output); +} + +#[test] +fn src_line_1839() { + let input = " chunk1 + \x20 + chunk2"; + let output = "
chunk1
+ \x20
+  chunk2
+
"; + run(input, output); +} + +#[test] +fn src_line_1854() { + let input = r#"Foo + bar +"#; + let output = r#"

Foo +bar

"#; + run(input, output); +} + +#[test] +fn src_line_1868() { + let input = r#" foo +bar"#; + let output = r#"
foo
+
+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_1881() { + let input = r#"# Heading + foo +Heading +------ + foo +----"#; + let output = r#"

Heading

+
foo
+
+

Heading

+
foo
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_1901() { + let input = r#" foo + bar"#; + let output = r#"
    foo
+bar
+
"#; + run(input, output); +} + +#[test] +fn src_line_1914() { + let input = " + \x20 + foo + \x20 +"; + let output = r#"
foo
+
"#; + run(input, output); +} + +#[test] +fn src_line_1928() { + let input = " foo \x20"; + let output = "
foo \x20
+
"; + run(input, output); +} + +#[test] +fn src_line_1983() { + let input = r#"``` +< + > +```"#; + let output = r#"
<
+ >
+
"#; + run(input, output); +} + +#[test] +fn src_line_1997() { + let input = r#"~~~ +< + > +~~~"#; + let output = r#"
<
+ >
+
"#; + run(input, output); +} + +#[test] +fn src_line_2010() { + let input = r#"`` +foo +``"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_2021() { + let input = r#"``` +aaa +~~~ +```"#; + let output = r#"
aaa
+~~~
+
"#; + run(input, output); +} + +#[test] +fn src_line_2033() { + let input = r#"~~~ +aaa +``` +~~~"#; + let output = r#"
aaa
+```
+
"#; + run(input, output); +} + +#[test] +fn src_line_2047() { + let input = r#"```` +aaa +``` +``````"#; + let output = r#"
aaa
+```
+
"#; + run(input, output); +} + +#[test] +fn src_line_2059() { + let input = r#"~~~~ +aaa +~~~ +~~~~"#; + let output = r#"
aaa
+~~~
+
"#; + run(input, output); +} + +#[test] +fn src_line_2074() { + let input = r#"```"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_2081() { + let input = r#"````` + +``` +aaa"#; + let output = r#"

+```
+aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2094() { + let input = r#"> ``` +> aaa + +bbb"#; + let output = r#"
+
aaa
+
+
+

bbb

"#; + run(input, output); +} + +#[test] +fn src_line_2110() { + let input = "``` + + \x20 +```"; + let output = "

+ \x20
+
"; + run(input, output); +} + +#[test] +fn src_line_2124() { + let input = r#"``` +```"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_2136() { + let input = r#" ``` + aaa +aaa +```"#; + let output = r#"
aaa
+aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2148() { + let input = r#" ``` +aaa + aaa +aaa + ```"#; + let output = r#"
aaa
+aaa
+aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2162() { + let input = r#" ``` + aaa + aaa + aaa + ```"#; + let output = r#"
aaa
+ aaa
+aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2178() { + let input = r#" ``` + aaa + ```"#; + let output = r#"
```
+aaa
+```
+
"#; + run(input, output); +} + +#[test] +fn src_line_2193() { + let input = r#"``` +aaa + ```"#; + let output = r#"
aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2203() { + let input = r#" ``` +aaa + ```"#; + let output = r#"
aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2215() { + let input = r#"``` +aaa + ```"#; + let output = r#"
aaa
+    ```
+
"#; + run(input, output); +} + +#[test] +fn src_line_2229() { + let input = r#"``` ``` +aaa"#; + let output = r#"

+aaa

"#; + run(input, output); +} + +#[test] +fn src_line_2238() { + let input = r#"~~~~~~ +aaa +~~~ ~~"#; + let output = r#"
aaa
+~~~ ~~
+
"#; + run(input, output); +} + +#[test] +fn src_line_2252() { + let input = r#"foo +``` +bar +``` +baz"#; + let output = r#"

foo

+
bar
+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_2269() { + let input = r#"foo +--- +~~~ +bar +~~~ +# baz"#; + let output = r#"

foo

+
bar
+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_2291() { + let input = r#"```ruby +def foo(x) + return 3 +end +```"#; + let output = r#"
def foo(x)
+  return 3
+end
+
"#; + run(input, output); +} + +#[test] +fn src_line_2305() { + let input = r#"~~~~ ruby startline=3 $%@#$ +def foo(x) + return 3 +end +~~~~~~~"#; + let output = r#"
def foo(x)
+  return 3
+end
+
"#; + run(input, output); +} + +#[test] +fn src_line_2319() { + let input = r#"````; +````"#; + let output = r#"
"#; + run(input, output); +} + +#[test] +fn src_line_2329() { + let input = r#"``` aa ``` +foo"#; + let output = r#"

aa +foo

"#; + run(input, output); +} + +#[test] +fn src_line_2340() { + let input = r#"~~~ aa ``` ~~~ +foo +~~~"#; + let output = r#"
foo
+
"#; + run(input, output); +} + +#[test] +fn src_line_2352() { + let input = r#"``` +``` aaa +```"#; + let output = r#"
``` aaa
+
"#; + run(input, output); +} + +#[test] +fn src_line_2431() { + let input = r#"
+
+**Hello**,
+
+_world_.
+
+
"#; + let output = r#"
+
+**Hello**,
+

world. +

+
"#; + run(input, output); +} + +#[test] +fn src_line_2460() { + let input = r#" + + + +
+ hi +
+ +okay."#; + let output = r#" + + + +
+ hi +
+

okay.

"#; + run(input, output); +} + +#[test] +fn src_line_2482() { + let input = r#"
+*foo*"#; + run(input, output); +} + +#[test] +fn src_line_2506() { + let input = r#"
+ +*Markdown* + +
"#; + let output = r#"
+

Markdown

+
"#; + run(input, output); +} + +#[test] +fn src_line_2522() { + let input = r#"
+
"#; + let output = r#"
+
"#; + run(input, output); +} + +#[test] +fn src_line_2533() { + let input = r#"
+
"#; + let output = r#"
+
"#; + run(input, output); +} + +#[test] +fn src_line_2545() { + let input = r#"
+*foo* + +*bar*"#; + let output = r#"
+*foo* +

bar

"#; + run(input, output); +} + +#[test] +fn src_line_2561() { + let input = r#"
"#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_2601() { + let input = r#"
+foo +
"#; + let output = r#"
+foo +
"#; + run(input, output); +} + +#[test] +fn src_line_2618() { + let input = r#"
+``` c +int x = 33; +```"#; + let output = r#"
+``` c +int x = 33; +```"#; + run(input, output); +} + +#[test] +fn src_line_2635() { + let input = r#" +*bar* +"#; + let output = r#" +*bar* +"#; + run(input, output); +} + +#[test] +fn src_line_2648() { + let input = r#" +*bar* +"#; + let output = r#" +*bar* +"#; + run(input, output); +} + +#[test] +fn src_line_2659() { + let input = r#" +*bar* +"#; + let output = r#" +*bar* +"#; + run(input, output); +} + +#[test] +fn src_line_2670() { + let input = r#" +*bar*"#; + let output = r#" +*bar*"#; + run(input, output); +} + +#[test] +fn src_line_2685() { + let input = r#" +*foo* +"#; + let output = r#" +*foo* +"#; + run(input, output); +} + +#[test] +fn src_line_2700() { + let input = r#" + +*foo* + +"#; + let output = r#" +

foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_2718() { + let input = r#"*foo*"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_2734() { + let input = r#"

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+okay"#; + let output = r#"

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2755() { + let input = r#" +okay"#; + let output = r#" +

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2774() { + let input = r#""#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_2794() { + let input = r#" +okay"#; + let output = r#" +

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2817() { + let input = r#" +*foo*"#; + let output = r#" +

foo

"#; + run(input, output); +} + +#[test] +fn src_line_2868() { + let input = r#"*bar* +*baz*"#; + let output = r#"*bar* +

baz

"#; + run(input, output); +} + +#[test] +fn src_line_2880() { + let input = r#"1. *bar*"#; + let output = r#"1. *bar*"#; + run(input, output); +} + +#[test] +fn src_line_2893() { + let input = r#" +okay"#; + let output = r#" +

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2911() { + let input = r#"'; + +?> +okay"#; + let output = r#"'; + +?> +

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2930() { + let input = r#""#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_2939() { + let input = r#" +okay"#; + let output = r#" +

okay

"#; + run(input, output); +} + +#[test] +fn src_line_2973() { + let input = r#" + + "#; + let output = r#" +
<!-- foo -->
+
"#; + run(input, output); +} + +#[test] +fn src_line_2984() { + let input = r#"
+ +
"#; + let output = r#"
+
<div>
+
"#; + run(input, output); +} + +#[test] +fn src_line_2998() { + let input = r#"Foo +
+bar +
"#; + let output = r#"

Foo

+
+bar +
"#; + run(input, output); +} + +#[test] +fn src_line_3015() { + let input = r#"
+bar +
+*foo*"#; + let output = r#"
+bar +
+*foo*"#; + run(input, output); +} + +#[test] +fn src_line_3030() { + let input = r#"Foo + +baz"#; + let output = r#"

Foo + +baz

"#; + run(input, output); +} + +#[test] +fn src_line_3071() { + let input = r#"
+ +*Emphasized* text. + +
"#; + let output = r#"
+

Emphasized text.

+
"#; + run(input, output); +} + +#[test] +fn src_line_3084() { + let input = r#"
+*Emphasized* text. +
"#; + let output = r#"
+*Emphasized* text. +
"#; + run(input, output); +} + +#[test] +fn src_line_3106() { + let input = r#" + + + + + + + +
+Hi +
"#; + let output = r#" + + + +
+Hi +
"#; + run(input, output); +} + +#[test] +fn src_line_3133() { + let input = r#" + + + + + + + +
+ Hi +
"#; + let output = r#" + +
<td>
+  Hi
+</td>
+
+ +
"#; + run(input, output); +} + +#[test] +fn src_line_3182() { + let input = r#"[foo]: /url "title" + +[foo]"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3191() { + let input = " [foo]:\x20 + /url \x20 + 'the title' \x20 + +[foo]"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3202() { + let input = r#"[Foo*bar\]]:my_(url) 'title (with parens)' + +[Foo*bar\]]"#; + let output = r#"

Foo*bar]

"#; + run(input, output); +} + +#[test] +fn src_line_3211() { + let input = r#"[Foo bar]: + +'title' + +[Foo bar]"#; + let output = r#"

Foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_3224() { + let input = r#"[foo]: /url ' +title +line1 +line2 +' + +[foo]"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3243() { + let input = r#"[foo]: /url 'title + +with blank line' + +[foo]"#; + let output = r#"

[foo]: /url 'title

+

with blank line'

+

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_3258() { + let input = r#"[foo]: +/url + +[foo]"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3270() { + let input = r#"[foo]: + +[foo]"#; + let output = r#"

[foo]:

+

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_3282() { + let input = r#"[foo]: <> + +[foo]"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3293() { + let input = r#"[foo]: (baz) + +[foo]"#; + let output = r#"

[foo]: (baz)

+

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_3306() { + let input = r#"[foo]: /url\bar\*baz "foo\"bar\baz" + +[foo]"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3317() { + let input = r#"[foo] + +[foo]: url"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3329() { + let input = r#"[foo] + +[foo]: first +[foo]: second"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3342() { + let input = r#"[FOO]: /url + +[Foo]"#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_3351() { + let input = r#"[ΑΓΩ]: /φου + +[αγω]"#; + let output = r#"

αγω

"#; + run(input, output); +} + +#[test] +fn src_line_3366() { + let input = r#"[foo]: /url"#; + let output = r#""#; + run(input, output); +} + +#[test] +fn src_line_3374() { + let input = r#"[ +foo +]: /url +bar"#; + let output = r#"

bar

"#; + run(input, output); +} + +#[test] +fn src_line_3387() { + let input = r#"[foo]: /url "title" ok"#; + let output = r#"

[foo]: /url "title" ok

"#; + run(input, output); +} + +#[test] +fn src_line_3396() { + let input = r#"[foo]: /url +"title" ok"#; + let output = r#"

"title" ok

"#; + run(input, output); +} + +#[test] +fn src_line_3407() { + let input = r#" [foo]: /url "title" + +[foo]"#; + let output = r#"
[foo]: /url "title"
+
+

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_3421() { + let input = r#"``` +[foo]: /url +``` + +[foo]"#; + let output = r#"
[foo]: /url
+
+

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_3436() { + let input = r#"Foo +[bar]: /baz + +[bar]"#; + let output = r#"

Foo +[bar]: /baz

+

[bar]

"#; + run(input, output); +} + +#[test] +fn src_line_3451() { + let input = r#"# [Foo] +[foo]: /url +> bar"#; + let output = r#"

Foo

+
+

bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3462() { + let input = r#"[foo]: /url +bar +=== +[foo]"#; + let output = r#"

bar

+

foo

"#; + run(input, output); +} + +#[test] +fn src_line_3472() { + let input = r#"[foo]: /url +=== +[foo]"#; + let output = r#"

=== +foo

"#; + run(input, output); +} + +#[test] +fn src_line_3485() { + let input = r#"[foo]: /foo-url "foo" +[bar]: /bar-url + "bar" +[baz]: /baz-url + +[foo], +[bar], +[baz]"#; + let output = r#"

foo, +bar, +baz

"#; + run(input, output); +} + +#[test] +fn src_line_3506() { + let input = r#"[foo] + +> [foo]: /url"#; + let output = r#"

foo

+
+
"#; + run(input, output); +} + +#[test] +fn src_line_3528() { + let input = r#"aaa + +bbb"#; + let output = r#"

aaa

+

bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3540() { + let input = r#"aaa +bbb + +ccc +ddd"#; + let output = r#"

aaa +bbb

+

ccc +ddd

"#; + run(input, output); +} + +#[test] +fn src_line_3556() { + let input = r#"aaa + + +bbb"#; + let output = r#"

aaa

+

bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3569() { + let input = r#" aaa + bbb"#; + let output = r#"

aaa +bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3581() { + let input = r#"aaa + bbb + ccc"#; + let output = r#"

aaa +bbb +ccc

"#; + run(input, output); +} + +#[test] +fn src_line_3595() { + let input = r#" aaa +bbb"#; + let output = r#"

aaa +bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3604() { + let input = r#" aaa +bbb"#; + let output = r#"
aaa
+
+

bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3618() { + let input = "aaa \x20 +bbb \x20"; + let output = r#"

aaa
+bbb

"#; + run(input, output); +} + +#[test] +fn src_line_3635() { + let input = " \x20 + +aaa + \x20 + +# aaa + + \x20"; + let output = r#"

aaa

+

aaa

"#; + run(input, output); +} + +#[test] +fn src_line_3703() { + let input = r#"> # Foo +> bar +> baz"#; + let output = r#"
+

Foo

+

bar +baz

+
"#; + run(input, output); +} + +#[test] +fn src_line_3718() { + let input = r#"># Foo +>bar +> baz"#; + let output = r#"
+

Foo

+

bar +baz

+
"#; + run(input, output); +} + +#[test] +fn src_line_3733() { + let input = r#" > # Foo + > bar + > baz"#; + let output = r#"
+

Foo

+

bar +baz

+
"#; + run(input, output); +} + +#[test] +fn src_line_3748() { + let input = r#" > # Foo + > bar + > baz"#; + let output = r#"
> # Foo
+> bar
+> baz
+
"#; + run(input, output); +} + +#[test] +fn src_line_3763() { + let input = r#"> # Foo +> bar +baz"#; + let output = r#"
+

Foo

+

bar +baz

+
"#; + run(input, output); +} + +#[test] +fn src_line_3779() { + let input = r#"> bar +baz +> foo"#; + let output = r#"
+

bar +baz +foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_3803() { + let input = r#"> foo +---"#; + let output = r#"
+

foo

+
+
"#; + run(input, output); +} + +#[test] +fn src_line_3823() { + let input = r#"> - foo +- bar"#; + let output = r#"
+
    +
  • foo
  • +
+
+
    +
  • bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_3841() { + let input = r#"> foo + bar"#; + let output = r#"
+
foo
+
+
+
bar
+
"#; + run(input, output); +} + +#[test] +fn src_line_3854() { + let input = r#"> ``` +foo +```"#; + let output = r#"
+
+
+

foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_3870() { + let input = r#"> foo + - bar"#; + let output = r#"
+

foo +- bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3894() { + let input = r#">"#; + let output = r#"
+
"#; + run(input, output); +} + +#[test] +fn src_line_3902() { + let input = "> +> \x20 +>\x20"; + let output = r#"
+
"#; + run(input, output); +} + +#[test] +fn src_line_3914() { + let input = "> +> foo +> \x20"; + let output = r#"
+

foo

+
"#; + run(input, output); +} + +#[test] +fn src_line_3927() { + let input = r#"> foo + +> bar"#; + let output = r#"
+

foo

+
+
+

bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3949() { + let input = r#"> foo +> bar"#; + let output = r#"
+

foo +bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3962() { + let input = r#"> foo +> +> bar"#; + let output = r#"
+

foo

+

bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3976() { + let input = r#"foo +> bar"#; + let output = r#"

foo

+
+

bar

+
"#; + run(input, output); +} + +#[test] +fn src_line_3990() { + let input = r#"> aaa +*** +> bbb"#; + let output = r#"
+

aaa

+
+
+
+

bbb

+
"#; + run(input, output); +} + +#[test] +fn src_line_4008() { + let input = r#"> bar +baz"#; + let output = r#"
+

bar +baz

+
"#; + run(input, output); +} + +#[test] +fn src_line_4019() { + let input = r#"> bar + +baz"#; + let output = r#"
+

bar

+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_4031() { + let input = r#"> bar +> +baz"#; + let output = r#"
+

bar

+
+

baz

"#; + run(input, output); +} + +#[test] +fn src_line_4047() { + let input = r#"> > > foo +bar"#; + let output = r#"
+
+
+

foo +bar

+
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_4062() { + let input = r#">>> foo +> bar +>>baz"#; + let output = r#"
+
+
+

foo +bar +baz

+
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_4084() { + let input = r#"> code + +> not code"#; + let output = r#"
+
code
+
+
+
+

not code

+
"#; + run(input, output); +} + +#[test] +fn src_line_4138() { + let input = r#"A paragraph +with two lines. + + indented code + +> A block quote."#; + let output = r#"

A paragraph +with two lines.

+
indented code
+
+
+

A block quote.

+
"#; + run(input, output); +} + +#[test] +fn src_line_4160() { + let input = r#"1. A paragraph + with two lines. + + indented code + + > A block quote."#; + let output = r#"
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4193() { + let input = r#"- one + + two"#; + let output = r#"
    +
  • one
  • +
+

two

"#; + run(input, output); +} + +#[test] +fn src_line_4205() { + let input = r#"- one + + two"#; + let output = r#"
    +
  • +

    one

    +

    two

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4219() { + let input = r#" - one + + two"#; + let output = r#"
    +
  • one
  • +
+
 two
+
"#; + run(input, output); +} + +#[test] +fn src_line_4232() { + let input = r#" - one + + two"#; + let output = r#"
    +
  • +

    one

    +

    two

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4254() { + let input = r#" > > 1. one +>> +>> two"#; + let output = r#"
+
+
    +
  1. +

    one

    +

    two

    +
  2. +
+
+
"#; + run(input, output); +} + +#[test] +fn src_line_4281() { + let input = r#">>- one +>> + > > two"#; + let output = r#"
+
+
    +
  • one
  • +
+

two

+
+
"#; + run(input, output); +} + +#[test] +fn src_line_4300() { + let input = r#"-one + +2.two"#; + let output = r#"

-one

+

2.two

"#; + run(input, output); +} + +#[test] +fn src_line_4313() { + let input = r#"- foo + + + bar"#; + let output = r#"
    +
  • +

    foo

    +

    bar

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4330() { + let input = r#"1. foo + + ``` + bar + ``` + + baz + + > bam"#; + let output = r#"
    +
  1. +

    foo

    +
    bar
    +
    +

    baz

    +
    +

    bam

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4358() { + let input = r#"- Foo + + bar + + + baz"#; + let output = r#"
    +
  • +

    Foo

    +
    bar
    +
    +
    +baz
    +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4380() { + let input = r#"123456789. ok"#; + let output = r#"
    +
  1. ok
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4389() { + let input = r#"1234567890. not ok"#; + let output = r#"

1234567890. not ok

"#; + run(input, output); +} + +#[test] +fn src_line_4398() { + let input = r#"0. ok"#; + let output = r#"
    +
  1. ok
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4407() { + let input = r#"003. ok"#; + let output = r#"
    +
  1. ok
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4418() { + let input = r#"-1. not ok"#; + let output = r#"

-1. not ok

"#; + run(input, output); +} + +#[test] +fn src_line_4441() { + let input = r#"- foo + + bar"#; + let output = r#"
    +
  • +

    foo

    +
    bar
    +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4458() { + let input = r#" 10. foo + + bar"#; + let output = r#"
    +
  1. +

    foo

    +
    bar
    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4477() { + let input = r#" indented code + +paragraph + + more code"#; + let output = r#"
indented code
+
+

paragraph

+
more code
+
"#; + run(input, output); +} + +#[test] +fn src_line_4492() { + let input = r#"1. indented code + + paragraph + + more code"#; + let output = r#"
    +
  1. +
    indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4514() { + let input = r#"1. indented code + + paragraph + + more code"#; + let output = r#"
    +
  1. +
     indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4541() { + let input = r#" foo + +bar"#; + let output = r#"

foo

+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_4551() { + let input = r#"- foo + + bar"#; + let output = r#"
    +
  • foo
  • +
+

bar

"#; + run(input, output); +} + +#[test] +fn src_line_4568() { + let input = r#"- foo + + bar"#; + let output = r#"
    +
  • +

    foo

    +

    bar

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4595() { + let input = r#"- + foo +- + ``` + bar + ``` +- + baz"#; + let output = r#"
    +
  • foo
  • +
  • +
    bar
    +
    +
  • +
  • +
    baz
    +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4621() { + let input = "- \x20 + foo"; + let output = r#"
    +
  • foo
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4635() { + let input = r#"- + + foo"#; + let output = r#"
    +
  • +
+

foo

"#; + run(input, output); +} + +#[test] +fn src_line_4649() { + let input = r#"- foo +- +- bar"#; + let output = r#"
    +
  • foo
  • +
  • +
  • bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4664() { + let input = "- foo +- \x20 +- bar"; + let output = r#"
    +
  • foo
  • +
  • +
  • bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4679() { + let input = r#"1. foo +2. +3. bar"#; + let output = r#"
    +
  1. foo
  2. +
  3. +
  4. bar
  5. +
"#; + run(input, output); +} + +#[test] +fn src_line_4694() { + let input = r#"*"#; + let output = r#"
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4704() { + let input = r#"foo +* + +foo +1."#; + let output = r#"

foo +*

+

foo +1.

"#; + run(input, output); +} + +#[test] +fn src_line_4726() { + let input = r#" 1. A paragraph + with two lines. + + indented code + + > A block quote."#; + let output = r#"
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4750() { + let input = r#" 1. A paragraph + with two lines. + + indented code + + > A block quote."#; + let output = r#"
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4774() { + let input = r#" 1. A paragraph + with two lines. + + indented code + + > A block quote."#; + let output = r#"
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4798() { + let input = r#" 1. A paragraph + with two lines. + + indented code + + > A block quote."#; + let output = r#"
1.  A paragraph
+    with two lines.
+
+        indented code
+
+    > A block quote.
+
"#; + run(input, output); +} + +#[test] +fn src_line_4828() { + let input = r#" 1. A paragraph +with two lines. + + indented code + + > A block quote."#; + let output = r#"
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4852() { + let input = r#" 1. A paragraph + with two lines."#; + let output = r#"
    +
  1. A paragraph +with two lines.
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4865() { + let input = r#"> 1. > Blockquote +continued here."#; + let output = r#"
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
"#; + run(input, output); +} + +#[test] +fn src_line_4882() { + let input = r#"> 1. > Blockquote +> continued here."#; + let output = r#"
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
"#; + run(input, output); +} + +#[test] +fn src_line_4910() { + let input = r#"- foo + - bar + - baz + - boo"#; + let output = r#"
    +
  • foo +
      +
    • bar +
        +
      • baz +
          +
        • boo
        • +
        +
      • +
      +
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4936() { + let input = r#"- foo + - bar + - baz + - boo"#; + let output = r#"
    +
  • foo
  • +
  • bar
  • +
  • baz
  • +
  • boo
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4953() { + let input = r#"10) foo + - bar"#; + let output = r#"
    +
  1. foo +
      +
    • bar
    • +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_4969() { + let input = r#"10) foo + - bar"#; + let output = r#"
    +
  1. foo
  2. +
+
    +
  • bar
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4984() { + let input = r#"- - foo"#; + let output = r#"
    +
  • +
      +
    • foo
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_4997() { + let input = r#"1. - 2. foo"#; + let output = r#"
    +
  1. +
      +
    • +
        +
      1. foo
      2. +
      +
    • +
    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_5016() { + let input = r#"- # Foo +- Bar + --- + baz"#; + let output = r#"
    +
  • +

    Foo

    +
  • +
  • +

    Bar

    +baz
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5252() { + let input = r#"- foo +- bar ++ baz"#; + let output = r#"
    +
  • foo
  • +
  • bar
  • +
+
    +
  • baz
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5267() { + let input = r#"1. foo +2. bar +3) baz"#; + let output = r#"
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_5286() { + let input = r#"Foo +- bar +- baz"#; + let output = r#"

Foo

+
    +
  • bar
  • +
  • baz
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5363() { + let input = r#"The number of windows in my house is +14. The number of doors is 6."#; + let output = r#"

The number of windows in my house is +14. The number of doors is 6.

"#; + run(input, output); +} + +#[test] +fn src_line_5373() { + let input = r#"The number of windows in my house is +1. The number of doors is 6."#; + let output = r#"

The number of windows in my house is

+
    +
  1. The number of doors is 6.
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_5387() { + let input = r#"- foo + +- bar + + +- baz"#; + let output = r#"
    +
  • +

    foo

    +
  • +
  • +

    bar

    +
  • +
  • +

    baz

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5408() { + let input = r#"- foo + - bar + - baz + + + bim"#; + let output = r#"
    +
  • foo +
      +
    • bar +
        +
      • +

        baz

        +

        bim

        +
      • +
      +
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5438() { + let input = r#"- foo +- bar + + + +- baz +- bim"#; + let output = r#"
    +
  • foo
  • +
  • bar
  • +
+ +
    +
  • baz
  • +
  • bim
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5459() { + let input = r#"- foo + + notcode + +- foo + + + + code"#; + let output = r#"
    +
  • +

    foo

    +

    notcode

    +
  • +
  • +

    foo

    +
  • +
+ +
code
+
"#; + run(input, output); +} + +#[test] +fn src_line_5490() { + let input = r#"- a + - b + - c + - d + - e + - f +- g"#; + let output = r#"
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d
  • +
  • e
  • +
  • f
  • +
  • g
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5511() { + let input = r#"1. a + + 2. b + + 3. c"#; + let output = r#"
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
  5. +

    c

    +
  6. +
"#; + run(input, output); +} + +#[test] +fn src_line_5535() { + let input = r#"- a + - b + - c + - d + - e"#; + let output = r#"
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d +- e
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5555() { + let input = r#"1. a + + 2. b + + 3. c"#; + let output = r#"
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
+
3. c
+
"#; + run(input, output); +} + +#[test] +fn src_line_5578() { + let input = r#"- a +- b + +- c"#; + let output = r#"
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    c

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5600() { + let input = r#"* a +* + +* c"#; + let output = r#"
    +
  • +

    a

    +
  • +
  • +
  • +

    c

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5622() { + let input = r#"- a +- b + + c +- d"#; + let output = r#"
    +
  • +

    a

    +
  • +
  • +

    b

    +

    c

    +
  • +
  • +

    d

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5644() { + let input = r#"- a +- b + + [ref]: /url +- d"#; + let output = r#"
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    d

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5667() { + let input = r#"- a +- ``` + b + + + ``` +- c"#; + let output = r#"
    +
  • a
  • +
  • +
    b
    +
    +
    +
    +
  • +
  • c
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5693() { + let input = r#"- a + - b + + c +- d"#; + let output = r#"
    +
  • a +
      +
    • +

      b

      +

      c

      +
    • +
    +
  • +
  • d
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5717() { + let input = r#"* a + > b + > +* c"#; + let output = r#"
    +
  • a +
    +

    b

    +
    +
  • +
  • c
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5737() { + let input = r#"- a + > b + ``` + c + ``` +- d"#; + let output = r#"
    +
  • a +
    +

    b

    +
    +
    c
    +
    +
  • +
  • d
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5760() { + let input = r#"- a"#; + let output = r#"
    +
  • a
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5769() { + let input = r#"- a + - b"#; + let output = r#"
    +
  • a +
      +
    • b
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5786() { + let input = r#"1. ``` + foo + ``` + + bar"#; + let output = r#"
    +
  1. +
    foo
    +
    +

    bar

    +
  2. +
"#; + run(input, output); +} + +#[test] +fn src_line_5805() { + let input = r#"* foo + * bar + + baz"#; + let output = r#"
    +
  • +

    foo

    +
      +
    • bar
    • +
    +

    baz

    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5823() { + let input = r#"- a + - b + - c + +- d + - e + - f"#; + let output = r#"
    +
  • +

    a

    +
      +
    • b
    • +
    • c
    • +
    +
  • +
  • +

    d

    +
      +
    • e
    • +
    • f
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn src_line_5857() { + let input = r#"`hi`lo`"#; + let output = r#"

hilo`

"#; + run(input, output); +} + +#[test] +fn src_line_5889() { + let input = r#"`foo`"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_5900() { + let input = r#"`` foo ` bar ``"#; + let output = r#"

foo ` bar

"#; + run(input, output); +} + +#[test] +fn src_line_5910() { + let input = r#"` `` `"#; + let output = r#"

``

"#; + run(input, output); +} + +#[test] +fn src_line_5918() { + let input = r#"` `` `"#; + let output = r#"

``

"#; + run(input, output); +} + +#[test] +fn src_line_5927() { + let input = r#"` a`"#; + let output = r#"

a

"#; + run(input, output); +} + +#[test] +fn src_line_5936() { + let input = r#"` b `"#; + let output = r#"

 b 

"#; + run(input, output); +} + +#[test] +fn src_line_5944() { + let input = r#"` ` +` `"#; + let output = r#"

  +

"#; + run(input, output); +} + +#[test] +fn src_line_5955() { + let input = "`` +foo +bar \x20 +baz +``"; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_5965() { + let input = "`` +foo\x20 +``"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_5976() { + let input = "`foo bar\x20 +baz`"; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_5993() { + let input = r#"`foo\`bar`"#; + let output = r#"

foo\bar`

"#; + run(input, output); +} + +#[test] +fn src_line_6004() { + let input = r#"``foo`bar``"#; + let output = r#"

foo`bar

"#; + run(input, output); +} + +#[test] +fn src_line_6010() { + let input = r#"` foo `` bar `"#; + let output = r#"

foo `` bar

"#; + run(input, output); +} + +#[test] +fn src_line_6022() { + let input = r#"*foo`*`"#; + let output = r#"

*foo*

"#; + run(input, output); +} + +#[test] +fn src_line_6031() { + let input = r#"[not a `link](/foo`)"#; + let output = r#"

[not a link](/foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6041() { + let input = r#"``"#; + let output = r#"

<a href="">`

"#; + run(input, output); +} + +#[test] +fn src_line_6050() { + let input = r#"
`"#; + let output = r#"

`

"#; + run(input, output); +} + +#[test] +fn src_line_6059() { + let input = r#"``"#; + let output = r#"

<http://foo.bar.baz>`

"#; + run(input, output); +} + +#[test] +fn src_line_6068() { + let input = r#"`"#; + let output = r#"

http://foo.bar.`baz`

"#; + run(input, output); +} + +#[test] +fn src_line_6078() { + let input = r#"```foo``"#; + let output = r#"

```foo``

"#; + run(input, output); +} + +#[test] +fn src_line_6085() { + let input = r#"`foo"#; + let output = r#"

`foo

"#; + run(input, output); +} + +#[test] +fn src_line_6094() { + let input = r#"`foo``bar``"#; + let output = r#"

`foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6311() { + let input = r#"*foo bar*"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6321() { + let input = r#"a * foo bar*"#; + let output = r#"

a * foo bar*

"#; + run(input, output); +} + +#[test] +fn src_line_6332() { + let input = r#"a*"foo"*"#; + let output = r#"

a*"foo"*

"#; + run(input, output); +} + +#[test] +fn src_line_6341() { + let input = r#"* a *"#; + let output = r#"

* a *

"#; + run(input, output); +} + +#[test] +fn src_line_6350() { + let input = r#"foo*bar*"#; + let output = r#"

foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6357() { + let input = r#"5*6*78"#; + let output = r#"

5678

"#; + run(input, output); +} + +#[test] +fn src_line_6366() { + let input = r#"_foo bar_"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6376() { + let input = r#"_ foo bar_"#; + let output = r#"

_ foo bar_

"#; + run(input, output); +} + +#[test] +fn src_line_6386() { + let input = r#"a_"foo"_"#; + let output = r#"

a_"foo"_

"#; + run(input, output); +} + +#[test] +fn src_line_6395() { + let input = r#"foo_bar_"#; + let output = r#"

foo_bar_

"#; + run(input, output); +} + +#[test] +fn src_line_6402() { + let input = r#"5_6_78"#; + let output = r#"

5_6_78

"#; + run(input, output); +} + +#[test] +fn src_line_6409() { + let input = r#"пристаням_стремятся_"#; + let output = r#"

пристаням_стремятся_

"#; + run(input, output); +} + +#[test] +fn src_line_6419() { + let input = r#"aa_"bb"_cc"#; + let output = r#"

aa_"bb"_cc

"#; + run(input, output); +} + +#[test] +fn src_line_6430() { + let input = r#"foo-_(bar)_"#; + let output = r#"

foo-(bar)

"#; + run(input, output); +} + +#[test] +fn src_line_6442() { + let input = r#"_foo*"#; + let output = r#"

_foo*

"#; + run(input, output); +} + +#[test] +fn src_line_6452() { + let input = r#"*foo bar *"#; + let output = r#"

*foo bar *

"#; + run(input, output); +} + +#[test] +fn src_line_6461() { + let input = r#"*foo bar +*"#; + let output = r#"

*foo bar +*

"#; + run(input, output); +} + +#[test] +fn src_line_6474() { + let input = r#"*(*foo)"#; + let output = r#"

*(*foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6484() { + let input = r#"*(*foo*)*"#; + let output = r#"

(foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6493() { + let input = r#"*foo*bar"#; + let output = r#"

foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6506() { + let input = r#"_foo bar _"#; + let output = r#"

_foo bar _

"#; + run(input, output); +} + +#[test] +fn src_line_6516() { + let input = r#"_(_foo)"#; + let output = r#"

_(_foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6525() { + let input = r#"_(_foo_)_"#; + let output = r#"

(foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6534() { + let input = r#"_foo_bar"#; + let output = r#"

_foo_bar

"#; + run(input, output); +} + +#[test] +fn src_line_6541() { + let input = r#"_пристаням_стремятся"#; + let output = r#"

_пристаням_стремятся

"#; + run(input, output); +} + +#[test] +fn src_line_6548() { + let input = r#"_foo_bar_baz_"#; + let output = r#"

foo_bar_baz

"#; + run(input, output); +} + +#[test] +fn src_line_6559() { + let input = r#"_(bar)_."#; + let output = r#"

(bar).

"#; + run(input, output); +} + +#[test] +fn src_line_6568() { + let input = r#"**foo bar**"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6578() { + let input = r#"** foo bar**"#; + let output = r#"

** foo bar**

"#; + run(input, output); +} + +#[test] +fn src_line_6589() { + let input = r#"a**"foo"**"#; + let output = r#"

a**"foo"**

"#; + run(input, output); +} + +#[test] +fn src_line_6598() { + let input = r#"foo**bar**"#; + let output = r#"

foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6607() { + let input = r#"__foo bar__"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6617() { + let input = r#"__ foo bar__"#; + let output = r#"

__ foo bar__

"#; + run(input, output); +} + +#[test] +fn src_line_6625() { + let input = r#"__ +foo bar__"#; + let output = r#"

__ +foo bar__

"#; + run(input, output); +} + +#[test] +fn src_line_6637() { + let input = r#"a__"foo"__"#; + let output = r#"

a__"foo"__

"#; + run(input, output); +} + +#[test] +fn src_line_6646() { + let input = r#"foo__bar__"#; + let output = r#"

foo__bar__

"#; + run(input, output); +} + +#[test] +fn src_line_6653() { + let input = r#"5__6__78"#; + let output = r#"

5__6__78

"#; + run(input, output); +} + +#[test] +fn src_line_6660() { + let input = r#"пристаням__стремятся__"#; + let output = r#"

пристаням__стремятся__

"#; + run(input, output); +} + +#[test] +fn src_line_6667() { + let input = r#"__foo, __bar__, baz__"#; + let output = r#"

foo, bar, baz

"#; + run(input, output); +} + +#[test] +fn src_line_6678() { + let input = r#"foo-__(bar)__"#; + let output = r#"

foo-(bar)

"#; + run(input, output); +} + +#[test] +fn src_line_6691() { + let input = r#"**foo bar **"#; + let output = r#"

**foo bar **

"#; + run(input, output); +} + +#[test] +fn src_line_6704() { + let input = r#"**(**foo)"#; + let output = r#"

**(**foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6714() { + let input = r#"*(**foo**)*"#; + let output = r#"

(foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6721() { + let input = r#"**Gomphocarpus (*Gomphocarpus physocarpus*, syn. +*Asclepias physocarpa*)**"#; + let output = r#"

Gomphocarpus (Gomphocarpus physocarpus, syn. +Asclepias physocarpa)

"#; + run(input, output); +} + +#[test] +fn src_line_6730() { + let input = r#"**foo "*bar*" foo**"#; + let output = r#"

foo "bar" foo

"#; + run(input, output); +} + +#[test] +fn src_line_6739() { + let input = r#"**foo**bar"#; + let output = r#"

foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6751() { + let input = r#"__foo bar __"#; + let output = r#"

__foo bar __

"#; + run(input, output); +} + +#[test] +fn src_line_6761() { + let input = r#"__(__foo)"#; + let output = r#"

__(__foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6771() { + let input = r#"_(__foo__)_"#; + let output = r#"

(foo)

"#; + run(input, output); +} + +#[test] +fn src_line_6780() { + let input = r#"__foo__bar"#; + let output = r#"

__foo__bar

"#; + run(input, output); +} + +#[test] +fn src_line_6787() { + let input = r#"__пристаням__стремятся"#; + let output = r#"

__пристаням__стремятся

"#; + run(input, output); +} + +#[test] +fn src_line_6794() { + let input = r#"__foo__bar__baz__"#; + let output = r#"

foo__bar__baz

"#; + run(input, output); +} + +#[test] +fn src_line_6805() { + let input = r#"__(bar)__."#; + let output = r#"

(bar).

"#; + run(input, output); +} + +#[test] +fn src_line_6817() { + let input = r#"*foo [bar](/url)*"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6824() { + let input = r#"*foo +bar*"#; + let output = r#"

foo +bar

"#; + run(input, output); +} + +#[test] +fn src_line_6836() { + let input = r#"_foo __bar__ baz_"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_6843() { + let input = r#"_foo _bar_ baz_"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_6850() { + let input = r#"__foo_ bar_"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6857() { + let input = r#"*foo *bar**"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6864() { + let input = r#"*foo **bar** baz*"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_6870() { + let input = r#"*foo**bar**baz*"#; + let output = r#"

foobarbaz

"#; + run(input, output); +} + +#[test] +fn src_line_6894() { + let input = r#"*foo**bar*"#; + let output = r#"

foo**bar

"#; + run(input, output); +} + +#[test] +fn src_line_6907() { + let input = r#"***foo** bar*"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6914() { + let input = r#"*foo **bar***"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6921() { + let input = r#"*foo**bar***"#; + let output = r#"

foobar

"#; + run(input, output); +} + +#[test] +fn src_line_6932() { + let input = r#"foo***bar***baz"#; + let output = r#"

foobarbaz

"#; + run(input, output); +} + +#[test] +fn src_line_6938() { + let input = r#"foo******bar*********baz"#; + let output = r#"

foobar***baz

"#; + run(input, output); +} + +#[test] +fn src_line_6947() { + let input = r#"*foo **bar *baz* bim** bop*"#; + let output = r#"

foo bar baz bim bop

"#; + run(input, output); +} + +#[test] +fn src_line_6954() { + let input = r#"*foo [*bar*](/url)*"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6963() { + let input = r#"** is not an empty emphasis"#; + let output = r#"

** is not an empty emphasis

"#; + run(input, output); +} + +#[test] +fn src_line_6970() { + let input = r#"**** is not an empty strong emphasis"#; + let output = r#"

**** is not an empty strong emphasis

"#; + run(input, output); +} + +#[test] +fn src_line_6983() { + let input = r#"**foo [bar](/url)**"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_6990() { + let input = r#"**foo +bar**"#; + let output = r#"

foo +bar

"#; + run(input, output); +} + +#[test] +fn src_line_7002() { + let input = r#"__foo _bar_ baz__"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_7009() { + let input = r#"__foo __bar__ baz__"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_7016() { + let input = r#"____foo__ bar__"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_7023() { + let input = r#"**foo **bar****"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_7030() { + let input = r#"**foo *bar* baz**"#; + let output = r#"

foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_7037() { + let input = r#"**foo*bar*baz**"#; + let output = r#"

foobarbaz

"#; + run(input, output); +} + +#[test] +fn src_line_7044() { + let input = r#"***foo* bar**"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_7051() { + let input = r#"**foo *bar***"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_7060() { + let input = r#"**foo *bar **baz** +bim* bop**"#; + let output = r#"

foo bar baz +bim bop

"#; + run(input, output); +} + +#[test] +fn src_line_7069() { + let input = r#"**foo [*bar*](/url)**"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_7078() { + let input = r#"__ is not an empty emphasis"#; + let output = r#"

__ is not an empty emphasis

"#; + run(input, output); +} + +#[test] +fn src_line_7085() { + let input = r#"____ is not an empty strong emphasis"#; + let output = r#"

____ is not an empty strong emphasis

"#; + run(input, output); +} + +#[test] +fn src_line_7095() { + let input = r#"foo ***"#; + let output = r#"

foo ***

"#; + run(input, output); +} + +#[test] +fn src_line_7102() { + let input = r#"foo *\**"#; + let output = r#"

foo *

"#; + run(input, output); +} + +#[test] +fn src_line_7109() { + let input = r#"foo *_*"#; + let output = r#"

foo _

"#; + run(input, output); +} + +#[test] +fn src_line_7116() { + let input = r#"foo *****"#; + let output = r#"

foo *****

"#; + run(input, output); +} + +#[test] +fn src_line_7123() { + let input = r#"foo **\***"#; + let output = r#"

foo *

"#; + run(input, output); +} + +#[test] +fn src_line_7130() { + let input = r#"foo **_**"#; + let output = r#"

foo _

"#; + run(input, output); +} + +#[test] +fn src_line_7141() { + let input = r#"**foo*"#; + let output = r#"

*foo

"#; + run(input, output); +} + +#[test] +fn src_line_7148() { + let input = r#"*foo**"#; + let output = r#"

foo*

"#; + run(input, output); +} + +#[test] +fn src_line_7155() { + let input = r#"***foo**"#; + let output = r#"

*foo

"#; + run(input, output); +} + +#[test] +fn src_line_7162() { + let input = r#"****foo*"#; + let output = r#"

***foo

"#; + run(input, output); +} + +#[test] +fn src_line_7169() { + let input = r#"**foo***"#; + let output = r#"

foo*

"#; + run(input, output); +} + +#[test] +fn src_line_7176() { + let input = r#"*foo****"#; + let output = r#"

foo***

"#; + run(input, output); +} + +#[test] +fn src_line_7186() { + let input = r#"foo ___"#; + let output = r#"

foo ___

"#; + run(input, output); +} + +#[test] +fn src_line_7193() { + let input = r#"foo _\__"#; + let output = r#"

foo _

"#; + run(input, output); +} + +#[test] +fn src_line_7200() { + let input = r#"foo _*_"#; + let output = r#"

foo *

"#; + run(input, output); +} + +#[test] +fn src_line_7207() { + let input = r#"foo _____"#; + let output = r#"

foo _____

"#; + run(input, output); +} + +#[test] +fn src_line_7214() { + let input = r#"foo __\___"#; + let output = r#"

foo _

"#; + run(input, output); +} + +#[test] +fn src_line_7221() { + let input = r#"foo __*__"#; + let output = r#"

foo *

"#; + run(input, output); +} + +#[test] +fn src_line_7228() { + let input = r#"__foo_"#; + let output = r#"

_foo

"#; + run(input, output); +} + +#[test] +fn src_line_7239() { + let input = r#"_foo__"#; + let output = r#"

foo_

"#; + run(input, output); +} + +#[test] +fn src_line_7246() { + let input = r#"___foo__"#; + let output = r#"

_foo

"#; + run(input, output); +} + +#[test] +fn src_line_7253() { + let input = r#"____foo_"#; + let output = r#"

___foo

"#; + run(input, output); +} + +#[test] +fn src_line_7260() { + let input = r#"__foo___"#; + let output = r#"

foo_

"#; + run(input, output); +} + +#[test] +fn src_line_7267() { + let input = r#"_foo____"#; + let output = r#"

foo___

"#; + run(input, output); +} + +#[test] +fn src_line_7277() { + let input = r#"**foo**"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7284() { + let input = r#"*_foo_*"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7291() { + let input = r#"__foo__"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7298() { + let input = r#"_*foo*_"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7308() { + let input = r#"****foo****"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7315() { + let input = r#"____foo____"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7326() { + let input = r#"******foo******"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7335() { + let input = r#"***foo***"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7342() { + let input = r#"_____foo_____"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7351() { + let input = r#"*foo _bar* baz_"#; + let output = r#"

foo _bar baz_

"#; + run(input, output); +} + +#[test] +fn src_line_7358() { + let input = r#"*foo __bar *baz bim__ bam*"#; + let output = r#"

foo bar *baz bim bam

"#; + run(input, output); +} + +#[test] +fn src_line_7367() { + let input = r#"**foo **bar baz**"#; + let output = r#"

**foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_7374() { + let input = r#"*foo *bar baz*"#; + let output = r#"

*foo bar baz

"#; + run(input, output); +} + +#[test] +fn src_line_7383() { + let input = r#"*[bar*](/url)"#; + let output = r#"

*bar*

"#; + run(input, output); +} + +#[test] +fn src_line_7390() { + let input = r#"_foo [bar_](/url)"#; + let output = r#"

_foo bar_

"#; + run(input, output); +} + +#[test] +fn src_line_7397() { + let input = r#"*"#; + let output = r#"

*

"#; + run(input, output); +} + +#[test] +fn src_line_7404() { + let input = r#"**"#; + let output = r#"

**

"#; + run(input, output); +} + +#[test] +fn src_line_7411() { + let input = r#"__"#; + let output = r#"

__

"#; + run(input, output); +} + +#[test] +fn src_line_7418() { + let input = r#"*a `*`*"#; + let output = r#"

a *

"#; + run(input, output); +} + +#[test] +fn src_line_7425() { + let input = r#"_a `_`_"#; + let output = r#"

a _

"#; + run(input, output); +} + +#[test] +fn src_line_7432() { + let input = r#"**a"#; + let output = r#"

**ahttp://foo.bar/?q=**

"#; + run(input, output); +} + +#[test] +fn src_line_7439() { + let input = r#"__a"#; + let output = r#"

__ahttp://foo.bar/?q=__

"#; + run(input, output); +} + +#[test] +fn src_line_7527() { + let input = r#"[link](/uri "title")"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7537() { + let input = r#"[link](/uri)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7543() { + let input = r#"[](./target.md)"#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_7550() { + let input = r#"[link]()"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7557() { + let input = r#"[link](<>)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7564() { + let input = r#"[]()"#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_7573() { + let input = r#"[link](/my uri)"#; + let output = r#"

[link](/my uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7579() { + let input = r#"[link](
)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7588() { + let input = r#"[link](foo +bar)"#; + let output = r#"

[link](foo +bar)

"#; + run(input, output); +} + +#[test] +fn src_line_7596() { + let input = r#"[link]()"#; + let output = r#"

[link]()

"#; + run(input, output); +} + +#[test] +fn src_line_7607() { + let input = r#"[a]()"#; + let output = r#"

a

"#; + run(input, output); +} + +#[test] +fn src_line_7615() { + let input = r#"[link]()"#; + let output = r#"

[link](<foo>)

"#; + run(input, output); +} + +#[test] +fn src_line_7624() { + let input = r#"[a]( +[a](c)"#; + let output = r#"

[a](<b)c +[a](<b)c> +[a](c)

"#; + run(input, output); +} + +#[test] +fn src_line_7636() { + let input = r#"[link](\(foo\))"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7645() { + let input = r#"[link](foo(and(bar)))"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7654() { + let input = r#"[link](foo(and(bar))"#; + let output = r#"

[link](foo(and(bar))

"#; + run(input, output); +} + +#[test] +fn src_line_7661() { + let input = r#"[link](foo\(and\(bar\))"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7668() { + let input = r#"[link]()"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7678() { + let input = r#"[link](foo\)\:)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7687() { + let input = r#"[link](#fragment) + +[link](http://example.com#fragment) + +[link](http://example.com?foo=3#frag)"#; + let output = r##"

link

+

link

+

link

"##; + run(input, output); +} + +#[test] +fn src_line_7703() { + let input = r#"[link](foo\bar)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7719() { + let input = r#"[link](foo%20bä)"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7730() { + let input = r#"[link]("title")"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7739() { + let input = r#"[link](/url "title") +[link](/url 'title') +[link](/url (title))"#; + let output = r#"

link +link +link

"#; + run(input, output); +} + +#[test] +fn src_line_7753() { + let input = r#"[link](/url "title \""")"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7764() { + let input = r#"[link](/url "title")"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7773() { + let input = r#"[link](/url "title "and" title")"#; + let output = r#"

[link](/url "title "and" title")

"#; + run(input, output); +} + +#[test] +fn src_line_7782() { + let input = r#"[link](/url 'title "and" title')"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7807() { + let input = r#"[link]( /uri + "title" )"#; + let output = r#"

link

"#; + run(input, output); +} + +#[test] +fn src_line_7818() { + let input = r#"[link] (/uri)"#; + let output = r#"

[link] (/uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7828() { + let input = r#"[link [foo [bar]]](/uri)"#; + let output = r#"

link [foo [bar]]

"#; + run(input, output); +} + +#[test] +fn src_line_7835() { + let input = r#"[link] bar](/uri)"#; + let output = r#"

[link] bar](/uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7842() { + let input = r#"[link [bar](/uri)"#; + let output = r#"

[link bar

"#; + run(input, output); +} + +#[test] +fn src_line_7849() { + let input = r#"[link \[bar](/uri)"#; + let output = r#"

link [bar

"#; + run(input, output); +} + +#[test] +fn src_line_7858() { + let input = r#"[link *foo **bar** `#`*](/uri)"#; + let output = r#"

link foo bar #

"#; + run(input, output); +} + +#[test] +fn src_line_7865() { + let input = r#"[![moon](moon.jpg)](/uri)"#; + let output = r#"

moon

"#; + run(input, output); +} + +#[test] +fn src_line_7874() { + let input = r#"[foo [bar](/uri)](/uri)"#; + let output = r#"

[foo bar](/uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7881() { + let input = r#"[foo *[bar [baz](/uri)](/uri)*](/uri)"#; + let output = r#"

[foo [bar baz](/uri)](/uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7888() { + let input = r#"![[[foo](uri1)](uri2)](uri3)"#; + let output = r#"

[foo](uri2)

"#; + run(input, output); +} + +#[test] +fn src_line_7898() { + let input = r#"*[foo*](/uri)"#; + let output = r#"

*foo*

"#; + run(input, output); +} + +#[test] +fn src_line_7905() { + let input = r#"[foo *bar](baz*)"#; + let output = r#"

foo *bar

"#; + run(input, output); +} + +#[test] +fn src_line_7915() { + let input = r#"*foo [bar* baz]"#; + let output = r#"

foo [bar baz]

"#; + run(input, output); +} + +#[test] +fn src_line_7925() { + let input = r#"[foo "#; + let output = r#"

[foo

"#; + run(input, output); +} + +#[test] +fn src_line_7932() { + let input = r#"[foo`](/uri)`"#; + let output = r#"

[foo](/uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7939() { + let input = r#"[foo"#; + let output = r#"

[foohttp://example.com/?search=](uri)

"#; + run(input, output); +} + +#[test] +fn src_line_7977() { + let input = r#"[foo][bar] + +[bar]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_7992() { + let input = r#"[link [foo [bar]]][ref] + +[ref]: /uri"#; + let output = r#"

link [foo [bar]]

"#; + run(input, output); +} + +#[test] +fn src_line_8001() { + let input = r#"[link \[bar][ref] + +[ref]: /uri"#; + let output = r#"

link [bar

"#; + run(input, output); +} + +#[test] +fn src_line_8012() { + let input = r#"[link *foo **bar** `#`*][ref] + +[ref]: /uri"#; + let output = r#"

link foo bar #

"#; + run(input, output); +} + +#[test] +fn src_line_8021() { + let input = r#"[![moon](moon.jpg)][ref] + +[ref]: /uri"#; + let output = r#"

moon

"#; + run(input, output); +} + +#[test] +fn src_line_8032() { + let input = r#"[foo [bar](/uri)][ref] + +[ref]: /uri"#; + let output = r#"

[foo bar]ref

"#; + run(input, output); +} + +#[test] +fn src_line_8041() { + let input = r#"[foo *bar [baz][ref]*][ref] + +[ref]: /uri"#; + let output = r#"

[foo bar baz]ref

"#; + run(input, output); +} + +#[test] +fn src_line_8056() { + let input = r#"*[foo*][ref] + +[ref]: /uri"#; + let output = r#"

*foo*

"#; + run(input, output); +} + +#[test] +fn src_line_8065() { + let input = r#"[foo *bar][ref]* + +[ref]: /uri"#; + let output = r#"

foo *bar*

"#; + run(input, output); +} + +#[test] +fn src_line_8077() { + let input = r#"[foo + +[ref]: /uri"#; + let output = r#"

[foo

"#; + run(input, output); +} + +#[test] +fn src_line_8086() { + let input = r#"[foo`][ref]` + +[ref]: /uri"#; + let output = r#"

[foo][ref]

"#; + run(input, output); +} + +#[test] +fn src_line_8095() { + let input = r#"[foo + +[ref]: /uri"#; + let output = r#"

[foohttp://example.com/?search=][ref]

"#; + run(input, output); +} + +#[test] +fn src_line_8106() { + let input = r#"[foo][BaR] + +[bar]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8117() { + let input = r#"[ẞ] + +[SS]: /url"#; + let output = r#"

ẞ

"#; + run(input, output); +} + +#[test] +fn src_line_8129() { + let input = r#"[Foo + bar]: /url + +[Baz][Foo bar]"#; + let output = r#"

Baz

"#; + run(input, output); +} + +#[test] +fn src_line_8142() { + let input = r#"[foo] [bar] + +[bar]: /url "title""#; + let output = r#"

[foo] bar

"#; + run(input, output); +} + +#[test] +fn src_line_8151() { + let input = r#"[foo] +[bar] + +[bar]: /url "title""#; + let output = r#"

[foo] +bar

"#; + run(input, output); +} + +#[test] +fn src_line_8192() { + let input = r#"[foo]: /url1 + +[foo]: /url2 + +[bar][foo]"#; + let output = r#"

bar

"#; + run(input, output); +} + +#[test] +fn src_line_8207() { + let input = r#"[bar][foo\!] + +[foo!]: /url"#; + let output = r#"

[bar][foo!]

"#; + run(input, output); +} + +#[test] +fn src_line_8219() { + let input = r#"[foo][ref[] + +[ref[]: /uri"#; + let output = r#"

[foo][ref[]

+

[ref[]: /uri

"#; + run(input, output); +} + +#[test] +fn src_line_8229() { + let input = r#"[foo][ref[bar]] + +[ref[bar]]: /uri"#; + let output = r#"

[foo][ref[bar]]

+

[ref[bar]]: /uri

"#; + run(input, output); +} + +#[test] +fn src_line_8239() { + let input = r#"[[[foo]]] + +[[[foo]]]: /url"#; + let output = r#"

[[[foo]]]

+

[[[foo]]]: /url

"#; + run(input, output); +} + +#[test] +fn src_line_8249() { + let input = r#"[foo][ref\[] + +[ref\[]: /uri"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8260() { + let input = r#"[bar\\]: /uri + +[bar\\]"#; + let output = r#"

bar\

"#; + run(input, output); +} + +#[test] +fn src_line_8272() { + let input = r#"[] + +[]: /uri"#; + let output = r#"

[]

+

[]: /uri

"#; + run(input, output); +} + +#[test] +fn src_line_8282() { + let input = r#"[ + ] + +[ + ]: /uri"#; + let output = r#"

[ +]

+

[ +]: /uri

"#; + run(input, output); +} + +#[test] +fn src_line_8305() { + let input = r#"[foo][] + +[foo]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8314() { + let input = r#"[*foo* bar][] + +[*foo* bar]: /url "title""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8325() { + let input = r#"[Foo][] + +[foo]: /url "title""#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_8338() { + let input = "[foo]\x20 +[] + +[foo]: /url \"title\""; + let output = r#"

foo +[]

"#; + run(input, output); +} + +#[test] +fn src_line_8358() { + let input = r#"[foo] + +[foo]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8367() { + let input = r#"[*foo* bar] + +[*foo* bar]: /url "title""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8376() { + let input = r#"[[*foo* bar]] + +[*foo* bar]: /url "title""#; + let output = r#"

[foo bar]

"#; + run(input, output); +} + +#[test] +fn src_line_8385() { + let input = r#"[[bar [foo] + +[foo]: /url"#; + let output = r#"

[[bar foo

"#; + run(input, output); +} + +#[test] +fn src_line_8396() { + let input = r#"[Foo] + +[foo]: /url "title""#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_8407() { + let input = r#"[foo] bar + +[foo]: /url"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8419() { + let input = r#"\[foo] + +[foo]: /url "title""#; + let output = r#"

[foo]

"#; + run(input, output); +} + +#[test] +fn src_line_8431() { + let input = r#"[foo*]: /url + +*[foo*]"#; + let output = r#"

*foo*

"#; + run(input, output); +} + +#[test] +fn src_line_8443() { + let input = r#"[foo][bar] + +[foo]: /url1 +[bar]: /url2"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8452() { + let input = r#"[foo][] + +[foo]: /url1"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8462() { + let input = r#"[foo]() + +[foo]: /url1"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8470() { + let input = r#"[foo](not a link) + +[foo]: /url1"#; + let output = r#"

foo(not a link)

"#; + run(input, output); +} + +#[test] +fn src_line_8481() { + let input = r#"[foo][bar][baz] + +[baz]: /url"#; + let output = r#"

[foo]bar

"#; + run(input, output); +} + +#[test] +fn src_line_8493() { + let input = r#"[foo][bar][baz] + +[baz]: /url1 +[bar]: /url2"#; + let output = r#"

foobaz

"#; + run(input, output); +} + +#[test] +fn src_line_8506() { + let input = r#"[foo][bar][baz] + +[baz]: /url1 +[foo]: /url2"#; + let output = r#"

[foo]bar

"#; + run(input, output); +} + +#[test] +fn src_line_8529() { + let input = r#"![foo](/url "title")"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8536() { + let input = r#"![foo *bar*] + +[foo *bar*]: train.jpg "train & tracks""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8545() { + let input = r#"![foo ![bar](/url)](/url2)"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8552() { + let input = r#"![foo [bar](/url)](/url2)"#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8566() { + let input = r#"![foo *bar*][] + +[foo *bar*]: train.jpg "train & tracks""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8575() { + let input = r#"![foo *bar*][foobar] + +[FOOBAR]: train.jpg "train & tracks""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8584() { + let input = r#"![foo](train.jpg)"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8591() { + let input = r#"My ![foo bar](/path/to/train.jpg "title" )"#; + let output = r#"

My foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8598() { + let input = r#"![foo]()"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8605() { + let input = r#"![](/url)"#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_8614() { + let input = r#"![foo][bar] + +[bar]: /url"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8623() { + let input = r#"![foo][bar] + +[BAR]: /url"#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8634() { + let input = r#"![foo][] + +[foo]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8643() { + let input = r#"![*foo* bar][] + +[*foo* bar]: /url "title""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8654() { + let input = r#"![Foo][] + +[foo]: /url "title""#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_8666() { + let input = "![foo]\x20 +[] + +[foo]: /url \"title\""; + let output = r#"

foo +[]

"#; + run(input, output); +} + +#[test] +fn src_line_8679() { + let input = r#"![foo] + +[foo]: /url "title""#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_8688() { + let input = r#"![*foo* bar] + +[*foo* bar]: /url "title""#; + let output = r#"

foo bar

"#; + run(input, output); +} + +#[test] +fn src_line_8699() { + let input = r#"![[foo]] + +[[foo]]: /url "title""#; + let output = r#"

![[foo]]

+

[[foo]]: /url "title"

"#; + run(input, output); +} + +#[test] +fn src_line_8711() { + let input = r#"![Foo] + +[foo]: /url "title""#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_8723() { + let input = r#"!\[foo] + +[foo]: /url "title""#; + let output = r#"

![foo]

"#; + run(input, output); +} + +#[test] +fn src_line_8735() { + let input = r#"\![foo] + +[foo]: /url "title""#; + let output = r#"

!foo

"#; + run(input, output); +} + +#[test] +fn src_line_8768() { + let input = r#""#; + let output = r#"

http://foo.bar.baz

"#; + run(input, output); +} + +#[test] +fn src_line_8775() { + let input = r#""#; + let output = r#"

http://foo.bar.baz/test?q=hello&id=22&boolean

"#; + run(input, output); +} + +#[test] +fn src_line_8782() { + let input = r#""#; + let output = r#"

irc://foo.bar:2233/baz

"#; + run(input, output); +} + +#[test] +fn src_line_8791() { + let input = r#""#; + let output = r#"

MAILTO:FOO@BAR.BAZ

"#; + run(input, output); +} + +#[test] +fn src_line_8803() { + let input = r#""#; + let output = r#"

a+b+c:d

"#; + run(input, output); +} + +#[test] +fn src_line_8810() { + let input = r#""#; + let output = r#"

made-up-scheme://foo,bar

"#; + run(input, output); +} + +#[test] +fn src_line_8817() { + let input = r#""#; + let output = r#"

http://../

"#; + run(input, output); +} + +#[test] +fn src_line_8824() { + let input = r#""#; + let output = r#"

localhost:5001/foo

"#; + run(input, output); +} + +#[test] +fn src_line_8833() { + let input = r#""#; + let output = r#"

<http://foo.bar/baz bim>

"#; + run(input, output); +} + +#[test] +fn src_line_8842() { + let input = r#""#; + let output = r#"

http://example.com/\[\

"#; + run(input, output); +} + +#[test] +fn src_line_8864() { + let input = r#""#; + let output = r#"

foo@bar.example.com

"#; + run(input, output); +} + +#[test] +fn src_line_8871() { + let input = r#""#; + let output = r#"

foo+special@Bar.baz-bar0.com

"#; + run(input, output); +} + +#[test] +fn src_line_8880() { + let input = r#""#; + let output = r#"

<foo+@bar.example.com>

"#; + run(input, output); +} + +#[test] +fn src_line_8889() { + let input = r#"<>"#; + let output = r#"

<>

"#; + run(input, output); +} + +#[test] +fn src_line_8896() { + let input = r#"< http://foo.bar >"#; + let output = r#"

< http://foo.bar >

"#; + run(input, output); +} + +#[test] +fn src_line_8903() { + let input = r#""#; + let output = r#"

<m:abc>

"#; + run(input, output); +} + +#[test] +fn src_line_8910() { + let input = r#""#; + let output = r#"

<foo.bar.baz>

"#; + run(input, output); +} + +#[test] +fn src_line_8917() { + let input = r#"http://example.com"#; + let output = r#"

http://example.com

"#; + run(input, output); +} + +#[test] +fn src_line_8924() { + let input = r#"foo@bar.example.com"#; + let output = r#"

foo@bar.example.com

"#; + run(input, output); +} + +#[test] +fn src_line_9005() { + let input = r#""#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9014() { + let input = r#""#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9023() { + let input = r#""#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9034() { + let input = r#""#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9045() { + let input = r#"Foo "#; + let output = r#"

Foo

"#; + run(input, output); +} + +#[test] +fn src_line_9054() { + let input = r#"<33> <__>"#; + let output = r#"

<33> <__>

"#; + run(input, output); +} + +#[test] +fn src_line_9063() { + let input = r#"
"#; + let output = r#"

<a h*#ref="hi">

"#; + run(input, output); +} + +#[test] +fn src_line_9072() { + let input = r#"
<a href="hi'> <a href=hi'>

"#; + run(input, output); +} + +#[test] +fn src_line_9081() { + let input = r#"< a>< +foo> +"#; + let output = r#"

< a>< +foo><bar/ > +<foo bar=baz +bim!bop />

"#; + run(input, output); +} + +#[test] +fn src_line_9096() { + let input = r#"
"#; + let output = r#"

<a href='bar'title=title>

"#; + run(input, output); +} + +#[test] +fn src_line_9105() { + let input = r#"
"#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9114() { + let input = r#""#; + let output = r#"

</a href="foo">

"#; + run(input, output); +} + +#[test] +fn src_line_9123() { + let input = r#"foo "#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9132() { + let input = r#"foo "#; + let output = r#"

foo <!-- not a comment -- two hyphens -->

"#; + run(input, output); +} + +#[test] +fn src_line_9141() { + let input = r#"foo foo --> + +foo "#; + let output = r#"

foo <!--> foo -->

+

foo <!-- foo--->

"#; + run(input, output); +} + +#[test] +fn src_line_9153() { + let input = r#"foo "#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9162() { + let input = r#"foo "#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9171() { + let input = r#"foo &<]]>"#; + let output = r#"

foo &<]]>

"#; + run(input, output); +} + +#[test] +fn src_line_9181() { + let input = r#"foo "#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9190() { + let input = r#"foo "#; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9197() { + let input = r#""#; + let output = r#"

<a href=""">

"#; + run(input, output); +} + +#[test] +fn src_line_9211() { + let input = "foo \x20 +baz"; + let output = r#"

foo
+baz

"#; + run(input, output); +} + +#[test] +fn src_line_9223() { + let input = r#"foo\ +baz"#; + let output = r#"

foo
+baz

"#; + run(input, output); +} + +#[test] +fn src_line_9234() { + let input = "foo \x20 +baz"; + let output = r#"

foo
+baz

"#; + run(input, output); +} + +#[test] +fn src_line_9245() { + let input = "foo \x20 + bar"; + let output = r#"

foo
+bar

"#; + run(input, output); +} + +#[test] +fn src_line_9254() { + let input = r#"foo\ + bar"#; + let output = r#"

foo
+bar

"#; + run(input, output); +} + +#[test] +fn src_line_9266() { + let input = "*foo \x20 +bar*"; + let output = r#"

foo
+bar

"#; + run(input, output); +} + +#[test] +fn src_line_9275() { + let input = r#"*foo\ +bar*"#; + let output = r#"

foo
+bar

"#; + run(input, output); +} + +#[test] +fn src_line_9286() { + let input = "`code \x20 +span`"; + let output = r#"

code span

"#; + run(input, output); +} + +#[test] +fn src_line_9294() { + let input = r#"`code\ +span`"#; + let output = r#"

code\ span

"#; + run(input, output); +} + +#[test] +fn src_line_9304() { + let input = "
"; + let output = "

"; + run(input, output); +} + +#[test] +fn src_line_9313() { + let input = r#""#; + let output = r#"

"#; + run(input, output); +} + +#[test] +fn src_line_9326() { + let input = r#"foo\"#; + let output = r#"

foo\

"#; + run(input, output); +} + +#[test] +fn src_line_9333() { + let input = "foo \x20"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9340() { + let input = r#"### foo\"#; + let output = r#"

foo\

"#; + run(input, output); +} + +#[test] +fn src_line_9347() { + let input = "### foo \x20"; + let output = r#"

foo

"#; + run(input, output); +} + +#[test] +fn src_line_9362() { + let input = r#"foo +baz"#; + let output = r#"

foo +baz

"#; + run(input, output); +} + +#[test] +fn src_line_9374() { + let input = "foo\x20 + baz"; + let output = r#"

foo +baz

"#; + run(input, output); +} + +#[test] +fn src_line_9394() { + let input = r#"hello $.;'there"#; + let output = r#"

hello $.;'there

"#; + run(input, output); +} + +#[test] +fn src_line_9401() { + let input = r#"Foo χρῆν"#; + let output = r#"

Foo χρῆν

"#; + run(input, output); +} + +#[test] +fn src_line_9410() { + let input = r#"Multiple spaces"#; + let output = r#"

Multiple spaces

"#; + run(input, output); +} +// end of auto-generated module +} diff --git a/crates/markdown-it/tests/extras.rs b/crates/markdown-it/tests/extras.rs new file mode 100644 index 0000000000000000000000000000000000000000..5f4d6eb51a62c8c5676c155ffd995894ea06604e --- /dev/null +++ b/crates/markdown-it/tests/extras.rs @@ -0,0 +1,202 @@ +use once_cell::sync::Lazy; + + +#[test] +fn title_example() { + let parser = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(parser); + + let ast = parser.parse("Hello **world**!"); + let html = ast.render(); + + assert_eq!(html, "

Hello world!

\n"); +} + +#[test] +fn lazy_singleton() { + static MD : Lazy = Lazy::new(|| { + let mut parser = markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(&mut parser); + parser + }); + + let ast = MD.parse("Hello **world**!"); + let html = ast.render(); + + assert_eq!(html, "

Hello world!

\n"); +} + +#[test] +fn no_plugins() { + let md = &mut markdown_it::MarkdownIt::new(); + let node = md.parse("hello\nworld"); + let result = node.render(); + assert_eq!(result, "hello\nworld\n"); +} + +#[test] +fn no_max_indent() { + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::block::paragraph::add(md); + markdown_it::plugins::cmark::block::list::add(md); + md.max_indent = i32::MAX; + let node = md.parse(" paragraph\n - item"); + let result = node.render(); + assert_eq!(result, "

paragraph

\n
    \n
  • item
  • \n
\n"); +} + + +/*#[test] +fn no_block_parser() { + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + md.remove_rule::(); + let node = md.parse("hello *world*"); + let result = node.render(); + assert_eq!(result, "hello world"); +}*/ + +fn run(input: &str, output: &str) { + let output = if output.is_empty() { "".to_owned() } else { output.to_owned() + "\n" }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + markdown_it::plugins::extra::beautify_links::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + node.walk(|node, _| assert!(node.srcmap.is_some())); + let result = node.render(); + assert_eq!(result, output); +} + +mod markdown_it_rs_extras { + use super::run; + + #[test] + fn regression_test_img() { + // ! at end of line + run("Hello!", "

Hello!

"); + } + + #[test] + fn regression_list_markers() { + run("- foo\n- bar", "
    \n
  • foo
  • \n
  • bar
  • \n
"); + run("1. foo\n1. bar", "
    \n
  1. foo
  2. \n
  3. bar
  4. \n
"); + } + + #[test] + fn tab_offset_in_lists() { + run(" > -\tfoo\n >\n > foo\n", +r#"
+
    +
  • +

    foo

    +
     foo
    +
    +
  • +
+
"#); + } + + #[test] + fn null_char_replacement() { + run("�", "

\u{FFFD}

"); + run("\0", "

\u{FFFD}

"); + } + + #[test] + fn cr_only_newlines() { + run("foo\rbar", "

foo\nbar

"); + run(" foo\r bar", "
foo\nbar\n
"); + } + + #[test] + fn cr_lf_newlines() { + run("foo\r\nbar", "

foo\nbar

"); + run(" foo\r\n bar", "
foo\nbar\n
"); + } + + #[test] + fn beautify_links() { + run("", + "

www.reddit.com/r/programming/comments/…/ifyqsqt/?…

"); + } + + #[test] + fn regression_test_newlines_with_images() { + run("There is a newline in this image ![here\nit is](https://github.com/executablebooks/)", + "

There is a newline in this image \"here\nit

"); + } + + #[test] + fn test_node_ext_propagation() { + use markdown_it::parser::block::{BlockRule, BlockState}; + use markdown_it::parser::core::CoreRule; + use markdown_it::parser::extset::NodeExt; + use markdown_it::parser::inline::{InlineRule, InlineState}; + use markdown_it::{MarkdownIt, Node}; + + #[derive(Debug, Default)] + struct NodeErrors(Vec<&'static str>); + impl NodeExt for NodeErrors {} + + struct MyInlineRule; + impl InlineRule for MyInlineRule { + const MARKER: char = '@'; + + fn run(state: &mut InlineState) -> Option<(Node, usize)> { + let err = state.node.ext.get_or_insert_default::(); + err.0.push("inline"); + None + } + } + + struct MyBlockRule; + impl BlockRule for MyBlockRule { + fn run(state: &mut BlockState) -> Option<(Node, usize)> { + let err = state.node.ext.get_or_insert_default::(); + err.0.push("block"); + None + } + } + + struct MyCoreRule; + impl CoreRule for MyCoreRule { + fn run(root: &mut Node, _md: &MarkdownIt) { + let err = root.ext.get_or_insert_default::(); + err.0.push("core"); + } + } + + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + + md.inline.add_rule::(); + md.block.add_rule::(); + md.add_rule::().after_all(); + + let text1 = r#"*hello @world*"#; + let ast = md.parse(text1); + let mut collected: Vec<&str> = vec![]; + + ast.walk_post(|node, _| { + if let Some(errors) = node.ext.get::() { + collected.extend(errors.0.iter()); + } + }); + + assert_eq!( + collected, + vec!["inline", "block", "core"], + ); + } +} + +mod examples { + include!("../examples/ferris/main.rs"); + + #[test] + fn test_examples() { + main(); + } +} + diff --git a/crates/markdown-it/tests/fixtures/README.md b/crates/markdown-it/tests/fixtures/README.md new file mode 100644 index 0000000000000000000000000000000000000000..57421a28e0f0f848e1fbf42fc01ff53f6714076e --- /dev/null +++ b/crates/markdown-it/tests/fixtures/README.md @@ -0,0 +1,28 @@ +This file generates tests for markdown-it rust library. It gets one argument +(file name) and edits that file in-place, replacing special markers inside +with tests generated from fixtures. + +Run it like this from tests/ folder: +```sh +for I in *.rs ; do deno run --allow-read --allow-write ./fixtures/testgen.js $I ; done +``` + +As an example, it replaces this: +```rs +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/commonmark/spec.txt +... any content here ... +/////////////////////////////////////////////////////////////////////////// +``` + +With approximately this: +```rs +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/commonmark/spec.txt +#[test] +fn line_123() { + run("*foo*", "foo"); +} +... more tests here ... +/////////////////////////////////////////////////////////////////////////// +``` diff --git a/crates/markdown-it/tests/fixtures/commonmark/bad.txt b/crates/markdown-it/tests/fixtures/commonmark/bad.txt new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/crates/markdown-it/tests/fixtures/commonmark/good.txt b/crates/markdown-it/tests/fixtures/commonmark/good.txt new file mode 100644 index 0000000000000000000000000000000000000000..c437019ffe77f87b7884ade2df6d222004c66734 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/commonmark/good.txt @@ -0,0 +1,7834 @@ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 355 + +. + foo baz bim +. +
foo	baz		bim
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 362 + +. + foo baz bim +. +
foo	baz		bim
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 369 + +. + a a + ὐ a +. +
a	a
+ὐ	a
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 382 + +. + - foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 395 + +. +- foo + + bar +. +
    +
  • +

    foo

    +
      bar
    +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 418 + +. +> foo +. +
+
  foo
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 427 + +. +- foo +. +
    +
  • +
      foo
    +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 439 + +. + foo + bar +. +
foo
+bar
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 448 + +. + - foo + - bar + - baz +. +
    +
  • foo +
      +
    • bar +
        +
      • baz
      • +
      +
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 466 + +. +# Foo +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 472 + +. +* * * +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 489 + +. +\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ +. +

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 499 + +. +\ \A\a\ \3\φ\« +. +

\ \A\a\ \3\φ\«

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 509 + +. +\*not emphasized* +\
not a tag +\[not a link](/foo) +\`not code` +1\. not a list +\* not a list +\# not a heading +\[foo]: /url "not a reference" +\ö not a character entity +. +

*not emphasized* +<br/> not a tag +[not a link](/foo) +`not code` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +&ouml; not a character entity

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 534 + +. +\\*emphasis* +. +

\emphasis

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 543 + +. +foo\ +bar +. +

foo
+bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 555 + +. +`` \[\` `` +. +

\[\`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 562 + +. + \[\] +. +
\[\]
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 570 + +. +~~~ +\[\] +~~~ +. +
\[\]
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 580 + +. + +. +

http://example.com?find=\*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 587 + +. + +. + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 597 + +. +[foo](/bar\* "ti\*tle") +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 604 + +. +[foo] + +[foo]: /bar\* "ti\*tle" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 613 + +. +``` foo\+bar +foo +``` +. +
foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 649 + +. +  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸ +. +

  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 668 + +. +# Ӓ Ϡ � +. +

# Ӓ Ϡ �

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 681 + +. +" ആ ಫ +. +

" ആ ಫ

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 690 + +. +  &x; &#; &#x; +� +&#abcdef0; +&ThisIsNotDefined; &hi?; +. +

&nbsp &x; &#; &#x; +&#87654321; +&#abcdef0; +&ThisIsNotDefined; &hi?;

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 707 + +. +© +. +

&copy

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 717 + +. +&MadeUpEntity; +. +

&MadeUpEntity;

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 728 + +. + +. + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 735 + +. +[foo](/föö "föö") +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 742 + +. +[foo] + +[foo]: /föö "föö" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 751 + +. +``` föö +foo +``` +. +
foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 764 + +. +`föö` +. +

f&ouml;&ouml;

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 771 + +. + föfö +. +
f&ouml;f&ouml;
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 783 + +. +*foo* +*foo* +. +

*foo* +foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 791 + +. +* foo + +* foo +. +

* foo

+
    +
  • foo
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 802 + +. +foo bar +. +

foo + +bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 810 + +. + foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 817 + +. +[a](url "tit") +. +

[a](url "tit")

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 840 + +. +- `one +- two` +. +
    +
  • `one
  • +
  • two`
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 879 + +. +*** +--- +___ +. +
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 892 + +. ++++ +. +

+++

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 899 + +. +=== +. +

===

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 908 + +. +-- +** +__ +. +

-- +** +__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 921 + +. + *** + *** + *** +. +
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 934 + +. + *** +. +
***
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 942 + +. +Foo + *** +. +

Foo +***

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 953 + +. +_____________________________________ +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 962 + +. + - - - +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 969 + +. + ** * ** * ** * ** +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 976 + +. +- - - - +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 985 + +. +- - - - +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 994 + +. +_ _ _ _ a + +a------ + +---a--- +. +

_ _ _ _ a

+

a------

+

---a---

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1010 + +. + *-* +. +

-

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1019 + +. +- foo +*** +- bar +. +
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1036 + +. +Foo +*** +bar +. +

Foo

+
+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1053 + +. +Foo +--- +bar +. +

Foo

+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1066 + +. +* Foo +* * * +* Bar +. +
    +
  • Foo
  • +
+
+
    +
  • Bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1083 + +. +- Foo +- * * * +. +
    +
  • Foo
  • +
  • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1112 + +. +# foo +## foo +### foo +#### foo +##### foo +###### foo +. +

foo

+

foo

+

foo

+

foo

+
foo
+
foo
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1131 + +. +####### foo +. +

####### foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1146 + +. +#5 bolt + +#hashtag +. +

#5 bolt

+

#hashtag

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1158 + +. +\## foo +. +

## foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1167 + +. +# foo *bar* \*baz\* +. +

foo bar *baz*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1176 + +. +# foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1185 + +. + ### foo + ## foo + # foo +. +

foo

+

foo

+

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1198 + +. + # foo +. +
# foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1206 + +. +foo + # bar +. +

foo +# bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1217 + +. +## foo ## + ### bar ### +. +

foo

+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1228 + +. +# foo ################################## +##### foo ## +. +

foo

+
foo
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1239 + +. +### foo ### +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1250 + +. +### foo ### b +. +

foo ### b

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1259 + +. +# foo# +. +

foo#

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1269 + +. +### foo \### +## foo #\## +# foo \# +. +

foo ###

+

foo ###

+

foo #

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1283 + +. +**** +## foo +**** +. +
+

foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1294 + +. +Foo bar +# baz +Bar foo +. +

Foo bar

+

baz

+

Bar foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1307 + +. +## +# +### ### +. +

+

+

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1350 + +. +Foo *bar* +========= + +Foo *bar* +--------- +. +

Foo bar

+

Foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1364 + +. +Foo *bar +baz* +==== +. +

Foo bar +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1378 + +. + Foo *bar +baz* +==== +. +

Foo bar +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1390 + +. +Foo +------------------------- + +Foo += +. +

Foo

+

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1405 + +. + Foo +--- + + Foo +----- + + Foo + === +. +

Foo

+

Foo

+

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1423 + +. + Foo + --- + + Foo +--- +. +
Foo
+---
+
+Foo
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1442 + +. +Foo + ---- +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1452 + +. +Foo + --- +. +

Foo +---

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1463 + +. +Foo += = + +Foo +--- - +. +

Foo += =

+

Foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1479 + +. +Foo +----- +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1489 + +. +Foo\ +---- +. +

Foo\

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1500 + +. +`Foo +---- +` + + +. +

`Foo

+

`

+

<a title="a lot

+

of dashes"/>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1519 + +. +> Foo +--- +. +
+

Foo

+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1530 + +. +> foo +bar +=== +. +
+

foo +bar +===

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1543 + +. +- Foo +--- +. +
    +
  • Foo
  • +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1558 + +. +Foo +Bar +--- +. +

Foo +Bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1571 + +. +--- +Foo +--- +Bar +--- +Baz +. +
+

Foo

+

Bar

+

Baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1588 + +. + +==== +. +

====

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1600 + +. +--- +--- +. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1609 + +. +- foo +----- +. +
    +
  • foo
  • +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1620 + +. + foo +--- +. +
foo
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1630 + +. +> foo +----- +. +
+

foo

+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1644 + +. +\> foo +------ +. +

> foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1675 + +. +Foo + +bar +--- +baz +. +

Foo

+

bar

+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1691 + +. +Foo +bar + +--- + +baz +. +

Foo +bar

+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1709 + +. +Foo +bar +* * * +baz +. +

Foo +bar

+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1724 + +. +Foo +bar +\--- +baz +. +

Foo +bar +--- +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1752 + +. + a simple + indented code block +. +
a simple
+  indented code block
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1766 + +. + - foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1780 + +. +1. foo + + - bar +. +
    +
  1. +

    foo

    +
      +
    • bar
    • +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1800 + +. +
+ *hi* + + - one +. +
<a/>
+*hi*
+
+- one
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1816 + +. + chunk1 + + chunk2 + + + + chunk3 +. +
chunk1
+
+chunk2
+
+
+
+chunk3
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1839 + +. + chunk1 + + chunk2 +. +
chunk1
+  
+  chunk2
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1854 + +. +Foo + bar + +. +

Foo +bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1868 + +. + foo +bar +. +
foo
+
+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1881 + +. +# Heading + foo +Heading +------ + foo +---- +. +

Heading

+
foo
+
+

Heading

+
foo
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1901 + +. + foo + bar +. +
    foo
+bar
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1914 + +. + + + foo + + +. +
foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1928 + +. + foo +. +
foo  
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1983 + +. +``` +< + > +``` +. +
<
+ >
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 1997 + +. +~~~ +< + > +~~~ +. +
<
+ >
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2010 + +. +`` +foo +`` +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2021 + +. +``` +aaa +~~~ +``` +. +
aaa
+~~~
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2033 + +. +~~~ +aaa +``` +~~~ +. +
aaa
+```
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2047 + +. +```` +aaa +``` +`````` +. +
aaa
+```
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2059 + +. +~~~~ +aaa +~~~ +~~~~ +. +
aaa
+~~~
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2074 + +. +``` +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2081 + +. +````` + +``` +aaa +. +

+```
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2094 + +. +> ``` +> aaa + +bbb +. +
+
aaa
+
+
+

bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2110 + +. +``` + + +``` +. +

+  
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2124 + +. +``` +``` +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2136 + +. + ``` + aaa +aaa +``` +. +
aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2148 + +. + ``` +aaa + aaa +aaa + ``` +. +
aaa
+aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2162 + +. + ``` + aaa + aaa + aaa + ``` +. +
aaa
+ aaa
+aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2178 + +. + ``` + aaa + ``` +. +
```
+aaa
+```
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2193 + +. +``` +aaa + ``` +. +
aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2203 + +. + ``` +aaa + ``` +. +
aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2215 + +. +``` +aaa + ``` +. +
aaa
+    ```
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2229 + +. +``` ``` +aaa +. +

+aaa

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2238 + +. +~~~~~~ +aaa +~~~ ~~ +. +
aaa
+~~~ ~~
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2252 + +. +foo +``` +bar +``` +baz +. +

foo

+
bar
+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2269 + +. +foo +--- +~~~ +bar +~~~ +# baz +. +

foo

+
bar
+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2291 + +. +```ruby +def foo(x) + return 3 +end +``` +. +
def foo(x)
+  return 3
+end
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2305 + +. +~~~~ ruby startline=3 $%@#$ +def foo(x) + return 3 +end +~~~~~~~ +. +
def foo(x)
+  return 3
+end
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2319 + +. +````; +```` +. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2329 + +. +``` aa ``` +foo +. +

aa +foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2340 + +. +~~~ aa ``` ~~~ +foo +~~~ +. +
foo
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2352 + +. +``` +``` aaa +``` +. +
``` aaa
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2431 + +. +
+
+**Hello**,
+
+_world_.
+
+
+. +
+
+**Hello**,
+

world. +

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2460 + +. + + + + +
+ hi +
+ +okay. +. + + + + +
+ hi +
+

okay.

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2482 + +. +
+*foo* +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2506 + +. +
+ +*Markdown* + +
+. +
+

Markdown

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2522 + +. +
+
+. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2533 + +. +
+
+. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2545 + +. +
+*foo* + +*bar* +. +
+*foo* +

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2561 + +. +
+. + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2601 + +. +
+foo +
+. +
+foo +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2618 + +. +
+``` c +int x = 33; +``` +. +
+``` c +int x = 33; +``` +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2635 + +. + +*bar* + +. + +*bar* + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2648 + +. + +*bar* + +. + +*bar* + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2659 + +. + +*bar* + +. + +*bar* + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2670 + +. + +*bar* +. + +*bar* +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2685 + +. + +*foo* + +. + +*foo* + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2700 + +. + + +*foo* + + +. + +

foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2718 + +. +*foo* +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2734 + +. +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+okay +. +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2755 + +. + +okay +. + +

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2774 + +. + +. + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2794 + +. + +okay +. + +

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2817 + +. + +*foo* +. + +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2868 + +. +*bar* +*baz* +. +*bar* +

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2880 + +. +1. *bar* +. +1. *bar* +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2893 + +. + +okay +. + +

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2911 + +. +'; + +?> +okay +. +'; + +?> +

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2930 + +. + +. + +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2939 + +. + +okay +. + +

okay

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2973 + +. + + + +. + +
<!-- foo -->
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2984 + +. +
+ +
+. +
+
<div>
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 2998 + +. +Foo +
+bar +
+. +

Foo

+
+bar +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3015 + +. +
+bar +
+*foo* +. +
+bar +
+*foo* +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3030 + +. +Foo + +baz +. +

Foo + +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3071 + +. +
+ +*Emphasized* text. + +
+. +
+

Emphasized text.

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3084 + +. +
+*Emphasized* text. +
+. +
+*Emphasized* text. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3106 + +. + + + + + + + + +
+Hi +
+. + + + + +
+Hi +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3133 + +. + + + + + + + + +
+ Hi +
+. + + +
<td>
+  Hi
+</td>
+
+ +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3182 + +. +[foo]: /url "title" + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3191 + +. + [foo]: + /url + 'the title' + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3202 + +. +[Foo*bar\]]:my_(url) 'title (with parens)' + +[Foo*bar\]] +. +

Foo*bar]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3211 + +. +[Foo bar]: + +'title' + +[Foo bar] +. +

Foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3224 + +. +[foo]: /url ' +title +line1 +line2 +' + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3243 + +. +[foo]: /url 'title + +with blank line' + +[foo] +. +

[foo]: /url 'title

+

with blank line'

+

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3258 + +. +[foo]: +/url + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3270 + +. +[foo]: + +[foo] +. +

[foo]:

+

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3282 + +. +[foo]: <> + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3293 + +. +[foo]: (baz) + +[foo] +. +

[foo]: (baz)

+

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3306 + +. +[foo]: /url\bar\*baz "foo\"bar\baz" + +[foo] +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3317 + +. +[foo] + +[foo]: url +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3329 + +. +[foo] + +[foo]: first +[foo]: second +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3342 + +. +[FOO]: /url + +[Foo] +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3351 + +. +[ΑΓΩ]: /φου + +[αγω] +. +

αγω

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3366 + +. +[foo]: /url +. +. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3374 + +. +[ +foo +]: /url +bar +. +

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3387 + +. +[foo]: /url "title" ok +. +

[foo]: /url "title" ok

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3396 + +. +[foo]: /url +"title" ok +. +

"title" ok

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3407 + +. + [foo]: /url "title" + +[foo] +. +
[foo]: /url "title"
+
+

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3421 + +. +``` +[foo]: /url +``` + +[foo] +. +
[foo]: /url
+
+

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3436 + +. +Foo +[bar]: /baz + +[bar] +. +

Foo +[bar]: /baz

+

[bar]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3451 + +. +# [Foo] +[foo]: /url +> bar +. +

Foo

+
+

bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3462 + +. +[foo]: /url +bar +=== +[foo] +. +

bar

+

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3472 + +. +[foo]: /url +=== +[foo] +. +

=== +foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3485 + +. +[foo]: /foo-url "foo" +[bar]: /bar-url + "bar" +[baz]: /baz-url + +[foo], +[bar], +[baz] +. +

foo, +bar, +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3506 + +. +[foo] + +> [foo]: /url +. +

foo

+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3528 + +. +aaa + +bbb +. +

aaa

+

bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3540 + +. +aaa +bbb + +ccc +ddd +. +

aaa +bbb

+

ccc +ddd

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3556 + +. +aaa + + +bbb +. +

aaa

+

bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3569 + +. + aaa + bbb +. +

aaa +bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3581 + +. +aaa + bbb + ccc +. +

aaa +bbb +ccc

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3595 + +. + aaa +bbb +. +

aaa +bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3604 + +. + aaa +bbb +. +
aaa
+
+

bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3618 + +. +aaa +bbb +. +

aaa
+bbb

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3635 + +. + + +aaa + + +# aaa + + +. +

aaa

+

aaa

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3703 + +. +> # Foo +> bar +> baz +. +
+

Foo

+

bar +baz

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3718 + +. +># Foo +>bar +> baz +. +
+

Foo

+

bar +baz

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3733 + +. + > # Foo + > bar + > baz +. +
+

Foo

+

bar +baz

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3748 + +. + > # Foo + > bar + > baz +. +
> # Foo
+> bar
+> baz
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3763 + +. +> # Foo +> bar +baz +. +
+

Foo

+

bar +baz

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3779 + +. +> bar +baz +> foo +. +
+

bar +baz +foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3803 + +. +> foo +--- +. +
+

foo

+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3823 + +. +> - foo +- bar +. +
+
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3841 + +. +> foo + bar +. +
+
foo
+
+
+
bar
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3854 + +. +> ``` +foo +``` +. +
+
+
+

foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3870 + +. +> foo + - bar +. +
+

foo +- bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3894 + +. +> +. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3902 + +. +> +> +> +. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3914 + +. +> +> foo +> +. +
+

foo

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3927 + +. +> foo + +> bar +. +
+

foo

+
+
+

bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3949 + +. +> foo +> bar +. +
+

foo +bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3962 + +. +> foo +> +> bar +. +
+

foo

+

bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3976 + +. +foo +> bar +. +

foo

+
+

bar

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 3990 + +. +> aaa +*** +> bbb +. +
+

aaa

+
+
+
+

bbb

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4008 + +. +> bar +baz +. +
+

bar +baz

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4019 + +. +> bar + +baz +. +
+

bar

+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4031 + +. +> bar +> +baz +. +
+

bar

+
+

baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4047 + +. +> > > foo +bar +. +
+
+
+

foo +bar

+
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4062 + +. +>>> foo +> bar +>>baz +. +
+
+
+

foo +bar +baz

+
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4084 + +. +> code + +> not code +. +
+
code
+
+
+
+

not code

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4138 + +. +A paragraph +with two lines. + + indented code + +> A block quote. +. +

A paragraph +with two lines.

+
indented code
+
+
+

A block quote.

+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4160 + +. +1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4193 + +. +- one + + two +. +
    +
  • one
  • +
+

two

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4205 + +. +- one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4219 + +. + - one + + two +. +
    +
  • one
  • +
+
 two
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4232 + +. + - one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4254 + +. + > > 1. one +>> +>> two +. +
+
+
    +
  1. +

    one

    +

    two

    +
  2. +
+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4281 + +. +>>- one +>> + > > two +. +
+
+
    +
  • one
  • +
+

two

+
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4300 + +. +-one + +2.two +. +

-one

+

2.two

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4313 + +. +- foo + + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4330 + +. +1. foo + + ``` + bar + ``` + + baz + + > bam +. +
    +
  1. +

    foo

    +
    bar
    +
    +

    baz

    +
    +

    bam

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4358 + +. +- Foo + + bar + + + baz +. +
    +
  • +

    Foo

    +
    bar
    +
    +
    +baz
    +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4380 + +. +123456789. ok +. +
    +
  1. ok
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4389 + +. +1234567890. not ok +. +

1234567890. not ok

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4398 + +. +0. ok +. +
    +
  1. ok
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4407 + +. +003. ok +. +
    +
  1. ok
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4418 + +. +-1. not ok +. +

-1. not ok

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4441 + +. +- foo + + bar +. +
    +
  • +

    foo

    +
    bar
    +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4458 + +. + 10. foo + + bar +. +
    +
  1. +

    foo

    +
    bar
    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4477 + +. + indented code + +paragraph + + more code +. +
indented code
+
+

paragraph

+
more code
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4492 + +. +1. indented code + + paragraph + + more code +. +
    +
  1. +
    indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4514 + +. +1. indented code + + paragraph + + more code +. +
    +
  1. +
     indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4541 + +. + foo + +bar +. +

foo

+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4551 + +. +- foo + + bar +. +
    +
  • foo
  • +
+

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4568 + +. +- foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4595 + +. +- + foo +- + ``` + bar + ``` +- + baz +. +
    +
  • foo
  • +
  • +
    bar
    +
    +
  • +
  • +
    baz
    +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4621 + +. +- + foo +. +
    +
  • foo
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4635 + +. +- + + foo +. +
    +
  • +
+

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4649 + +. +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4664 + +. +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4679 + +. +1. foo +2. +3. bar +. +
    +
  1. foo
  2. +
  3. +
  4. bar
  5. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4694 + +. +* +. +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4704 + +. +foo +* + +foo +1. +. +

foo +*

+

foo +1.

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4726 + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4750 + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4774 + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4798 + +. + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
1.  A paragraph
+    with two lines.
+
+        indented code
+
+    > A block quote.
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4828 + +. + 1. A paragraph +with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4852 + +. + 1. A paragraph + with two lines. +. +
    +
  1. A paragraph +with two lines.
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4865 + +. +> 1. > Blockquote +continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4882 + +. +> 1. > Blockquote +> continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4910 + +. +- foo + - bar + - baz + - boo +. +
    +
  • foo +
      +
    • bar +
        +
      • baz +
          +
        • boo
        • +
        +
      • +
      +
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4936 + +. +- foo + - bar + - baz + - boo +. +
    +
  • foo
  • +
  • bar
  • +
  • baz
  • +
  • boo
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4953 + +. +10) foo + - bar +. +
    +
  1. foo +
      +
    • bar
    • +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4969 + +. +10) foo + - bar +. +
    +
  1. foo
  2. +
+
    +
  • bar
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4984 + +. +- - foo +. +
    +
  • +
      +
    • foo
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 4997 + +. +1. - 2. foo +. +
    +
  1. +
      +
    • +
        +
      1. foo
      2. +
      +
    • +
    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5016 + +. +- # Foo +- Bar + --- + baz +. +
    +
  • +

    Foo

    +
  • +
  • +

    Bar

    +baz
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5252 + +. +- foo +- bar ++ baz +. +
    +
  • foo
  • +
  • bar
  • +
+
    +
  • baz
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5267 + +. +1. foo +2. bar +3) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5286 + +. +Foo +- bar +- baz +. +

Foo

+
    +
  • bar
  • +
  • baz
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5363 + +. +The number of windows in my house is +14. The number of doors is 6. +. +

The number of windows in my house is +14. The number of doors is 6.

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5373 + +. +The number of windows in my house is +1. The number of doors is 6. +. +

The number of windows in my house is

+
    +
  1. The number of doors is 6.
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5387 + +. +- foo + +- bar + + +- baz +. +
    +
  • +

    foo

    +
  • +
  • +

    bar

    +
  • +
  • +

    baz

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5408 + +. +- foo + - bar + - baz + + + bim +. +
    +
  • foo +
      +
    • bar +
        +
      • +

        baz

        +

        bim

        +
      • +
      +
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5438 + +. +- foo +- bar + + + +- baz +- bim +. +
    +
  • foo
  • +
  • bar
  • +
+ +
    +
  • baz
  • +
  • bim
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5459 + +. +- foo + + notcode + +- foo + + + + code +. +
    +
  • +

    foo

    +

    notcode

    +
  • +
  • +

    foo

    +
  • +
+ +
code
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5490 + +. +- a + - b + - c + - d + - e + - f +- g +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d
  • +
  • e
  • +
  • f
  • +
  • g
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5511 + +. +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
  5. +

    c

    +
  6. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5535 + +. +- a + - b + - c + - d + - e +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d +- e
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5555 + +. +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
+
3. c
+
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5578 + +. +- a +- b + +- c +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    c

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5600 + +. +* a +* + +* c +. +
    +
  • +

    a

    +
  • +
  • +
  • +

    c

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5622 + +. +- a +- b + + c +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +

    c

    +
  • +
  • +

    d

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5644 + +. +- a +- b + + [ref]: /url +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    d

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5667 + +. +- a +- ``` + b + + + ``` +- c +. +
    +
  • a
  • +
  • +
    b
    +
    +
    +
    +
  • +
  • c
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5693 + +. +- a + - b + + c +- d +. +
    +
  • a +
      +
    • +

      b

      +

      c

      +
    • +
    +
  • +
  • d
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5717 + +. +* a + > b + > +* c +. +
    +
  • a +
    +

    b

    +
    +
  • +
  • c
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5737 + +. +- a + > b + ``` + c + ``` +- d +. +
    +
  • a +
    +

    b

    +
    +
    c
    +
    +
  • +
  • d
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5760 + +. +- a +. +
    +
  • a
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5769 + +. +- a + - b +. +
    +
  • a +
      +
    • b
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5786 + +. +1. ``` + foo + ``` + + bar +. +
    +
  1. +
    foo
    +
    +

    bar

    +
  2. +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5805 + +. +* foo + * bar + + baz +. +
    +
  • +

    foo

    +
      +
    • bar
    • +
    +

    baz

    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5823 + +. +- a + - b + - c + +- d + - e + - f +. +
    +
  • +

    a

    +
      +
    • b
    • +
    • c
    • +
    +
  • +
  • +

    d

    +
      +
    • e
    • +
    • f
    • +
    +
  • +
+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5857 + +. +`hi`lo` +. +

hilo`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5889 + +. +`foo` +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5900 + +. +`` foo ` bar `` +. +

foo ` bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5910 + +. +` `` ` +. +

``

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5918 + +. +` `` ` +. +

``

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5927 + +. +` a` +. +

a

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5936 + +. +` b ` +. +

 b 

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5944 + +. +` ` +` ` +. +

  +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5955 + +. +`` +foo +bar +baz +`` +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5965 + +. +`` +foo +`` +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5976 + +. +`foo bar +baz` +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 5993 + +. +`foo\`bar` +. +

foo\bar`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6004 + +. +``foo`bar`` +. +

foo`bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6010 + +. +` foo `` bar ` +. +

foo `` bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6022 + +. +*foo`*` +. +

*foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6031 + +. +[not a `link](/foo`) +. +

[not a link](/foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6041 + +. +`` +. +

<a href="">`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6050 + +. +
` +. +

`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6059 + +. +`` +. +

<http://foo.bar.baz>`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6068 + +. +` +. +

http://foo.bar.`baz`

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6078 + +. +```foo`` +. +

```foo``

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6085 + +. +`foo +. +

`foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6094 + +. +`foo``bar`` +. +

`foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6311 + +. +*foo bar* +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6321 + +. +a * foo bar* +. +

a * foo bar*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6332 + +. +a*"foo"* +. +

a*"foo"*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6341 + +. +* a * +. +

* a *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6350 + +. +foo*bar* +. +

foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6357 + +. +5*6*78 +. +

5678

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6366 + +. +_foo bar_ +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6376 + +. +_ foo bar_ +. +

_ foo bar_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6386 + +. +a_"foo"_ +. +

a_"foo"_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6395 + +. +foo_bar_ +. +

foo_bar_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6402 + +. +5_6_78 +. +

5_6_78

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6409 + +. +пристаням_стремятся_ +. +

пристаням_стремятся_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6419 + +. +aa_"bb"_cc +. +

aa_"bb"_cc

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6430 + +. +foo-_(bar)_ +. +

foo-(bar)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6442 + +. +_foo* +. +

_foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6452 + +. +*foo bar * +. +

*foo bar *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6461 + +. +*foo bar +* +. +

*foo bar +*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6474 + +. +*(*foo) +. +

*(*foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6484 + +. +*(*foo*)* +. +

(foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6493 + +. +*foo*bar +. +

foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6506 + +. +_foo bar _ +. +

_foo bar _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6516 + +. +_(_foo) +. +

_(_foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6525 + +. +_(_foo_)_ +. +

(foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6534 + +. +_foo_bar +. +

_foo_bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6541 + +. +_пристаням_стремятся +. +

_пристаням_стремятся

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6548 + +. +_foo_bar_baz_ +. +

foo_bar_baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6559 + +. +_(bar)_. +. +

(bar).

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6568 + +. +**foo bar** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6578 + +. +** foo bar** +. +

** foo bar**

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6589 + +. +a**"foo"** +. +

a**"foo"**

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6598 + +. +foo**bar** +. +

foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6607 + +. +__foo bar__ +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6617 + +. +__ foo bar__ +. +

__ foo bar__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6625 + +. +__ +foo bar__ +. +

__ +foo bar__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6637 + +. +a__"foo"__ +. +

a__"foo"__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6646 + +. +foo__bar__ +. +

foo__bar__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6653 + +. +5__6__78 +. +

5__6__78

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6660 + +. +пристаням__стремятся__ +. +

пристаням__стремятся__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6667 + +. +__foo, __bar__, baz__ +. +

foo, bar, baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6678 + +. +foo-__(bar)__ +. +

foo-(bar)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6691 + +. +**foo bar ** +. +

**foo bar **

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6704 + +. +**(**foo) +. +

**(**foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6714 + +. +*(**foo**)* +. +

(foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6721 + +. +**Gomphocarpus (*Gomphocarpus physocarpus*, syn. +*Asclepias physocarpa*)** +. +

Gomphocarpus (Gomphocarpus physocarpus, syn. +Asclepias physocarpa)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6730 + +. +**foo "*bar*" foo** +. +

foo "bar" foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6739 + +. +**foo**bar +. +

foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6751 + +. +__foo bar __ +. +

__foo bar __

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6761 + +. +__(__foo) +. +

__(__foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6771 + +. +_(__foo__)_ +. +

(foo)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6780 + +. +__foo__bar +. +

__foo__bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6787 + +. +__пристаням__стремятся +. +

__пристаням__стремятся

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6794 + +. +__foo__bar__baz__ +. +

foo__bar__baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6805 + +. +__(bar)__. +. +

(bar).

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6817 + +. +*foo [bar](/url)* +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6824 + +. +*foo +bar* +. +

foo +bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6836 + +. +_foo __bar__ baz_ +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6843 + +. +_foo _bar_ baz_ +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6850 + +. +__foo_ bar_ +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6857 + +. +*foo *bar** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6864 + +. +*foo **bar** baz* +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6870 + +. +*foo**bar**baz* +. +

foobarbaz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6894 + +. +*foo**bar* +. +

foo**bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6907 + +. +***foo** bar* +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6914 + +. +*foo **bar*** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6921 + +. +*foo**bar*** +. +

foobar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6932 + +. +foo***bar***baz +. +

foobarbaz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6938 + +. +foo******bar*********baz +. +

foobar***baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6947 + +. +*foo **bar *baz* bim** bop* +. +

foo bar baz bim bop

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6954 + +. +*foo [*bar*](/url)* +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6963 + +. +** is not an empty emphasis +. +

** is not an empty emphasis

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6970 + +. +**** is not an empty strong emphasis +. +

**** is not an empty strong emphasis

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6983 + +. +**foo [bar](/url)** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 6990 + +. +**foo +bar** +. +

foo +bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7002 + +. +__foo _bar_ baz__ +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7009 + +. +__foo __bar__ baz__ +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7016 + +. +____foo__ bar__ +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7023 + +. +**foo **bar**** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7030 + +. +**foo *bar* baz** +. +

foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7037 + +. +**foo*bar*baz** +. +

foobarbaz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7044 + +. +***foo* bar** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7051 + +. +**foo *bar*** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7060 + +. +**foo *bar **baz** +bim* bop** +. +

foo bar baz +bim bop

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7069 + +. +**foo [*bar*](/url)** +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7078 + +. +__ is not an empty emphasis +. +

__ is not an empty emphasis

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7085 + +. +____ is not an empty strong emphasis +. +

____ is not an empty strong emphasis

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7095 + +. +foo *** +. +

foo ***

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7102 + +. +foo *\** +. +

foo *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7109 + +. +foo *_* +. +

foo _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7116 + +. +foo ***** +. +

foo *****

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7123 + +. +foo **\*** +. +

foo *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7130 + +. +foo **_** +. +

foo _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7141 + +. +**foo* +. +

*foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7148 + +. +*foo** +. +

foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7155 + +. +***foo** +. +

*foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7162 + +. +****foo* +. +

***foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7169 + +. +**foo*** +. +

foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7176 + +. +*foo**** +. +

foo***

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7186 + +. +foo ___ +. +

foo ___

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7193 + +. +foo _\__ +. +

foo _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7200 + +. +foo _*_ +. +

foo *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7207 + +. +foo _____ +. +

foo _____

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7214 + +. +foo __\___ +. +

foo _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7221 + +. +foo __*__ +. +

foo *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7228 + +. +__foo_ +. +

_foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7239 + +. +_foo__ +. +

foo_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7246 + +. +___foo__ +. +

_foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7253 + +. +____foo_ +. +

___foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7260 + +. +__foo___ +. +

foo_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7267 + +. +_foo____ +. +

foo___

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7277 + +. +**foo** +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7284 + +. +*_foo_* +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7291 + +. +__foo__ +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7298 + +. +_*foo*_ +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7308 + +. +****foo**** +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7315 + +. +____foo____ +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7326 + +. +******foo****** +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7335 + +. +***foo*** +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7342 + +. +_____foo_____ +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7351 + +. +*foo _bar* baz_ +. +

foo _bar baz_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7358 + +. +*foo __bar *baz bim__ bam* +. +

foo bar *baz bim bam

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7367 + +. +**foo **bar baz** +. +

**foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7374 + +. +*foo *bar baz* +. +

*foo bar baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7383 + +. +*[bar*](/url) +. +

*bar*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7390 + +. +_foo [bar_](/url) +. +

_foo bar_

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7397 + +. +* +. +

*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7404 + +. +** +. +

**

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7411 + +. +__ +. +

__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7418 + +. +*a `*`* +. +

a *

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7425 + +. +_a `_`_ +. +

a _

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7432 + +. +**a +. +

**ahttp://foo.bar/?q=**

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7439 + +. +__a +. +

__ahttp://foo.bar/?q=__

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7527 + +. +[link](/uri "title") +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7537 + +. +[link](/uri) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7543 + +. +[](./target.md) +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7550 + +. +[link]() +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7557 + +. +[link](<>) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7564 + +. +[]() +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7573 + +. +[link](/my uri) +. +

[link](/my uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7579 + +. +[link](
) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7588 + +. +[link](foo +bar) +. +

[link](foo +bar)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7596 + +. +[link]() +. +

[link]()

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7607 + +. +[a]() +. +

a

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7615 + +. +[link]() +. +

[link](<foo>)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7624 + +. +[a]( +[a](c) +. +

[a](<b)c +[a](<b)c> +[a](c)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7636 + +. +[link](\(foo\)) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7645 + +. +[link](foo(and(bar))) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7654 + +. +[link](foo(and(bar)) +. +

[link](foo(and(bar))

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7661 + +. +[link](foo\(and\(bar\)) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7668 + +. +[link]() +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7678 + +. +[link](foo\)\:) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7687 + +. +[link](#fragment) + +[link](http://example.com#fragment) + +[link](http://example.com?foo=3#frag) +. +

link

+

link

+

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7703 + +. +[link](foo\bar) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7719 + +. +[link](foo%20bä) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7730 + +. +[link]("title") +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7739 + +. +[link](/url "title") +[link](/url 'title') +[link](/url (title)) +. +

link +link +link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7753 + +. +[link](/url "title \""") +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7764 + +. +[link](/url "title") +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7773 + +. +[link](/url "title "and" title") +. +

[link](/url "title "and" title")

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7782 + +. +[link](/url 'title "and" title') +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7807 + +. +[link]( /uri + "title" ) +. +

link

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7818 + +. +[link] (/uri) +. +

[link] (/uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7828 + +. +[link [foo [bar]]](/uri) +. +

link [foo [bar]]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7835 + +. +[link] bar](/uri) +. +

[link] bar](/uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7842 + +. +[link [bar](/uri) +. +

[link bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7849 + +. +[link \[bar](/uri) +. +

link [bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7858 + +. +[link *foo **bar** `#`*](/uri) +. +

link foo bar #

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7865 + +. +[![moon](moon.jpg)](/uri) +. +

moon

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7874 + +. +[foo [bar](/uri)](/uri) +. +

[foo bar](/uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7881 + +. +[foo *[bar [baz](/uri)](/uri)*](/uri) +. +

[foo [bar baz](/uri)](/uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7888 + +. +![[[foo](uri1)](uri2)](uri3) +. +

[foo](uri2)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7898 + +. +*[foo*](/uri) +. +

*foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7905 + +. +[foo *bar](baz*) +. +

foo *bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7915 + +. +*foo [bar* baz] +. +

foo [bar baz]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7925 + +. +[foo +. +

[foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7932 + +. +[foo`](/uri)` +. +

[foo](/uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7939 + +. +[foo +. +

[foohttp://example.com/?search=](uri)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7977 + +. +[foo][bar] + +[bar]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 7992 + +. +[link [foo [bar]]][ref] + +[ref]: /uri +. +

link [foo [bar]]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8001 + +. +[link \[bar][ref] + +[ref]: /uri +. +

link [bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8012 + +. +[link *foo **bar** `#`*][ref] + +[ref]: /uri +. +

link foo bar #

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8021 + +. +[![moon](moon.jpg)][ref] + +[ref]: /uri +. +

moon

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8032 + +. +[foo [bar](/uri)][ref] + +[ref]: /uri +. +

[foo bar]ref

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8041 + +. +[foo *bar [baz][ref]*][ref] + +[ref]: /uri +. +

[foo bar baz]ref

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8056 + +. +*[foo*][ref] + +[ref]: /uri +. +

*foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8065 + +. +[foo *bar][ref]* + +[ref]: /uri +. +

foo *bar*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8077 + +. +[foo + +[ref]: /uri +. +

[foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8086 + +. +[foo`][ref]` + +[ref]: /uri +. +

[foo][ref]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8095 + +. +[foo + +[ref]: /uri +. +

[foohttp://example.com/?search=][ref]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8106 + +. +[foo][BaR] + +[bar]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8117 + +. +[ẞ] + +[SS]: /url +. +

ẞ

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8129 + +. +[Foo + bar]: /url + +[Baz][Foo bar] +. +

Baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8142 + +. +[foo] [bar] + +[bar]: /url "title" +. +

[foo] bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8151 + +. +[foo] +[bar] + +[bar]: /url "title" +. +

[foo] +bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8192 + +. +[foo]: /url1 + +[foo]: /url2 + +[bar][foo] +. +

bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8207 + +. +[bar][foo\!] + +[foo!]: /url +. +

[bar][foo!]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8219 + +. +[foo][ref[] + +[ref[]: /uri +. +

[foo][ref[]

+

[ref[]: /uri

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8229 + +. +[foo][ref[bar]] + +[ref[bar]]: /uri +. +

[foo][ref[bar]]

+

[ref[bar]]: /uri

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8239 + +. +[[[foo]]] + +[[[foo]]]: /url +. +

[[[foo]]]

+

[[[foo]]]: /url

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8249 + +. +[foo][ref\[] + +[ref\[]: /uri +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8260 + +. +[bar\\]: /uri + +[bar\\] +. +

bar\

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8272 + +. +[] + +[]: /uri +. +

[]

+

[]: /uri

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8282 + +. +[ + ] + +[ + ]: /uri +. +

[ +]

+

[ +]: /uri

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8305 + +. +[foo][] + +[foo]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8314 + +. +[*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8325 + +. +[Foo][] + +[foo]: /url "title" +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8338 + +. +[foo] +[] + +[foo]: /url "title" +. +

foo +[]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8358 + +. +[foo] + +[foo]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8367 + +. +[*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8376 + +. +[[*foo* bar]] + +[*foo* bar]: /url "title" +. +

[foo bar]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8385 + +. +[[bar [foo] + +[foo]: /url +. +

[[bar foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8396 + +. +[Foo] + +[foo]: /url "title" +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8407 + +. +[foo] bar + +[foo]: /url +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8419 + +. +\[foo] + +[foo]: /url "title" +. +

[foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8431 + +. +[foo*]: /url + +*[foo*] +. +

*foo*

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8443 + +. +[foo][bar] + +[foo]: /url1 +[bar]: /url2 +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8452 + +. +[foo][] + +[foo]: /url1 +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8462 + +. +[foo]() + +[foo]: /url1 +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8470 + +. +[foo](not a link) + +[foo]: /url1 +. +

foo(not a link)

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8481 + +. +[foo][bar][baz] + +[baz]: /url +. +

[foo]bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8493 + +. +[foo][bar][baz] + +[baz]: /url1 +[bar]: /url2 +. +

foobaz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8506 + +. +[foo][bar][baz] + +[baz]: /url1 +[foo]: /url2 +. +

[foo]bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8529 + +. +![foo](/url "title") +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8536 + +. +![foo *bar*] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8545 + +. +![foo ![bar](/url)](/url2) +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8552 + +. +![foo [bar](/url)](/url2) +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8566 + +. +![foo *bar*][] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8575 + +. +![foo *bar*][foobar] + +[FOOBAR]: train.jpg "train & tracks" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8584 + +. +![foo](train.jpg) +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8591 + +. +My ![foo bar](/path/to/train.jpg "title" ) +. +

My foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8598 + +. +![foo]() +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8605 + +. +![](/url) +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8614 + +. +![foo][bar] + +[bar]: /url +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8623 + +. +![foo][bar] + +[BAR]: /url +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8634 + +. +![foo][] + +[foo]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8643 + +. +![*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8654 + +. +![Foo][] + +[foo]: /url "title" +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8666 + +. +![foo] +[] + +[foo]: /url "title" +. +

foo +[]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8679 + +. +![foo] + +[foo]: /url "title" +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8688 + +. +![*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8699 + +. +![[foo]] + +[[foo]]: /url "title" +. +

![[foo]]

+

[[foo]]: /url "title"

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8711 + +. +![Foo] + +[foo]: /url "title" +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8723 + +. +!\[foo] + +[foo]: /url "title" +. +

![foo]

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8735 + +. +\![foo] + +[foo]: /url "title" +. +

!foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8768 + +. + +. +

http://foo.bar.baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8775 + +. + +. +

http://foo.bar.baz/test?q=hello&id=22&boolean

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8782 + +. + +. +

irc://foo.bar:2233/baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8791 + +. + +. +

MAILTO:FOO@BAR.BAZ

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8803 + +. + +. +

a+b+c:d

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8810 + +. + +. +

made-up-scheme://foo,bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8817 + +. + +. +

http://../

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8824 + +. + +. +

localhost:5001/foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8833 + +. + +. +

<http://foo.bar/baz bim>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8842 + +. + +. +

http://example.com/\[\

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8864 + +. + +. +

foo@bar.example.com

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8871 + +. + +. +

foo+special@Bar.baz-bar0.com

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8880 + +. + +. +

<foo+@bar.example.com>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8889 + +. +<> +. +

<>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8896 + +. +< http://foo.bar > +. +

< http://foo.bar >

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8903 + +. + +. +

<m:abc>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8910 + +. + +. +

<foo.bar.baz>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8917 + +. +http://example.com +. +

http://example.com

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 8924 + +. +foo@bar.example.com +. +

foo@bar.example.com

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9005 + +. + +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9014 + +. + +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9023 + +. + +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9034 + +. + +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9045 + +. +Foo +. +

Foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9054 + +. +<33> <__> +. +

<33> <__>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9063 + +. +
+. +

<a h*#ref="hi">

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9072 + +. +
+. +

</a href="foo">

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9123 + +. +foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9132 + +. +foo +. +

foo <!-- not a comment -- two hyphens -->

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9141 + +. +foo foo --> + +foo +. +

foo <!--> foo -->

+

foo <!-- foo--->

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9153 + +. +foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9162 + +. +foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9171 + +. +foo &<]]> +. +

foo &<]]>

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9181 + +. +foo
+. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9190 + +. +foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9197 + +. + +. +

<a href=""">

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9211 + +. +foo +baz +. +

foo
+baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9223 + +. +foo\ +baz +. +

foo
+baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9234 + +. +foo +baz +. +

foo
+baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9245 + +. +foo + bar +. +

foo
+bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9254 + +. +foo\ + bar +. +

foo
+bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9266 + +. +*foo +bar* +. +

foo
+bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9275 + +. +*foo\ +bar* +. +

foo
+bar

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9286 + +. +`code +span` +. +

code span

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9294 + +. +`code\ +span` +. +

code\ span

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9304 + +. +
+. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9313 + +. + +. +

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9326 + +. +foo\ +. +

foo\

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9333 + +. +foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9340 + +. +### foo\ +. +

foo\

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9347 + +. +### foo +. +

foo

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9362 + +. +foo +baz +. +

foo +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9374 + +. +foo + baz +. +

foo +baz

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9394 + +. +hello $.;'there +. +

hello $.;'there

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9401 + +. +Foo χρῆν +. +

Foo χρῆν

+. + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +src line: 9410 + +. +Multiple spaces +. +

Multiple spaces

+. + diff --git a/crates/markdown-it/tests/fixtures/commonmark/spec.txt b/crates/markdown-it/tests/fixtures/commonmark/spec.txt new file mode 100644 index 0000000000000000000000000000000000000000..e6f3137578df44514fc1340f76220d54c6ea9ca5 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/commonmark/spec.txt @@ -0,0 +1,9756 @@ +--- +title: CommonMark Spec +author: John MacFarlane +version: 0.30 +date: '2021-06-19' +license: '[CC-BY-SA 4.0](http://creativecommons.org/licenses/by-sa/4.0/)' +... + +# Introduction + +## What is Markdown? + +Markdown is a plain text format for writing structured documents, +based on conventions for indicating formatting in email +and usenet posts. It was developed by John Gruber (with +help from Aaron Swartz) and released in 2004 in the form of a +[syntax description](http://daringfireball.net/projects/markdown/syntax) +and a Perl script (`Markdown.pl`) for converting Markdown to +HTML. In the next decade, dozens of implementations were +developed in many languages. Some extended the original +Markdown syntax with conventions for footnotes, tables, and +other document elements. Some allowed Markdown documents to be +rendered in formats other than HTML. Websites like Reddit, +StackOverflow, and GitHub had millions of people using Markdown. +And Markdown started to be used beyond the web, to author books, +articles, slide shows, letters, and lecture notes. + +What distinguishes Markdown from many other lightweight markup +syntaxes, which are often easier to write, is its readability. +As Gruber writes: + +> The overriding design goal for Markdown's formatting syntax is +> to make it as readable as possible. The idea is that a +> Markdown-formatted document should be publishable as-is, as +> plain text, without looking like it's been marked up with tags +> or formatting instructions. +> () + +The point can be illustrated by comparing a sample of +[AsciiDoc](http://www.methods.co.nz/asciidoc/) with +an equivalent sample of Markdown. Here is a sample of +AsciiDoc from the AsciiDoc manual: + +``` +1. List item one. ++ +List item one continued with a second paragraph followed by an +Indented block. ++ +................. +$ ls *.sh +$ mv *.sh ~/tmp +................. ++ +List item continued with a third paragraph. + +2. List item two continued with an open block. ++ +-- +This paragraph is part of the preceding list item. + +a. This list is nested and does not require explicit item +continuation. ++ +This paragraph is part of the preceding list item. + +b. List item b. + +This paragraph belongs to item two of the outer list. +-- +``` + +And here is the equivalent in Markdown: +``` +1. List item one. + + List item one continued with a second paragraph followed by an + Indented block. + + $ ls *.sh + $ mv *.sh ~/tmp + + List item continued with a third paragraph. + +2. List item two continued with an open block. + + This paragraph is part of the preceding list item. + + 1. This list is nested and does not require explicit item continuation. + + This paragraph is part of the preceding list item. + + 2. List item b. + + This paragraph belongs to item two of the outer list. +``` + +The AsciiDoc version is, arguably, easier to write. You don't need +to worry about indentation. But the Markdown version is much easier +to read. The nesting of list items is apparent to the eye in the +source, not just in the processed document. + +## Why is a spec needed? + +John Gruber's [canonical description of Markdown's +syntax](http://daringfireball.net/projects/markdown/syntax) +does not specify the syntax unambiguously. Here are some examples of +questions it does not answer: + +1. How much indentation is needed for a sublist? The spec says that + continuation paragraphs need to be indented four spaces, but is + not fully explicit about sublists. It is natural to think that + they, too, must be indented four spaces, but `Markdown.pl` does + not require that. This is hardly a "corner case," and divergences + between implementations on this issue often lead to surprises for + users in real documents. (See [this comment by John + Gruber](http://article.gmane.org/gmane.text.markdown.general/1997).) + +2. Is a blank line needed before a block quote or heading? + Most implementations do not require the blank line. However, + this can lead to unexpected results in hard-wrapped text, and + also to ambiguities in parsing (note that some implementations + put the heading inside the blockquote, while others do not). + (John Gruber has also spoken [in favor of requiring the blank + lines](http://article.gmane.org/gmane.text.markdown.general/2146).) + +3. Is a blank line needed before an indented code block? + (`Markdown.pl` requires it, but this is not mentioned in the + documentation, and some implementations do not require it.) + + ``` markdown + paragraph + code? + ``` + +4. What is the exact rule for determining when list items get + wrapped in `

` tags? Can a list be partially "loose" and partially + "tight"? What should we do with a list like this? + + ``` markdown + 1. one + + 2. two + 3. three + ``` + + Or this? + + ``` markdown + 1. one + - a + + - b + 2. two + ``` + + (There are some relevant comments by John Gruber + [here](http://article.gmane.org/gmane.text.markdown.general/2554).) + +5. Can list markers be indented? Can ordered list markers be right-aligned? + + ``` markdown + 8. item 1 + 9. item 2 + 10. item 2a + ``` + +6. Is this one list with a thematic break in its second item, + or two lists separated by a thematic break? + + ``` markdown + * a + * * * * * + * b + ``` + +7. When list markers change from numbers to bullets, do we have + two lists or one? (The Markdown syntax description suggests two, + but the perl scripts and many other implementations produce one.) + + ``` markdown + 1. fee + 2. fie + - foe + - fum + ``` + +8. What are the precedence rules for the markers of inline structure? + For example, is the following a valid link, or does the code span + take precedence ? + + ``` markdown + [a backtick (`)](/url) and [another backtick (`)](/url). + ``` + +9. What are the precedence rules for markers of emphasis and strong + emphasis? For example, how should the following be parsed? + + ``` markdown + *foo *bar* baz* + ``` + +10. What are the precedence rules between block-level and inline-level + structure? For example, how should the following be parsed? + + ``` markdown + - `a long code span can contain a hyphen like this + - and it can screw things up` + ``` + +11. Can list items include section headings? (`Markdown.pl` does not + allow this, but does allow blockquotes to include headings.) + + ``` markdown + - # Heading + ``` + +12. Can list items be empty? + + ``` markdown + * a + * + * b + ``` + +13. Can link references be defined inside block quotes or list items? + + ``` markdown + > Blockquote [foo]. + > + > [foo]: /url + ``` + +14. If there are multiple definitions for the same reference, which takes + precedence? + + ``` markdown + [foo]: /url1 + [foo]: /url2 + + [foo][] + ``` + +In the absence of a spec, early implementers consulted `Markdown.pl` +to resolve these ambiguities. But `Markdown.pl` was quite buggy, and +gave manifestly bad results in many cases, so it was not a +satisfactory replacement for a spec. + +Because there is no unambiguous spec, implementations have diverged +considerably. As a result, users are often surprised to find that +a document that renders one way on one system (say, a GitHub wiki) +renders differently on another (say, converting to docbook using +pandoc). To make matters worse, because nothing in Markdown counts +as a "syntax error," the divergence often isn't discovered right away. + +## About this document + +This document attempts to specify Markdown syntax unambiguously. +It contains many examples with side-by-side Markdown and +HTML. These are intended to double as conformance tests. An +accompanying script `spec_tests.py` can be used to run the tests +against any Markdown program: + + python test/spec_tests.py --spec spec.txt --program PROGRAM + +Since this document describes how Markdown is to be parsed into +an abstract syntax tree, it would have made sense to use an abstract +representation of the syntax tree instead of HTML. But HTML is capable +of representing the structural distinctions we need to make, and the +choice of HTML for the tests makes it possible to run the tests against +an implementation without writing an abstract syntax tree renderer. + +Note that not every feature of the HTML samples is mandated by +the spec. For example, the spec says what counts as a link +destination, but it doesn't mandate that non-ASCII characters in +the URL be percent-encoded. To use the automatic tests, +implementers will need to provide a renderer that conforms to +the expectations of the spec examples (percent-encoding +non-ASCII characters in URLs). But a conforming implementation +can use a different renderer and may choose not to +percent-encode non-ASCII characters in URLs. + +This document is generated from a text file, `spec.txt`, written +in Markdown with a small extension for the side-by-side tests. +The script `tools/makespec.py` can be used to convert `spec.txt` into +HTML or CommonMark (which can then be converted into other formats). + +In the examples, the `→` character is used to represent tabs. + +# Preliminaries + +## Characters and lines + +Any sequence of [characters] is a valid CommonMark +document. + +A [character](@) is a Unicode code point. Although some +code points (for example, combining accents) do not correspond to +characters in an intuitive sense, all code points count as characters +for purposes of this spec. + +This spec does not specify an encoding; it thinks of lines as composed +of [characters] rather than bytes. A conforming parser may be limited +to a certain encoding. + +A [line](@) is a sequence of zero or more [characters] +other than line feed (`U+000A`) or carriage return (`U+000D`), +followed by a [line ending] or by the end of file. + +A [line ending](@) is a line feed (`U+000A`), a carriage return +(`U+000D`) not followed by a line feed, or a carriage return and a +following line feed. + +A line containing no characters, or a line containing only spaces +(`U+0020`) or tabs (`U+0009`), is called a [blank line](@). + +The following definitions of character classes will be used in this spec: + +A [Unicode whitespace character](@) is +any code point in the Unicode `Zs` general category, or a tab (`U+0009`), +line feed (`U+000A`), form feed (`U+000C`), or carriage return (`U+000D`). + +[Unicode whitespace](@) is a sequence of one or more +[Unicode whitespace characters]. + +A [tab](@) is `U+0009`. + +A [space](@) is `U+0020`. + +An [ASCII control character](@) is a character between `U+0000–1F` (both +including) or `U+007F`. + +An [ASCII punctuation character](@) +is `!`, `"`, `#`, `$`, `%`, `&`, `'`, `(`, `)`, +`*`, `+`, `,`, `-`, `.`, `/` (U+0021–2F), +`:`, `;`, `<`, `=`, `>`, `?`, `@` (U+003A–0040), +`[`, `\`, `]`, `^`, `_`, `` ` `` (U+005B–0060), +`{`, `|`, `}`, or `~` (U+007B–007E). + +A [Unicode punctuation character](@) is an [ASCII +punctuation character] or anything in +the general Unicode categories `Pc`, `Pd`, `Pe`, `Pf`, `Pi`, `Po`, or `Ps`. + +## Tabs + +Tabs in lines are not expanded to [spaces]. However, +in contexts where spaces help to define block structure, +tabs behave as if they were replaced by spaces with a tab stop +of 4 characters. + +Thus, for example, a tab can be used instead of four spaces +in an indented code block. (Note, however, that internal +tabs are passed through as literal tabs, not expanded to +spaces.) + +```````````````````````````````` example +→foo→baz→→bim +. +

foo→baz→→bim
+
+```````````````````````````````` + +```````````````````````````````` example + →foo→baz→→bim +. +
foo→baz→→bim
+
+```````````````````````````````` + +```````````````````````````````` example + a→a + ὐ→a +. +
a→a
+ὐ→a
+
+```````````````````````````````` + +In the following example, a continuation paragraph of a list +item is indented with a tab; this has exactly the same effect +as indentation with four spaces would: + +```````````````````````````````` example + - foo + +→bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +- foo + +→→bar +. +
    +
  • +

    foo

    +
      bar
    +
    +
  • +
+```````````````````````````````` + +Normally the `>` that begins a block quote may be followed +optionally by a space, which is not considered part of the +content. In the following case `>` is followed by a tab, +which is treated as if it were expanded into three spaces. +Since one of these spaces is considered part of the +delimiter, `foo` is considered to be indented six spaces +inside the block quote context, so we get an indented +code block starting with two spaces. + +```````````````````````````````` example +>→→foo +. +
+
  foo
+
+
+```````````````````````````````` + +```````````````````````````````` example +-→→foo +. +
    +
  • +
      foo
    +
    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example + foo +→bar +. +
foo
+bar
+
+```````````````````````````````` + +```````````````````````````````` example + - foo + - bar +→ - baz +. +
    +
  • foo +
      +
    • bar +
        +
      • baz
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +#→Foo +. +

Foo

+```````````````````````````````` + +```````````````````````````````` example +*→*→*→ +. +
+```````````````````````````````` + + +## Insecure characters + +For security reasons, the Unicode character `U+0000` must be replaced +with the REPLACEMENT CHARACTER (`U+FFFD`). + + +## Backslash escapes + +Any ASCII punctuation character may be backslash-escaped: + +```````````````````````````````` example +\!\"\#\$\%\&\'\(\)\*\+\,\-\.\/\:\;\<\=\>\?\@\[\\\]\^\_\`\{\|\}\~ +. +

!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~

+```````````````````````````````` + + +Backslashes before other characters are treated as literal +backslashes: + +```````````````````````````````` example +\→\A\a\ \3\φ\« +. +

\→\A\a\ \3\φ\«

+```````````````````````````````` + + +Escaped characters are treated as regular characters and do +not have their usual Markdown meanings: + +```````````````````````````````` example +\*not emphasized* +\
not a tag +\[not a link](/foo) +\`not code` +1\. not a list +\* not a list +\# not a heading +\[foo]: /url "not a reference" +\ö not a character entity +. +

*not emphasized* +<br/> not a tag +[not a link](/foo) +`not code` +1. not a list +* not a list +# not a heading +[foo]: /url "not a reference" +&ouml; not a character entity

+```````````````````````````````` + + +If a backslash is itself escaped, the following character is not: + +```````````````````````````````` example +\\*emphasis* +. +

\emphasis

+```````````````````````````````` + + +A backslash at the end of the line is a [hard line break]: + +```````````````````````````````` example +foo\ +bar +. +

foo
+bar

+```````````````````````````````` + + +Backslash escapes do not work in code blocks, code spans, autolinks, or +raw HTML: + +```````````````````````````````` example +`` \[\` `` +. +

\[\`

+```````````````````````````````` + + +```````````````````````````````` example + \[\] +. +
\[\]
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~ +\[\] +~~~ +. +
\[\]
+
+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://example.com?find=\*

+```````````````````````````````` + + +```````````````````````````````` example + +. + +```````````````````````````````` + + +But they work in all other contexts, including URLs and link titles, +link references, and [info strings] in [fenced code blocks]: + +```````````````````````````````` example +[foo](/bar\* "ti\*tle") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo] + +[foo]: /bar\* "ti\*tle" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +``` foo\+bar +foo +``` +. +
foo
+
+```````````````````````````````` + + +## Entity and numeric character references + +Valid HTML entity references and numeric character references +can be used in place of the corresponding Unicode character, +with the following exceptions: + +- Entity and character references are not recognized in code + blocks and code spans. + +- Entity and character references cannot stand in place of + special characters that define structural elements in + CommonMark. For example, although `*` can be used + in place of a literal `*` character, `*` cannot replace + `*` in emphasis delimiters, bullet list markers, or thematic + breaks. + +Conforming CommonMark parsers need not store information about +whether a particular character was represented in the source +using a Unicode character or an entity reference. + +[Entity references](@) consist of `&` + any of the valid +HTML5 entity names + `;`. The +document +is used as an authoritative source for the valid entity +references and their corresponding code points. + +```````````````````````````````` example +  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸ +. +

  & © Æ Ď +¾ ℋ ⅆ +∲ ≧̸

+```````````````````````````````` + + +[Decimal numeric character +references](@) +consist of `&#` + a string of 1--7 arabic digits + `;`. A +numeric character reference is parsed as the corresponding +Unicode character. Invalid Unicode code points will be replaced by +the REPLACEMENT CHARACTER (`U+FFFD`). For security reasons, +the code point `U+0000` will also be replaced by `U+FFFD`. + +```````````````````````````````` example +# Ӓ Ϡ � +. +

# Ӓ Ϡ �

+```````````````````````````````` + + +[Hexadecimal numeric character +references](@) consist of `&#` + +either `X` or `x` + a string of 1-6 hexadecimal digits + `;`. +They too are parsed as the corresponding Unicode character (this +time specified with a hexadecimal numeral instead of decimal). + +```````````````````````````````` example +" ആ ಫ +. +

" ആ ಫ

+```````````````````````````````` + + +Here are some nonentities: + +```````````````````````````````` example +  &x; &#; &#x; +� +&#abcdef0; +&ThisIsNotDefined; &hi?; +. +

&nbsp &x; &#; &#x; +&#87654321; +&#abcdef0; +&ThisIsNotDefined; &hi?;

+```````````````````````````````` + + +Although HTML5 does accept some entity references +without a trailing semicolon (such as `©`), these are not +recognized here, because it makes the grammar too ambiguous: + +```````````````````````````````` example +© +. +

&copy

+```````````````````````````````` + + +Strings that are not on the list of HTML5 named entities are not +recognized as entity references either: + +```````````````````````````````` example +&MadeUpEntity; +. +

&MadeUpEntity;

+```````````````````````````````` + + +Entity and numeric character references are recognized in any +context besides code spans or code blocks, including +URLs, [link titles], and [fenced code block][] [info strings]: + +```````````````````````````````` example + +. + +```````````````````````````````` + + +```````````````````````````````` example +[foo](/föö "föö") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo] + +[foo]: /föö "föö" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +``` föö +foo +``` +. +
foo
+
+```````````````````````````````` + + +Entity and numeric character references are treated as literal +text in code spans and code blocks: + +```````````````````````````````` example +`föö` +. +

f&ouml;&ouml;

+```````````````````````````````` + + +```````````````````````````````` example + föfö +. +
f&ouml;f&ouml;
+
+```````````````````````````````` + + +Entity and numeric character references cannot be used +in place of symbols indicating structure in CommonMark +documents. + +```````````````````````````````` example +*foo* +*foo* +. +

*foo* +foo

+```````````````````````````````` + +```````````````````````````````` example +* foo + +* foo +. +

* foo

+
    +
  • foo
  • +
+```````````````````````````````` + +```````````````````````````````` example +foo bar +. +

foo + +bar

+```````````````````````````````` + +```````````````````````````````` example + foo +. +

→foo

+```````````````````````````````` + + +```````````````````````````````` example +[a](url "tit") +. +

[a](url "tit")

+```````````````````````````````` + + + +# Blocks and inlines + +We can think of a document as a sequence of +[blocks](@)---structural elements like paragraphs, block +quotations, lists, headings, rules, and code blocks. Some blocks (like +block quotes and list items) contain other blocks; others (like +headings and paragraphs) contain [inline](@) content---text, +links, emphasized text, images, code spans, and so on. + +## Precedence + +Indicators of block structure always take precedence over indicators +of inline structure. So, for example, the following is a list with +two items, not a list with one item containing a code span: + +```````````````````````````````` example +- `one +- two` +. +
    +
  • `one
  • +
  • two`
  • +
+```````````````````````````````` + + +This means that parsing can proceed in two steps: first, the block +structure of the document can be discerned; second, text lines inside +paragraphs, headings, and other block constructs can be parsed for inline +structure. The second step requires information about link reference +definitions that will be available only at the end of the first +step. Note that the first step requires processing lines in sequence, +but the second can be parallelized, since the inline parsing of +one block element does not affect the inline parsing of any other. + +## Container blocks and leaf blocks + +We can divide blocks into two types: +[container blocks](#container-blocks), +which can contain other blocks, and [leaf blocks](#leaf-blocks), +which cannot. + +# Leaf blocks + +This section describes the different kinds of leaf block that make up a +Markdown document. + +## Thematic breaks + +A line consisting of optionally up to three spaces of indentation, followed by a +sequence of three or more matching `-`, `_`, or `*` characters, each followed +optionally by any number of spaces or tabs, forms a +[thematic break](@). + +```````````````````````````````` example +*** +--- +___ +. +
+
+
+```````````````````````````````` + + +Wrong characters: + +```````````````````````````````` example ++++ +. +

+++

+```````````````````````````````` + + +```````````````````````````````` example +=== +. +

===

+```````````````````````````````` + + +Not enough characters: + +```````````````````````````````` example +-- +** +__ +. +

-- +** +__

+```````````````````````````````` + + +Up to three spaces of indentation are allowed: + +```````````````````````````````` example + *** + *** + *** +. +
+
+
+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example + *** +. +
***
+
+```````````````````````````````` + + +```````````````````````````````` example +Foo + *** +. +

Foo +***

+```````````````````````````````` + + +More than three characters may be used: + +```````````````````````````````` example +_____________________________________ +. +
+```````````````````````````````` + + +Spaces and tabs are allowed between the characters: + +```````````````````````````````` example + - - - +. +
+```````````````````````````````` + + +```````````````````````````````` example + ** * ** * ** * ** +. +
+```````````````````````````````` + + +```````````````````````````````` example +- - - - +. +
+```````````````````````````````` + + +Spaces and tabs are allowed at the end: + +```````````````````````````````` example +- - - - +. +
+```````````````````````````````` + + +However, no other characters may occur in the line: + +```````````````````````````````` example +_ _ _ _ a + +a------ + +---a--- +. +

_ _ _ _ a

+

a------

+

---a---

+```````````````````````````````` + + +It is required that all of the characters other than spaces or tabs be the same. +So, this is not a thematic break: + +```````````````````````````````` example + *-* +. +

-

+```````````````````````````````` + + +Thematic breaks do not need blank lines before or after: + +```````````````````````````````` example +- foo +*** +- bar +. +
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+```````````````````````````````` + + +Thematic breaks can interrupt a paragraph: + +```````````````````````````````` example +Foo +*** +bar +. +

Foo

+
+

bar

+```````````````````````````````` + + +If a line of dashes that meets the above conditions for being a +thematic break could also be interpreted as the underline of a [setext +heading], the interpretation as a +[setext heading] takes precedence. Thus, for example, +this is a setext heading, not a paragraph followed by a thematic break: + +```````````````````````````````` example +Foo +--- +bar +. +

Foo

+

bar

+```````````````````````````````` + + +When both a thematic break and a list item are possible +interpretations of a line, the thematic break takes precedence: + +```````````````````````````````` example +* Foo +* * * +* Bar +. +
    +
  • Foo
  • +
+
+
    +
  • Bar
  • +
+```````````````````````````````` + + +If you want a thematic break in a list item, use a different bullet: + +```````````````````````````````` example +- Foo +- * * * +. +
    +
  • Foo
  • +
  • +
    +
  • +
+```````````````````````````````` + + +## ATX headings + +An [ATX heading](@) +consists of a string of characters, parsed as inline content, between an +opening sequence of 1--6 unescaped `#` characters and an optional +closing sequence of any number of unescaped `#` characters. +The opening sequence of `#` characters must be followed by spaces or tabs, or +by the end of line. The optional closing sequence of `#`s must be preceded by +spaces or tabs and may be followed by spaces or tabs only. The opening +`#` character may be preceded by up to three spaces of indentation. The raw +contents of the heading are stripped of leading and trailing space or tabs +before being parsed as inline content. The heading level is equal to the number +of `#` characters in the opening sequence. + +Simple headings: + +```````````````````````````````` example +# foo +## foo +### foo +#### foo +##### foo +###### foo +. +

foo

+

foo

+

foo

+

foo

+
foo
+
foo
+```````````````````````````````` + + +More than six `#` characters is not a heading: + +```````````````````````````````` example +####### foo +. +

####### foo

+```````````````````````````````` + + +At least one space or tab is required between the `#` characters and the +heading's contents, unless the heading is empty. Note that many +implementations currently do not require the space. However, the +space was required by the +[original ATX implementation](http://www.aaronsw.com/2002/atx/atx.py), +and it helps prevent things like the following from being parsed as +headings: + +```````````````````````````````` example +#5 bolt + +#hashtag +. +

#5 bolt

+

#hashtag

+```````````````````````````````` + + +This is not a heading, because the first `#` is escaped: + +```````````````````````````````` example +\## foo +. +

## foo

+```````````````````````````````` + + +Contents are parsed as inlines: + +```````````````````````````````` example +# foo *bar* \*baz\* +. +

foo bar *baz*

+```````````````````````````````` + + +Leading and trailing spaces or tabs are ignored in parsing inline content: + +```````````````````````````````` example +# foo +. +

foo

+```````````````````````````````` + + +Up to three spaces of indentation are allowed: + +```````````````````````````````` example + ### foo + ## foo + # foo +. +

foo

+

foo

+

foo

+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example + # foo +. +
# foo
+
+```````````````````````````````` + + +```````````````````````````````` example +foo + # bar +. +

foo +# bar

+```````````````````````````````` + + +A closing sequence of `#` characters is optional: + +```````````````````````````````` example +## foo ## + ### bar ### +. +

foo

+

bar

+```````````````````````````````` + + +It need not be the same length as the opening sequence: + +```````````````````````````````` example +# foo ################################## +##### foo ## +. +

foo

+
foo
+```````````````````````````````` + + +Spaces or tabs are allowed after the closing sequence: + +```````````````````````````````` example +### foo ### +. +

foo

+```````````````````````````````` + + +A sequence of `#` characters with anything but spaces or tabs following it +is not a closing sequence, but counts as part of the contents of the +heading: + +```````````````````````````````` example +### foo ### b +. +

foo ### b

+```````````````````````````````` + + +The closing sequence must be preceded by a space or tab: + +```````````````````````````````` example +# foo# +. +

foo#

+```````````````````````````````` + + +Backslash-escaped `#` characters do not count as part +of the closing sequence: + +```````````````````````````````` example +### foo \### +## foo #\## +# foo \# +. +

foo ###

+

foo ###

+

foo #

+```````````````````````````````` + + +ATX headings need not be separated from surrounding content by blank +lines, and they can interrupt paragraphs: + +```````````````````````````````` example +**** +## foo +**** +. +
+

foo

+
+```````````````````````````````` + + +```````````````````````````````` example +Foo bar +# baz +Bar foo +. +

Foo bar

+

baz

+

Bar foo

+```````````````````````````````` + + +ATX headings can be empty: + +```````````````````````````````` example +## +# +### ### +. +

+

+

+```````````````````````````````` + + +## Setext headings + +A [setext heading](@) consists of one or more +lines of text, not interrupted by a blank line, of which the first line does not +have more than 3 spaces of indentation, followed by +a [setext heading underline]. The lines of text must be such +that, were they not followed by the setext heading underline, +they would be interpreted as a paragraph: they cannot be +interpretable as a [code fence], [ATX heading][ATX headings], +[block quote][block quotes], [thematic break][thematic breaks], +[list item][list items], or [HTML block][HTML blocks]. + +A [setext heading underline](@) is a sequence of +`=` characters or a sequence of `-` characters, with no more than 3 +spaces of indentation and any number of trailing spaces or tabs. If a line +containing a single `-` can be interpreted as an +empty [list items], it should be interpreted this way +and not as a [setext heading underline]. + +The heading is a level 1 heading if `=` characters are used in +the [setext heading underline], and a level 2 heading if `-` +characters are used. The contents of the heading are the result +of parsing the preceding lines of text as CommonMark inline +content. + +In general, a setext heading need not be preceded or followed by a +blank line. However, it cannot interrupt a paragraph, so when a +setext heading comes after a paragraph, a blank line is needed between +them. + +Simple examples: + +```````````````````````````````` example +Foo *bar* +========= + +Foo *bar* +--------- +. +

Foo bar

+

Foo bar

+```````````````````````````````` + + +The content of the header may span more than one line: + +```````````````````````````````` example +Foo *bar +baz* +==== +. +

Foo bar +baz

+```````````````````````````````` + +The contents are the result of parsing the headings's raw +content as inlines. The heading's raw content is formed by +concatenating the lines and removing initial and final +spaces or tabs. + +```````````````````````````````` example + Foo *bar +baz*→ +==== +. +

Foo bar +baz

+```````````````````````````````` + + +The underlining can be any length: + +```````````````````````````````` example +Foo +------------------------- + +Foo += +. +

Foo

+

Foo

+```````````````````````````````` + + +The heading content can be preceded by up to three spaces of indentation, and +need not line up with the underlining: + +```````````````````````````````` example + Foo +--- + + Foo +----- + + Foo + === +. +

Foo

+

Foo

+

Foo

+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example + Foo + --- + + Foo +--- +. +
Foo
+---
+
+Foo
+
+
+```````````````````````````````` + + +The setext heading underline can be preceded by up to three spaces of +indentation, and may have trailing spaces or tabs: + +```````````````````````````````` example +Foo + ---- +. +

Foo

+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example +Foo + --- +. +

Foo +---

+```````````````````````````````` + + +The setext heading underline cannot contain internal spaces or tabs: + +```````````````````````````````` example +Foo += = + +Foo +--- - +. +

Foo += =

+

Foo

+
+```````````````````````````````` + + +Trailing spaces or tabs in the content line do not cause a hard line break: + +```````````````````````````````` example +Foo +----- +. +

Foo

+```````````````````````````````` + + +Nor does a backslash at the end: + +```````````````````````````````` example +Foo\ +---- +. +

Foo\

+```````````````````````````````` + + +Since indicators of block structure take precedence over +indicators of inline structure, the following are setext headings: + +```````````````````````````````` example +`Foo +---- +` + + +. +

`Foo

+

`

+

<a title="a lot

+

of dashes"/>

+```````````````````````````````` + + +The setext heading underline cannot be a [lazy continuation +line] in a list item or block quote: + +```````````````````````````````` example +> Foo +--- +. +
+

Foo

+
+
+```````````````````````````````` + + +```````````````````````````````` example +> foo +bar +=== +. +
+

foo +bar +===

+
+```````````````````````````````` + + +```````````````````````````````` example +- Foo +--- +. +
    +
  • Foo
  • +
+
+```````````````````````````````` + + +A blank line is needed between a paragraph and a following +setext heading, since otherwise the paragraph becomes part +of the heading's content: + +```````````````````````````````` example +Foo +Bar +--- +. +

Foo +Bar

+```````````````````````````````` + + +But in general a blank line is not required before or after +setext headings: + +```````````````````````````````` example +--- +Foo +--- +Bar +--- +Baz +. +
+

Foo

+

Bar

+

Baz

+```````````````````````````````` + + +Setext headings cannot be empty: + +```````````````````````````````` example + +==== +. +

====

+```````````````````````````````` + + +Setext heading text lines must not be interpretable as block +constructs other than paragraphs. So, the line of dashes +in these examples gets interpreted as a thematic break: + +```````````````````````````````` example +--- +--- +. +
+
+```````````````````````````````` + + +```````````````````````````````` example +- foo +----- +. +
    +
  • foo
  • +
+
+```````````````````````````````` + + +```````````````````````````````` example + foo +--- +. +
foo
+
+
+```````````````````````````````` + + +```````````````````````````````` example +> foo +----- +. +
+

foo

+
+
+```````````````````````````````` + + +If you want a heading with `> foo` as its literal text, you can +use backslash escapes: + +```````````````````````````````` example +\> foo +------ +. +

> foo

+```````````````````````````````` + + +**Compatibility note:** Most existing Markdown implementations +do not allow the text of setext headings to span multiple lines. +But there is no consensus about how to interpret + +``` markdown +Foo +bar +--- +baz +``` + +One can find four different interpretations: + +1. paragraph "Foo", heading "bar", paragraph "baz" +2. paragraph "Foo bar", thematic break, paragraph "baz" +3. paragraph "Foo bar --- baz" +4. heading "Foo bar", paragraph "baz" + +We find interpretation 4 most natural, and interpretation 4 +increases the expressive power of CommonMark, by allowing +multiline headings. Authors who want interpretation 1 can +put a blank line after the first paragraph: + +```````````````````````````````` example +Foo + +bar +--- +baz +. +

Foo

+

bar

+

baz

+```````````````````````````````` + + +Authors who want interpretation 2 can put blank lines around +the thematic break, + +```````````````````````````````` example +Foo +bar + +--- + +baz +. +

Foo +bar

+
+

baz

+```````````````````````````````` + + +or use a thematic break that cannot count as a [setext heading +underline], such as + +```````````````````````````````` example +Foo +bar +* * * +baz +. +

Foo +bar

+
+

baz

+```````````````````````````````` + + +Authors who want interpretation 3 can use backslash escapes: + +```````````````````````````````` example +Foo +bar +\--- +baz +. +

Foo +bar +--- +baz

+```````````````````````````````` + + +## Indented code blocks + +An [indented code block](@) is composed of one or more +[indented chunks] separated by blank lines. +An [indented chunk](@) is a sequence of non-blank lines, +each preceded by four or more spaces of indentation. The contents of the code +block are the literal contents of the lines, including trailing +[line endings], minus four spaces of indentation. +An indented code block has no [info string]. + +An indented code block cannot interrupt a paragraph, so there must be +a blank line between a paragraph and a following indented code block. +(A blank line is not needed, however, between a code block and a following +paragraph.) + +```````````````````````````````` example + a simple + indented code block +. +
a simple
+  indented code block
+
+```````````````````````````````` + + +If there is any ambiguity between an interpretation of indentation +as a code block and as indicating that material belongs to a [list +item][list items], the list item interpretation takes precedence: + +```````````````````````````````` example + - foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. foo + + - bar +. +
    +
  1. +

    foo

    +
      +
    • bar
    • +
    +
  2. +
+```````````````````````````````` + + + +The contents of a code block are literal text, and do not get parsed +as Markdown: + +```````````````````````````````` example +
+ *hi* + + - one +. +
<a/>
+*hi*
+
+- one
+
+```````````````````````````````` + + +Here we have three chunks separated by blank lines: + +```````````````````````````````` example + chunk1 + + chunk2 + + + + chunk3 +. +
chunk1
+
+chunk2
+
+
+
+chunk3
+
+```````````````````````````````` + + +Any initial spaces or tabs beyond four spaces of indentation will be included in +the content, even in interior blank lines: + +```````````````````````````````` example + chunk1 + + chunk2 +. +
chunk1
+  
+  chunk2
+
+```````````````````````````````` + + +An indented code block cannot interrupt a paragraph. (This +allows hanging indents and the like.) + +```````````````````````````````` example +Foo + bar + +. +

Foo +bar

+```````````````````````````````` + + +However, any non-blank line with fewer than four spaces of indentation ends +the code block immediately. So a paragraph may occur immediately +after indented code: + +```````````````````````````````` example + foo +bar +. +
foo
+
+

bar

+```````````````````````````````` + + +And indented code can occur immediately before and after other kinds of +blocks: + +```````````````````````````````` example +# Heading + foo +Heading +------ + foo +---- +. +

Heading

+
foo
+
+

Heading

+
foo
+
+
+```````````````````````````````` + + +The first line can be preceded by more than four spaces of indentation: + +```````````````````````````````` example + foo + bar +. +
    foo
+bar
+
+```````````````````````````````` + + +Blank lines preceding or following an indented code block +are not included in it: + +```````````````````````````````` example + + + foo + + +. +
foo
+
+```````````````````````````````` + + +Trailing spaces or tabs are included in the code block's content: + +```````````````````````````````` example + foo +. +
foo  
+
+```````````````````````````````` + + + +## Fenced code blocks + +A [code fence](@) is a sequence +of at least three consecutive backtick characters (`` ` ``) or +tildes (`~`). (Tildes and backticks cannot be mixed.) +A [fenced code block](@) +begins with a code fence, preceded by up to three spaces of indentation. + +The line with the opening code fence may optionally contain some text +following the code fence; this is trimmed of leading and trailing +spaces or tabs and called the [info string](@). If the [info string] comes +after a backtick fence, it may not contain any backtick +characters. (The reason for this restriction is that otherwise +some inline code would be incorrectly interpreted as the +beginning of a fenced code block.) + +The content of the code block consists of all subsequent lines, until +a closing [code fence] of the same type as the code block +began with (backticks or tildes), and with at least as many backticks +or tildes as the opening code fence. If the leading code fence is +preceded by N spaces of indentation, then up to N spaces of indentation are +removed from each line of the content (if present). (If a content line is not +indented, it is preserved unchanged. If it is indented N spaces or less, all +of the indentation is removed.) + +The closing code fence may be preceded by up to three spaces of indentation, and +may be followed only by spaces or tabs, which are ignored. If the end of the +containing block (or document) is reached and no closing code fence +has been found, the code block contains all of the lines after the +opening code fence until the end of the containing block (or +document). (An alternative spec would require backtracking in the +event that a closing code fence is not found. But this makes parsing +much less efficient, and there seems to be no real down side to the +behavior described here.) + +A fenced code block may interrupt a paragraph, and does not require +a blank line either before or after. + +The content of a code fence is treated as literal text, not parsed +as inlines. The first word of the [info string] is typically used to +specify the language of the code sample, and rendered in the `class` +attribute of the `code` tag. However, this spec does not mandate any +particular treatment of the [info string]. + +Here is a simple example with backticks: + +```````````````````````````````` example +``` +< + > +``` +. +
<
+ >
+
+```````````````````````````````` + + +With tildes: + +```````````````````````````````` example +~~~ +< + > +~~~ +. +
<
+ >
+
+```````````````````````````````` + +Fewer than three backticks is not enough: + +```````````````````````````````` example +`` +foo +`` +. +

foo

+```````````````````````````````` + +The closing code fence must use the same character as the opening +fence: + +```````````````````````````````` example +``` +aaa +~~~ +``` +. +
aaa
+~~~
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~ +aaa +``` +~~~ +. +
aaa
+```
+
+```````````````````````````````` + + +The closing code fence must be at least as long as the opening fence: + +```````````````````````````````` example +```` +aaa +``` +`````` +. +
aaa
+```
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~~ +aaa +~~~ +~~~~ +. +
aaa
+~~~
+
+```````````````````````````````` + + +Unclosed code blocks are closed by the end of the document +(or the enclosing [block quote][block quotes] or [list item][list items]): + +```````````````````````````````` example +``` +. +
+```````````````````````````````` + + +```````````````````````````````` example +````` + +``` +aaa +. +

+```
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example +> ``` +> aaa + +bbb +. +
+
aaa
+
+
+

bbb

+```````````````````````````````` + + +A code block can have all empty lines as its content: + +```````````````````````````````` example +``` + + +``` +. +

+  
+
+```````````````````````````````` + + +A code block can be empty: + +```````````````````````````````` example +``` +``` +. +
+```````````````````````````````` + + +Fences can be indented. If the opening fence is indented, +content lines will have equivalent opening indentation removed, +if present: + +```````````````````````````````` example + ``` + aaa +aaa +``` +. +
aaa
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` +aaa + aaa +aaa + ``` +. +
aaa
+aaa
+aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` + aaa + aaa + aaa + ``` +. +
aaa
+ aaa
+aaa
+
+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example + ``` + aaa + ``` +. +
```
+aaa
+```
+
+```````````````````````````````` + + +Closing fences may be preceded by up to three spaces of indentation, and their +indentation need not match that of the opening fence: + +```````````````````````````````` example +``` +aaa + ``` +. +
aaa
+
+```````````````````````````````` + + +```````````````````````````````` example + ``` +aaa + ``` +. +
aaa
+
+```````````````````````````````` + + +This is not a closing fence, because it is indented 4 spaces: + +```````````````````````````````` example +``` +aaa + ``` +. +
aaa
+    ```
+
+```````````````````````````````` + + + +Code fences (opening and closing) cannot contain internal spaces or tabs: + +```````````````````````````````` example +``` ``` +aaa +. +

+aaa

+```````````````````````````````` + + +```````````````````````````````` example +~~~~~~ +aaa +~~~ ~~ +. +
aaa
+~~~ ~~
+
+```````````````````````````````` + + +Fenced code blocks can interrupt paragraphs, and can be followed +directly by paragraphs, without a blank line between: + +```````````````````````````````` example +foo +``` +bar +``` +baz +. +

foo

+
bar
+
+

baz

+```````````````````````````````` + + +Other blocks can also occur before and after fenced code blocks +without an intervening blank line: + +```````````````````````````````` example +foo +--- +~~~ +bar +~~~ +# baz +. +

foo

+
bar
+
+

baz

+```````````````````````````````` + + +An [info string] can be provided after the opening code fence. +Although this spec doesn't mandate any particular treatment of +the info string, the first word is typically used to specify +the language of the code block. In HTML output, the language is +normally indicated by adding a class to the `code` element consisting +of `language-` followed by the language name. + +```````````````````````````````` example +```ruby +def foo(x) + return 3 +end +``` +. +
def foo(x)
+  return 3
+end
+
+```````````````````````````````` + + +```````````````````````````````` example +~~~~ ruby startline=3 $%@#$ +def foo(x) + return 3 +end +~~~~~~~ +. +
def foo(x)
+  return 3
+end
+
+```````````````````````````````` + + +```````````````````````````````` example +````; +```` +. +
+```````````````````````````````` + + +[Info strings] for backtick code blocks cannot contain backticks: + +```````````````````````````````` example +``` aa ``` +foo +. +

aa +foo

+```````````````````````````````` + + +[Info strings] for tilde code blocks can contain backticks and tildes: + +```````````````````````````````` example +~~~ aa ``` ~~~ +foo +~~~ +. +
foo
+
+```````````````````````````````` + + +Closing code fences cannot have [info strings]: + +```````````````````````````````` example +``` +``` aaa +``` +. +
``` aaa
+
+```````````````````````````````` + + + +## HTML blocks + +An [HTML block](@) is a group of lines that is treated +as raw HTML (and will not be escaped in HTML output). + +There are seven kinds of [HTML block], which can be defined by their +start and end conditions. The block begins with a line that meets a +[start condition](@) (after up to three optional spaces of indentation). +It ends with the first subsequent line that meets a matching +[end condition](@), or the last line of the document, or the last line of +the [container block](#container-blocks) containing the current HTML +block, if no line is encountered that meets the [end condition]. If +the first line meets both the [start condition] and the [end +condition], the block will contain just that line. + +1. **Start condition:** line begins with the string ``, or the end of the line.\ +**End condition:** line contains an end tag +`
`, ``, ``, or `` (case-insensitive; it +need not match the start tag). + +2. **Start condition:** line begins with the string ``. + +3. **Start condition:** line begins with the string ``. + +4. **Start condition:** line begins with the string ``. + +5. **Start condition:** line begins with the string +``. + +6. **Start condition:** line begins the string `<` or ``, or +the string `/>`.\ +**End condition:** line is followed by a [blank line]. + +7. **Start condition:** line begins with a complete [open tag] +(with any [tag name] other than `pre`, `script`, +`style`, or `textarea`) or a complete [closing tag], +followed by zero or more spaces and tabs, followed by the end of the line.\ +**End condition:** line is followed by a [blank line]. + +HTML blocks continue until they are closed by their appropriate +[end condition], or the last line of the document or other [container +block](#container-blocks). This means any HTML **within an HTML +block** that might otherwise be recognised as a start condition will +be ignored by the parser and passed through as-is, without changing +the parser's state. + +For instance, `
` within an HTML block started by `` will not affect
+the parser state; as the HTML block was started in by start condition 6, it
+will end at any blank line. This can be surprising:
+
+```````````````````````````````` example
+
+
+**Hello**,
+
+_world_.
+
+
+. +
+
+**Hello**,
+

world. +

+
+```````````````````````````````` + +In this case, the HTML block is terminated by the blank line — the `**Hello**` +text remains verbatim — and regular parsing resumes, with a paragraph, +emphasised `world` and inline and block HTML following. + +All types of [HTML blocks] except type 7 may interrupt +a paragraph. Blocks of type 7 may not interrupt a paragraph. +(This restriction is intended to prevent unwanted interpretation +of long tags inside a wrapped paragraph as starting HTML blocks.) + +Some simple examples follow. Here are some basic HTML blocks +of type 6: + +```````````````````````````````` example + + + + +
+ hi +
+ +okay. +. + + + + +
+ hi +
+

okay.

+```````````````````````````````` + + +```````````````````````````````` example +
+*foo* +```````````````````````````````` + + +Here we have two HTML blocks with a Markdown paragraph between them: + +```````````````````````````````` example +
+ +*Markdown* + +
+. +
+

Markdown

+
+```````````````````````````````` + + +The tag on the first line can be partial, as long +as it is split where there would be whitespace: + +```````````````````````````````` example +
+
+. +
+
+```````````````````````````````` + + +```````````````````````````````` example +
+
+. +
+
+```````````````````````````````` + + +An open tag need not be closed: +```````````````````````````````` example +
+*foo* + +*bar* +. +
+*foo* +

bar

+```````````````````````````````` + + + +A partial tag need not even be completed (garbage +in, garbage out): + +```````````````````````````````` example +
+. + +```````````````````````````````` + + +```````````````````````````````` example +
+foo +
+. +
+foo +
+```````````````````````````````` + + +Everything until the next blank line or end of document +gets included in the HTML block. So, in the following +example, what looks like a Markdown code block +is actually part of the HTML block, which continues until a blank +line or the end of the document is reached: + +```````````````````````````````` example +
+``` c +int x = 33; +``` +. +
+``` c +int x = 33; +``` +```````````````````````````````` + + +To start an [HTML block] with a tag that is *not* in the +list of block-level tags in (6), you must put the tag by +itself on the first line (and it must be complete): + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +In type 7 blocks, the [tag name] can be anything: + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +```````````````````````````````` example + +*bar* + +. + +*bar* + +```````````````````````````````` + + +```````````````````````````````` example + +*bar* +. + +*bar* +```````````````````````````````` + + +These rules are designed to allow us to work with tags that +can function as either block-level or inline-level tags. +The `` tag is a nice example. We can surround content with +`` tags in three different ways. In this case, we get a raw +HTML block, because the `` tag is on a line by itself: + +```````````````````````````````` example + +*foo* + +. + +*foo* + +```````````````````````````````` + + +In this case, we get a raw HTML block that just includes +the `` tag (because it ends with the following blank +line). So the contents get interpreted as CommonMark: + +```````````````````````````````` example + + +*foo* + + +. + +

foo

+
+```````````````````````````````` + + +Finally, in this case, the `` tags are interpreted +as [raw HTML] *inside* the CommonMark paragraph. (Because +the tag is not on a line by itself, we get inline HTML +rather than an [HTML block].) + +```````````````````````````````` example +*foo* +. +

foo

+```````````````````````````````` + + +HTML tags designed to contain literal content +(`pre`, `script`, `style`, `textarea`), comments, processing instructions, +and declarations are treated somewhat differently. +Instead of ending at the first blank line, these blocks +end at the first line containing a corresponding end tag. +As a result, these blocks can contain blank lines: + +A pre tag (type 1): + +```````````````````````````````` example +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+okay +. +

+import Text.HTML.TagSoup
+
+main :: IO ()
+main = print $ parseTags tags
+
+

okay

+```````````````````````````````` + + +A script tag (type 1): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +A textarea tag (type 1): + +```````````````````````````````` example + +. + +```````````````````````````````` + +A style tag (type 1): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +If there is no matching end tag, the block will end at the +end of the document (or the enclosing [block quote][block quotes] +or [list item][list items]): + +```````````````````````````````` example + +*foo* +. + +

foo

+```````````````````````````````` + + +```````````````````````````````` example +*bar* +*baz* +. +*bar* +

baz

+```````````````````````````````` + + +Note that anything on the last line after the +end tag will be included in the [HTML block]: + +```````````````````````````````` example +1. *bar* +. +1. *bar* +```````````````````````````````` + + +A comment (type 2): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + + +A processing instruction (type 3): + +```````````````````````````````` example +'; + +?> +okay +. +'; + +?> +

okay

+```````````````````````````````` + + +A declaration (type 4): + +```````````````````````````````` example + +. + +```````````````````````````````` + + +CDATA (type 5): + +```````````````````````````````` example + +okay +. + +

okay

+```````````````````````````````` + + +The opening tag can be preceded by up to three spaces of indentation, but not +four: + +```````````````````````````````` example + + + +. + +
<!-- foo -->
+
+```````````````````````````````` + + +```````````````````````````````` example +
+ +
+. +
+
<div>
+
+```````````````````````````````` + + +An HTML block of types 1--6 can interrupt a paragraph, and need not be +preceded by a blank line. + +```````````````````````````````` example +Foo +
+bar +
+. +

Foo

+
+bar +
+```````````````````````````````` + + +However, a following blank line is needed, except at the end of +a document, and except for blocks of types 1--5, [above][HTML +block]: + +```````````````````````````````` example +
+bar +
+*foo* +. +
+bar +
+*foo* +```````````````````````````````` + + +HTML blocks of type 7 cannot interrupt a paragraph: + +```````````````````````````````` example +Foo + +baz +. +

Foo + +baz

+```````````````````````````````` + + +This rule differs from John Gruber's original Markdown syntax +specification, which says: + +> The only restrictions are that block-level HTML elements — +> e.g. `
`, ``, `
`, `

`, etc. — must be separated from +> surrounding content by blank lines, and the start and end tags of the +> block should not be indented with spaces or tabs. + +In some ways Gruber's rule is more restrictive than the one given +here: + +- It requires that an HTML block be preceded by a blank line. +- It does not allow the start tag to be indented. +- It requires a matching end tag, which it also does not allow to + be indented. + +Most Markdown implementations (including some of Gruber's own) do not +respect all of these restrictions. + +There is one respect, however, in which Gruber's rule is more liberal +than the one given here, since it allows blank lines to occur inside +an HTML block. There are two reasons for disallowing them here. +First, it removes the need to parse balanced tags, which is +expensive and can require backtracking from the end of the document +if no matching end tag is found. Second, it provides a very simple +and flexible way of including Markdown content inside HTML tags: +simply separate the Markdown from the HTML using blank lines: + +Compare: + +```````````````````````````````` example +

+ +*Emphasized* text. + +
+. +
+

Emphasized text.

+
+```````````````````````````````` + + +```````````````````````````````` example +
+*Emphasized* text. +
+. +
+*Emphasized* text. +
+```````````````````````````````` + + +Some Markdown implementations have adopted a convention of +interpreting content inside tags as text if the open tag has +the attribute `markdown=1`. The rule given above seems a simpler and +more elegant way of achieving the same expressive power, which is also +much simpler to parse. + +The main potential drawback is that one can no longer paste HTML +blocks into Markdown documents with 100% reliability. However, +*in most cases* this will work fine, because the blank lines in +HTML are usually followed by HTML block tags. For example: + +```````````````````````````````` example +
+ + + + + + + +
+Hi +
+. + + + + +
+Hi +
+```````````````````````````````` + + +There are problems, however, if the inner tags are indented +*and* separated by spaces, as then they will be interpreted as +an indented code block: + +```````````````````````````````` example + + + + + + + + +
+ Hi +
+. + + +
<td>
+  Hi
+</td>
+
+ +
+```````````````````````````````` + + +Fortunately, blank lines are usually not necessary and can be +deleted. The exception is inside `
` tags, but as described
+[above][HTML blocks], raw HTML blocks starting with `
`
+*can* contain blank lines.
+
+## Link reference definitions
+
+A [link reference definition](@)
+consists of a [link label], optionally preceded by up to three spaces of
+indentation, followed
+by a colon (`:`), optional spaces or tabs (including up to one
+[line ending]), a [link destination],
+optional spaces or tabs (including up to one
+[line ending]), and an optional [link
+title], which if it is present must be separated
+from the [link destination] by spaces or tabs.
+No further character may occur.
+
+A [link reference definition]
+does not correspond to a structural element of a document.  Instead, it
+defines a label which can be used in [reference links]
+and reference-style [images] elsewhere in the document.  [Link
+reference definitions] can come either before or after the links that use
+them.
+
+```````````````````````````````` example
+[foo]: /url "title"
+
+[foo]
+.
+

foo

+```````````````````````````````` + + +```````````````````````````````` example + [foo]: + /url + 'the title' + +[foo] +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[Foo*bar\]]:my_(url) 'title (with parens)' + +[Foo*bar\]] +. +

Foo*bar]

+```````````````````````````````` + + +```````````````````````````````` example +[Foo bar]: + +'title' + +[Foo bar] +. +

Foo bar

+```````````````````````````````` + + +The title may extend over multiple lines: + +```````````````````````````````` example +[foo]: /url ' +title +line1 +line2 +' + +[foo] +. +

foo

+```````````````````````````````` + + +However, it may not contain a [blank line]: + +```````````````````````````````` example +[foo]: /url 'title + +with blank line' + +[foo] +. +

[foo]: /url 'title

+

with blank line'

+

[foo]

+```````````````````````````````` + + +The title may be omitted: + +```````````````````````````````` example +[foo]: +/url + +[foo] +. +

foo

+```````````````````````````````` + + +The link destination may not be omitted: + +```````````````````````````````` example +[foo]: + +[foo] +. +

[foo]:

+

[foo]

+```````````````````````````````` + + However, an empty link destination may be specified using + angle brackets: + +```````````````````````````````` example +[foo]: <> + +[foo] +. +

foo

+```````````````````````````````` + +The title must be separated from the link destination by +spaces or tabs: + +```````````````````````````````` example +[foo]: (baz) + +[foo] +. +

[foo]: (baz)

+

[foo]

+```````````````````````````````` + + +Both title and destination can contain backslash escapes +and literal backslashes: + +```````````````````````````````` example +[foo]: /url\bar\*baz "foo\"bar\baz" + +[foo] +. +

foo

+```````````````````````````````` + + +A link can come before its corresponding definition: + +```````````````````````````````` example +[foo] + +[foo]: url +. +

foo

+```````````````````````````````` + + +If there are several matching definitions, the first one takes +precedence: + +```````````````````````````````` example +[foo] + +[foo]: first +[foo]: second +. +

foo

+```````````````````````````````` + + +As noted in the section on [Links], matching of labels is +case-insensitive (see [matches]). + +```````````````````````````````` example +[FOO]: /url + +[Foo] +. +

Foo

+```````````````````````````````` + + +```````````````````````````````` example +[ΑΓΩ]: /φου + +[αγω] +. +

αγω

+```````````````````````````````` + + +Whether something is a [link reference definition] is +independent of whether the link reference it defines is +used in the document. Thus, for example, the following +document contains just a link reference definition, and +no visible content: + +```````````````````````````````` example +[foo]: /url +. +```````````````````````````````` + + +Here is another one: + +```````````````````````````````` example +[ +foo +]: /url +bar +. +

bar

+```````````````````````````````` + + +This is not a link reference definition, because there are +characters other than spaces or tabs after the title: + +```````````````````````````````` example +[foo]: /url "title" ok +. +

[foo]: /url "title" ok

+```````````````````````````````` + + +This is a link reference definition, but it has no title: + +```````````````````````````````` example +[foo]: /url +"title" ok +. +

"title" ok

+```````````````````````````````` + + +This is not a link reference definition, because it is indented +four spaces: + +```````````````````````````````` example + [foo]: /url "title" + +[foo] +. +
[foo]: /url "title"
+
+

[foo]

+```````````````````````````````` + + +This is not a link reference definition, because it occurs inside +a code block: + +```````````````````````````````` example +``` +[foo]: /url +``` + +[foo] +. +
[foo]: /url
+
+

[foo]

+```````````````````````````````` + + +A [link reference definition] cannot interrupt a paragraph. + +```````````````````````````````` example +Foo +[bar]: /baz + +[bar] +. +

Foo +[bar]: /baz

+

[bar]

+```````````````````````````````` + + +However, it can directly follow other block elements, such as headings +and thematic breaks, and it need not be followed by a blank line. + +```````````````````````````````` example +# [Foo] +[foo]: /url +> bar +. +

Foo

+
+

bar

+
+```````````````````````````````` + +```````````````````````````````` example +[foo]: /url +bar +=== +[foo] +. +

bar

+

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo]: /url +=== +[foo] +. +

=== +foo

+```````````````````````````````` + + +Several [link reference definitions] +can occur one after another, without intervening blank lines. + +```````````````````````````````` example +[foo]: /foo-url "foo" +[bar]: /bar-url + "bar" +[baz]: /baz-url + +[foo], +[bar], +[baz] +. +

foo, +bar, +baz

+```````````````````````````````` + + +[Link reference definitions] can occur +inside block containers, like lists and block quotations. They +affect the entire document, not just the container in which they +are defined: + +```````````````````````````````` example +[foo] + +> [foo]: /url +. +

foo

+
+
+```````````````````````````````` + + +## Paragraphs + +A sequence of non-blank lines that cannot be interpreted as other +kinds of blocks forms a [paragraph](@). +The contents of the paragraph are the result of parsing the +paragraph's raw content as inlines. The paragraph's raw content +is formed by concatenating the lines and removing initial and final +spaces or tabs. + +A simple example with two paragraphs: + +```````````````````````````````` example +aaa + +bbb +. +

aaa

+

bbb

+```````````````````````````````` + + +Paragraphs can contain multiple lines, but no blank lines: + +```````````````````````````````` example +aaa +bbb + +ccc +ddd +. +

aaa +bbb

+

ccc +ddd

+```````````````````````````````` + + +Multiple blank lines between paragraphs have no effect: + +```````````````````````````````` example +aaa + + +bbb +. +

aaa

+

bbb

+```````````````````````````````` + + +Leading spaces or tabs are skipped: + +```````````````````````````````` example + aaa + bbb +. +

aaa +bbb

+```````````````````````````````` + + +Lines after the first may be indented any amount, since indented +code blocks cannot interrupt paragraphs. + +```````````````````````````````` example +aaa + bbb + ccc +. +

aaa +bbb +ccc

+```````````````````````````````` + + +However, the first line may be preceded by up to three spaces of indentation. +Four spaces of indentation is too many: + +```````````````````````````````` example + aaa +bbb +. +

aaa +bbb

+```````````````````````````````` + + +```````````````````````````````` example + aaa +bbb +. +
aaa
+
+

bbb

+```````````````````````````````` + + +Final spaces or tabs are stripped before inline parsing, so a paragraph +that ends with two or more spaces will not end with a [hard line +break]: + +```````````````````````````````` example +aaa +bbb +. +

aaa
+bbb

+```````````````````````````````` + + +## Blank lines + +[Blank lines] between block-level elements are ignored, +except for the role they play in determining whether a [list] +is [tight] or [loose]. + +Blank lines at the beginning and end of the document are also ignored. + +```````````````````````````````` example + + +aaa + + +# aaa + + +. +

aaa

+

aaa

+```````````````````````````````` + + + +# Container blocks + +A [container block](#container-blocks) is a block that has other +blocks as its contents. There are two basic kinds of container blocks: +[block quotes] and [list items]. +[Lists] are meta-containers for [list items]. + +We define the syntax for container blocks recursively. The general +form of the definition is: + +> If X is a sequence of blocks, then the result of +> transforming X in such-and-such a way is a container of type Y +> with these blocks as its content. + +So, we explain what counts as a block quote or list item by explaining +how these can be *generated* from their contents. This should suffice +to define the syntax, although it does not give a recipe for *parsing* +these constructions. (A recipe is provided below in the section entitled +[A parsing strategy](#appendix-a-parsing-strategy).) + +## Block quotes + +A [block quote marker](@), +optionally preceded by up to three spaces of indentation, +consists of (a) the character `>` together with a following space of +indentation, or (b) a single character `>` not followed by a space of +indentation. + +The following rules define [block quotes]: + +1. **Basic case.** If a string of lines *Ls* constitute a sequence + of blocks *Bs*, then the result of prepending a [block quote + marker] to the beginning of each line in *Ls* + is a [block quote](#block-quotes) containing *Bs*. + +2. **Laziness.** If a string of lines *Ls* constitute a [block + quote](#block-quotes) with contents *Bs*, then the result of deleting + the initial [block quote marker] from one or + more lines in which the next character other than a space or tab after the + [block quote marker] is [paragraph continuation + text] is a block quote with *Bs* as its content. + [Paragraph continuation text](@) is text + that will be parsed as part of the content of a paragraph, but does + not occur at the beginning of the paragraph. + +3. **Consecutiveness.** A document cannot contain two [block + quotes] in a row unless there is a [blank line] between them. + +Nothing else counts as a [block quote](#block-quotes). + +Here is a simple example: + +```````````````````````````````` example +> # Foo +> bar +> baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +The space or tab after the `>` characters can be omitted: + +```````````````````````````````` example +># Foo +>bar +> baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +The `>` characters can be preceded by up to three spaces of indentation: + +```````````````````````````````` example + > # Foo + > bar + > baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +Four spaces of indentation is too many: + +```````````````````````````````` example + > # Foo + > bar + > baz +. +
> # Foo
+> bar
+> baz
+
+```````````````````````````````` + + +The Laziness clause allows us to omit the `>` before +[paragraph continuation text]: + +```````````````````````````````` example +> # Foo +> bar +baz +. +
+

Foo

+

bar +baz

+
+```````````````````````````````` + + +A block quote can contain some lazy and some non-lazy +continuation lines: + +```````````````````````````````` example +> bar +baz +> foo +. +
+

bar +baz +foo

+
+```````````````````````````````` + + +Laziness only applies to lines that would have been continuations of +paragraphs had they been prepended with [block quote markers]. +For example, the `> ` cannot be omitted in the second line of + +``` markdown +> foo +> --- +``` + +without changing the meaning: + +```````````````````````````````` example +> foo +--- +. +
+

foo

+
+
+```````````````````````````````` + + +Similarly, if we omit the `> ` in the second line of + +``` markdown +> - foo +> - bar +``` + +then the block quote ends after the first line: + +```````````````````````````````` example +> - foo +- bar +. +
+
    +
  • foo
  • +
+
+
    +
  • bar
  • +
+```````````````````````````````` + + +For the same reason, we can't omit the `> ` in front of +subsequent lines of an indented or fenced code block: + +```````````````````````````````` example +> foo + bar +. +
+
foo
+
+
+
bar
+
+```````````````````````````````` + + +```````````````````````````````` example +> ``` +foo +``` +. +
+
+
+

foo

+
+```````````````````````````````` + + +Note that in the following case, we have a [lazy +continuation line]: + +```````````````````````````````` example +> foo + - bar +. +
+

foo +- bar

+
+```````````````````````````````` + + +To see why, note that in + +```markdown +> foo +> - bar +``` + +the `- bar` is indented too far to start a list, and can't +be an indented code block because indented code blocks cannot +interrupt paragraphs, so it is [paragraph continuation text]. + +A block quote can be empty: + +```````````````````````````````` example +> +. +
+
+```````````````````````````````` + + +```````````````````````````````` example +> +> +> +. +
+
+```````````````````````````````` + + +A block quote can have initial or final blank lines: + +```````````````````````````````` example +> +> foo +> +. +
+

foo

+
+```````````````````````````````` + + +A blank line always separates block quotes: + +```````````````````````````````` example +> foo + +> bar +. +
+

foo

+
+
+

bar

+
+```````````````````````````````` + + +(Most current Markdown implementations, including John Gruber's +original `Markdown.pl`, will parse this example as a single block quote +with two paragraphs. But it seems better to allow the author to decide +whether two block quotes or one are wanted.) + +Consecutiveness means that if we put these block quotes together, +we get a single block quote: + +```````````````````````````````` example +> foo +> bar +. +
+

foo +bar

+
+```````````````````````````````` + + +To get a block quote with two paragraphs, use: + +```````````````````````````````` example +> foo +> +> bar +. +
+

foo

+

bar

+
+```````````````````````````````` + + +Block quotes can interrupt paragraphs: + +```````````````````````````````` example +foo +> bar +. +

foo

+
+

bar

+
+```````````````````````````````` + + +In general, blank lines are not needed before or after block +quotes: + +```````````````````````````````` example +> aaa +*** +> bbb +. +
+

aaa

+
+
+
+

bbb

+
+```````````````````````````````` + + +However, because of laziness, a blank line is needed between +a block quote and a following paragraph: + +```````````````````````````````` example +> bar +baz +. +
+

bar +baz

+
+```````````````````````````````` + + +```````````````````````````````` example +> bar + +baz +. +
+

bar

+
+

baz

+```````````````````````````````` + + +```````````````````````````````` example +> bar +> +baz +. +
+

bar

+
+

baz

+```````````````````````````````` + + +It is a consequence of the Laziness rule that any number +of initial `>`s may be omitted on a continuation line of a +nested block quote: + +```````````````````````````````` example +> > > foo +bar +. +
+
+
+

foo +bar

+
+
+
+```````````````````````````````` + + +```````````````````````````````` example +>>> foo +> bar +>>baz +. +
+
+
+

foo +bar +baz

+
+
+
+```````````````````````````````` + + +When including an indented code block in a block quote, +remember that the [block quote marker] includes +both the `>` and a following space of indentation. So *five spaces* are needed +after the `>`: + +```````````````````````````````` example +> code + +> not code +. +
+
code
+
+
+
+

not code

+
+```````````````````````````````` + + + +## List items + +A [list marker](@) is a +[bullet list marker] or an [ordered list marker]. + +A [bullet list marker](@) +is a `-`, `+`, or `*` character. + +An [ordered list marker](@) +is a sequence of 1--9 arabic digits (`0-9`), followed by either a +`.` character or a `)` character. (The reason for the length +limit is that with 10 digits we start seeing integer overflows +in some browsers.) + +The following rules define [list items]: + +1. **Basic case.** If a sequence of lines *Ls* constitute a sequence of + blocks *Bs* starting with a character other than a space or tab, and *M* is + a list marker of width *W* followed by 1 ≤ *N* ≤ 4 spaces of indentation, + then the result of prepending *M* and the following spaces to the first line + of Ls*, and indenting subsequent lines of *Ls* by *W + N* spaces, is a + list item with *Bs* as its contents. The type of the list item + (bullet or ordered) is determined by the type of its list marker. + If the list item is ordered, then it is also assigned a start + number, based on the ordered list marker. + + Exceptions: + + 1. When the first list item in a [list] interrupts + a paragraph---that is, when it starts on a line that would + otherwise count as [paragraph continuation text]---then (a) + the lines *Ls* must not begin with a blank line, and (b) if + the list item is ordered, the start number must be 1. + 2. If any line is a [thematic break][thematic breaks] then + that line is not a list item. + +For example, let *Ls* be the lines + +```````````````````````````````` example +A paragraph +with two lines. + + indented code + +> A block quote. +. +

A paragraph +with two lines.

+
indented code
+
+
+

A block quote.

+
+```````````````````````````````` + + +And let *M* be the marker `1.`, and *N* = 2. Then rule #1 says +that the following is an ordered list item with start number 1, +and the same contents as *Ls*: + +```````````````````````````````` example +1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +The most important thing to notice is that the position of +the text after the list marker determines how much indentation +is needed in subsequent blocks in the list item. If the list +marker takes up two spaces of indentation, and there are three spaces between +the list marker and the next character other than a space or tab, then blocks +must be indented five spaces in order to fall under the list +item. + +Here are some examples showing how far content must be indented to be +put under the list item: + +```````````````````````````````` example +- one + + two +. +
    +
  • one
  • +
+

two

+```````````````````````````````` + + +```````````````````````````````` example +- one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example + - one + + two +. +
    +
  • one
  • +
+
 two
+
+```````````````````````````````` + + +```````````````````````````````` example + - one + + two +. +
    +
  • +

    one

    +

    two

    +
  • +
+```````````````````````````````` + + +It is tempting to think of this in terms of columns: the continuation +blocks must be indented at least to the column of the first character other than +a space or tab after the list marker. However, that is not quite right. +The spaces of indentation after the list marker determine how much relative +indentation is needed. Which column this indentation reaches will depend on +how the list item is embedded in other constructions, as shown by +this example: + +```````````````````````````````` example + > > 1. one +>> +>> two +. +
+
+
    +
  1. +

    one

    +

    two

    +
  2. +
+
+
+```````````````````````````````` + + +Here `two` occurs in the same column as the list marker `1.`, +but is actually contained in the list item, because there is +sufficient indentation after the last containing blockquote marker. + +The converse is also possible. In the following example, the word `two` +occurs far to the right of the initial text of the list item, `one`, but +it is not considered part of the list item, because it is not indented +far enough past the blockquote marker: + +```````````````````````````````` example +>>- one +>> + > > two +. +
+
+
    +
  • one
  • +
+

two

+
+
+```````````````````````````````` + + +Note that at least one space or tab is needed between the list marker and +any following content, so these are not list items: + +```````````````````````````````` example +-one + +2.two +. +

-one

+

2.two

+```````````````````````````````` + + +A list item may contain blocks that are separated by more than +one blank line. + +```````````````````````````````` example +- foo + + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +A list item may contain any kind of block: + +```````````````````````````````` example +1. foo + + ``` + bar + ``` + + baz + + > bam +. +
    +
  1. +

    foo

    +
    bar
    +
    +

    baz

    +
    +

    bam

    +
    +
  2. +
+```````````````````````````````` + + +A list item that contains an indented code block will preserve +empty lines within the code block verbatim. + +```````````````````````````````` example +- Foo + + bar + + + baz +. +
    +
  • +

    Foo

    +
    bar
    +
    +
    +baz
    +
    +
  • +
+```````````````````````````````` + +Note that ordered list start numbers must be nine digits or less: + +```````````````````````````````` example +123456789. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +```````````````````````````````` example +1234567890. not ok +. +

1234567890. not ok

+```````````````````````````````` + + +A start number may begin with 0s: + +```````````````````````````````` example +0. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +```````````````````````````````` example +003. ok +. +
    +
  1. ok
  2. +
+```````````````````````````````` + + +A start number may not be negative: + +```````````````````````````````` example +-1. not ok +. +

-1. not ok

+```````````````````````````````` + + + +2. **Item starting with indented code.** If a sequence of lines *Ls* + constitute a sequence of blocks *Bs* starting with an indented code + block, and *M* is a list marker of width *W* followed by + one space of indentation, then the result of prepending *M* and the + following space to the first line of *Ls*, and indenting subsequent lines + of *Ls* by *W + 1* spaces, is a list item with *Bs* as its contents. + If a line is empty, then it need not be indented. The type of the + list item (bullet or ordered) is determined by the type of its list + marker. If the list item is ordered, then it is also assigned a + start number, based on the ordered list marker. + +An indented code block will have to be preceded by four spaces of indentation +beyond the edge of the region where text will be included in the list item. +In the following case that is 6 spaces: + +```````````````````````````````` example +- foo + + bar +. +
    +
  • +

    foo

    +
    bar
    +
    +
  • +
+```````````````````````````````` + + +And in this case it is 11 spaces: + +```````````````````````````````` example + 10. foo + + bar +. +
    +
  1. +

    foo

    +
    bar
    +
    +
  2. +
+```````````````````````````````` + + +If the *first* block in the list item is an indented code block, +then by rule #2, the contents must be preceded by *one* space of indentation +after the list marker: + +```````````````````````````````` example + indented code + +paragraph + + more code +. +
indented code
+
+

paragraph

+
more code
+
+```````````````````````````````` + + +```````````````````````````````` example +1. indented code + + paragraph + + more code +. +
    +
  1. +
    indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+```````````````````````````````` + + +Note that an additional space of indentation is interpreted as space +inside the code block: + +```````````````````````````````` example +1. indented code + + paragraph + + more code +. +
    +
  1. +
     indented code
    +
    +

    paragraph

    +
    more code
    +
    +
  2. +
+```````````````````````````````` + + +Note that rules #1 and #2 only apply to two cases: (a) cases +in which the lines to be included in a list item begin with a +characer other than a space or tab, and (b) cases in which +they begin with an indented code +block. In a case like the following, where the first block begins with +three spaces of indentation, the rules do not allow us to form a list item by +indenting the whole thing and prepending a list marker: + +```````````````````````````````` example + foo + +bar +. +

foo

+

bar

+```````````````````````````````` + + +```````````````````````````````` example +- foo + + bar +. +
    +
  • foo
  • +
+

bar

+```````````````````````````````` + + +This is not a significant restriction, because when a block is preceded by up to +three spaces of indentation, the indentation can always be removed without +a change in interpretation, allowing rule #1 to be applied. So, in +the above case: + +```````````````````````````````` example +- foo + + bar +. +
    +
  • +

    foo

    +

    bar

    +
  • +
+```````````````````````````````` + + +3. **Item starting with a blank line.** If a sequence of lines *Ls* + starting with a single [blank line] constitute a (possibly empty) + sequence of blocks *Bs*, and *M* is a list marker of width *W*, + then the result of prepending *M* to the first line of *Ls*, and + preceding subsequent lines of *Ls* by *W + 1* spaces of indentation, is a + list item with *Bs* as its contents. + If a line is empty, then it need not be indented. The type of the + list item (bullet or ordered) is determined by the type of its list + marker. If the list item is ordered, then it is also assigned a + start number, based on the ordered list marker. + +Here are some list items that start with a blank line but are not empty: + +```````````````````````````````` example +- + foo +- + ``` + bar + ``` +- + baz +. +
    +
  • foo
  • +
  • +
    bar
    +
    +
  • +
  • +
    baz
    +
    +
  • +
+```````````````````````````````` + +When the list item starts with a blank line, the number of spaces +following the list marker doesn't change the required indentation: + +```````````````````````````````` example +- + foo +. +
    +
  • foo
  • +
+```````````````````````````````` + + +A list item can begin with at most one blank line. +In the following example, `foo` is not part of the list +item: + +```````````````````````````````` example +- + + foo +. +
    +
  • +
+

foo

+```````````````````````````````` + + +Here is an empty bullet list item: + +```````````````````````````````` example +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+```````````````````````````````` + + +It does not matter whether there are spaces or tabs following the [list marker]: + +```````````````````````````````` example +- foo +- +- bar +. +
    +
  • foo
  • +
  • +
  • bar
  • +
+```````````````````````````````` + + +Here is an empty ordered list item: + +```````````````````````````````` example +1. foo +2. +3. bar +. +
    +
  1. foo
  2. +
  3. +
  4. bar
  5. +
+```````````````````````````````` + + +A list may start or end with an empty list item: + +```````````````````````````````` example +* +. +
    +
  • +
+```````````````````````````````` + +However, an empty list item cannot interrupt a paragraph: + +```````````````````````````````` example +foo +* + +foo +1. +. +

foo +*

+

foo +1.

+```````````````````````````````` + + +4. **Indentation.** If a sequence of lines *Ls* constitutes a list item + according to rule #1, #2, or #3, then the result of preceding each line + of *Ls* by up to three spaces of indentation (the same for each line) also + constitutes a list item with the same contents and attributes. If a line is + empty, then it need not be indented. + +Indented one space: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indented two spaces: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indented three spaces: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Four spaces indent gives a code block: + +```````````````````````````````` example + 1. A paragraph + with two lines. + + indented code + + > A block quote. +. +
1.  A paragraph
+    with two lines.
+
+        indented code
+
+    > A block quote.
+
+```````````````````````````````` + + + +5. **Laziness.** If a string of lines *Ls* constitute a [list + item](#list-items) with contents *Bs*, then the result of deleting + some or all of the indentation from one or more lines in which the + next character other than a space or tab after the indentation is + [paragraph continuation text] is a + list item with the same contents and attributes. The unindented + lines are called + [lazy continuation line](@)s. + +Here is an example with [lazy continuation lines]: + +```````````````````````````````` example + 1. A paragraph +with two lines. + + indented code + + > A block quote. +. +
    +
  1. +

    A paragraph +with two lines.

    +
    indented code
    +
    +
    +

    A block quote.

    +
    +
  2. +
+```````````````````````````````` + + +Indentation can be partially deleted: + +```````````````````````````````` example + 1. A paragraph + with two lines. +. +
    +
  1. A paragraph +with two lines.
  2. +
+```````````````````````````````` + + +These examples show how laziness can work in nested structures: + +```````````````````````````````` example +> 1. > Blockquote +continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+```````````````````````````````` + + +```````````````````````````````` example +> 1. > Blockquote +> continued here. +. +
+
    +
  1. +
    +

    Blockquote +continued here.

    +
    +
  2. +
+
+```````````````````````````````` + + + +6. **That's all.** Nothing that is not counted as a list item by rules + #1--5 counts as a [list item](#list-items). + +The rules for sublists follow from the general rules +[above][List items]. A sublist must be indented the same number +of spaces of indentation a paragraph would need to be in order to be included +in the list item. + +So, in this case we need two spaces indent: + +```````````````````````````````` example +- foo + - bar + - baz + - boo +. +
    +
  • foo +
      +
    • bar +
        +
      • baz +
          +
        • boo
        • +
        +
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + + +One is not enough: + +```````````````````````````````` example +- foo + - bar + - baz + - boo +. +
    +
  • foo
  • +
  • bar
  • +
  • baz
  • +
  • boo
  • +
+```````````````````````````````` + + +Here we need four, because the list marker is wider: + +```````````````````````````````` example +10) foo + - bar +. +
    +
  1. foo +
      +
    • bar
    • +
    +
  2. +
+```````````````````````````````` + + +Three is not enough: + +```````````````````````````````` example +10) foo + - bar +. +
    +
  1. foo
  2. +
+
    +
  • bar
  • +
+```````````````````````````````` + + +A list may be the first block in a list item: + +```````````````````````````````` example +- - foo +. +
    +
  • +
      +
    • foo
    • +
    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. - 2. foo +. +
    +
  1. +
      +
    • +
        +
      1. foo
      2. +
      +
    • +
    +
  2. +
+```````````````````````````````` + + +A list item can contain a heading: + +```````````````````````````````` example +- # Foo +- Bar + --- + baz +. +
    +
  • +

    Foo

    +
  • +
  • +

    Bar

    +baz
  • +
+```````````````````````````````` + + +### Motivation + +John Gruber's Markdown spec says the following about list items: + +1. "List markers typically start at the left margin, but may be indented + by up to three spaces. List markers must be followed by one or more + spaces or a tab." + +2. "To make lists look nice, you can wrap items with hanging indents.... + But if you don't want to, you don't have to." + +3. "List items may consist of multiple paragraphs. Each subsequent + paragraph in a list item must be indented by either 4 spaces or one + tab." + +4. "It looks nice if you indent every line of the subsequent paragraphs, + but here again, Markdown will allow you to be lazy." + +5. "To put a blockquote within a list item, the blockquote's `>` + delimiters need to be indented." + +6. "To put a code block within a list item, the code block needs to be + indented twice — 8 spaces or two tabs." + +These rules specify that a paragraph under a list item must be indented +four spaces (presumably, from the left margin, rather than the start of +the list marker, but this is not said), and that code under a list item +must be indented eight spaces instead of the usual four. They also say +that a block quote must be indented, but not by how much; however, the +example given has four spaces indentation. Although nothing is said +about other kinds of block-level content, it is certainly reasonable to +infer that *all* block elements under a list item, including other +lists, must be indented four spaces. This principle has been called the +*four-space rule*. + +The four-space rule is clear and principled, and if the reference +implementation `Markdown.pl` had followed it, it probably would have +become the standard. However, `Markdown.pl` allowed paragraphs and +sublists to start with only two spaces indentation, at least on the +outer level. Worse, its behavior was inconsistent: a sublist of an +outer-level list needed two spaces indentation, but a sublist of this +sublist needed three spaces. It is not surprising, then, that different +implementations of Markdown have developed very different rules for +determining what comes under a list item. (Pandoc and python-Markdown, +for example, stuck with Gruber's syntax description and the four-space +rule, while discount, redcarpet, marked, PHP Markdown, and others +followed `Markdown.pl`'s behavior more closely.) + +Unfortunately, given the divergences between implementations, there +is no way to give a spec for list items that will be guaranteed not +to break any existing documents. However, the spec given here should +correctly handle lists formatted with either the four-space rule or +the more forgiving `Markdown.pl` behavior, provided they are laid out +in a way that is natural for a human to read. + +The strategy here is to let the width and indentation of the list marker +determine the indentation necessary for blocks to fall under the list +item, rather than having a fixed and arbitrary number. The writer can +think of the body of the list item as a unit which gets indented to the +right enough to fit the list marker (and any indentation on the list +marker). (The laziness rule, #5, then allows continuation lines to be +unindented if needed.) + +This rule is superior, we claim, to any rule requiring a fixed level of +indentation from the margin. The four-space rule is clear but +unnatural. It is quite unintuitive that + +``` markdown +- foo + + bar + + - baz +``` + +should be parsed as two lists with an intervening paragraph, + +``` html +
    +
  • foo
  • +
+

bar

+
    +
  • baz
  • +
+``` + +as the four-space rule demands, rather than a single list, + +``` html +
    +
  • +

    foo

    +

    bar

    +
      +
    • baz
    • +
    +
  • +
+``` + +The choice of four spaces is arbitrary. It can be learned, but it is +not likely to be guessed, and it trips up beginners regularly. + +Would it help to adopt a two-space rule? The problem is that such +a rule, together with the rule allowing up to three spaces of indentation for +the initial list marker, allows text that is indented *less than* the +original list marker to be included in the list item. For example, +`Markdown.pl` parses + +``` markdown + - one + + two +``` + +as a single list item, with `two` a continuation paragraph: + +``` html +
    +
  • +

    one

    +

    two

    +
  • +
+``` + +and similarly + +``` markdown +> - one +> +> two +``` + +as + +``` html +
+
    +
  • +

    one

    +

    two

    +
  • +
+
+``` + +This is extremely unintuitive. + +Rather than requiring a fixed indent from the margin, we could require +a fixed indent (say, two spaces, or even one space) from the list marker (which +may itself be indented). This proposal would remove the last anomaly +discussed. Unlike the spec presented above, it would count the following +as a list item with a subparagraph, even though the paragraph `bar` +is not indented as far as the first paragraph `foo`: + +``` markdown + 10. foo + + bar +``` + +Arguably this text does read like a list item with `bar` as a subparagraph, +which may count in favor of the proposal. However, on this proposal indented +code would have to be indented six spaces after the list marker. And this +would break a lot of existing Markdown, which has the pattern: + +``` markdown +1. foo + + indented code +``` + +where the code is indented eight spaces. The spec above, by contrast, will +parse this text as expected, since the code block's indentation is measured +from the beginning of `foo`. + +The one case that needs special treatment is a list item that *starts* +with indented code. How much indentation is required in that case, since +we don't have a "first paragraph" to measure from? Rule #2 simply stipulates +that in such cases, we require one space indentation from the list marker +(and then the normal four spaces for the indented code). This will match the +four-space rule in cases where the list marker plus its initial indentation +takes four spaces (a common case), but diverge in other cases. + +## Lists + +A [list](@) is a sequence of one or more +list items [of the same type]. The list items +may be separated by any number of blank lines. + +Two list items are [of the same type](@) +if they begin with a [list marker] of the same type. +Two list markers are of the +same type if (a) they are bullet list markers using the same character +(`-`, `+`, or `*`) or (b) they are ordered list numbers with the same +delimiter (either `.` or `)`). + +A list is an [ordered list](@) +if its constituent list items begin with +[ordered list markers], and a +[bullet list](@) if its constituent list +items begin with [bullet list markers]. + +The [start number](@) +of an [ordered list] is determined by the list number of +its initial list item. The numbers of subsequent list items are +disregarded. + +A list is [loose](@) if any of its constituent +list items are separated by blank lines, or if any of its constituent +list items directly contain two block-level elements with a blank line +between them. Otherwise a list is [tight](@). +(The difference in HTML output is that paragraphs in a loose list are +wrapped in `

` tags, while paragraphs in a tight list are not.) + +Changing the bullet or ordered list delimiter starts a new list: + +```````````````````````````````` example +- foo +- bar ++ baz +. +

    +
  • foo
  • +
  • bar
  • +
+
    +
  • baz
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. foo +2. bar +3) baz +. +
    +
  1. foo
  2. +
  3. bar
  4. +
+
    +
  1. baz
  2. +
+```````````````````````````````` + + +In CommonMark, a list can interrupt a paragraph. That is, +no blank line is needed to separate a paragraph from a following +list: + +```````````````````````````````` example +Foo +- bar +- baz +. +

Foo

+
    +
  • bar
  • +
  • baz
  • +
+```````````````````````````````` + +`Markdown.pl` does not allow this, through fear of triggering a list +via a numeral in a hard-wrapped line: + +``` markdown +The number of windows in my house is +14. The number of doors is 6. +``` + +Oddly, though, `Markdown.pl` *does* allow a blockquote to +interrupt a paragraph, even though the same considerations might +apply. + +In CommonMark, we do allow lists to interrupt paragraphs, for +two reasons. First, it is natural and not uncommon for people +to start lists without blank lines: + +``` markdown +I need to buy +- new shoes +- a coat +- a plane ticket +``` + +Second, we are attracted to a + +> [principle of uniformity](@): +> if a chunk of text has a certain +> meaning, it will continue to have the same meaning when put into a +> container block (such as a list item or blockquote). + +(Indeed, the spec for [list items] and [block quotes] presupposes +this principle.) This principle implies that if + +``` markdown + * I need to buy + - new shoes + - a coat + - a plane ticket +``` + +is a list item containing a paragraph followed by a nested sublist, +as all Markdown implementations agree it is (though the paragraph +may be rendered without `

` tags, since the list is "tight"), +then + +``` markdown +I need to buy +- new shoes +- a coat +- a plane ticket +``` + +by itself should be a paragraph followed by a nested sublist. + +Since it is well established Markdown practice to allow lists to +interrupt paragraphs inside list items, the [principle of +uniformity] requires us to allow this outside list items as +well. ([reStructuredText](http://docutils.sourceforge.net/rst.html) +takes a different approach, requiring blank lines before lists +even inside other list items.) + +In order to solve of unwanted lists in paragraphs with +hard-wrapped numerals, we allow only lists starting with `1` to +interrupt paragraphs. Thus, + +```````````````````````````````` example +The number of windows in my house is +14. The number of doors is 6. +. +

The number of windows in my house is +14. The number of doors is 6.

+```````````````````````````````` + +We may still get an unintended result in cases like + +```````````````````````````````` example +The number of windows in my house is +1. The number of doors is 6. +. +

The number of windows in my house is

+
    +
  1. The number of doors is 6.
  2. +
+```````````````````````````````` + +but this rule should prevent most spurious list captures. + +There can be any number of blank lines between items: + +```````````````````````````````` example +- foo + +- bar + + +- baz +. +
    +
  • +

    foo

    +
  • +
  • +

    bar

    +
  • +
  • +

    baz

    +
  • +
+```````````````````````````````` + +```````````````````````````````` example +- foo + - bar + - baz + + + bim +. +
    +
  • foo +
      +
    • bar +
        +
      • +

        baz

        +

        bim

        +
      • +
      +
    • +
    +
  • +
+```````````````````````````````` + + +To separate consecutive lists of the same type, or to separate a +list from an indented code block that would otherwise be parsed +as a subparagraph of the final list item, you can insert a blank HTML +comment: + +```````````````````````````````` example +- foo +- bar + + + +- baz +- bim +. +
    +
  • foo
  • +
  • bar
  • +
+ +
    +
  • baz
  • +
  • bim
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- foo + + notcode + +- foo + + + + code +. +
    +
  • +

    foo

    +

    notcode

    +
  • +
  • +

    foo

    +
  • +
+ +
code
+
+```````````````````````````````` + + +List items need not be indented to the same level. The following +list items will be treated as items at the same list level, +since none is indented enough to belong to the previous list +item: + +```````````````````````````````` example +- a + - b + - c + - d + - e + - f +- g +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d
  • +
  • e
  • +
  • f
  • +
  • g
  • +
+```````````````````````````````` + + +```````````````````````````````` example +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
  5. +

    c

    +
  6. +
+```````````````````````````````` + +Note, however, that list items may not be preceded by more than +three spaces of indentation. Here `- e` is treated as a paragraph continuation +line, because it is indented more than three spaces: + +```````````````````````````````` example +- a + - b + - c + - d + - e +. +
    +
  • a
  • +
  • b
  • +
  • c
  • +
  • d +- e
  • +
+```````````````````````````````` + +And here, `3. c` is treated as in indented code block, +because it is indented four spaces and preceded by a +blank line. + +```````````````````````````````` example +1. a + + 2. b + + 3. c +. +
    +
  1. +

    a

    +
  2. +
  3. +

    b

    +
  4. +
+
3. c
+
+```````````````````````````````` + + +This is a loose list, because there is a blank line between +two of the list items: + +```````````````````````````````` example +- a +- b + +- c +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    c

    +
  • +
+```````````````````````````````` + + +So is this, with a empty second item: + +```````````````````````````````` example +* a +* + +* c +. +
    +
  • +

    a

    +
  • +
  • +
  • +

    c

    +
  • +
+```````````````````````````````` + + +These are loose lists, even though there are no blank lines between the items, +because one of the items directly contains two block-level elements +with a blank line between them: + +```````````````````````````````` example +- a +- b + + c +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +

    c

    +
  • +
  • +

    d

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a +- b + + [ref]: /url +- d +. +
    +
  • +

    a

    +
  • +
  • +

    b

    +
  • +
  • +

    d

    +
  • +
+```````````````````````````````` + + +This is a tight list, because the blank lines are in a code block: + +```````````````````````````````` example +- a +- ``` + b + + + ``` +- c +. +
    +
  • a
  • +
  • +
    b
    +
    +
    +
    +
  • +
  • c
  • +
+```````````````````````````````` + + +This is a tight list, because the blank line is between two +paragraphs of a sublist. So the sublist is loose while +the outer list is tight: + +```````````````````````````````` example +- a + - b + + c +- d +. +
    +
  • a +
      +
    • +

      b

      +

      c

      +
    • +
    +
  • +
  • d
  • +
+```````````````````````````````` + + +This is a tight list, because the blank line is inside the +block quote: + +```````````````````````````````` example +* a + > b + > +* c +. +
    +
  • a +
    +

    b

    +
    +
  • +
  • c
  • +
+```````````````````````````````` + + +This list is tight, because the consecutive block elements +are not separated by blank lines: + +```````````````````````````````` example +- a + > b + ``` + c + ``` +- d +. +
    +
  • a +
    +

    b

    +
    +
    c
    +
    +
  • +
  • d
  • +
+```````````````````````````````` + + +A single-paragraph list is tight: + +```````````````````````````````` example +- a +. +
    +
  • a
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a + - b +. +
    +
  • a +
      +
    • b
    • +
    +
  • +
+```````````````````````````````` + + +This list is loose, because of the blank line between the +two block elements in the list item: + +```````````````````````````````` example +1. ``` + foo + ``` + + bar +. +
    +
  1. +
    foo
    +
    +

    bar

    +
  2. +
+```````````````````````````````` + + +Here the outer list is loose, the inner list tight: + +```````````````````````````````` example +* foo + * bar + + baz +. +
    +
  • +

    foo

    +
      +
    • bar
    • +
    +

    baz

    +
  • +
+```````````````````````````````` + + +```````````````````````````````` example +- a + - b + - c + +- d + - e + - f +. +
    +
  • +

    a

    +
      +
    • b
    • +
    • c
    • +
    +
  • +
  • +

    d

    +
      +
    • e
    • +
    • f
    • +
    +
  • +
+```````````````````````````````` + + +# Inlines + +Inlines are parsed sequentially from the beginning of the character +stream to the end (left to right, in left-to-right languages). +Thus, for example, in + +```````````````````````````````` example +`hi`lo` +. +

hilo`

+```````````````````````````````` + +`hi` is parsed as code, leaving the backtick at the end as a literal +backtick. + + + +## Code spans + +A [backtick string](@) +is a string of one or more backtick characters (`` ` ``) that is neither +preceded nor followed by a backtick. + +A [code span](@) begins with a backtick string and ends with +a backtick string of equal length. The contents of the code span are +the characters between these two backtick strings, normalized in the +following ways: + +- First, [line endings] are converted to [spaces]. +- If the resulting string both begins *and* ends with a [space] + character, but does not consist entirely of [space] + characters, a single [space] character is removed from the + front and back. This allows you to include code that begins + or ends with backtick characters, which must be separated by + whitespace from the opening or closing backtick strings. + +This is a simple code span: + +```````````````````````````````` example +`foo` +. +

foo

+```````````````````````````````` + + +Here two backticks are used, because the code contains a backtick. +This example also illustrates stripping of a single leading and +trailing space: + +```````````````````````````````` example +`` foo ` bar `` +. +

foo ` bar

+```````````````````````````````` + + +This example shows the motivation for stripping leading and trailing +spaces: + +```````````````````````````````` example +` `` ` +. +

``

+```````````````````````````````` + +Note that only *one* space is stripped: + +```````````````````````````````` example +` `` ` +. +

``

+```````````````````````````````` + +The stripping only happens if the space is on both +sides of the string: + +```````````````````````````````` example +` a` +. +

a

+```````````````````````````````` + +Only [spaces], and not [unicode whitespace] in general, are +stripped in this way: + +```````````````````````````````` example +` b ` +. +

 b 

+```````````````````````````````` + +No stripping occurs if the code span contains only spaces: + +```````````````````````````````` example +` ` +` ` +. +

  +

+```````````````````````````````` + + +[Line endings] are treated like spaces: + +```````````````````````````````` example +`` +foo +bar +baz +`` +. +

foo bar baz

+```````````````````````````````` + +```````````````````````````````` example +`` +foo +`` +. +

foo

+```````````````````````````````` + + +Interior spaces are not collapsed: + +```````````````````````````````` example +`foo bar +baz` +. +

foo bar baz

+```````````````````````````````` + +Note that browsers will typically collapse consecutive spaces +when rendering `` elements, so it is recommended that +the following CSS be used: + + code{white-space: pre-wrap;} + + +Note that backslash escapes do not work in code spans. All backslashes +are treated literally: + +```````````````````````````````` example +`foo\`bar` +. +

foo\bar`

+```````````````````````````````` + + +Backslash escapes are never needed, because one can always choose a +string of *n* backtick characters as delimiters, where the code does +not contain any strings of exactly *n* backtick characters. + +```````````````````````````````` example +``foo`bar`` +. +

foo`bar

+```````````````````````````````` + +```````````````````````````````` example +` foo `` bar ` +. +

foo `` bar

+```````````````````````````````` + + +Code span backticks have higher precedence than any other inline +constructs except HTML tags and autolinks. Thus, for example, this is +not parsed as emphasized text, since the second `*` is part of a code +span: + +```````````````````````````````` example +*foo`*` +. +

*foo*

+```````````````````````````````` + + +And this is not parsed as a link: + +```````````````````````````````` example +[not a `link](/foo`) +. +

[not a link](/foo)

+```````````````````````````````` + + +Code spans, HTML tags, and autolinks have the same precedence. +Thus, this is code: + +```````````````````````````````` example +`` +. +

<a href="">`

+```````````````````````````````` + + +But this is an HTML tag: + +```````````````````````````````` example +
` +. +

`

+```````````````````````````````` + + +And this is code: + +```````````````````````````````` example +`` +. +

<http://foo.bar.baz>`

+```````````````````````````````` + + +But this is an autolink: + +```````````````````````````````` example +` +. +

http://foo.bar.`baz`

+```````````````````````````````` + + +When a backtick string is not closed by a matching backtick string, +we just have literal backticks: + +```````````````````````````````` example +```foo`` +. +

```foo``

+```````````````````````````````` + + +```````````````````````````````` example +`foo +. +

`foo

+```````````````````````````````` + +The following case also illustrates the need for opening and +closing backtick strings to be equal in length: + +```````````````````````````````` example +`foo``bar`` +. +

`foobar

+```````````````````````````````` + + +## Emphasis and strong emphasis + +John Gruber's original [Markdown syntax +description](http://daringfireball.net/projects/markdown/syntax#em) says: + +> Markdown treats asterisks (`*`) and underscores (`_`) as indicators of +> emphasis. Text wrapped with one `*` or `_` will be wrapped with an HTML +> `` tag; double `*`'s or `_`'s will be wrapped with an HTML `` +> tag. + +This is enough for most users, but these rules leave much undecided, +especially when it comes to nested emphasis. The original +`Markdown.pl` test suite makes it clear that triple `***` and +`___` delimiters can be used for strong emphasis, and most +implementations have also allowed the following patterns: + +``` markdown +***strong emph*** +***strong** in emph* +***emph* in strong** +**in strong *emph*** +*in emph **strong*** +``` + +The following patterns are less widely supported, but the intent +is clear and they are useful (especially in contexts like bibliography +entries): + +``` markdown +*emph *with emph* in it* +**strong **with strong** in it** +``` + +Many implementations have also restricted intraword emphasis to +the `*` forms, to avoid unwanted emphasis in words containing +internal underscores. (It is best practice to put these in code +spans, but users often do not.) + +``` markdown +internal emphasis: foo*bar*baz +no emphasis: foo_bar_baz +``` + +The rules given below capture all of these patterns, while allowing +for efficient parsing strategies that do not backtrack. + +First, some definitions. A [delimiter run](@) is either +a sequence of one or more `*` characters that is not preceded or +followed by a non-backslash-escaped `*` character, or a sequence +of one or more `_` characters that is not preceded or followed by +a non-backslash-escaped `_` character. + +A [left-flanking delimiter run](@) is +a [delimiter run] that is (1) not followed by [Unicode whitespace], +and either (2a) not followed by a [Unicode punctuation character], or +(2b) followed by a [Unicode punctuation character] and +preceded by [Unicode whitespace] or a [Unicode punctuation character]. +For purposes of this definition, the beginning and the end of +the line count as Unicode whitespace. + +A [right-flanking delimiter run](@) is +a [delimiter run] that is (1) not preceded by [Unicode whitespace], +and either (2a) not preceded by a [Unicode punctuation character], or +(2b) preceded by a [Unicode punctuation character] and +followed by [Unicode whitespace] or a [Unicode punctuation character]. +For purposes of this definition, the beginning and the end of +the line count as Unicode whitespace. + +Here are some examples of delimiter runs. + + - left-flanking but not right-flanking: + + ``` + ***abc + _abc + **"abc" + _"abc" + ``` + + - right-flanking but not left-flanking: + + ``` + abc*** + abc_ + "abc"** + "abc"_ + ``` + + - Both left and right-flanking: + + ``` + abc***def + "abc"_"def" + ``` + + - Neither left nor right-flanking: + + ``` + abc *** def + a _ b + ``` + +(The idea of distinguishing left-flanking and right-flanking +delimiter runs based on the character before and the character +after comes from Roopesh Chander's +[vfmd](http://www.vfmd.org/vfmd-spec/specification/#procedure-for-identifying-emphasis-tags). +vfmd uses the terminology "emphasis indicator string" instead of "delimiter +run," and its rules for distinguishing left- and right-flanking runs +are a bit more complex than the ones given here.) + +The following rules define emphasis and strong emphasis: + +1. A single `*` character [can open emphasis](@) + iff (if and only if) it is part of a [left-flanking delimiter run]. + +2. A single `_` character [can open emphasis] iff + it is part of a [left-flanking delimiter run] + and either (a) not part of a [right-flanking delimiter run] + or (b) part of a [right-flanking delimiter run] + preceded by a [Unicode punctuation character]. + +3. A single `*` character [can close emphasis](@) + iff it is part of a [right-flanking delimiter run]. + +4. A single `_` character [can close emphasis] iff + it is part of a [right-flanking delimiter run] + and either (a) not part of a [left-flanking delimiter run] + or (b) part of a [left-flanking delimiter run] + followed by a [Unicode punctuation character]. + +5. A double `**` [can open strong emphasis](@) + iff it is part of a [left-flanking delimiter run]. + +6. A double `__` [can open strong emphasis] iff + it is part of a [left-flanking delimiter run] + and either (a) not part of a [right-flanking delimiter run] + or (b) part of a [right-flanking delimiter run] + preceded by a [Unicode punctuation character]. + +7. A double `**` [can close strong emphasis](@) + iff it is part of a [right-flanking delimiter run]. + +8. A double `__` [can close strong emphasis] iff + it is part of a [right-flanking delimiter run] + and either (a) not part of a [left-flanking delimiter run] + or (b) part of a [left-flanking delimiter run] + followed by a [Unicode punctuation character]. + +9. Emphasis begins with a delimiter that [can open emphasis] and ends + with a delimiter that [can close emphasis], and that uses the same + character (`_` or `*`) as the opening delimiter. The + opening and closing delimiters must belong to separate + [delimiter runs]. If one of the delimiters can both + open and close emphasis, then the sum of the lengths of the + delimiter runs containing the opening and closing delimiters + must not be a multiple of 3 unless both lengths are + multiples of 3. + +10. Strong emphasis begins with a delimiter that + [can open strong emphasis] and ends with a delimiter that + [can close strong emphasis], and that uses the same character + (`_` or `*`) as the opening delimiter. The + opening and closing delimiters must belong to separate + [delimiter runs]. If one of the delimiters can both open + and close strong emphasis, then the sum of the lengths of + the delimiter runs containing the opening and closing + delimiters must not be a multiple of 3 unless both lengths + are multiples of 3. + +11. A literal `*` character cannot occur at the beginning or end of + `*`-delimited emphasis or `**`-delimited strong emphasis, unless it + is backslash-escaped. + +12. A literal `_` character cannot occur at the beginning or end of + `_`-delimited emphasis or `__`-delimited strong emphasis, unless it + is backslash-escaped. + +Where rules 1--12 above are compatible with multiple parsings, +the following principles resolve ambiguity: + +13. The number of nestings should be minimized. Thus, for example, + an interpretation `...` is always preferred to + `...`. + +14. An interpretation `...` is always + preferred to `...`. + +15. When two potential emphasis or strong emphasis spans overlap, + so that the second begins before the first ends and ends after + the first ends, the first takes precedence. Thus, for example, + `*foo _bar* baz_` is parsed as `foo _bar baz_` rather + than `*foo bar* baz`. + +16. When there are two potential emphasis or strong emphasis spans + with the same closing delimiter, the shorter one (the one that + opens later) takes precedence. Thus, for example, + `**foo **bar baz**` is parsed as `**foo bar baz` + rather than `foo **bar baz`. + +17. Inline code spans, links, images, and HTML tags group more tightly + than emphasis. So, when there is a choice between an interpretation + that contains one of these elements and one that does not, the + former always wins. Thus, for example, `*[foo*](bar)` is + parsed as `*foo*` rather than as + `[foo](bar)`. + +These rules can be illustrated through a series of examples. + +Rule 1: + +```````````````````````````````` example +*foo bar* +. +

foo bar

+```````````````````````````````` + + +This is not emphasis, because the opening `*` is followed by +whitespace, and hence not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a * foo bar* +. +

a * foo bar*

+```````````````````````````````` + + +This is not emphasis, because the opening `*` is preceded +by an alphanumeric and followed by punctuation, and hence +not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a*"foo"* +. +

a*"foo"*

+```````````````````````````````` + + +Unicode nonbreaking spaces count as whitespace, too: + +```````````````````````````````` example +* a * +. +

* a *

+```````````````````````````````` + + +Intraword emphasis with `*` is permitted: + +```````````````````````````````` example +foo*bar* +. +

foobar

+```````````````````````````````` + + +```````````````````````````````` example +5*6*78 +. +

5678

+```````````````````````````````` + + +Rule 2: + +```````````````````````````````` example +_foo bar_ +. +

foo bar

+```````````````````````````````` + + +This is not emphasis, because the opening `_` is followed by +whitespace: + +```````````````````````````````` example +_ foo bar_ +. +

_ foo bar_

+```````````````````````````````` + + +This is not emphasis, because the opening `_` is preceded +by an alphanumeric and followed by punctuation: + +```````````````````````````````` example +a_"foo"_ +. +

a_"foo"_

+```````````````````````````````` + + +Emphasis with `_` is not allowed inside words: + +```````````````````````````````` example +foo_bar_ +. +

foo_bar_

+```````````````````````````````` + + +```````````````````````````````` example +5_6_78 +. +

5_6_78

+```````````````````````````````` + + +```````````````````````````````` example +пристаням_стремятся_ +. +

пристаням_стремятся_

+```````````````````````````````` + + +Here `_` does not generate emphasis, because the first delimiter run +is right-flanking and the second left-flanking: + +```````````````````````````````` example +aa_"bb"_cc +. +

aa_"bb"_cc

+```````````````````````````````` + + +This is emphasis, even though the opening delimiter is +both left- and right-flanking, because it is preceded by +punctuation: + +```````````````````````````````` example +foo-_(bar)_ +. +

foo-(bar)

+```````````````````````````````` + + +Rule 3: + +This is not emphasis, because the closing delimiter does +not match the opening delimiter: + +```````````````````````````````` example +_foo* +. +

_foo*

+```````````````````````````````` + + +This is not emphasis, because the closing `*` is preceded by +whitespace: + +```````````````````````````````` example +*foo bar * +. +

*foo bar *

+```````````````````````````````` + + +A line ending also counts as whitespace: + +```````````````````````````````` example +*foo bar +* +. +

*foo bar +*

+```````````````````````````````` + + +This is not emphasis, because the second `*` is +preceded by punctuation and followed by an alphanumeric +(hence it is not part of a [right-flanking delimiter run]: + +```````````````````````````````` example +*(*foo) +. +

*(*foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with this example: + +```````````````````````````````` example +*(*foo*)* +. +

(foo)

+```````````````````````````````` + + +Intraword emphasis with `*` is allowed: + +```````````````````````````````` example +*foo*bar +. +

foobar

+```````````````````````````````` + + + +Rule 4: + +This is not emphasis, because the closing `_` is preceded by +whitespace: + +```````````````````````````````` example +_foo bar _ +. +

_foo bar _

+```````````````````````````````` + + +This is not emphasis, because the second `_` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +_(_foo) +. +

_(_foo)

+```````````````````````````````` + + +This is emphasis within emphasis: + +```````````````````````````````` example +_(_foo_)_ +. +

(foo)

+```````````````````````````````` + + +Intraword emphasis is disallowed for `_`: + +```````````````````````````````` example +_foo_bar +. +

_foo_bar

+```````````````````````````````` + + +```````````````````````````````` example +_пристаням_стремятся +. +

_пристаням_стремятся

+```````````````````````````````` + + +```````````````````````````````` example +_foo_bar_baz_ +. +

foo_bar_baz

+```````````````````````````````` + + +This is emphasis, even though the closing delimiter is +both left- and right-flanking, because it is followed by +punctuation: + +```````````````````````````````` example +_(bar)_. +. +

(bar).

+```````````````````````````````` + + +Rule 5: + +```````````````````````````````` example +**foo bar** +. +

foo bar

+```````````````````````````````` + + +This is not strong emphasis, because the opening delimiter is +followed by whitespace: + +```````````````````````````````` example +** foo bar** +. +

** foo bar**

+```````````````````````````````` + + +This is not strong emphasis, because the opening `**` is preceded +by an alphanumeric and followed by punctuation, and hence +not part of a [left-flanking delimiter run]: + +```````````````````````````````` example +a**"foo"** +. +

a**"foo"**

+```````````````````````````````` + + +Intraword strong emphasis with `**` is permitted: + +```````````````````````````````` example +foo**bar** +. +

foobar

+```````````````````````````````` + + +Rule 6: + +```````````````````````````````` example +__foo bar__ +. +

foo bar

+```````````````````````````````` + + +This is not strong emphasis, because the opening delimiter is +followed by whitespace: + +```````````````````````````````` example +__ foo bar__ +. +

__ foo bar__

+```````````````````````````````` + + +A line ending counts as whitespace: +```````````````````````````````` example +__ +foo bar__ +. +

__ +foo bar__

+```````````````````````````````` + + +This is not strong emphasis, because the opening `__` is preceded +by an alphanumeric and followed by punctuation: + +```````````````````````````````` example +a__"foo"__ +. +

a__"foo"__

+```````````````````````````````` + + +Intraword strong emphasis is forbidden with `__`: + +```````````````````````````````` example +foo__bar__ +. +

foo__bar__

+```````````````````````````````` + + +```````````````````````````````` example +5__6__78 +. +

5__6__78

+```````````````````````````````` + + +```````````````````````````````` example +пристаням__стремятся__ +. +

пристаням__стремятся__

+```````````````````````````````` + + +```````````````````````````````` example +__foo, __bar__, baz__ +. +

foo, bar, baz

+```````````````````````````````` + + +This is strong emphasis, even though the opening delimiter is +both left- and right-flanking, because it is preceded by +punctuation: + +```````````````````````````````` example +foo-__(bar)__ +. +

foo-(bar)

+```````````````````````````````` + + + +Rule 7: + +This is not strong emphasis, because the closing delimiter is preceded +by whitespace: + +```````````````````````````````` example +**foo bar ** +. +

**foo bar **

+```````````````````````````````` + + +(Nor can it be interpreted as an emphasized `*foo bar *`, because of +Rule 11.) + +This is not strong emphasis, because the second `**` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +**(**foo) +. +

**(**foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with these examples: + +```````````````````````````````` example +*(**foo**)* +. +

(foo)

+```````````````````````````````` + + +```````````````````````````````` example +**Gomphocarpus (*Gomphocarpus physocarpus*, syn. +*Asclepias physocarpa*)** +. +

Gomphocarpus (Gomphocarpus physocarpus, syn. +Asclepias physocarpa)

+```````````````````````````````` + + +```````````````````````````````` example +**foo "*bar*" foo** +. +

foo "bar" foo

+```````````````````````````````` + + +Intraword emphasis: + +```````````````````````````````` example +**foo**bar +. +

foobar

+```````````````````````````````` + + +Rule 8: + +This is not strong emphasis, because the closing delimiter is +preceded by whitespace: + +```````````````````````````````` example +__foo bar __ +. +

__foo bar __

+```````````````````````````````` + + +This is not strong emphasis, because the second `__` is +preceded by punctuation and followed by an alphanumeric: + +```````````````````````````````` example +__(__foo) +. +

__(__foo)

+```````````````````````````````` + + +The point of this restriction is more easily appreciated +with this example: + +```````````````````````````````` example +_(__foo__)_ +. +

(foo)

+```````````````````````````````` + + +Intraword strong emphasis is forbidden with `__`: + +```````````````````````````````` example +__foo__bar +. +

__foo__bar

+```````````````````````````````` + + +```````````````````````````````` example +__пристаням__стремятся +. +

__пристаням__стремятся

+```````````````````````````````` + + +```````````````````````````````` example +__foo__bar__baz__ +. +

foo__bar__baz

+```````````````````````````````` + + +This is strong emphasis, even though the closing delimiter is +both left- and right-flanking, because it is followed by +punctuation: + +```````````````````````````````` example +__(bar)__. +. +

(bar).

+```````````````````````````````` + + +Rule 9: + +Any nonempty sequence of inline elements can be the contents of an +emphasized span. + +```````````````````````````````` example +*foo [bar](/url)* +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo +bar* +. +

foo +bar

+```````````````````````````````` + + +In particular, emphasis and strong emphasis can be nested +inside emphasis: + +```````````````````````````````` example +_foo __bar__ baz_ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +_foo _bar_ baz_ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +__foo_ bar_ +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo *bar** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo **bar** baz* +. +

foo bar baz

+```````````````````````````````` + +```````````````````````````````` example +*foo**bar**baz* +. +

foobarbaz

+```````````````````````````````` + +Note that in the preceding case, the interpretation + +``` markdown +

foobarbaz

+``` + + +is precluded by the condition that a delimiter that +can both open and close (like the `*` after `foo`) +cannot form emphasis if the sum of the lengths of +the delimiter runs containing the opening and +closing delimiters is a multiple of 3 unless +both lengths are multiples of 3. + + +For the same reason, we don't get two consecutive +emphasis sections in this example: + +```````````````````````````````` example +*foo**bar* +. +

foo**bar

+```````````````````````````````` + + +The same condition ensures that the following +cases are all strong emphasis nested inside +emphasis, even when the interior whitespace is +omitted: + + +```````````````````````````````` example +***foo** bar* +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo **bar*** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo**bar*** +. +

foobar

+```````````````````````````````` + + +When the lengths of the interior closing and opening +delimiter runs are *both* multiples of 3, though, +they can match to create emphasis: + +```````````````````````````````` example +foo***bar***baz +. +

foobarbaz

+```````````````````````````````` + +```````````````````````````````` example +foo******bar*********baz +. +

foobar***baz

+```````````````````````````````` + + +Indefinite levels of nesting are possible: + +```````````````````````````````` example +*foo **bar *baz* bim** bop* +. +

foo bar baz bim bop

+```````````````````````````````` + + +```````````````````````````````` example +*foo [*bar*](/url)* +. +

foo bar

+```````````````````````````````` + + +There can be no empty emphasis or strong emphasis: + +```````````````````````````````` example +** is not an empty emphasis +. +

** is not an empty emphasis

+```````````````````````````````` + + +```````````````````````````````` example +**** is not an empty strong emphasis +. +

**** is not an empty strong emphasis

+```````````````````````````````` + + + +Rule 10: + +Any nonempty sequence of inline elements can be the contents of an +strongly emphasized span. + +```````````````````````````````` example +**foo [bar](/url)** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo +bar** +. +

foo +bar

+```````````````````````````````` + + +In particular, emphasis and strong emphasis can be nested +inside strong emphasis: + +```````````````````````````````` example +__foo _bar_ baz__ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +__foo __bar__ baz__ +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +____foo__ bar__ +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo **bar**** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo *bar* baz** +. +

foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +**foo*bar*baz** +. +

foobarbaz

+```````````````````````````````` + + +```````````````````````````````` example +***foo* bar** +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +**foo *bar*** +. +

foo bar

+```````````````````````````````` + + +Indefinite levels of nesting are possible: + +```````````````````````````````` example +**foo *bar **baz** +bim* bop** +. +

foo bar baz +bim bop

+```````````````````````````````` + + +```````````````````````````````` example +**foo [*bar*](/url)** +. +

foo bar

+```````````````````````````````` + + +There can be no empty emphasis or strong emphasis: + +```````````````````````````````` example +__ is not an empty emphasis +. +

__ is not an empty emphasis

+```````````````````````````````` + + +```````````````````````````````` example +____ is not an empty strong emphasis +. +

____ is not an empty strong emphasis

+```````````````````````````````` + + + +Rule 11: + +```````````````````````````````` example +foo *** +. +

foo ***

+```````````````````````````````` + + +```````````````````````````````` example +foo *\** +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo *_* +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo ***** +. +

foo *****

+```````````````````````````````` + + +```````````````````````````````` example +foo **\*** +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo **_** +. +

foo _

+```````````````````````````````` + + +Note that when delimiters do not match evenly, Rule 11 determines +that the excess literal `*` characters will appear outside of the +emphasis, rather than inside it: + +```````````````````````````````` example +**foo* +. +

*foo

+```````````````````````````````` + + +```````````````````````````````` example +*foo** +. +

foo*

+```````````````````````````````` + + +```````````````````````````````` example +***foo** +. +

*foo

+```````````````````````````````` + + +```````````````````````````````` example +****foo* +. +

***foo

+```````````````````````````````` + + +```````````````````````````````` example +**foo*** +. +

foo*

+```````````````````````````````` + + +```````````````````````````````` example +*foo**** +. +

foo***

+```````````````````````````````` + + + +Rule 12: + +```````````````````````````````` example +foo ___ +. +

foo ___

+```````````````````````````````` + + +```````````````````````````````` example +foo _\__ +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo _*_ +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +foo _____ +. +

foo _____

+```````````````````````````````` + + +```````````````````````````````` example +foo __\___ +. +

foo _

+```````````````````````````````` + + +```````````````````````````````` example +foo __*__ +. +

foo *

+```````````````````````````````` + + +```````````````````````````````` example +__foo_ +. +

_foo

+```````````````````````````````` + + +Note that when delimiters do not match evenly, Rule 12 determines +that the excess literal `_` characters will appear outside of the +emphasis, rather than inside it: + +```````````````````````````````` example +_foo__ +. +

foo_

+```````````````````````````````` + + +```````````````````````````````` example +___foo__ +. +

_foo

+```````````````````````````````` + + +```````````````````````````````` example +____foo_ +. +

___foo

+```````````````````````````````` + + +```````````````````````````````` example +__foo___ +. +

foo_

+```````````````````````````````` + + +```````````````````````````````` example +_foo____ +. +

foo___

+```````````````````````````````` + + +Rule 13 implies that if you want emphasis nested directly inside +emphasis, you must use different delimiters: + +```````````````````````````````` example +**foo** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +*_foo_* +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +__foo__ +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +_*foo*_ +. +

foo

+```````````````````````````````` + + +However, strong emphasis within strong emphasis is possible without +switching delimiters: + +```````````````````````````````` example +****foo**** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +____foo____ +. +

foo

+```````````````````````````````` + + + +Rule 13 can be applied to arbitrarily long sequences of +delimiters: + +```````````````````````````````` example +******foo****** +. +

foo

+```````````````````````````````` + + +Rule 14: + +```````````````````````````````` example +***foo*** +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +_____foo_____ +. +

foo

+```````````````````````````````` + + +Rule 15: + +```````````````````````````````` example +*foo _bar* baz_ +. +

foo _bar baz_

+```````````````````````````````` + + +```````````````````````````````` example +*foo __bar *baz bim__ bam* +. +

foo bar *baz bim bam

+```````````````````````````````` + + +Rule 16: + +```````````````````````````````` example +**foo **bar baz** +. +

**foo bar baz

+```````````````````````````````` + + +```````````````````````````````` example +*foo *bar baz* +. +

*foo bar baz

+```````````````````````````````` + + +Rule 17: + +```````````````````````````````` example +*[bar*](/url) +. +

*bar*

+```````````````````````````````` + + +```````````````````````````````` example +_foo [bar_](/url) +. +

_foo bar_

+```````````````````````````````` + + +```````````````````````````````` example +* +. +

*

+```````````````````````````````` + + +```````````````````````````````` example +** +. +

**

+```````````````````````````````` + + +```````````````````````````````` example +__ +. +

__

+```````````````````````````````` + + +```````````````````````````````` example +*a `*`* +. +

a *

+```````````````````````````````` + + +```````````````````````````````` example +_a `_`_ +. +

a _

+```````````````````````````````` + + +```````````````````````````````` example +**a +. +

**ahttp://foo.bar/?q=**

+```````````````````````````````` + + +```````````````````````````````` example +__a +. +

__ahttp://foo.bar/?q=__

+```````````````````````````````` + + + +## Links + +A link contains [link text] (the visible text), a [link destination] +(the URI that is the link destination), and optionally a [link title]. +There are two basic kinds of links in Markdown. In [inline links] the +destination and title are given immediately after the link text. In +[reference links] the destination and title are defined elsewhere in +the document. + +A [link text](@) consists of a sequence of zero or more +inline elements enclosed by square brackets (`[` and `]`). The +following rules apply: + +- Links may not contain other links, at any level of nesting. If + multiple otherwise valid link definitions appear nested inside each + other, the inner-most definition is used. + +- Brackets are allowed in the [link text] only if (a) they + are backslash-escaped or (b) they appear as a matched pair of brackets, + with an open bracket `[`, a sequence of zero or more inlines, and + a close bracket `]`. + +- Backtick [code spans], [autolinks], and raw [HTML tags] bind more tightly + than the brackets in link text. Thus, for example, + `` [foo`]` `` could not be a link text, since the second `]` + is part of a code span. + +- The brackets in link text bind more tightly than markers for + [emphasis and strong emphasis]. Thus, for example, `*[foo*](url)` is a link. + +A [link destination](@) consists of either + +- a sequence of zero or more characters between an opening `<` and a + closing `>` that contains no line endings or unescaped + `<` or `>` characters, or + +- a nonempty sequence of characters that does not start with `<`, + does not include [ASCII control characters][ASCII control character] + or [space] character, and includes parentheses only if (a) they are + backslash-escaped or (b) they are part of a balanced pair of + unescaped parentheses. + (Implementations may impose limits on parentheses nesting to + avoid performance issues, but at least three levels of nesting + should be supported.) + +A [link title](@) consists of either + +- a sequence of zero or more characters between straight double-quote + characters (`"`), including a `"` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between straight single-quote + characters (`'`), including a `'` character only if it is + backslash-escaped, or + +- a sequence of zero or more characters between matching parentheses + (`(...)`), including a `(` or `)` character only if it is + backslash-escaped. + +Although [link titles] may span multiple lines, they may not contain +a [blank line]. + +An [inline link](@) consists of a [link text] followed immediately +by a left parenthesis `(`, an optional [link destination], an optional +[link title], and a right parenthesis `)`. +These four components may be separated by spaces, tabs, and up to one line +ending. +If both [link destination] and [link title] are present, they *must* be +separated by spaces, tabs, and up to one line ending. + +The link's text consists of the inlines contained +in the [link text] (excluding the enclosing square brackets). +The link's URI consists of the link destination, excluding enclosing +`<...>` if present, with backslash-escapes in effect as described +above. The link's title consists of the link title, excluding its +enclosing delimiters, with backslash-escapes in effect as described +above. + +Here is a simple inline link: + +```````````````````````````````` example +[link](/uri "title") +. +

link

+```````````````````````````````` + + +The title, the link text and even +the destination may be omitted: + +```````````````````````````````` example +[link](/uri) +. +

link

+```````````````````````````````` + +```````````````````````````````` example +[](./target.md) +. +

+```````````````````````````````` + + +```````````````````````````````` example +[link]() +. +

link

+```````````````````````````````` + + +```````````````````````````````` example +[link](<>) +. +

link

+```````````````````````````````` + + +```````````````````````````````` example +[]() +. +

+```````````````````````````````` + +The destination can only contain spaces if it is +enclosed in pointy brackets: + +```````````````````````````````` example +[link](/my uri) +. +

[link](/my uri)

+```````````````````````````````` + +```````````````````````````````` example +[link](
) +. +

link

+```````````````````````````````` + +The destination cannot contain line endings, +even if enclosed in pointy brackets: + +```````````````````````````````` example +[link](foo +bar) +. +

[link](foo +bar)

+```````````````````````````````` + +```````````````````````````````` example +[link]() +. +

[link]()

+```````````````````````````````` + +The destination can contain `)` if it is enclosed +in pointy brackets: + +```````````````````````````````` example +[a]() +. +

a

+```````````````````````````````` + +Pointy brackets that enclose links must be unescaped: + +```````````````````````````````` example +[link]() +. +

[link](<foo>)

+```````````````````````````````` + +These are not links, because the opening pointy bracket +is not matched properly: + +```````````````````````````````` example +[a]( +[a](c) +. +

[a](<b)c +[a](<b)c> +[a](c)

+```````````````````````````````` + +Parentheses inside the link destination may be escaped: + +```````````````````````````````` example +[link](\(foo\)) +. +

link

+```````````````````````````````` + +Any number of parentheses are allowed without escaping, as long as they are +balanced: + +```````````````````````````````` example +[link](foo(and(bar))) +. +

link

+```````````````````````````````` + +However, if you have unbalanced parentheses, you need to escape or use the +`<...>` form: + +```````````````````````````````` example +[link](foo(and(bar)) +. +

[link](foo(and(bar))

+```````````````````````````````` + + +```````````````````````````````` example +[link](foo\(and\(bar\)) +. +

link

+```````````````````````````````` + + +```````````````````````````````` example +[link]() +. +

link

+```````````````````````````````` + + +Parentheses and other symbols can also be escaped, as usual +in Markdown: + +```````````````````````````````` example +[link](foo\)\:) +. +

link

+```````````````````````````````` + + +A link can contain fragment identifiers and queries: + +```````````````````````````````` example +[link](#fragment) + +[link](http://example.com#fragment) + +[link](http://example.com?foo=3#frag) +. +

link

+

link

+

link

+```````````````````````````````` + + +Note that a backslash before a non-escapable character is +just a backslash: + +```````````````````````````````` example +[link](foo\bar) +. +

link

+```````````````````````````````` + + +URL-escaping should be left alone inside the destination, as all +URL-escaped characters are also valid URL characters. Entity and +numerical character references in the destination will be parsed +into the corresponding Unicode code points, as usual. These may +be optionally URL-escaped when written as HTML, but this spec +does not enforce any particular policy for rendering URLs in +HTML or other formats. Renderers may make different decisions +about how to escape or normalize URLs in the output. + +```````````````````````````````` example +[link](foo%20bä) +. +

link

+```````````````````````````````` + + +Note that, because titles can often be parsed as destinations, +if you try to omit the destination and keep the title, you'll +get unexpected results: + +```````````````````````````````` example +[link]("title") +. +

link

+```````````````````````````````` + + +Titles may be in single quotes, double quotes, or parentheses: + +```````````````````````````````` example +[link](/url "title") +[link](/url 'title') +[link](/url (title)) +. +

link +link +link

+```````````````````````````````` + + +Backslash escapes and entity and numeric character references +may be used in titles: + +```````````````````````````````` example +[link](/url "title \""") +. +

link

+```````````````````````````````` + + +Titles must be separated from the link using spaces, tabs, and up to one line +ending. +Other [Unicode whitespace] like non-breaking space doesn't work. + +```````````````````````````````` example +[link](/url "title") +. +

link

+```````````````````````````````` + + +Nested balanced quotes are not allowed without escaping: + +```````````````````````````````` example +[link](/url "title "and" title") +. +

[link](/url "title "and" title")

+```````````````````````````````` + + +But it is easy to work around this by using a different quote type: + +```````````````````````````````` example +[link](/url 'title "and" title') +. +

link

+```````````````````````````````` + + +(Note: `Markdown.pl` did allow double quotes inside a double-quoted +title, and its test suite included a test demonstrating this. +But it is hard to see a good rationale for the extra complexity this +brings, since there are already many ways---backslash escaping, +entity and numeric character references, or using a different +quote type for the enclosing title---to write titles containing +double quotes. `Markdown.pl`'s handling of titles has a number +of other strange features. For example, it allows single-quoted +titles in inline links, but not reference links. And, in +reference links but not inline links, it allows a title to begin +with `"` and end with `)`. `Markdown.pl` 1.0.1 even allows +titles with no closing quotation mark, though 1.0.2b8 does not. +It seems preferable to adopt a simple, rational rule that works +the same way in inline links and link reference definitions.) + +Spaces, tabs, and up to one line ending is allowed around the destination and +title: + +```````````````````````````````` example +[link]( /uri + "title" ) +. +

link

+```````````````````````````````` + + +But it is not allowed between the link text and the +following parenthesis: + +```````````````````````````````` example +[link] (/uri) +. +

[link] (/uri)

+```````````````````````````````` + + +The link text may contain balanced brackets, but not unbalanced ones, +unless they are escaped: + +```````````````````````````````` example +[link [foo [bar]]](/uri) +. +

link [foo [bar]]

+```````````````````````````````` + + +```````````````````````````````` example +[link] bar](/uri) +. +

[link] bar](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[link [bar](/uri) +. +

[link bar

+```````````````````````````````` + + +```````````````````````````````` example +[link \[bar](/uri) +. +

link [bar

+```````````````````````````````` + + +The link text may contain inline content: + +```````````````````````````````` example +[link *foo **bar** `#`*](/uri) +. +

link foo bar #

+```````````````````````````````` + + +```````````````````````````````` example +[![moon](moon.jpg)](/uri) +. +

moon

+```````````````````````````````` + + +However, links may not contain other links, at any level of nesting. + +```````````````````````````````` example +[foo [bar](/uri)](/uri) +. +

[foo bar](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[foo *[bar [baz](/uri)](/uri)*](/uri) +. +

[foo [bar baz](/uri)](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +![[[foo](uri1)](uri2)](uri3) +. +

[foo](uri2)

+```````````````````````````````` + + +These cases illustrate the precedence of link text grouping over +emphasis grouping: + +```````````````````````````````` example +*[foo*](/uri) +. +

*foo*

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar](baz*) +. +

foo *bar

+```````````````````````````````` + + +Note that brackets that *aren't* part of links do not take +precedence: + +```````````````````````````````` example +*foo [bar* baz] +. +

foo [bar baz]

+```````````````````````````````` + + +These cases illustrate the precedence of HTML tags, code spans, +and autolinks over link grouping: + +```````````````````````````````` example +[foo +. +

[foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo`](/uri)` +. +

[foo](/uri)

+```````````````````````````````` + + +```````````````````````````````` example +[foo +. +

[foohttp://example.com/?search=](uri)

+```````````````````````````````` + + +There are three kinds of [reference link](@)s: +[full](#full-reference-link), [collapsed](#collapsed-reference-link), +and [shortcut](#shortcut-reference-link). + +A [full reference link](@) +consists of a [link text] immediately followed by a [link label] +that [matches] a [link reference definition] elsewhere in the document. + +A [link label](@) begins with a left bracket (`[`) and ends +with the first right bracket (`]`) that is not backslash-escaped. +Between these brackets there must be at least one character that is not a space, +tab, or line ending. +Unescaped square bracket characters are not allowed inside the +opening and closing square brackets of [link labels]. A link +label can have at most 999 characters inside the square +brackets. + +One label [matches](@) +another just in case their normalized forms are equal. To normalize a +label, strip off the opening and closing brackets, +perform the *Unicode case fold*, strip leading and trailing +spaces, tabs, and line endings, and collapse consecutive internal +spaces, tabs, and line endings to a single space. If there are multiple +matching reference link definitions, the one that comes first in the +document is used. (It is desirable in such cases to emit a warning.) + +The link's URI and title are provided by the matching [link +reference definition]. + +Here is a simple example: + +```````````````````````````````` example +[foo][bar] + +[bar]: /url "title" +. +

foo

+```````````````````````````````` + + +The rules for the [link text] are the same as with +[inline links]. Thus: + +The link text may contain balanced brackets, but not unbalanced ones, +unless they are escaped: + +```````````````````````````````` example +[link [foo [bar]]][ref] + +[ref]: /uri +. +

link [foo [bar]]

+```````````````````````````````` + + +```````````````````````````````` example +[link \[bar][ref] + +[ref]: /uri +. +

link [bar

+```````````````````````````````` + + +The link text may contain inline content: + +```````````````````````````````` example +[link *foo **bar** `#`*][ref] + +[ref]: /uri +. +

link foo bar #

+```````````````````````````````` + + +```````````````````````````````` example +[![moon](moon.jpg)][ref] + +[ref]: /uri +. +

moon

+```````````````````````````````` + + +However, links may not contain other links, at any level of nesting. + +```````````````````````````````` example +[foo [bar](/uri)][ref] + +[ref]: /uri +. +

[foo bar]ref

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar [baz][ref]*][ref] + +[ref]: /uri +. +

[foo bar baz]ref

+```````````````````````````````` + + +(In the examples above, we have two [shortcut reference links] +instead of one [full reference link].) + +The following cases illustrate the precedence of link text grouping over +emphasis grouping: + +```````````````````````````````` example +*[foo*][ref] + +[ref]: /uri +. +

*foo*

+```````````````````````````````` + + +```````````````````````````````` example +[foo *bar][ref]* + +[ref]: /uri +. +

foo *bar*

+```````````````````````````````` + + +These cases illustrate the precedence of HTML tags, code spans, +and autolinks over link grouping: + +```````````````````````````````` example +[foo + +[ref]: /uri +. +

[foo

+```````````````````````````````` + + +```````````````````````````````` example +[foo`][ref]` + +[ref]: /uri +. +

[foo][ref]

+```````````````````````````````` + + +```````````````````````````````` example +[foo + +[ref]: /uri +. +

[foohttp://example.com/?search=][ref]

+```````````````````````````````` + + +Matching is case-insensitive: + +```````````````````````````````` example +[foo][BaR] + +[bar]: /url "title" +. +

foo

+```````````````````````````````` + + +Unicode case fold is used: + +```````````````````````````````` example +[ẞ] + +[SS]: /url +. +

ẞ

+```````````````````````````````` + + +Consecutive internal spaces, tabs, and line endings are treated as one space for +purposes of determining matching: + +```````````````````````````````` example +[Foo + bar]: /url + +[Baz][Foo bar] +. +

Baz

+```````````````````````````````` + + +No spaces, tabs, or line endings are allowed between the [link text] and the +[link label]: + +```````````````````````````````` example +[foo] [bar] + +[bar]: /url "title" +. +

[foo] bar

+```````````````````````````````` + + +```````````````````````````````` example +[foo] +[bar] + +[bar]: /url "title" +. +

[foo] +bar

+```````````````````````````````` + + +This is a departure from John Gruber's original Markdown syntax +description, which explicitly allows whitespace between the link +text and the link label. It brings reference links in line with +[inline links], which (according to both original Markdown and +this spec) cannot have whitespace after the link text. More +importantly, it prevents inadvertent capture of consecutive +[shortcut reference links]. If whitespace is allowed between the +link text and the link label, then in the following we will have +a single reference link, not two shortcut reference links, as +intended: + +``` markdown +[foo] +[bar] + +[foo]: /url1 +[bar]: /url2 +``` + +(Note that [shortcut reference links] were introduced by Gruber +himself in a beta version of `Markdown.pl`, but never included +in the official syntax description. Without shortcut reference +links, it is harmless to allow space between the link text and +link label; but once shortcut references are introduced, it is +too dangerous to allow this, as it frequently leads to +unintended results.) + +When there are multiple matching [link reference definitions], +the first is used: + +```````````````````````````````` example +[foo]: /url1 + +[foo]: /url2 + +[bar][foo] +. +

bar

+```````````````````````````````` + + +Note that matching is performed on normalized strings, not parsed +inline content. So the following does not match, even though the +labels define equivalent inline content: + +```````````````````````````````` example +[bar][foo\!] + +[foo!]: /url +. +

[bar][foo!]

+```````````````````````````````` + + +[Link labels] cannot contain brackets, unless they are +backslash-escaped: + +```````````````````````````````` example +[foo][ref[] + +[ref[]: /uri +. +

[foo][ref[]

+

[ref[]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[foo][ref[bar]] + +[ref[bar]]: /uri +. +

[foo][ref[bar]]

+

[ref[bar]]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[[[foo]]] + +[[[foo]]]: /url +. +

[[[foo]]]

+

[[[foo]]]: /url

+```````````````````````````````` + + +```````````````````````````````` example +[foo][ref\[] + +[ref\[]: /uri +. +

foo

+```````````````````````````````` + + +Note that in this example `]` is not backslash-escaped: + +```````````````````````````````` example +[bar\\]: /uri + +[bar\\] +. +

bar\

+```````````````````````````````` + + +A [link label] must contain at least one character that is not a space, tab, or +line ending: + +```````````````````````````````` example +[] + +[]: /uri +. +

[]

+

[]: /uri

+```````````````````````````````` + + +```````````````````````````````` example +[ + ] + +[ + ]: /uri +. +

[ +]

+

[ +]: /uri

+```````````````````````````````` + + +A [collapsed reference link](@) +consists of a [link label] that [matches] a +[link reference definition] elsewhere in the +document, followed by the string `[]`. +The contents of the first link label are parsed as inlines, +which are used as the link's text. The link's URI and title are +provided by the matching reference link definition. Thus, +`[foo][]` is equivalent to `[foo][foo]`. + +```````````````````````````````` example +[foo][] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +[Foo][] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + + +As with full reference links, spaces, tabs, or line endings are not +allowed between the two sets of brackets: + +```````````````````````````````` example +[foo] +[] + +[foo]: /url "title" +. +

foo +[]

+```````````````````````````````` + + +A [shortcut reference link](@) +consists of a [link label] that [matches] a +[link reference definition] elsewhere in the +document and is not followed by `[]` or a link label. +The contents of the first link label are parsed as inlines, +which are used as the link's text. The link's URI and title +are provided by the matching link reference definition. +Thus, `[foo]` is equivalent to `[foo][]`. + +```````````````````````````````` example +[foo] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +[*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +[[*foo* bar]] + +[*foo* bar]: /url "title" +. +

[foo bar]

+```````````````````````````````` + + +```````````````````````````````` example +[[bar [foo] + +[foo]: /url +. +

[[bar foo

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +[Foo] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +A space after the link text should be preserved: + +```````````````````````````````` example +[foo] bar + +[foo]: /url +. +

foo bar

+```````````````````````````````` + + +If you just want bracketed text, you can backslash-escape the +opening bracket to avoid links: + +```````````````````````````````` example +\[foo] + +[foo]: /url "title" +. +

[foo]

+```````````````````````````````` + + +Note that this is a link, because a link label ends with the first +following closing bracket: + +```````````````````````````````` example +[foo*]: /url + +*[foo*] +. +

*foo*

+```````````````````````````````` + + +Full and compact references take precedence over shortcut +references: + +```````````````````````````````` example +[foo][bar] + +[foo]: /url1 +[bar]: /url2 +. +

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo][] + +[foo]: /url1 +. +

foo

+```````````````````````````````` + +Inline links also take precedence: + +```````````````````````````````` example +[foo]() + +[foo]: /url1 +. +

foo

+```````````````````````````````` + +```````````````````````````````` example +[foo](not a link) + +[foo]: /url1 +. +

foo(not a link)

+```````````````````````````````` + +In the following case `[bar][baz]` is parsed as a reference, +`[foo]` as normal text: + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url +. +

[foo]bar

+```````````````````````````````` + + +Here, though, `[foo][bar]` is parsed as a reference, since +`[bar]` is defined: + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url1 +[bar]: /url2 +. +

foobaz

+```````````````````````````````` + + +Here `[foo]` is not parsed as a shortcut reference, because it +is followed by a link label (even though `[bar]` is not defined): + +```````````````````````````````` example +[foo][bar][baz] + +[baz]: /url1 +[foo]: /url2 +. +

[foo]bar

+```````````````````````````````` + + + +## Images + +Syntax for images is like the syntax for links, with one +difference. Instead of [link text], we have an +[image description](@). The rules for this are the +same as for [link text], except that (a) an +image description starts with `![` rather than `[`, and +(b) an image description may contain links. +An image description has inline elements +as its contents. When an image is rendered to HTML, +this is standardly used as the image's `alt` attribute. + +```````````````````````````````` example +![foo](/url "title") +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![foo *bar*] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo ![bar](/url)](/url2) +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo [bar](/url)](/url2) +. +

foo bar

+```````````````````````````````` + + +Though this spec is concerned with parsing, not rendering, it is +recommended that in rendering to HTML, only the plain string content +of the [image description] be used. Note that in +the above example, the alt attribute's value is `foo bar`, not `foo +[bar](/url)` or `foo bar`. Only the plain string +content is rendered, without formatting. + +```````````````````````````````` example +![foo *bar*][] + +[foo *bar*]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo *bar*][foobar] + +[FOOBAR]: train.jpg "train & tracks" +. +

foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo](train.jpg) +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +My ![foo bar](/path/to/train.jpg "title" ) +. +

My foo bar

+```````````````````````````````` + + +```````````````````````````````` example +![foo]() +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![](/url) +. +

+```````````````````````````````` + + +Reference-style: + +```````````````````````````````` example +![foo][bar] + +[bar]: /url +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![foo][bar] + +[BAR]: /url +. +

foo

+```````````````````````````````` + + +Collapsed: + +```````````````````````````````` example +![foo][] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![*foo* bar][] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +The labels are case-insensitive: + +```````````````````````````````` example +![Foo][] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +As with reference links, spaces, tabs, and line endings, are not allowed +between the two sets of brackets: + +```````````````````````````````` example +![foo] +[] + +[foo]: /url "title" +. +

foo +[]

+```````````````````````````````` + + +Shortcut: + +```````````````````````````````` example +![foo] + +[foo]: /url "title" +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +![*foo* bar] + +[*foo* bar]: /url "title" +. +

foo bar

+```````````````````````````````` + + +Note that link labels cannot contain unescaped brackets: + +```````````````````````````````` example +![[foo]] + +[[foo]]: /url "title" +. +

![[foo]]

+

[[foo]]: /url "title"

+```````````````````````````````` + + +The link labels are case-insensitive: + +```````````````````````````````` example +![Foo] + +[foo]: /url "title" +. +

Foo

+```````````````````````````````` + + +If you just want a literal `!` followed by bracketed text, you can +backslash-escape the opening `[`: + +```````````````````````````````` example +!\[foo] + +[foo]: /url "title" +. +

![foo]

+```````````````````````````````` + + +If you want a link after a literal `!`, backslash-escape the +`!`: + +```````````````````````````````` example +\![foo] + +[foo]: /url "title" +. +

!foo

+```````````````````````````````` + + +## Autolinks + +[Autolink](@)s are absolute URIs and email addresses inside +`<` and `>`. They are parsed as links, with the URL or email address +as the link label. + +A [URI autolink](@) consists of `<`, followed by an +[absolute URI] followed by `>`. It is parsed as +a link to the URI, with the URI as the link's label. + +An [absolute URI](@), +for these purposes, consists of a [scheme] followed by a colon (`:`) +followed by zero or more characters other [ASCII control +characters][ASCII control character], [space], `<`, and `>`. +If the URI includes these characters, they must be percent-encoded +(e.g. `%20` for a space). + +For purposes of this spec, a [scheme](@) is any sequence +of 2--32 characters beginning with an ASCII letter and followed +by any combination of ASCII letters, digits, or the symbols plus +("+"), period ("."), or hyphen ("-"). + +Here are some valid autolinks: + +```````````````````````````````` example + +. +

http://foo.bar.baz

+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://foo.bar.baz/test?q=hello&id=22&boolean

+```````````````````````````````` + + +```````````````````````````````` example + +. +

irc://foo.bar:2233/baz

+```````````````````````````````` + + +Uppercase is also fine: + +```````````````````````````````` example + +. +

MAILTO:FOO@BAR.BAZ

+```````````````````````````````` + + +Note that many strings that count as [absolute URIs] for +purposes of this spec are not valid URIs, because their +schemes are not registered or because of other problems +with their syntax: + +```````````````````````````````` example + +. +

a+b+c:d

+```````````````````````````````` + + +```````````````````````````````` example + +. +

made-up-scheme://foo,bar

+```````````````````````````````` + + +```````````````````````````````` example + +. +

http://../

+```````````````````````````````` + + +```````````````````````````````` example + +. +

localhost:5001/foo

+```````````````````````````````` + + +Spaces are not allowed in autolinks: + +```````````````````````````````` example + +. +

<http://foo.bar/baz bim>

+```````````````````````````````` + + +Backslash-escapes do not work inside autolinks: + +```````````````````````````````` example + +. +

http://example.com/\[\

+```````````````````````````````` + + +An [email autolink](@) +consists of `<`, followed by an [email address], +followed by `>`. The link's label is the email address, +and the URL is `mailto:` followed by the email address. + +An [email address](@), +for these purposes, is anything that matches +the [non-normative regex from the HTML5 +spec](https://html.spec.whatwg.org/multipage/forms.html#e-mail-state-(type=email)): + + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])? + (?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + +Examples of email autolinks: + +```````````````````````````````` example + +. +

foo@bar.example.com

+```````````````````````````````` + + +```````````````````````````````` example + +. +

foo+special@Bar.baz-bar0.com

+```````````````````````````````` + + +Backslash-escapes do not work inside email autolinks: + +```````````````````````````````` example + +. +

<foo+@bar.example.com>

+```````````````````````````````` + + +These are not autolinks: + +```````````````````````````````` example +<> +. +

<>

+```````````````````````````````` + + +```````````````````````````````` example +< http://foo.bar > +. +

< http://foo.bar >

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<m:abc>

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<foo.bar.baz>

+```````````````````````````````` + + +```````````````````````````````` example +http://example.com +. +

http://example.com

+```````````````````````````````` + + +```````````````````````````````` example +foo@bar.example.com +. +

foo@bar.example.com

+```````````````````````````````` + + +## Raw HTML + +Text between `<` and `>` that looks like an HTML tag is parsed as a +raw HTML tag and will be rendered in HTML without escaping. +Tag and attribute names are not limited to current HTML tags, +so custom tags (and even, say, DocBook tags) may be used. + +Here is the grammar for tags: + +A [tag name](@) consists of an ASCII letter +followed by zero or more ASCII letters, digits, or +hyphens (`-`). + +An [attribute](@) consists of spaces, tabs, and up to one line ending, +an [attribute name], and an optional +[attribute value specification]. + +An [attribute name](@) +consists of an ASCII letter, `_`, or `:`, followed by zero or more ASCII +letters, digits, `_`, `.`, `:`, or `-`. (Note: This is the XML +specification restricted to ASCII. HTML5 is laxer.) + +An [attribute value specification](@) +consists of optional spaces, tabs, and up to one line ending, +a `=` character, optional spaces, tabs, and up to one line ending, +and an [attribute value]. + +An [attribute value](@) +consists of an [unquoted attribute value], +a [single-quoted attribute value], or a [double-quoted attribute value]. + +An [unquoted attribute value](@) +is a nonempty string of characters not +including spaces, tabs, line endings, `"`, `'`, `=`, `<`, `>`, or `` ` ``. + +A [single-quoted attribute value](@) +consists of `'`, zero or more +characters not including `'`, and a final `'`. + +A [double-quoted attribute value](@) +consists of `"`, zero or more +characters not including `"`, and a final `"`. + +An [open tag](@) consists of a `<` character, a [tag name], +zero or more [attributes], optional spaces, tabs, and up to one line ending, +an optional `/` character, and a `>` character. + +A [closing tag](@) consists of the string ``. + +An [HTML comment](@) consists of ``, +where *text* does not start with `>` or `->`, does not end with `-`, +and does not contain `--`. (See the +[HTML5 spec](http://www.w3.org/TR/html5/syntax.html#comments).) + +A [processing instruction](@) +consists of the string ``, and the string +`?>`. + +A [declaration](@) consists of the string ``, and the character `>`. + +A [CDATA section](@) consists of +the string ``, and the string `]]>`. + +An [HTML tag](@) consists of an [open tag], a [closing tag], +an [HTML comment], a [processing instruction], a [declaration], +or a [CDATA section]. + +Here are some simple open tags: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Empty elements: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Whitespace is allowed: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +With attributes: + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Custom tag names can be used: + +```````````````````````````````` example +Foo +. +

Foo

+```````````````````````````````` + + +Illegal tag names, not parsed as HTML: + +```````````````````````````````` example +<33> <__> +. +

<33> <__>

+```````````````````````````````` + + +Illegal attribute names: + +```````````````````````````````` example +
+. +

<a h*#ref="hi">

+```````````````````````````````` + + +Illegal attribute values: + +```````````````````````````````` example +
+. +

</a href="foo">

+```````````````````````````````` + + +Comments: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +foo +. +

foo <!-- not a comment -- two hyphens -->

+```````````````````````````````` + + +Not comments: + +```````````````````````````````` example +foo foo --> + +foo +. +

foo <!--> foo -->

+

foo <!-- foo--->

+```````````````````````````````` + + +Processing instructions: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +Declarations: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +CDATA sections: + +```````````````````````````````` example +foo &<]]> +. +

foo &<]]>

+```````````````````````````````` + + +Entity and numeric character references are preserved in HTML +attributes: + +```````````````````````````````` example +foo
+. +

foo

+```````````````````````````````` + + +Backslash escapes do not work in HTML attributes: + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example + +. +

<a href=""">

+```````````````````````````````` + + +## Hard line breaks + +A line ending (not in a code span or HTML tag) that is preceded +by two or more spaces and does not occur at the end of a block +is parsed as a [hard line break](@) (rendered +in HTML as a `
` tag): + +```````````````````````````````` example +foo +baz +. +

foo
+baz

+```````````````````````````````` + + +For a more visible alternative, a backslash before the +[line ending] may be used instead of two or more spaces: + +```````````````````````````````` example +foo\ +baz +. +

foo
+baz

+```````````````````````````````` + + +More than two spaces can be used: + +```````````````````````````````` example +foo +baz +. +

foo
+baz

+```````````````````````````````` + + +Leading spaces at the beginning of the next line are ignored: + +```````````````````````````````` example +foo + bar +. +

foo
+bar

+```````````````````````````````` + + +```````````````````````````````` example +foo\ + bar +. +

foo
+bar

+```````````````````````````````` + + +Hard line breaks can occur inside emphasis, links, and other constructs +that allow inline content: + +```````````````````````````````` example +*foo +bar* +. +

foo
+bar

+```````````````````````````````` + + +```````````````````````````````` example +*foo\ +bar* +. +

foo
+bar

+```````````````````````````````` + + +Hard line breaks do not occur inside code spans + +```````````````````````````````` example +`code +span` +. +

code span

+```````````````````````````````` + + +```````````````````````````````` example +`code\ +span` +. +

code\ span

+```````````````````````````````` + + +or HTML tags: + +```````````````````````````````` example +
+. +

+```````````````````````````````` + + +```````````````````````````````` example + +. +

+```````````````````````````````` + + +Hard line breaks are for separating inline content within a block. +Neither syntax for hard line breaks works at the end of a paragraph or +other block element: + +```````````````````````````````` example +foo\ +. +

foo\

+```````````````````````````````` + + +```````````````````````````````` example +foo +. +

foo

+```````````````````````````````` + + +```````````````````````````````` example +### foo\ +. +

foo\

+```````````````````````````````` + + +```````````````````````````````` example +### foo +. +

foo

+```````````````````````````````` + + +## Soft line breaks + +A regular line ending (not in a code span or HTML tag) that is not +preceded by two or more spaces or a backslash is parsed as a +[softbreak](@). (A soft line break may be rendered in HTML either as a +[line ending] or as a space. The result will be the same in +browsers. In the examples here, a [line ending] will be used.) + +```````````````````````````````` example +foo +baz +. +

foo +baz

+```````````````````````````````` + + +Spaces at the end of the line and beginning of the next line are +removed: + +```````````````````````````````` example +foo + baz +. +

foo +baz

+```````````````````````````````` + + +A conforming parser may render a soft line break in HTML either as a +line ending or as a space. + +A renderer may also provide an option to render soft line breaks +as hard line breaks. + +## Textual content + +Any characters not given an interpretation by the above rules will +be parsed as plain textual content. + +```````````````````````````````` example +hello $.;'there +. +

hello $.;'there

+```````````````````````````````` + + +```````````````````````````````` example +Foo χρῆν +. +

Foo χρῆν

+```````````````````````````````` + + +Internal spaces are preserved verbatim: + +```````````````````````````````` example +Multiple spaces +. +

Multiple spaces

+```````````````````````````````` + + + + +# Appendix: A parsing strategy + +In this appendix we describe some features of the parsing strategy +used in the CommonMark reference implementations. + +## Overview + +Parsing has two phases: + +1. In the first phase, lines of input are consumed and the block +structure of the document---its division into paragraphs, block quotes, +list items, and so on---is constructed. Text is assigned to these +blocks but not parsed. Link reference definitions are parsed and a +map of links is constructed. + +2. In the second phase, the raw text contents of paragraphs and headings +are parsed into sequences of Markdown inline elements (strings, +code spans, links, emphasis, and so on), using the map of link +references constructed in phase 1. + +At each point in processing, the document is represented as a tree of +**blocks**. The root of the tree is a `document` block. The `document` +may have any number of other blocks as **children**. These children +may, in turn, have other blocks as children. The last child of a block +is normally considered **open**, meaning that subsequent lines of input +can alter its contents. (Blocks that are not open are **closed**.) +Here, for example, is a possible document tree, with the open blocks +marked by arrows: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## Phase 1: block structure + +Each line that is processed has an effect on this tree. The line is +analyzed and, depending on its contents, the document may be altered +in one or more of the following ways: + +1. One or more open blocks may be closed. +2. One or more new blocks may be created as children of the + last open block. +3. Text may be added to the last (deepest) open block remaining + on the tree. + +Once a line has been incorporated into the tree in this way, +it can be discarded, so input can be read in a stream. + +For each line, we follow this procedure: + +1. First we iterate through the open blocks, starting with the +root document, and descending through last children down to the last +open block. Each block imposes a condition that the line must satisfy +if the block is to remain open. For example, a block quote requires a +`>` character. A paragraph requires a non-blank line. +In this phase we may match all or just some of the open +blocks. But we cannot close unmatched blocks yet, because we may have a +[lazy continuation line]. + +2. Next, after consuming the continuation markers for existing +blocks, we look for new block starts (e.g. `>` for a block quote). +If we encounter a new block start, we close any blocks unmatched +in step 1 before creating the new block as a child of the last +matched container block. + +3. Finally, we look at the remainder of the line (after block +markers like `>`, list markers, and indentation have been consumed). +This is text that can be incorporated into the last open +block (a paragraph, code block, heading, or raw HTML). + +Setext headings are formed when we see a line of a paragraph +that is a [setext heading underline]. + +Reference link definitions are detected when a paragraph is closed; +the accumulated text lines are parsed to see if they begin with +one or more reference link definitions. Any remainder becomes a +normal paragraph. + +We can see how this works by considering how the tree above is +generated by four lines of Markdown: + +``` markdown +> Lorem ipsum dolor +sit amet. +> - Qui *quodsi iracundia* +> - aliquando id +``` + +At the outset, our document model is just + +``` tree +-> document +``` + +The first line of our text, + +``` markdown +> Lorem ipsum dolor +``` + +causes a `block_quote` block to be created as a child of our +open `document` block, and a `paragraph` block as a child of +the `block_quote`. Then the text is added to the last open +block, the `paragraph`: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor" +``` + +The next line, + +``` markdown +sit amet. +``` + +is a "lazy continuation" of the open `paragraph`, so it gets added +to the paragraph's text: + +``` tree +-> document + -> block_quote + -> paragraph + "Lorem ipsum dolor\nsit amet." +``` + +The third line, + +``` markdown +> - Qui *quodsi iracundia* +``` + +causes the `paragraph` block to be closed, and a new `list` block +opened as a child of the `block_quote`. A `list_item` is also +added as a child of the `list`, and a `paragraph` as a child of +the `list_item`. The text is then added to the new `paragraph`: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + -> list_item + -> paragraph + "Qui *quodsi iracundia*" +``` + +The fourth line, + +``` markdown +> - aliquando id +``` + +causes the `list_item` (and its child the `paragraph`) to be closed, +and a new `list_item` opened up as child of the `list`. A `paragraph` +is added as a child of the new `list_item`, to contain the text. +We thus obtain the final tree: + +``` tree +-> document + -> block_quote + paragraph + "Lorem ipsum dolor\nsit amet." + -> list (type=bullet tight=true bullet_char=-) + list_item + paragraph + "Qui *quodsi iracundia*" + -> list_item + -> paragraph + "aliquando id" +``` + +## Phase 2: inline structure + +Once all of the input has been parsed, all open blocks are closed. + +We then "walk the tree," visiting every node, and parse raw +string contents of paragraphs and headings as inlines. At this +point we have seen all the link reference definitions, so we can +resolve reference links as we go. + +``` tree +document + block_quote + paragraph + str "Lorem ipsum dolor" + softbreak + str "sit amet." + list (type=bullet tight=true bullet_char=-) + list_item + paragraph + str "Qui " + emph + str "quodsi iracundia" + list_item + paragraph + str "aliquando id" +``` + +Notice how the [line ending] in the first paragraph has +been parsed as a `softbreak`, and the asterisks in the first list item +have become an `emph`. + +### An algorithm for parsing nested emphasis and links + +By far the trickiest part of inline parsing is handling emphasis, +strong emphasis, links, and images. This is done using the following +algorithm. + +When we're parsing inlines and we hit either + +- a run of `*` or `_` characters, or +- a `[` or `![` + +we insert a text node with these symbols as its literal content, and we +add a pointer to this text node to the [delimiter stack](@). + +The [delimiter stack] is a doubly linked list. Each +element contains a pointer to a text node, plus information about + +- the type of delimiter (`[`, `![`, `*`, `_`) +- the number of delimiters, +- whether the delimiter is "active" (all are active to start), and +- whether the delimiter is a potential opener, a potential closer, + or both (which depends on what sort of characters precede + and follow the delimiters). + +When we hit a `]` character, we call the *look for link or image* +procedure (see below). + +When we hit the end of the input, we call the *process emphasis* +procedure (see below), with `stack_bottom` = NULL. + +#### *look for link or image* + +Starting at the top of the delimiter stack, we look backwards +through the stack for an opening `[` or `![` delimiter. + +- If we don't find one, we return a literal text node `]`. + +- If we do find one, but it's not *active*, we remove the inactive + delimiter from the stack, and return a literal text node `]`. + +- If we find one and it's active, then we parse ahead to see if + we have an inline link/image, reference link/image, compact reference + link/image, or shortcut reference link/image. + + + If we don't, then we remove the opening delimiter from the + delimiter stack and return a literal text node `]`. + + + If we do, then + + * We return a link or image node whose children are the inlines + after the text node pointed to by the opening delimiter. + + * We run *process emphasis* on these inlines, with the `[` opener + as `stack_bottom`. + + * We remove the opening delimiter. + + * If we have a link (and not an image), we also set all + `[` delimiters before the opening delimiter to *inactive*. (This + will prevent us from getting links within links.) + +#### *process emphasis* + +Parameter `stack_bottom` sets a lower bound to how far we +descend in the [delimiter stack]. If it is NULL, we can +go all the way to the bottom. Otherwise, we stop before +visiting `stack_bottom`. + +Let `current_position` point to the element on the [delimiter stack] +just above `stack_bottom` (or the first element if `stack_bottom` +is NULL). + +We keep track of the `openers_bottom` for each delimiter +type (`*`, `_`), indexed to the length of the closing delimiter run +(modulo 3) and to whether the closing delimiter can also be an +opener. Initialize this to `stack_bottom`. + +Then we repeat the following until we run out of potential +closers: + +- Move `current_position` forward in the delimiter stack (if needed) + until we find the first potential closer with delimiter `*` or `_`. + (This will be the potential closer closest + to the beginning of the input -- the first one in parse order.) + +- Now, look back in the stack (staying above `stack_bottom` and + the `openers_bottom` for this delimiter type) for the + first matching potential opener ("matching" means same delimiter). + +- If one is found: + + + Figure out whether we have emphasis or strong emphasis: + if both closer and opener spans have length >= 2, we have + strong, otherwise regular. + + + Insert an emph or strong emph node accordingly, after + the text node corresponding to the opener. + + + Remove any delimiters between the opener and closer from + the delimiter stack. + + + Remove 1 (for regular emph) or 2 (for strong emph) delimiters + from the opening and closing text nodes. If they become empty + as a result, remove them and remove the corresponding element + of the delimiter stack. If the closing node is removed, reset + `current_position` to the next element in the stack. + +- If none is found: + + + Set `openers_bottom` to the element before `current_position`. + (We know that there are no openers for this kind of closer up to and + including this point, so this puts a lower bound on future searches.) + + + If the closer at `current_position` is not a potential opener, + remove it from the delimiter stack (since we know it can't + be a closer either). + + + Advance `current_position` to the next element in the stack. + +After we're done, we remove all delimiters above `stack_bottom` from the +delimiter stack. diff --git a/crates/markdown-it/tests/fixtures/deno.lock b/crates/markdown-it/tests/fixtures/deno.lock new file mode 100644 index 0000000000000000000000000000000000000000..3c6ebbc57e70cdc3caf7d7ec09fb70460f79d713 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/deno.lock @@ -0,0 +1,72 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "npm:markdown-it-testgen": "npm:markdown-it-testgen@0.1.6" + }, + "npm": { + "argparse@1.0.10": { + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "sprintf-js@1.0.3" + } + }, + "assertion-error@1.0.0": { + "integrity": "sha512-g/gZV+G476cnmtYI+Ko9d5khxSoCSoom/EaNmmCfwpOvBXEJ18qwFrxfP1/CsIqk2no1sAKKwxndV0tP7ROOFQ==", + "dependencies": {} + }, + "chai@1.10.0": { + "integrity": "sha512-E3L9M2SeQU1XagJkE9KJyTAXXHKJkJ1EsKkFp0Rl53lYa3mro2PVgYHNiCb2YRa2nUeyg7aqmI1EIcSBayNd5w==", + "dependencies": { + "assertion-error": "assertion-error@1.0.0", + "deep-eql": "deep-eql@0.1.3" + } + }, + "deep-eql@0.1.3": { + "integrity": "sha512-6sEotTRGBFiNcqVoeHwnfopbSpi5NbH1VWJmYCVkmxMmaVTT0bUTrNaGyBwhgP4MZL012W/mkzIn3Da+iDYweg==", + "dependencies": { + "type-detect": "type-detect@0.1.1" + } + }, + "esprima@4.0.1": { + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dependencies": {} + }, + "js-yaml@3.14.1": { + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dependencies": { + "argparse": "argparse@1.0.10", + "esprima": "esprima@4.0.1" + } + }, + "markdown-it-testgen@0.1.6": { + "integrity": "sha512-hYC71G4Mcv3Y7fLTsi4PyoHKSs0P4UgkpzmGBtUYoR/TS83lFbfXUMaI71OiMJ9r4p3fbMhHBwdNTLhSDwmt6Q==", + "dependencies": { + "chai": "chai@1.10.0", + "js-yaml": "js-yaml@3.14.1", + "object-assign": "object-assign@4.1.1" + } + }, + "object-assign@4.1.1": { + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dependencies": {} + }, + "sprintf-js@1.0.3": { + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dependencies": {} + }, + "type-detect@0.1.1": { + "integrity": "sha512-5rqszGVwYgBoDkIm2oUtvkfZMQ0vk29iDMU0W2qCa3rG0vPDNczCMT4hV/bLBgLg8k8ri6+u3Zbt+S/14eMzlA==", + "dependencies": {} + } + } + }, + "remote": {}, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:markdown-it-testgen@^0.1.6" + ] + } + } +} diff --git a/crates/markdown-it/tests/fixtures/markdown-it/markdown-it-rs.txt b/crates/markdown-it/tests/fixtures/markdown-it/markdown-it-rs.txt new file mode 100644 index 0000000000000000000000000000000000000000..cd286926b2e9c7da2f41e603a2101d31f37db96c --- /dev/null +++ b/crates/markdown-it/tests/fixtures/markdown-it/markdown-it-rs.txt @@ -0,0 +1,6 @@ +regression test for panic in #40 +. +![hello'"`world](x)\ +. +

hello'"`world\

+. diff --git a/crates/markdown-it/tests/fixtures/markdown-it/smartquotes.txt b/crates/markdown-it/tests/fixtures/markdown-it/smartquotes.txt new file mode 100644 index 0000000000000000000000000000000000000000..afab445bdbfa65ba20514afa25fa249830a1267a --- /dev/null +++ b/crates/markdown-it/tests/fixtures/markdown-it/smartquotes.txt @@ -0,0 +1,192 @@ +Should parse nested quotes: +. +"foo 'bar' baz" + +'foo 'bar' baz' +. +

“foo ‘bar’ baz”

+

‘foo ‘bar’ baz’

+. + + +Should not overlap quotes: +. +'foo "bar' baz" +. +

‘foo "bar’ baz"

+. + + +Should match quotes on the same level: +. +"foo *bar* baz" +. +

“foo bar baz”

+. + + +Should handle adjacent nested quotes: +. +'"double in single"' + +"'single in double'" +. +

‘“double in single”’

+

“‘single in double’”

+. + + + +Should not match quotes on different levels: +. +*"foo* bar" + +"foo *bar"* + +*"foo* bar *baz"* +. +

"foo bar"

+

"foo bar"

+

"foo bar baz"

+. + +Smartquotes should not overlap with other tags: +. +*foo "bar* *baz" quux* +. +

foo "bar baz" quux

+. + + +Should try and find matching quote in this case: +. +"foo "bar 'baz" +. +

"foo “bar 'baz”

+. + + +Should not touch 'inches' in quotes: +. +"Monitor 21"" and "Monitor"" +. +

“Monitor 21"” and “Monitor”"

+. + + +Should render an apostrophe as a rsquo: +. +This isn't and can't be the best approach to implement this... +. +

This isn’t and can’t be the best approach to implement this…

+. + + +Apostrophe could end the word, that's why original smartypants replaces all of them as rsquo: +. +users' stuff +. +

users’ stuff

+. + +Quotes between punctuation chars: + +. +"(hai)". +. +

“(hai)”.

+. + +Quotes at the start/end of the tokens: +. +"*foo* bar" + +"foo *bar*" + +"*foo bar*" +. +

“foo bar”

+

“foo bar”

+

“foo bar”

+. + +Should treat softbreak as a space: +. +"this" +and "that". + +"this" and +"that". +. +

“this” +and “that”.

+

“this” and +“that”.

+. + +Should treat hardbreak as a space: +. +"this"\ +and "that". + +"this" and\ +"that". +. +

“this”
+and “that”.

+

“this” and
+“that”.

+. + +Should allow quotes adjacent to other punctuation characters, #643: +. +The dog---"'man's' best friend" +. +

The dog—“‘man’s’ best friend”

+. + +Should parse quotes adjacent to code block, #677: +. +"test `code`" + +"`code` test" +. +

“test code”

+

“code test”

+. + +Should parse quotes adjacent to inline html, #677: +. +"test
" + +"
test" +. +

“test
”

+

“
test”

+. + +Should be escapable: +. +"foo" + +\"foo" + +"foo\" +. +

“foo”

+

"foo"

+

"foo"

+. + +Should not replace entities: +. +"foo" + +"foo" + +"foo" +. +

"foo"

+

"foo"

+

"foo"

+. diff --git a/crates/markdown-it/tests/fixtures/markdown-it/tables.txt b/crates/markdown-it/tests/fixtures/markdown-it/tables.txt new file mode 100644 index 0000000000000000000000000000000000000000..4737df7d65ff72649ecc8e82e7118b57418af923 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/markdown-it/tables.txt @@ -0,0 +1,808 @@ +Simple: +. +| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| Cell 3 | Cell 4 +. + + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3Cell 4
+. + + +Column alignment: +. +| Header 1 | Header 2 | Header 3 | Header 4 | +| :------: | -------: | :------- | -------- | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | +| Cell 5 | Cell 6 | Cell 7 | Cell 8 | +. + + + + + + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
+. + + +Nested emphases: +. +Header 1|Header 2|Header 3|Header 4 +:-------|:------:|-------:|-------- +Cell 1 |Cell 2 |Cell 3 |Cell 4 +*Cell 5*|Cell 6 |Cell 7 |Cell 8 +. + + + + + + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
+. + + +Nested tables inside blockquotes: +. +> foo|foo +> ---|--- +> bar|bar +baz|baz +. +
+ + + + + + + + + + + + + +
foofoo
barbar
+
+

baz|baz

+. + + +Minimal one-column: +. +| foo +|---- +| test2 +. + + + + + + + + + + + +
foo
test2
+. + + +This is parsed as one big table: +. +- foo|foo +---|--- +bar|bar +. + + + + + + + + + + + + + +
- foofoo
barbar
+. + + +Second line should not contain symbols except "-", ":", "|" and " ": +. +foo|foo +-----|-----s +bar|bar +. +

foo|foo +-----|-----s +bar|bar

+. + + +Second line should contain "|" symbol: +. +foo|foo +-----:----- +bar|bar +. +

foo|foo +-----:----- +bar|bar

+. + + +Second line should not have empty columns in the middle: +. +foo|foo +-----||----- +bar|bar +. +

foo|foo +-----||----- +bar|bar

+. + + +Wrong alignment symbol position: +. +foo|foo +-----|-::- +bar|bar +. +

foo|foo +-----|-::- +bar|bar

+. + + +Title line should contain "|" symbol: +. +foo +-----|----- +bar|bar +. +

foo +-----|----- +bar|bar

+. + + +Allow tabs as a separator on 2nd line +. +| foo | bar | +| --- | --- | +| baz | quux | +. + + + + + + + + + + + + + +
foobar
bazquux
+. + + +Should terminate paragraph: +. +paragraph +foo|foo +---|--- +bar|bar +. +

paragraph

+ + + + + + + + + + + + + +
foofoo
barbar
+. + + +Another complicated backticks case +. +| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| \\\`|\\\` +. + + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
\`\`
+. + +`\` in tables should not count as escaped backtick +. +# | 1 | 2 +--|--|-- +x | `\` | `x` +. + + + + + + + + + + + + + + + +
#12
x\x
+. + +Tables should handle escaped backticks +. +# | 1 | 2 +--|--|-- +x | \`\` | `x` +. + + + + + + + + + + + + + + + +
#12
x``x
+. + + +An amount of rows might be different across the table (issue #171): +. +| 1 | 2 | +| :-----: | :-----: | +| 3 | 4 | 5 | 6 | +. + + + + + + + + + + + + + +
12
34
+. + + +An amount of rows might be different across the table #2: +. +| 1 | 2 | 3 | 4 | +| :-----: | :-----: | :-----: | :-----: | +| 5 | 6 | +. + + + + + + + + + + + + + + + + + +
1234
56
+. + + +Allow one-column tables (issue #171): +. +| foo | +:-----: +| bar | +. + + + + + + + + + + + +
foo
bar
+. + + +Allow indented tables (issue #325): +. + | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b | +. + + + + + + + + + + + + + +
Col1aCol2a
Col1bCol2b
+. + + +Tables should not be indented more than 4 spaces (1st line): +. + | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b | +. +
| Col1a | Col2a |
+
+

| ----- | ----- | +| Col1b | Col2b |

+. + + +Tables should not be indented more than 4 spaces (2nd line): +. + | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b | +. +

| Col1a | Col2a | +| ----- | ----- | +| Col1b | Col2b |

+. + + +Tables should not be indented more than 4 spaces (3rd line): +. + | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b | +. + + + + + + + +
Col1aCol2a
+
| Col1b | Col2b |
+
+. + + +Allow tables with empty body: +. + | Col1a | Col2a | + | ----- | ----- | +. + + + + + + + +
Col1aCol2a
+. + + +Align row should be at least as large as any actual rows: +. +Col1a | Col1b | Col1c +----- | ----- +Col2a | Col2b | Col2c +. +

Col1a | Col1b | Col1c +----- | ----- +Col2a | Col2b | Col2c

+. + +Escaped pipes inside backticks don't split cells: +. +| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| `Cell 3\|` | Cell 4 +. + + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3|Cell 4
+. + +Escape before escaped Pipes inside backticks don't split cells: +. +| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| `Cell 3\\|` | Cell 4 +. + + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3\|Cell 4
+. + +Regression test for #721, table in a list indented with tabs: +. +- Level 1 + + - Level 2 + + | Column 1 | Column 2 | + | -------- | -------- | + | abcdefgh | ijklmnop | +. +
    +
  • +

    Level 1

    +
      +
    • +

      Level 2

      + + + + + + + + + + + + + +
      Column 1Column 2
      abcdefghijklmnop
      +
    • +
    +
  • +
+. + + +Table without any columns is not a table, #724 +. +| +| +| +. +

| +| +|

+. + + +GFM 4.10 Tables (extension), Example 198 +. +| foo | bar | +| --- | --- | +| baz | bim | +. + + + + + + + + + + + + + +
foobar
bazbim
+. + +GFM 4.10 Tables (extension), Example 199 +. +| abc | defghi | +:-: | -----------: +bar | baz +. + + + + + + + + + + + + + +
abcdefghi
barbaz
+. + +GFM 4.10 Tables (extension), Example 200 +. +| f\|oo | +| ------ | +| b `\|` az | +| b **\|** im | +. + + + + + + + + + + + + + + +
f|oo
b | az
b | im
+. + +GFM 4.10 Tables (extension), Example 201 +. +| abc | def | +| --- | --- | +| bar | baz | +> bar +. + + + + + + + + + + + + + +
abcdef
barbaz
+
+

bar

+
+. + +GFM 4.10 Tables (extension), Example 202 +. +| abc | def | +| --- | --- | +| bar | baz | +bar + +bar +. + + + + + + + + + + + + + + + + + +
abcdef
barbaz
bar
+

bar

+. + +GFM 4.10 Tables (extension), Example 203 +. +| abc | def | +| --- | +| bar | +. +

| abc | def | +| — | +| bar |

+. + +GFM 4.10 Tables (extension), Example 204 +. +| abc | def | +| --- | --- | +| bar | +| bar | baz | boo | +. + + + + + + + + + + + + + + + + + +
abcdef
bar
barbaz
+. + +GFM 4.10 Tables (extension), Example 205 +. +| abc | def | +| --- | --- | +. + + + + + + + +
abcdef
+. + +A list takes precedence in case of ambiguity +. +a | b +- | - +1 | 2 +. +

a | b

+
    +
  • | - +1 | 2
  • +
+. diff --git a/crates/markdown-it/tests/fixtures/markdown-it/typographer-extra.txt b/crates/markdown-it/tests/fixtures/markdown-it/typographer-extra.txt new file mode 100644 index 0000000000000000000000000000000000000000..29e12e7cde46e338f8fe18a2674ce43a807a49bb --- /dev/null +++ b/crates/markdown-it/tests/fixtures/markdown-it/typographer-extra.txt @@ -0,0 +1,14 @@ +don't touch text in autolinks +. +URL with (C) (c) (R) (r) (TM) (tm): https://example.com/(c)(r)(tm)/(C)(R)(TM) what do you think? +. +

URL with © © ® ® ™ ™: https://example.com/(c)(r)(tm)/(C)(R)(TM) what do you think?

+. + + +replacements for TM should allow mixed case tM and Tm +. +These two should both end up the same as (TM) and (tm): (tM), (Tm). +. +

These two should both end up the same as ™ and ™: ™, ™.

+. diff --git a/crates/markdown-it/tests/fixtures/markdown-it/typographer.txt b/crates/markdown-it/tests/fixtures/markdown-it/typographer.txt new file mode 100644 index 0000000000000000000000000000000000000000..9e4bd7665539b5ebde5da39487f764a89d07958c --- /dev/null +++ b/crates/markdown-it/tests/fixtures/markdown-it/typographer.txt @@ -0,0 +1,110 @@ +. +(bad) +. +

(bad)

+. + + +copyright +. +(c) (C) +. +

Š Š

+. + + +reserved +. +(r) (R) +. +

ÂŽ ÂŽ

+. + + +trademark +. +(tm) (TM) +. +

™ ™

+. + + +plus-minus +. ++-5 +. +

Âą5

+. + + +ellipsis +. +test.. test... test..... test?..... test!.... +. +

test… test… test… test?.. test!..

+. + + +dupes +. +!!!!!! ???? ,, +. +

!!! ??? ,

+. + +copyright should be escapable +. +\(c) +. +

(c)

+. + +shouldn't replace entities +. +(c) (c) (c) +. +

(c) (c) Š

+. + + +dashes +. +---markdownit --- super--- + +markdownit---awesome + +abc ---- + +--markdownit -- super-- + +markdownit--awesome +. +

—markdownit — super—

+

markdownit—awesome

+

abc ----

+

–markdownit – super–

+

markdownit–awesome

+. + +dashes should be escapable +. +foo \-- bar + +foo -\- bar +. +

foo -- bar

+

foo -- bar

+. + +regression tests for #624 +. +1---2---3 + +1--2--3 + +1 -- -- 3 +. +

1—2—3

+

1–2–3

+

1 – – 3

+. diff --git a/crates/markdown-it/tests/fixtures/package.json b/crates/markdown-it/tests/fixtures/package.json new file mode 100644 index 0000000000000000000000000000000000000000..ba28121e534c900147332613d306f4ca736613a9 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/package.json @@ -0,0 +1,6 @@ +{ + "type": "module", + "dependencies": { + "markdown-it-testgen": "^0.1.6" + } +} diff --git a/crates/markdown-it/tests/fixtures/testgen.js b/crates/markdown-it/tests/fixtures/testgen.js new file mode 100644 index 0000000000000000000000000000000000000000..9278a271f3f8f5a87abcd39527594520d5e058f1 --- /dev/null +++ b/crates/markdown-it/tests/fixtures/testgen.js @@ -0,0 +1,99 @@ +// use `deno run --allow-read --allow-write ./fixtures/testgen.js ./commonmark.rs` to run this script + +if (Deno.args.length !== 1) { + console.error(` +Usage: deno run --allow-read --allow-write ./fixtures/testgen.js ./commonmark.rs +`) + Deno.exit(1) +} + +import testgen from 'npm:markdown-it-testgen@^0.1.6' + +function rust_escape(s) { + if (s.match(/( $|\t)/m)) { + return '"' + s.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/ $/mg, '\\x20').replace(/\t/g, '\\t') + '"' + } + + if (s.match(/#"|"#/)) return 'r##"' + s + '"##' + return 'r#"' + s + '"#' +} + +let identmap = new Set() +function ident(str) { + str = str.toLowerCase() + .replace(/[^a-z0-9]+/g, '_') + .replace(/^_+|_+$/g, '') + + if (!str) str = 'unnamed' + + let result = str + let idx = 0 + while (identmap.has(result)) { + result = str + '_' + (++idx) + } + + identmap.add(result) + return result +} + +function generate(fixture) { + return ` +#[test] +fn ${ident(fixture.header)}() { + let input = ${rust_escape(fixture.first.text.replace(/\n$/, ''))}; + let output = ${rust_escape(fixture.second.text.replace(/\n$/, ''))}; + run(input, output); +} +`.trim() +} + +let input_raw = await Deno.readFile(Deno.args[0]) +let input = new TextDecoder().decode(input_raw) +let lines = [] +let state = 'passthrough' + +for (let line of input.split('\n')) { + switch (state) { + case 'passthrough': + lines.push(line) + if (line.match(/^\/{4,}$/)) { + state = 'maybeheader' + } + break + case 'skipping': + if (line.match(/^\/{4,}$/)) { + lines.push(line) + state = 'maybeheader' + } + break + case 'maybeheader': + lines.push(line) + let match = line.match(/^\/{2,}\s+TESTGEN:\s*(.+)\s*$/) + if (match) { + let has_data = false + lines.push('#[rustfmt::skip]') + lines.push(`mod ${ident(match[1])} {`) + lines.push('use super::run;') + lines.push('// this part of the file is auto-generated') + lines.push('// don\'t edit it, otherwise your changes might be lost') + testgen.load(match[1], data => { + data.fixtures.forEach((data, idx) => { + has_data = true + if (idx++ > 0) lines.push('') + let generated = generate(data).replace(/\r?\n$/, '') + lines = lines.concat(generated.split('\n')) + }) + }) + lines.push('// end of auto-generated module') + lines.push('}') + if (!has_data) throw new Error(`no data found for ${match[1]}`) + state = 'skipping' + } + break + default: + throw Error('unknown state') + } +} + +await Deno.rename(Deno.args[0], Deno.args[0] + '.old') +await Deno.writeFile(Deno.args[0], new TextEncoder().encode(lines.join('\n') + '\n')) diff --git a/crates/markdown-it/tests/linkify.rs b/crates/markdown-it/tests/linkify.rs new file mode 100644 index 0000000000000000000000000000000000000000..010b4e0ed9075bc6ff2c0a1ef78bcd33b4f90078 --- /dev/null +++ b/crates/markdown-it/tests/linkify.rs @@ -0,0 +1,170 @@ +#![cfg(feature = "linkify")] +fn run(input: &str, output: &str) { + let output = if output.is_empty() { "".to_owned() } else { output.to_owned() + "\n" }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + markdown_it::plugins::extra::linkify::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + + // make sure we have sourcemaps for everything + node.walk(|node, _| assert!(node.srcmap.is_some())); + + let result = node.render(); + assert_eq!(result, output); + + // make sure it doesn't crash without trailing \n + let _ = md.parse(input.trim_end()); +} + +#[test] +fn linkify() { + let input = r#"url http://www.youtube.com/watch?v=5Jt5GEr4AYg."#; + let output = r#"

url http://www.youtube.com/watch?v=5Jt5GEr4AYg.

"#; + run(input, output); +} + +#[test] +fn don_t_touch_text_in_links() { + let input = r#"[https://example.com](https://example.com)"#; + let output = r#"

https://example.com

"#; + run(input, output); +} + +#[test] +fn don_t_touch_text_in_autolinks() { + let input = r#""#; + let output = r#"

https://example.com

"#; + run(input, output); +} + +#[test] +fn don_t_touch_text_in_html_a_tags() { + let input = r#"https://example.com"#; + let output = r#"

https://example.com

"#; + run(input, output); +} + +#[test] +fn entities_inside_raw_links() { + let input = r#"https://example.com/foo&bar"#; + let output = r#"

https://example.com/foo&amp;bar

"#; + run(input, output); +} + +#[test] +fn emphasis_inside_raw_links_asterisk_can_happen_in_links_with_params() { + let input = r#"https://example.com/foo*bar*baz"#; + let output = r#"

https://example.com/foo*bar*baz

"#; + run(input, output); +} + +#[test] +fn emphasis_inside_raw_links_underscore() { + let input = r#"http://example.org/foo._bar_-_baz"#; + let output = r#"

http://example.org/foo._bar_-_baz

"#; + run(input, output); +} + +// not accepted as link by rust linkify +/*#[test] +fn backticks_inside_raw_links() { + let input = r#"https://example.com/foo`bar`baz"#; + let output = r#"

https://example.com/foo`bar`baz

"#; + run(input, output); +}*/ + +#[test] +fn links_inside_raw_links() { + let input = r#"https://example.com/foo[123](456)bar"#; + let output = r#"

https://example.com/foo[123](456)bar

"#; + run(input, output); +} + +#[test] +fn escapes_not_allowed_at_the_start() { + let input = r#"\https://example.com"#; + let output = r#"

\https://example.com

"#; + run(input, output); +} + +#[test] +fn escapes_not_allowed_at_comma() { + let input = r#"https\://example.com"#; + let output = r#"

https://example.com

"#; + run(input, output); +} + +#[test] +fn escapes_not_allowed_at_slashes() { + let input = r#"https:\//aa.org https://bb.org"#; + let output = r#"

https://aa.org https://bb.org

"#; + run(input, output); +} + +#[test] +fn fuzzy_link_shouldn_t_match_cc_org() { + let input = r#"https:/\/cc.org"#; + let output = r#"

https://cc.org

"#; + run(input, output); +} + +#[test] +fn bold_links_exclude_markup_of_pairs_from_link_tail() { + let input = r#"**http://example.com/foobar**"#; + let output = r#"

http://example.com/foobar

"#; + run(input, output); +} + +/*#[test] +fn match_links_without_protocol() { + let input = r#"www.example.org"#; + let output = r#"

www.example.org

"#; + run(input, output); +}*/ + +/*#[test] +fn emails() { + let input = r#"test@example.com + +mailto:test@example.com"#; + let output = r#"

test@example.com

+

mailto:test@example.com

"#; + run(input, output); +}*/ + +#[test] +fn typorgapher_should_not_break_href() { + let input = r#"http://example.com/(c)"#; + let output = r#"

http://example.com/(c)

"#; + run(input, output); +} + +#[test] +fn coverage_prefix_not_valid() { + let input = r#"http:/example.com/"#; + let output = r#"

http:/example.com/

"#; + run(input, output); +} + +#[test] +fn coverage_negative_link_level() { + let input = r#"[https://example.com](https://example.com)"#; + let output = r#"

https://example.com

"#; + run(input, output); +} + +/*#[test] +fn emphasis_with_real_link() { + let input = r#"http://cdecl.ridiculousfish.com/?q=int+%28*f%29+%28float+*%29%3B"#; + let output = r#"

http://cdecl.ridiculousfish.com/?q=int+(*f)+(float+*)%3B

"#; + run(input, output); +}*/ + +#[test] +fn emphasis_with_real_link_1() { + let input = r#"https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf"#; + let output = r#"

https://www.sell.fi/sites/default/files/elainlaakarilehti/tieteelliset_artikkelit/kahkonen_t._et_al.canine_pancreatitis-_review.pdf

"#; + run(input, output); +} + diff --git a/crates/markdown-it/tests/markdown-it-smartquotes.rs b/crates/markdown-it/tests/markdown-it-smartquotes.rs new file mode 100644 index 0000000000000000000000000000000000000000..76c5d146837ab463a071727151d0f8f820ca80be --- /dev/null +++ b/crates/markdown-it/tests/markdown-it-smartquotes.rs @@ -0,0 +1,214 @@ +fn run(input: &str, output: &str) { + let output = if output.is_empty() { + "".to_owned() + } else { + output.to_owned() + "\n" + }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + markdown_it::plugins::extra::linkify::add(md); + markdown_it::plugins::extra::typographer::add(md); + markdown_it::plugins::extra::smartquotes::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + + // make sure we have sourcemaps for everything + node.walk(|node, _| assert!(node.srcmap.is_some())); + + let result = node.render(); + assert_eq!(result, output); + + // make sure it doesn't crash without trailing \n + let _ = md.parse(input.trim_end()); +} +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/markdown-it/smartquotes.txt +#[rustfmt::skip] +mod fixtures_markdown_it_smartquotes_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn should_parse_nested_quotes() { + let input = r#""foo 'bar' baz" + +'foo 'bar' baz'"#; + let output = r#"

“foo ‘bar’ baz”

+

‘foo ‘bar’ baz’

"#; + run(input, output); +} + +#[test] +fn should_not_overlap_quotes() { + let input = r#"'foo "bar' baz""#; + let output = r#"

‘foo "bar’ baz"

"#; + run(input, output); +} + +#[test] +fn should_match_quotes_on_the_same_level() { + let input = r#""foo *bar* baz""#; + let output = r#"

“foo bar baz”

"#; + run(input, output); +} + +#[test] +fn should_handle_adjacent_nested_quotes() { + let input = r#"'"double in single"' + +"'single in double'""#; + let output = r#"

‘“double in single”’

+

“‘single in double’”

"#; + run(input, output); +} + +#[test] +fn should_not_match_quotes_on_different_levels() { + let input = r#"*"foo* bar" + +"foo *bar"* + +*"foo* bar *baz"*"#; + let output = r#"

"foo bar"

+

"foo bar"

+

"foo bar baz"

"#; + run(input, output); +} + +#[test] +fn smartquotes_should_not_overlap_with_other_tags() { + let input = r#"*foo "bar* *baz" quux*"#; + let output = r#"

foo "bar baz" quux

"#; + run(input, output); +} + +#[test] +fn should_try_and_find_matching_quote_in_this_case() { + let input = r#""foo "bar 'baz""#; + let output = r#"

"foo “bar 'baz”

"#; + run(input, output); +} + +#[test] +fn should_not_touch_inches_in_quotes() { + let input = r#""Monitor 21"" and "Monitor"""#; + let output = r#"

“Monitor 21"” and “Monitor”"

"#; + run(input, output); +} + +#[test] +fn should_render_an_apostrophe_as_a_rsquo() { + let input = r#"This isn't and can't be the best approach to implement this..."#; + let output = r#"

This isn’t and can’t be the best approach to implement this…

"#; + run(input, output); +} + +#[test] +fn apostrophe_could_end_the_word_that_s_why_original_smartypants_replaces_all_of_them_as_rsquo() { + let input = r#"users' stuff"#; + let output = r#"

users’ stuff

"#; + run(input, output); +} + +#[test] +fn quotes_between_punctuation_chars() { + let input = r#""(hai)"."#; + let output = r#"

“(hai)”.

"#; + run(input, output); +} + +#[test] +fn quotes_at_the_start_end_of_the_tokens() { + let input = r#""*foo* bar" + +"foo *bar*" + +"*foo bar*""#; + let output = r#"

“foo bar”

+

“foo bar”

+

“foo bar”

"#; + run(input, output); +} + +#[test] +fn should_treat_softbreak_as_a_space() { + let input = r#""this" +and "that". + +"this" and +"that"."#; + let output = r#"

“this” +and “that”.

+

“this” and +“that”.

"#; + run(input, output); +} + +#[test] +fn should_treat_hardbreak_as_a_space() { + let input = r#""this"\ +and "that". + +"this" and\ +"that"."#; + let output = r#"

“this”
+and “that”.

+

“this” and
+“that”.

"#; + run(input, output); +} + +#[test] +fn should_allow_quotes_adjacent_to_other_punctuation_characters_643() { + let input = r#"The dog---"'man's' best friend""#; + let output = r#"

The dog—“‘man’s’ best friend”

"#; + run(input, output); +} + +#[test] +fn should_parse_quotes_adjacent_to_code_block_677() { + let input = r#""test `code`" + +"`code` test""#; + let output = r#"

“test code”

+

“code test”

"#; + run(input, output); +} + +#[test] +fn should_parse_quotes_adjacent_to_inline_html_677() { + let input = r#""test
" + +"
test""#; + let output = r#"

“test
”

+

“
test”

"#; + run(input, output); +} + +#[test] +fn should_be_escapable() { + let input = r#""foo" + +\"foo" + +"foo\""#; + let output = r#"

“foo”

+

"foo"

+

"foo"

"#; + run(input, output); +} + +#[test] +fn should_not_replace_entities() { + let input = r#""foo" + +"foo" + +"foo""#; + let output = r#"

"foo"

+

"foo"

+

"foo"

"#; + run(input, output); +} +// end of auto-generated module +} diff --git a/crates/markdown-it/tests/markdown-it-typographer.rs b/crates/markdown-it/tests/markdown-it-typographer.rs new file mode 100644 index 0000000000000000000000000000000000000000..eff648b5908441fd76681a4ba0c20daff0f3edb4 --- /dev/null +++ b/crates/markdown-it/tests/markdown-it-typographer.rs @@ -0,0 +1,157 @@ +fn run(input: &str, output: &str) { + let output = if output.is_empty() { + "".to_owned() + } else { + output.to_owned() + "\n" + }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + markdown_it::plugins::extra::linkify::add(md); + markdown_it::plugins::extra::typographer::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + + // make sure we have sourcemaps for everything + node.walk(|node, _| assert!(node.srcmap.is_some())); + + let result = node.render(); + assert_eq!(result, output); + + // make sure it doesn't crash without trailing \n + let _ = md.parse(input.trim_end()); +} +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/markdown-it/typographer-extra.txt +#[rustfmt::skip] +mod fixtures_markdown_it_typographer_extra_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn don_t_touch_text_in_autolinks() { + let input = r#"URL with (C) (c) (R) (r) (TM) (tm): https://example.com/(c)(r)(tm)/(C)(R)(TM) what do you think?"#; + let output = r#"

URL with © © ® ® ™ ™: https://example.com/(c)(r)(tm)/(C)(R)(TM) what do you think?

"#; + run(input, output); +} + +#[test] +fn replacements_for_tm_should_allow_mixed_case_tm_and_tm() { + let input = r#"These two should both end up the same as (TM) and (tm): (tM), (Tm)."#; + let output = r#"

These two should both end up the same as ™ and ™: ™, ™.

"#; + run(input, output); +} +// end of auto-generated module +} +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/markdown-it/typographer.txt +#[rustfmt::skip] +mod fixtures_markdown_it_typographer_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn unnamed() { + let input = r#"(bad)"#; + let output = r#"

(bad)

"#; + run(input, output); +} + +#[test] +fn copyright() { + let input = r#"(c) (C)"#; + let output = r#"

Š Š

"#; + run(input, output); +} + +#[test] +fn reserved() { + let input = r#"(r) (R)"#; + let output = r#"

ÂŽ ÂŽ

"#; + run(input, output); +} + +#[test] +fn trademark() { + let input = r#"(tm) (TM)"#; + let output = r#"

™ ™

"#; + run(input, output); +} + +#[test] +fn plus_minus() { + let input = r#"+-5"#; + let output = r#"

Âą5

"#; + run(input, output); +} + +#[test] +fn ellipsis() { + let input = r#"test.. test... test..... test?..... test!...."#; + let output = r#"

test… test… test… test?.. test!..

"#; + run(input, output); +} + +#[test] +fn dupes() { + let input = r#"!!!!!! ???? ,,"#; + let output = r#"

!!! ??? ,

"#; + run(input, output); +} + +#[test] +fn copyright_should_be_escapable() { + let input = r#"\(c)"#; + let output = r#"

(c)

"#; + run(input, output); +} + +#[test] +fn shouldn_t_replace_entities() { + let input = r#"(c) (c) (c)"#; + let output = r#"

(c) (c) Š

"#; + run(input, output); +} + +#[test] +fn dashes() { + let input = r#"---markdownit --- super--- + +markdownit---awesome + +abc ---- + +--markdownit -- super-- + +markdownit--awesome"#; + let output = r#"

—markdownit — super—

+

markdownit—awesome

+

abc ----

+

–markdownit – super–

+

markdownit–awesome

"#; + run(input, output); +} + +#[test] +fn dashes_should_be_escapable() { + let input = r#"foo \-- bar + +foo -\- bar"#; + let output = r#"

foo -- bar

+

foo -- bar

"#; + run(input, output); +} + +#[test] +fn regression_tests_for_624() { + let input = r#"1---2---3 + +1--2--3 + +1 -- -- 3"#; + let output = r#"

1—2—3

+

1–2–3

+

1 – – 3

"#; + run(input, output); +} +// end of auto-generated module +} diff --git a/crates/markdown-it/tests/markdown-it.rs b/crates/markdown-it/tests/markdown-it.rs new file mode 100644 index 0000000000000000000000000000000000000000..8ff82ff54f124f45b247f1013f1b791b03235d9b --- /dev/null +++ b/crates/markdown-it/tests/markdown-it.rs @@ -0,0 +1,828 @@ + +fn run(input: &str, output: &str) { + let output = if output.is_empty() { "".to_owned() } else { output.to_owned() + "\n" }; + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + markdown_it::plugins::extra::typographer::add(md); + markdown_it::plugins::extra::tables::add(md); + let node = md.parse(&(input.to_owned() + "\n")); + + // make sure we have sourcemaps for everything + node.walk(|node, _| assert!(node.srcmap.is_some())); + + let result = node.render(); + assert_eq!(result, output); + + // make sure it doesn't crash without trailing \n + let _ = md.parse(input.trim_end()); +} + +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/markdown-it/tables.txt +#[rustfmt::skip] +mod fixtures_markdown_it_tables_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn simple() { + let input = r#"| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| Cell 3 | Cell 4"#; + let output = r#" + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3Cell 4
"#; + run(input, output); +} + +#[test] +fn column_alignment() { + let input = r#"| Header 1 | Header 2 | Header 3 | Header 4 | +| :------: | -------: | :------- | -------- | +| Cell 1 | Cell 2 | Cell 3 | Cell 4 | +| Cell 5 | Cell 6 | Cell 7 | Cell 8 |"#; + let output = r#" + + + + + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
"#; + run(input, output); +} + +#[test] +fn nested_emphases() { + let input = r#"Header 1|Header 2|Header 3|Header 4 +:-------|:------:|-------:|-------- +Cell 1 |Cell 2 |Cell 3 |Cell 4 +*Cell 5*|Cell 6 |Cell 7 |Cell 8"#; + let output = r#" + + + + + + + + + + + + + + + + + + + + + + +
Header 1Header 2Header 3Header 4
Cell 1Cell 2Cell 3Cell 4
Cell 5Cell 6Cell 7Cell 8
"#; + run(input, output); +} + +#[test] +fn nested_tables_inside_blockquotes() { + let input = r#"> foo|foo +> ---|--- +> bar|bar +baz|baz"#; + let output = r#"
+ + + + + + + + + + + + + +
foofoo
barbar
+
+

baz|baz

"#; + run(input, output); +} + +#[test] +fn minimal_one_column() { + let input = r#"| foo +|---- +| test2"#; + let output = r#" + + + + + + + + + + +
foo
test2
"#; + run(input, output); +} + +#[test] +fn this_is_parsed_as_one_big_table() { + let input = r#"- foo|foo +---|--- +bar|bar"#; + let output = r#" + + + + + + + + + + + + +
- foofoo
barbar
"#; + run(input, output); +} + +#[test] +fn second_line_should_not_contain_symbols_except_and() { + let input = r#"foo|foo +-----|-----s +bar|bar"#; + let output = r#"

foo|foo +-----|-----s +bar|bar

"#; + run(input, output); +} + +#[test] +fn second_line_should_contain_symbol() { + let input = r#"foo|foo +-----:----- +bar|bar"#; + let output = r#"

foo|foo +-----:----- +bar|bar

"#; + run(input, output); +} + +#[test] +fn second_line_should_not_have_empty_columns_in_the_middle() { + let input = r#"foo|foo +-----||----- +bar|bar"#; + let output = r#"

foo|foo +-----||----- +bar|bar

"#; + run(input, output); +} + +#[test] +fn wrong_alignment_symbol_position() { + let input = r#"foo|foo +-----|-::- +bar|bar"#; + let output = r#"

foo|foo +-----|-::- +bar|bar

"#; + run(input, output); +} + +#[test] +fn title_line_should_contain_symbol() { + let input = r#"foo +-----|----- +bar|bar"#; + let output = r#"

foo +-----|----- +bar|bar

"#; + run(input, output); +} + +#[test] +fn allow_tabs_as_a_separator_on_2nd_line() { + let input = "|\tfoo\t|\tbar\t| +|\t---\t|\t---\t| +|\tbaz\t|\tquux\t|"; + let output = r#" + + + + + + + + + + + + +
foobar
bazquux
"#; + run(input, output); +} + +#[test] +fn should_terminate_paragraph() { + let input = r#"paragraph +foo|foo +---|--- +bar|bar"#; + let output = r#"

paragraph

+ + + + + + + + + + + + + +
foofoo
barbar
"#; + run(input, output); +} + +#[test] +fn another_complicated_backticks_case() { + let input = r#"| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| \\\`|\\\`"#; + let output = r#" + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
\`\`
"#; + run(input, output); +} + +#[test] +fn in_tables_should_not_count_as_escaped_backtick() { + let input = r#"# | 1 | 2 +--|--|-- +x | `\` | `x`"#; + let output = r#" + + + + + + + + + + + + + + +
#12
x\x
"#; + run(input, output); +} + +#[test] +fn tables_should_handle_escaped_backticks() { + let input = r#"# | 1 | 2 +--|--|-- +x | \`\` | `x`"#; + let output = r#" + + + + + + + + + + + + + + +
#12
x``x
"#; + run(input, output); +} + +#[test] +fn an_amount_of_rows_might_be_different_across_the_table_issue_171() { + let input = r#"| 1 | 2 | +| :-----: | :-----: | +| 3 | 4 | 5 | 6 |"#; + let output = r#" + + + + + + + + + + + + +
12
34
"#; + run(input, output); +} + +#[test] +fn an_amount_of_rows_might_be_different_across_the_table_2() { + let input = r#"| 1 | 2 | 3 | 4 | +| :-----: | :-----: | :-----: | :-----: | +| 5 | 6 |"#; + let output = r#" + + + + + + + + + + + + + + + + +
1234
56
"#; + run(input, output); +} + +#[test] +fn allow_one_column_tables_issue_171() { + let input = r#"| foo | +:-----: +| bar |"#; + let output = r#" + + + + + + + + + + +
foo
bar
"#; + run(input, output); +} + +#[test] +fn allow_indented_tables_issue_325() { + let input = r#" | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b |"#; + let output = r#" + + + + + + + + + + + + +
Col1aCol2a
Col1bCol2b
"#; + run(input, output); +} + +#[test] +fn tables_should_not_be_indented_more_than_4_spaces_1st_line() { + let input = r#" | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b |"#; + let output = r#"
| Col1a | Col2a |
+
+

| ----- | ----- | +| Col1b | Col2b |

"#; + run(input, output); +} + +#[test] +fn tables_should_not_be_indented_more_than_4_spaces_2nd_line() { + let input = r#" | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b |"#; + let output = r#"

| Col1a | Col2a | +| ----- | ----- | +| Col1b | Col2b |

"#; + run(input, output); +} + +#[test] +fn tables_should_not_be_indented_more_than_4_spaces_3rd_line() { + let input = r#" | Col1a | Col2a | + | ----- | ----- | + | Col1b | Col2b |"#; + let output = r#" + + + + + + +
Col1aCol2a
+
| Col1b | Col2b |
+
"#; + run(input, output); +} + +#[test] +fn allow_tables_with_empty_body() { + let input = r#" | Col1a | Col2a | + | ----- | ----- |"#; + let output = r#" + + + + + + +
Col1aCol2a
"#; + run(input, output); +} + +#[test] +fn align_row_should_be_at_least_as_large_as_any_actual_rows() { + let input = r#"Col1a | Col1b | Col1c +----- | ----- +Col2a | Col2b | Col2c"#; + let output = r#"

Col1a | Col1b | Col1c +----- | ----- +Col2a | Col2b | Col2c

"#; + run(input, output); +} + +#[test] +fn escaped_pipes_inside_backticks_don_t_split_cells() { + let input = r#"| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| `Cell 3\|` | Cell 4"#; + let output = r#" + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3|Cell 4
"#; + run(input, output); +} + +#[test] +fn escape_before_escaped_pipes_inside_backticks_don_t_split_cells() { + let input = r#"| Heading 1 | Heading 2 +| --------- | --------- +| Cell 1 | Cell 2 +| `Cell 3\\|` | Cell 4"#; + let output = r#" + + + + + + + + + + + + + + + + +
Heading 1Heading 2
Cell 1Cell 2
Cell 3\|Cell 4
"#; + run(input, output); +} + +#[test] +fn regression_test_for_721_table_in_a_list_indented_with_tabs() { + let input = "- Level 1 + +\t- Level 2 + +\t\t| Column 1 | Column 2 | +\t\t| -------- | -------- | +\t\t| abcdefgh | ijklmnop |"; + let output = r#"
    +
  • +

    Level 1

    +
      +
    • +

      Level 2

      + + + + + + + + + + + + + +
      Column 1Column 2
      abcdefghijklmnop
      +
    • +
    +
  • +
"#; + run(input, output); +} + +#[test] +fn table_without_any_columns_is_not_a_table_724() { + let input = r#"| +| +|"#; + let output = r#"

| +| +|

"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_198() { + let input = r#"| foo | bar | +| --- | --- | +| baz | bim |"#; + let output = r#" + + + + + + + + + + + + +
foobar
bazbim
"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_199() { + let input = r#"| abc | defghi | +:-: | -----------: +bar | baz"#; + let output = r#" + + + + + + + + + + + + +
abcdefghi
barbaz
"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_200() { + let input = r#"| f\|oo | +| ------ | +| b `\|` az | +| b **\|** im |"#; + let output = r#" + + + + + + + + + + + + + +
f|oo
b | az
b | im
"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_201() { + let input = r#"| abc | def | +| --- | --- | +| bar | baz | +> bar"#; + let output = r#" + + + + + + + + + + + + +
abcdef
barbaz
+
+

bar

+
"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_202() { + let input = r#"| abc | def | +| --- | --- | +| bar | baz | +bar + +bar"#; + let output = r#" + + + + + + + + + + + + + + + + +
abcdef
barbaz
bar
+

bar

"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_203() { + let input = r#"| abc | def | +| --- | +| bar |"#; + let output = r#"

| abc | def | +| — | +| bar |

"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_204() { + let input = r#"| abc | def | +| --- | --- | +| bar | +| bar | baz | boo |"#; + let output = r#" + + + + + + + + + + + + + + + + +
abcdef
bar
barbaz
"#; + run(input, output); +} + +#[test] +fn gfm_4_10_tables_extension_example_205() { + let input = r#"| abc | def | +| --- | --- |"#; + let output = r#" + + + + + + +
abcdef
"#; + run(input, output); +} + +#[test] +fn a_list_takes_precedence_in_case_of_ambiguity() { + let input = r#"a | b +- | - +1 | 2"#; + let output = r#"

a | b

+
    +
  • | - +1 | 2
  • +
"#; + run(input, output); +} +// end of auto-generated module +} +/////////////////////////////////////////////////////////////////////////// +// TESTGEN: fixtures/markdown-it/markdown-it-rs.txt +#[rustfmt::skip] +mod fixtures_markdown_it_markdown_it_rs_txt { +use super::run; +// this part of the file is auto-generated +// don't edit it, otherwise your changes might be lost +#[test] +fn regression_test_for_panic_in_40() { + let input = r#"![hello'"`world](x)\"#; + let output = r#"

hello'"`world\

"#; + run(input, output); +} +// end of auto-generated module +} diff --git a/crates/markdown-it/tests/pathological.rs b/crates/markdown-it/tests/pathological.rs new file mode 100644 index 0000000000000000000000000000000000000000..a13300caeb4e684beb7d0a84f83a443dbd28cb8d --- /dev/null +++ b/crates/markdown-it/tests/pathological.rs @@ -0,0 +1,140 @@ +// run it like this: +// cargo test --test pathological --jobs 1 -- --nocapture --test-threads=1 +use markdown_it::MarkdownIt; +use once_cell::sync::Lazy; +use std::time::SystemTime; + +static MD : Lazy = Lazy::new(|| { + let mut parser = markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(&mut parser); + markdown_it::plugins::html::add(&mut parser); + markdown_it::plugins::extra::add(&mut parser); + parser +}); + +fn run(src: &str) { + let now = SystemTime::now(); + MD.parse(src); + dbg!(now.elapsed().ok().unwrap()); +} + + +mod commonmark { + // Ported from cmark, https://github.com/commonmark/cmark/blob/master/test/pathological_tests.py + use super::run; + + #[test] + fn nested_inlines() { + run(&format!("{}{}{}", "*".repeat(100000), "a", "*".repeat(100000))); + } + + #[test] + fn nested_strong_emph() { + // suspiciously slow + run(&format!("{}{}{}", "*a **a".repeat(5000), "b", " a** a*".repeat(5000))); + } + + #[test] + fn many_emph_closers_with_no_openers() { + run(&"a_ ".repeat(100000)); + } + + #[test] + fn many_emph_openers_with_no_closers() { + run(&"_a ".repeat(100000)); + } + + #[test] + fn many_link_closers_with_no_openers() { + run(&"a]".repeat(100000)); + } + + #[test] + fn many_link_openers_with_no_closers() { + run(&"[a".repeat(50000)); + } + + #[test] + fn mismatched_openers_and_closers() { + // most probably a bug + run(&"*a_ ".repeat(50000)); + } + + #[test] + fn commonmark_cmark_389() { + run(&format!("{}{}", "*a ".repeat(2000), "_a*_ ".repeat(2000))); + } + + #[test] + fn openers_and_closers_multiple_of_3() { + run(&format!("{}{}", "a**b", "c* ".repeat(50000))); + } + + #[test] + fn link_openers_and_emph_closers() { + run(&"[ a_".repeat(50000)); + } + + #[test] + fn link_pattern_repeated() { + run(&"[ (](".repeat(100000)); + } + + #[test] + fn nested_brackets() { + run(&format!("{}{}{}", "[".repeat(50000), "a", "]".repeat(50000))); + } + + #[test] + fn nested_block_quotes() { + run(&format!("{}{}", "> ".repeat(50000), "a")); + } + + #[test] + fn deeply_nested_lists() { + let src = (0..5000).map(|x| format!("{}{}", " ".repeat(x), "* a\n")).collect::>().join(""); + run(&src); + } + + #[test] + fn backticks() { + let src = (0..1000).map(|x| format!("{}{}", "e", "`".repeat(x))).collect::>().join(""); + run(&src); + } + + #[test] + fn unclosed_links_a() { + run(&"[a](")); + } + + #[test] + fn hardbreak_whitespaces_pattern() { + run(&format!("{}{}{}", "x", " ".repeat(100000), "x \nx")); + } +} + diff --git a/crates/markdown-it/tests/sourcemaps.rs b/crates/markdown-it/tests/sourcemaps.rs new file mode 100644 index 0000000000000000000000000000000000000000..25eca75001e2eb2b4b54911d8450bcb921654db2 --- /dev/null +++ b/crates/markdown-it/tests/sourcemaps.rs @@ -0,0 +1,376 @@ +use markdown_it::Node; +use markdown_it::common::sourcemap::SourceWithLineStarts; + +fn run(input: &str, f: fn (&Node, SourceWithLineStarts)) { + let md = &mut markdown_it::MarkdownIt::new(); + markdown_it::plugins::cmark::add(md); + markdown_it::plugins::html::add(md); + let node = md.parse(input); + node.walk(|node, _| assert!(node.srcmap.is_some())); + f(&node, SourceWithLineStarts::new(input)); +} + +fn getmap(node: &Node, map: &SourceWithLineStarts) -> ((u32, u32), (u32, u32)) { + node.srcmap.unwrap().get_positions(map) +} + +#[test] +fn paragraph() { + // same as commonmark.js + run("foo \n \n \n\n barbaz\n\tquux \n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 1), (1, 6)), + ); + assert_eq!( + getmap(&node.children[1], &map), + ((5, 3), (6, 8)), + ); + }); +} + +#[test] +fn hr() { + // same as commonmark.js + run(" --- \n\n * * *\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 2), (1, 6)), + ); + assert_eq!( + getmap(&node.children[1], &map), + ((3, 3), (3, 7)), + ); + }); +} + +#[test] +fn heading() { + // same as commonmark.js + run(" \n ### foo ### \n\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((2, 3), (2, 15)), + ); + }); + + run(" #\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (1, 3)), + ); + }); +} + +#[test] +fn lheading() { + // same as commonmark.js + run(" foo\n bar\n ----\n\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (3, 5)), + ); + }); +} + +#[test] +fn fence() { + // same as commonmark.js + run(" ~~~ foo ~~~\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (1, 13)), + ); + }); + + run(" ```\n 12\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (2, 3)), + ); + }); + + run("```\n\n\n\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 1), (4, 0)), + ); + }); + + run("~~~\na\nb\n~~~ \nc\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 1), (4, 5)), + ); + }); +} + +#[test] +fn html_block() { + // same as commonmark.js + run("
\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (1, 7)), + ); + }); + + run("
\n
\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 1), (2, 8)), + ); + }); +} + +#[test] +fn code_block() { + // this should be (1, 5), (1, 9) + // for simplicity, we point source maps for block tags to first + // nonspace character, but it isn't quite correct for code blocks + run(" foo\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 7), (1, 9)), + ); + }); + + run(" a\n b\n c\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 4), (3, 6)), + ); + }); + + // this I believe to be error in commonmark, code block + // only have 1 line as per spec, but cmark reports 3 lines + run(" foobar \n \n \n\nbar\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 5), (1, 12)), + ); + }); +} + +#[test] +fn blockquotes() { + // same as commonmark.js + run(" > foo \n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 3), (1, 9)), + ); + }); + + run("> foo\nbar\n\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 1), (2, 3)), + ); + }); +} + +#[test] +fn lists() { + // same as commonmark.js + run(" 1. foo\n 2. bar\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 2), (2, 7)), + ); + + assert_eq!( + getmap(&node.children[0].children[0], &map), + ((1, 2), (1, 7)), + ); + }); + + run(" - foo\n\n - bar\n", |node, map| { + assert_eq!( + getmap(&node.children[0], &map), + ((1, 2), (3, 6)), + ); + + assert_eq!( + getmap(&node.children[0].children[0], &map), + ((1, 2), (2, 0)), + ); + + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((3, 2), (3, 6)), + ); + }); +} + +#[test] +fn autolinks() { + run("foo bar", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 23)), + ); + + assert_eq!( + getmap(&node.children[0].children[1].children[0], &map), + ((1, 6), (1, 22)), + ); + }); +} + +#[test] +fn emphasis() { + run("***foo***", |node, map| { + assert_eq!( + getmap(&node.children[0].children[0], &map), + ((1, 1), (1, 9)), + ); + + assert_eq!( + getmap(&node.children[0].children[0].children[0], &map), + ((1, 2), (1, 8)), + ); + }); + + run("aaa **bb _cc_ dd** eee", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 18)), + ); + + assert_eq!( + getmap(&node.children[0].children[1].children[1], &map), + ((1, 10), (1, 13)), + ); + }); +} + +#[test] +fn newline() { + run("foo \nbar \nbaz\nquux", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 4), (2, 0)), + ); + + assert_eq!( + getmap(&node.children[0].children[3], &map), + ((2, 4), (3, 0)), + ); + + assert_eq!( + getmap(&node.children[0].children[5], &map), + ((4, 0), (4, 0)), + ); + + /*let marks : Vec<_> = node.children[0].children.iter().map(|x| getmap(x, &map)).collect(); + assert_eq!(marks, [ + ((1, 1), (1, 5)), + ((2, 0), (2, 1)), + ((2, 1), (2, 3)), + ((3, 0), (3, 0)), + ((3, 1), (3, 3)), + ]);*/ + }); +} + +#[test] +fn escapes() { + run("foo\\Δ\\*bar", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 4), (1, 5)), + ); + + assert_eq!( + getmap(&node.children[0].children[2], &map), + ((1, 6), (1, 7)), + ); + }); + + run(" foo \\\n bar ", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 8), (2, 2)), + ); + }); +} + +#[test] +fn entities() { + run("aa   bb  cc", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 4), (1, 9)), + ); + + assert_eq!( + getmap(&node.children[0].children[3], &map), + ((1, 14), (1, 18)), + ); + + /*let marks : Vec<_> = node.children[0].children.iter().map(|x| getmap(x, &map)).collect(); + assert_eq!(marks, [ + ((1, 1), (1, 5)), + ((2, 0), (2, 1)), + ((2, 1), (2, 3)), + ((3, 0), (3, 0)), + ((3, 1), (3, 3)), + ]);*/ + }); +} + +#[test] +fn html_inline() { + run("foo baz", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 9)), + ); + }); +} + +#[test] +fn backticks() { + run("foo ```bar``` baz", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 13)), + ); + + assert_eq!( + getmap(&node.children[0].children[1].children[0], &map), + ((1, 8), (1, 10)), + ); + }); + + run("foo ` bar ` baz", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 11)), + ); + + assert_eq!( + getmap(&node.children[0].children[1].children[0], &map), + ((1, 7), (1, 9)), + ); + }); +} + +#[test] +fn imglink() { + run("foo [bar](baz) quux", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 14)), + ); + }); + + run("foo ![bar](baz) quux", |node, map| { + assert_eq!( + getmap(&node.children[0].children[1], &map), + ((1, 5), (1, 15)), + ); + }); +} + diff --git a/src/lib.rs b/src/lib.rs index 1a28f0bf9cdf8612993047fbe09c4e72c5c83d86..c18bd2e443a87a3ff0d6d3da4922048e5b442860 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1099,6 +1099,20 @@ mod tests { ); } + // Regression: EmphPairScanner panicked (subtract-with-overflow) when an + // emphasis span crossed multiple blockquote continuation lines because + // `token_len` was derived from srcmap file-byte offsets (inclusive of + // stripped `> ` bytes) instead of inline-text offsets. + #[test] + fn multiline_blockquote_emphasis_does_not_panic() { + let src = "> *bottom row left: the past like 4 times i've gone with akira to that\n\ + > pizza place, besides last time in december, they've always seated us at that\n\ + > exact same table. it's really cute.*"; + let out = run(src); + assert!(out.contains(""), "emphasis must be rendered"); + assert!(out.contains("
"), "blockquote must be rendered"); + } + // ------------------------------------------------------------------------- // componentImports takes precedence over layout for the same element type // ------------------------------------------------------------------------- -- 2.54.0