Add md-footnote
This commit is contained in:
15
crates/lib/md-dev/Cargo.toml
Normal file
15
crates/lib/md-dev/Cargo.toml
Normal file
@@ -0,0 +1,15 @@
|
||||
# Clone of
|
||||
# https://github.com/markdown-it-rust/markdown-it-plugins.rs
|
||||
|
||||
[package]
|
||||
name = "md-dev"
|
||||
version = "0.2.0"
|
||||
publish = false
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
prettydiff = { workspace = true }
|
||||
109
crates/lib/md-dev/src/lib.rs
Normal file
109
crates/lib/md-dev/src/lib.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
//! development utilities
|
||||
//!
|
||||
//! This contains shared code for reading test fixtures,
|
||||
//! testing for differences, and regenerating expected output.
|
||||
|
||||
use prettydiff::diff_lines;
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct FixtureFile {
|
||||
pub file: PathBuf,
|
||||
pub title: String,
|
||||
pub input: String,
|
||||
pub expected: String,
|
||||
}
|
||||
|
||||
/// Read a fixture file into a FixtureFile struct
|
||||
pub fn read_fixture_file(file: PathBuf) -> FixtureFile {
|
||||
#[expect(clippy::unwrap_used)]
|
||||
let text = std::fs::read_to_string(&file).unwrap();
|
||||
|
||||
let mut lines = text.lines();
|
||||
let mut title = String::new();
|
||||
let mut input = String::new();
|
||||
let mut expected = String::new();
|
||||
loop {
|
||||
match lines.next() {
|
||||
None => panic!("no '....' line found to signal start of input"),
|
||||
Some(line) if line.starts_with("....") => break,
|
||||
Some(line) => {
|
||||
title.push_str(line);
|
||||
title.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match lines.next() {
|
||||
None => panic!("no '....' line found to signal start of expected output"),
|
||||
Some(line) if line.starts_with("....") => break,
|
||||
Some(line) => {
|
||||
input.push_str(line);
|
||||
input.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
loop {
|
||||
match lines.next() {
|
||||
None => break,
|
||||
Some(line) => {
|
||||
expected.push_str(line);
|
||||
expected.push('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
// strip preceding empty line in input
|
||||
while input.starts_with('\n') {
|
||||
input = input[1..].to_string();
|
||||
}
|
||||
// strip trailing empty lines from input
|
||||
while input.ends_with('\n') {
|
||||
input.pop();
|
||||
}
|
||||
// strip preceding empty line in expected
|
||||
while expected.starts_with('\n') {
|
||||
expected = expected[1..].to_string();
|
||||
}
|
||||
|
||||
FixtureFile {
|
||||
file,
|
||||
title,
|
||||
input,
|
||||
expected,
|
||||
}
|
||||
}
|
||||
|
||||
/// Assert that the actual output matches the expected output,
|
||||
/// and panic with a diff if it does not.
|
||||
pub fn assert_no_diff(f: FixtureFile, actual: &str) {
|
||||
if actual.trim_end() != f.expected.trim_end() {
|
||||
let diff = diff_lines(&f.expected, actual);
|
||||
|
||||
// if environmental variable FORCE_REGEN is set, overwrite the expected output
|
||||
if std::env::var("FORCE_REGEN").is_ok() {
|
||||
let written = std::fs::write(
|
||||
f.file,
|
||||
format!(
|
||||
"{}\n......\n\n{}\n\n......\n\n{}\n",
|
||||
f.title.trim_end(),
|
||||
f.input,
|
||||
actual.trim_end()
|
||||
),
|
||||
)
|
||||
.is_ok();
|
||||
if written {
|
||||
panic!(
|
||||
"\n{}\nDiff:\n{}\n\nRegenerated expected output",
|
||||
f.title, diff
|
||||
);
|
||||
}
|
||||
panic!(
|
||||
"\n{}\nDiff:\n{}\n\nFailed to regenerate expected output",
|
||||
f.title, diff
|
||||
)
|
||||
}
|
||||
panic!(
|
||||
"\n{}\nDiff:\n{}\nSet FORCE_REGEN=true to update fixture",
|
||||
f.title, diff
|
||||
);
|
||||
}
|
||||
}
|
||||
21
crates/lib/md-footnote/Cargo.toml
Normal file
21
crates/lib/md-footnote/Cargo.toml
Normal file
@@ -0,0 +1,21 @@
|
||||
# Clone of
|
||||
# https://github.com/markdown-it-rust/markdown-it-plugins.rs
|
||||
|
||||
[package]
|
||||
name = "md-footnote"
|
||||
version = "0.2.0"
|
||||
description = "A markdown-it plugin for parsing footnotes"
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0"
|
||||
rust-version = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
markdown-it = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
md-dev = { workspace = true }
|
||||
testing = { workspace = true }
|
||||
59
crates/lib/md-footnote/README.md
Normal file
59
crates/lib/md-footnote/README.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# markdown-it-footnote.rs
|
||||
|
||||
[<img alt="crates.io" src="https://img.shields.io/crates/v/markdown-it-footnote.svg?style=for-the-badge&color=fc8d62&logo=rust" height="20">](https://crates.io/crates/markdown-it-footnote)
|
||||
|
||||
A [markdown-it.rs](https://crates.io/crates/markdown-it) plugin to process footnotes.
|
||||
|
||||
It is based on the [pandoc definition](http://johnmacfarlane.net/pandoc/README.html#footnotes):
|
||||
|
||||
```md
|
||||
Normal footnote:
|
||||
|
||||
Here is a footnote reference,[^1] and another.[^longnote]
|
||||
|
||||
Here is an inline note.^[my note is here!]
|
||||
|
||||
[^1]: Here is the footnote.
|
||||
|
||||
[^longnote]: Here's one with multiple blocks.
|
||||
|
||||
Subsequent paragraphs are indented to show that they
|
||||
belong to the previous footnote.
|
||||
```
|
||||
|
||||
See the [tests](tests/fixtures) for more examples.
|
||||
|
||||
## Usage
|
||||
|
||||
To load the full plugin:
|
||||
|
||||
```rust
|
||||
let parser = &mut markdown_it::MarkdownIt::new();
|
||||
markdown_it::plugins::cmark::add(parser);
|
||||
|
||||
md_footnote::add(parser);
|
||||
|
||||
let ast = parser.parse("Example^[my note]");
|
||||
let html = ast.render();
|
||||
```
|
||||
|
||||
Alternatively, you can load the separate components:
|
||||
|
||||
```rust
|
||||
let parser = &mut markdown_it::MarkdownIt::new();
|
||||
markdown_it::plugins::cmark::add(parser);
|
||||
|
||||
md_footnote::definitions::add(md);
|
||||
md_footnote::references::add(md);
|
||||
md_footnote::inline::add(md);
|
||||
md_footnote::collect::add(md);
|
||||
md_footnote::back_refs::add(md);
|
||||
```
|
||||
|
||||
Which have the following roles:
|
||||
|
||||
- `definitions`: parse footnote definitions, e.g. `[^1]: foo`
|
||||
- `references`: parse footnote references, e.g. `[^1]`
|
||||
- `inline`: parse inline footnotes, e.g. `^[foo]`
|
||||
- `collect`: collect footnote definitions (removing duplicate/unreferenced ones) and move them to be the last child of the root node.
|
||||
- `back_refs`: add anchor(s) to footnote definitions, with links back to the reference(s)
|
||||
107
crates/lib/md-footnote/src/back_refs.rs
Normal file
107
crates/lib/md-footnote/src/back_refs.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
//! Plugin to add anchor(s) to footnote definitions,
|
||||
//! with links back to the reference(s).
|
||||
//!
|
||||
//! ```rust
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! markdown_it::plugins::cmark::add(parser);
|
||||
//! md_footnote::references::add(parser);
|
||||
//! md_footnote::definitions::add(parser);
|
||||
//! md_footnote::back_refs::add(parser);
|
||||
//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote");
|
||||
//! let mut names = vec![];
|
||||
//! root.walk(|node,_| { names.push(node.name()); });
|
||||
//! assert_eq!(names, vec![
|
||||
//! "markdown_it::parser::core::root::Root",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "md_footnote::references::FootnoteReference",
|
||||
//! "md_footnote::definitions::FootnoteDefinition",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! "md_footnote::back_refs::FootnoteRefAnchor",
|
||||
//! ]);
|
||||
//! ```
|
||||
use markdown_it::{
|
||||
MarkdownIt, Node, NodeValue,
|
||||
parser::core::{CoreRule, Root},
|
||||
plugins::cmark::block::paragraph::Paragraph,
|
||||
};
|
||||
|
||||
use crate::{FootnoteMap, definitions::FootnoteDefinition};
|
||||
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
// insert this rule into parser
|
||||
md.add_rule::<FootnoteBackrefRule>();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FootnoteRefAnchor {
|
||||
pub ref_ids: Vec<usize>,
|
||||
}
|
||||
impl NodeValue for FootnoteRefAnchor {
|
||||
fn render(&self, _: &Node, fmt: &mut dyn markdown_it::Renderer) {
|
||||
for ref_id in self.ref_ids.iter() {
|
||||
fmt.text(" ");
|
||||
fmt.open(
|
||||
"a",
|
||||
&[
|
||||
("href", format!("#fnref{}", ref_id)),
|
||||
("class", String::from("footnote-backref")),
|
||||
],
|
||||
);
|
||||
// # ↩ with escape code to prevent display as Apple Emoji on iOS
|
||||
fmt.text("\u{21a9}\u{FE0E}");
|
||||
fmt.close("a");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is an extension for the markdown parser.
|
||||
struct FootnoteBackrefRule;
|
||||
|
||||
impl CoreRule for FootnoteBackrefRule {
|
||||
fn run(root: &mut Node, _: &MarkdownIt) {
|
||||
// TODO this seems very cumbersome
|
||||
// but it is also how the markdown_it::InlineParserRule works
|
||||
#[expect(clippy::unwrap_used)]
|
||||
let data = root.cast_mut::<Root>().unwrap();
|
||||
|
||||
let root_ext = std::mem::take(&mut data.ext);
|
||||
let map = match root_ext.get::<FootnoteMap>() {
|
||||
Some(map) => map,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// walk through the AST and add backref anchors to footnote definitions
|
||||
root.walk_mut(|node, _| {
|
||||
if let Some(def_node) = node.cast::<FootnoteDefinition>() {
|
||||
let ref_ids = {
|
||||
match def_node.def_id {
|
||||
Some(def_id) => map.referenced_by(def_id),
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
if !ref_ids.is_empty() {
|
||||
// if the final child is a paragraph node,
|
||||
// append the anchor to its children,
|
||||
// otherwise simply append to the end of the node children
|
||||
match node.children.last_mut() {
|
||||
Some(last) => {
|
||||
if last.is::<Paragraph>() {
|
||||
last.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
|
||||
} else {
|
||||
node.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
node.children.push(Node::new(FootnoteRefAnchor { ref_ids }));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
let data = root.cast_mut::<Root>().unwrap();
|
||||
data.ext = root_ext;
|
||||
}
|
||||
}
|
||||
140
crates/lib/md-footnote/src/collect.rs
Normal file
140
crates/lib/md-footnote/src/collect.rs
Normal file
@@ -0,0 +1,140 @@
|
||||
//! Plugin to collect footnote definitions,
|
||||
//! removing duplicate/unreferenced ones,
|
||||
//! and move them to be the last child of the root node.
|
||||
//!
|
||||
//! ```rust
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! markdown_it::plugins::cmark::add(parser);
|
||||
//! md_footnote::references::add(parser);
|
||||
//! md_footnote::definitions::add(parser);
|
||||
//! md_footnote::collect::add(parser);
|
||||
//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote\n\n> quote");
|
||||
//! let mut names = vec![];
|
||||
//! root.walk(|node,_| { names.push(node.name()); });
|
||||
//! assert_eq!(names, vec![
|
||||
//! "markdown_it::parser::core::root::Root",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "md_footnote::references::FootnoteReference",
|
||||
//! "markdown_it::plugins::cmark::block::blockquote::Blockquote",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! "md_footnote::collect::FootnotesContainerNode",
|
||||
//! "md_footnote::definitions::FootnoteDefinition",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! ]);
|
||||
//! ```
|
||||
use markdown_it::{
|
||||
MarkdownIt, Node, NodeValue,
|
||||
parser::core::{CoreRule, Root},
|
||||
plugins::cmark::block::paragraph::Paragraph,
|
||||
};
|
||||
|
||||
use crate::{FootnoteMap, definitions::FootnoteDefinition};
|
||||
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
// insert this rule into parser
|
||||
md.add_rule::<FootnoteCollectRule>();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct PlaceholderNode;
|
||||
impl NodeValue for PlaceholderNode {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FootnotesContainerNode;
|
||||
impl NodeValue for FootnotesContainerNode {
|
||||
fn render(&self, node: &Node, fmt: &mut dyn markdown_it::Renderer) {
|
||||
let mut attrs = node.attrs.clone();
|
||||
attrs.push(("class", "footnotes".into()));
|
||||
fmt.cr();
|
||||
fmt.self_close("hr", &[("class", "footnotes-sep".into())]);
|
||||
fmt.cr();
|
||||
fmt.open("section", &attrs);
|
||||
fmt.cr();
|
||||
fmt.open("ol", &[("class", "footnotes-list".into())]);
|
||||
fmt.cr();
|
||||
fmt.contents(&node.children);
|
||||
fmt.cr();
|
||||
fmt.close("ol");
|
||||
fmt.cr();
|
||||
fmt.close("section");
|
||||
fmt.cr();
|
||||
}
|
||||
}
|
||||
|
||||
// This is an extension for the markdown parser.
|
||||
struct FootnoteCollectRule;
|
||||
|
||||
impl CoreRule for FootnoteCollectRule {
|
||||
// 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) {
|
||||
// TODO this seems very cumbersome
|
||||
// but it is also how the markdown_it::InlineParserRule works
|
||||
#[expect(clippy::unwrap_used)]
|
||||
let data = root.cast_mut::<Root>().unwrap();
|
||||
|
||||
let root_ext = std::mem::take(&mut data.ext);
|
||||
let map = match root_ext.get::<FootnoteMap>() {
|
||||
Some(map) => map,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// walk through the AST and extract all footnote definitions
|
||||
let mut defs = vec![];
|
||||
root.walk_mut(|node, _| {
|
||||
// TODO could use drain_filter if it becomes stable: https://github.com/rust-lang/rust/issues/43244
|
||||
// defs.extend(
|
||||
// node.children
|
||||
// .drain_filter(|child| !child.is::<FootnoteDefinition>())
|
||||
// .collect(),
|
||||
// );
|
||||
|
||||
for child in node.children.iter_mut() {
|
||||
if child.is::<FootnoteDefinition>() {
|
||||
let mut extracted = std::mem::replace(child, Node::new(PlaceholderNode));
|
||||
match extracted.cast::<FootnoteDefinition>() {
|
||||
Some(def_node) => {
|
||||
// skip footnotes that are not referenced
|
||||
match def_node.def_id {
|
||||
Some(def_id) => {
|
||||
if map.referenced_by(def_id).is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
if def_node.inline {
|
||||
// for inline footnotes,
|
||||
// we need to wrap the definition's children in a paragraph
|
||||
let mut para = Node::new(Paragraph);
|
||||
std::mem::swap(&mut para.children, &mut extracted.children);
|
||||
extracted.children = vec![para];
|
||||
}
|
||||
}
|
||||
None => continue,
|
||||
}
|
||||
defs.push(extracted);
|
||||
}
|
||||
}
|
||||
node.children.retain(|child| !child.is::<PlaceholderNode>());
|
||||
});
|
||||
if defs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
// wrap the definitions in a container and append them to the root
|
||||
let mut wrapper = Node::new(FootnotesContainerNode);
|
||||
wrapper.children = defs;
|
||||
root.children.push(wrapper);
|
||||
|
||||
#[expect(clippy::unwrap_used)]
|
||||
let data = root.cast_mut::<Root>().unwrap();
|
||||
|
||||
data.ext = root_ext;
|
||||
}
|
||||
}
|
||||
179
crates/lib/md-footnote/src/definitions.rs
Normal file
179
crates/lib/md-footnote/src/definitions.rs
Normal file
@@ -0,0 +1,179 @@
|
||||
//! Plugin to parse footnote definitions
|
||||
//!
|
||||
//! ```rust
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! markdown_it::plugins::cmark::add(parser);
|
||||
//! md_footnote::definitions::add(parser);
|
||||
//! let root = parser.parse("[^label]: This is a footnote");
|
||||
//! let mut names = vec![];
|
||||
//! root.walk(|node,_| { names.push(node.name()); });
|
||||
//! assert_eq!(names, vec![
|
||||
//! "markdown_it::parser::core::root::Root",
|
||||
//! "md_footnote::definitions::FootnoteDefinition",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! ]);
|
||||
//! ```
|
||||
|
||||
use markdown_it::parser::block::{BlockRule, BlockState};
|
||||
use markdown_it::plugins::cmark::block::reference::ReferenceScanner;
|
||||
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
|
||||
|
||||
use crate::FootnoteMap;
|
||||
|
||||
/// Add the footnote definition plugin to the parser
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
// insert this rule into block subparser
|
||||
md.block
|
||||
.add_rule::<FootnoteDefinitionScanner>()
|
||||
.before::<ReferenceScanner>();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// AST node for footnote definition
|
||||
pub struct FootnoteDefinition {
|
||||
pub label: Option<String>,
|
||||
pub def_id: Option<usize>,
|
||||
pub inline: bool,
|
||||
}
|
||||
|
||||
impl NodeValue for FootnoteDefinition {
|
||||
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
|
||||
let mut attrs = node.attrs.clone();
|
||||
if let Some(def_id) = self.def_id {
|
||||
attrs.push(("id", format!("fn{}", def_id)));
|
||||
}
|
||||
attrs.push(("class", "footnote-item".into()));
|
||||
|
||||
fmt.cr();
|
||||
fmt.open("li", &attrs);
|
||||
fmt.contents(&node.children);
|
||||
fmt.close("li");
|
||||
fmt.cr();
|
||||
}
|
||||
}
|
||||
|
||||
/// An extension for the block subparser.
|
||||
struct FootnoteDefinitionScanner;
|
||||
|
||||
impl FootnoteDefinitionScanner {
|
||||
fn is_def(state: &mut BlockState<'_, '_>) -> Option<(String, usize)> {
|
||||
if state.line_indent(state.line) >= state.md.max_indent {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut chars = state.get_line(state.line).chars();
|
||||
|
||||
// check line starts with the correct syntax
|
||||
let Some('[') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
let Some('^') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// gather the label
|
||||
let mut label = String::new();
|
||||
// The labels in footnote references may not contain spaces, tabs, or newlines.
|
||||
// Backslash escapes form part of the label and do not escape anything
|
||||
loop {
|
||||
match chars.next() {
|
||||
None => return None,
|
||||
Some(']') => {
|
||||
if let Some(':') = chars.next() {
|
||||
break;
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some(' ') => return None,
|
||||
Some(c) => label.push(c),
|
||||
}
|
||||
}
|
||||
if label.is_empty() {
|
||||
return None;
|
||||
}
|
||||
// get number of spaces to next non-space character
|
||||
let mut spaces = 0;
|
||||
loop {
|
||||
match chars.next() {
|
||||
None => break,
|
||||
Some(' ') => spaces += 1,
|
||||
Some('\t') => spaces += 1, // spaces += 4 - spaces % 4,
|
||||
Some(_) => break,
|
||||
}
|
||||
}
|
||||
Some((label, spaces))
|
||||
}
|
||||
}
|
||||
|
||||
impl BlockRule for FootnoteDefinitionScanner {
|
||||
fn check(state: &mut BlockState<'_, '_>) -> Option<()> {
|
||||
// can interrupt a block elements,
|
||||
// but only if its a child of another footnote definition
|
||||
// TODO I think strictly only paragraphs should be interrupted, but this is not yet possible in markdown-it.rs
|
||||
if state.node.is::<FootnoteDefinition>() && Self::is_def(state).is_some() {
|
||||
return Some(());
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn run(state: &mut BlockState<'_, '_>) -> Option<(Node, usize)> {
|
||||
let (label, spaces) = Self::is_def(state)?;
|
||||
|
||||
// record the footnote label, so we can match references to it later
|
||||
let foot_map = state.root_ext.get_or_insert_default::<FootnoteMap>();
|
||||
let def_id = foot_map.add_def(&label);
|
||||
|
||||
// temporarily set the current node to the footnote definition
|
||||
// so child nodes are added to it
|
||||
let new_node = Node::new(FootnoteDefinition {
|
||||
label: Some(label.clone()),
|
||||
def_id,
|
||||
inline: false,
|
||||
});
|
||||
let old_node = std::mem::replace(&mut state.node, new_node);
|
||||
|
||||
// store the current line and its offsets, so we can restore later
|
||||
let first_line = state.line;
|
||||
let first_line_offsets = state.line_offsets[first_line].clone();
|
||||
|
||||
// temporarily change the first line offsets to account for the footnote label
|
||||
// TODO this is not quite the same as pandoc where spaces >= 8 is code block (here >= 4)
|
||||
state.line_offsets[first_line].first_nonspace += "[^]:".len() + label.len() + spaces;
|
||||
state.line_offsets[first_line].indent_nonspace += "[^]:".len() as i32 + spaces as i32;
|
||||
// tokenize with a +4 space indent
|
||||
state.blk_indent += 4;
|
||||
state.md.block.tokenize(state);
|
||||
state.blk_indent -= 4;
|
||||
|
||||
// get the number of lines the footnote definition occupies
|
||||
let num_lines = state.line - first_line;
|
||||
|
||||
// restore the first line and its offsets
|
||||
state.line_offsets[first_line] = first_line_offsets;
|
||||
state.line = first_line;
|
||||
|
||||
// restore the original node and return the footnote and number of lines it occupies
|
||||
Some((std::mem::replace(&mut state.node, old_node), num_lines))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn it_works() {
|
||||
let parser = &mut markdown_it::MarkdownIt::new();
|
||||
markdown_it::plugins::cmark::add(parser);
|
||||
markdown_it::plugins::sourcepos::add(parser);
|
||||
add(parser);
|
||||
let node = parser.parse("[^note]: a\n\nhallo\nthere\n");
|
||||
// println!("{:#?}", node);
|
||||
assert!(node.children.first().unwrap().is::<FootnoteDefinition>());
|
||||
|
||||
// let text = node.render();
|
||||
// assert_eq!(text, "hallo\n")
|
||||
}
|
||||
}
|
||||
147
crates/lib/md-footnote/src/inline.rs
Normal file
147
crates/lib/md-footnote/src/inline.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
//! Plugin to parse inline footnotes
|
||||
//!
|
||||
//! ```rust
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! markdown_it::plugins::cmark::add(parser);
|
||||
//! md_footnote::inline::add(parser);
|
||||
//! let root = parser.parse("Example^[This is a footnote]");
|
||||
//! let mut names = vec![];
|
||||
//! root.walk(|node,_| { names.push(node.name()); });
|
||||
//! assert_eq!(names, vec![
|
||||
//! "markdown_it::parser::core::root::Root",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! "md_footnote::inline::InlineFootnote",
|
||||
//! "md_footnote::definitions::FootnoteDefinition",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text",
|
||||
//! "md_footnote::references::FootnoteReference"
|
||||
//! ]);
|
||||
//! ```
|
||||
use markdown_it::{
|
||||
MarkdownIt, Node, NodeValue,
|
||||
parser::inline::{InlineRule, InlineState},
|
||||
};
|
||||
|
||||
use crate::{FootnoteMap, definitions::FootnoteDefinition};
|
||||
|
||||
/// Add the inline footnote plugin to the parser
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
// insert this rule into inline subparser
|
||||
md.inline.add_rule::<InlineFootnoteScanner>();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct InlineFootnote;
|
||||
impl NodeValue for InlineFootnote {
|
||||
fn render(&self, node: &Node, fmt: &mut dyn markdown_it::Renderer) {
|
||||
// simply pass-through to children
|
||||
fmt.contents(&node.children);
|
||||
}
|
||||
}
|
||||
|
||||
// This is an extension for the inline subparser.
|
||||
struct InlineFootnoteScanner;
|
||||
|
||||
impl InlineRule for InlineFootnoteScanner {
|
||||
const MARKER: char = '^';
|
||||
|
||||
fn check(state: &mut InlineState<'_, '_>) -> Option<usize> {
|
||||
let mut chars = state.src[state.pos..state.pos_max].chars();
|
||||
|
||||
// check line starts with the correct syntax
|
||||
let Some('^') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
let Some('[') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let content_start = state.pos + 2;
|
||||
|
||||
match parse_footnote(state, content_start) {
|
||||
Some(content_end) => Some(content_end + 1 - state.pos),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> {
|
||||
let mut chars = state.src[state.pos..state.pos_max].chars();
|
||||
|
||||
// check line starts with the correct syntax
|
||||
let Some('^') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
let Some('[') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let content_start = state.pos + 2;
|
||||
|
||||
match parse_footnote(state, content_start) {
|
||||
Some(content_end) => {
|
||||
let foot_map = state.root_ext.get_or_insert_default::<FootnoteMap>();
|
||||
let (def_id, ref_id) = foot_map.add_inline_def();
|
||||
|
||||
// create node and set it as current
|
||||
let current_node = std::mem::replace(
|
||||
&mut state.node,
|
||||
Node::new(FootnoteDefinition {
|
||||
label: None,
|
||||
def_id: Some(def_id),
|
||||
inline: true,
|
||||
}),
|
||||
);
|
||||
|
||||
// perform nested parsing
|
||||
let start = state.pos;
|
||||
let max = state.pos_max;
|
||||
state.pos = content_start;
|
||||
state.pos_max = content_end;
|
||||
state.md.inline.tokenize(state);
|
||||
state.pos = start;
|
||||
state.pos_max = max;
|
||||
|
||||
// restore current node
|
||||
let def_node = std::mem::replace(&mut state.node, current_node);
|
||||
|
||||
let ref_node = Node::new(crate::references::FootnoteReference {
|
||||
label: None,
|
||||
ref_id,
|
||||
def_id,
|
||||
});
|
||||
|
||||
// wrap the footnote definition and reference in an outer node to return
|
||||
let mut outer_node = Node::new(InlineFootnote);
|
||||
outer_node.children = vec![def_node, ref_node];
|
||||
|
||||
Some((outer_node, content_end + 1 - state.pos))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// returns the end position of the footnote
|
||||
// this function assumes that first character ("[") already matches;
|
||||
fn parse_footnote(state: &mut InlineState<'_, '_>, start: usize) -> Option<usize> {
|
||||
let old_pos = state.pos;
|
||||
let mut label_end = None;
|
||||
state.pos = start + 1;
|
||||
let mut found = false;
|
||||
while let Some(ch) = state.src[state.pos..state.pos_max].chars().next() {
|
||||
if ch == ']' {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
state.md.inline.skip_token(state);
|
||||
}
|
||||
|
||||
if found {
|
||||
label_end = Some(state.pos);
|
||||
}
|
||||
|
||||
// restore old state
|
||||
state.pos = old_pos;
|
||||
|
||||
label_end
|
||||
}
|
||||
89
crates/lib/md-footnote/src/lib.rs
Normal file
89
crates/lib/md-footnote/src/lib.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
//! A [markdown_it] plugin for parsing footnotes
|
||||
//!
|
||||
//! ```
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! md_footnote::add(parser);
|
||||
//! let node = parser.parse("[^note]\n\n[^note]: A footnote\n");
|
||||
//! ```
|
||||
use std::collections::HashMap;
|
||||
|
||||
use markdown_it::{MarkdownIt, parser::extset::RootExt};
|
||||
|
||||
pub mod back_refs;
|
||||
pub mod collect;
|
||||
pub mod definitions;
|
||||
pub mod inline;
|
||||
pub mod references;
|
||||
|
||||
// Silence lints
|
||||
#[cfg(test)]
|
||||
use md_dev as _;
|
||||
|
||||
#[cfg(test)]
|
||||
use testing as _;
|
||||
|
||||
/// Add the full footnote plugin to the parser
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
definitions::add(md);
|
||||
references::add(md);
|
||||
inline::add(md);
|
||||
collect::add(md);
|
||||
back_refs::add(md);
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
/// The set of parsed footnote definition labels,
|
||||
/// stored in the root node.
|
||||
pub struct FootnoteMap {
|
||||
def_counter: usize,
|
||||
ref_counter: usize,
|
||||
label_to_def: HashMap<String, usize>,
|
||||
def_to_refs: HashMap<usize, Vec<usize>>,
|
||||
}
|
||||
impl RootExt for FootnoteMap {}
|
||||
impl FootnoteMap {
|
||||
/// Create an ID for the definition,
|
||||
/// or return None if a definition already exists for the label
|
||||
pub fn add_def(&mut self, label: &str) -> Option<usize> {
|
||||
if self.label_to_def.contains_key(label) {
|
||||
return None;
|
||||
}
|
||||
self.def_counter += 1;
|
||||
self.label_to_def
|
||||
.insert(String::from(label), self.def_counter);
|
||||
Some(self.def_counter)
|
||||
}
|
||||
/// Create an ID for the reference and return (def_id, ref_id),
|
||||
/// or return None if no definition exists for the label
|
||||
pub fn add_ref(&mut self, label: &str) -> Option<(usize, usize)> {
|
||||
match self.label_to_def.get(label) {
|
||||
Some(def_id) => {
|
||||
self.ref_counter += 1;
|
||||
// self.def_to_refs.get_mut(&def_id).unwrap().push(self.ref_counter);
|
||||
match self.def_to_refs.get_mut(def_id) {
|
||||
Some(refs) => refs.push(self.ref_counter),
|
||||
None => {
|
||||
self.def_to_refs.insert(*def_id, vec![self.ref_counter]);
|
||||
}
|
||||
}
|
||||
Some((*def_id, self.ref_counter))
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
/// Add an inline definition and return (def_id, ref_id)
|
||||
pub fn add_inline_def(&mut self) -> (usize, usize) {
|
||||
self.def_counter += 1;
|
||||
self.ref_counter += 1;
|
||||
self.def_to_refs
|
||||
.insert(self.def_counter, vec![self.ref_counter]);
|
||||
(self.def_counter, self.ref_counter)
|
||||
}
|
||||
/// return the IDs of all references to the given definition ID
|
||||
pub fn referenced_by(&self, def_id: usize) -> Vec<usize> {
|
||||
match self.def_to_refs.get(&def_id) {
|
||||
Some(ids) => ids.clone(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
108
crates/lib/md-footnote/src/references.rs
Normal file
108
crates/lib/md-footnote/src/references.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
//! Plugin to parse footnote references
|
||||
//!
|
||||
//! ```rust
|
||||
//! let parser = &mut markdown_it::MarkdownIt::new();
|
||||
//! markdown_it::plugins::cmark::add(parser);
|
||||
//! md_footnote::references::add(parser);
|
||||
//! md_footnote::definitions::add(parser);
|
||||
//! let root = parser.parse("[^label]\n\n[^label]: This is a footnote");
|
||||
//! let mut names = vec![];
|
||||
//! root.walk(|node,_| { names.push(node.name()); });
|
||||
//! assert_eq!(names, vec![
|
||||
//! "markdown_it::parser::core::root::Root",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "md_footnote::references::FootnoteReference",
|
||||
//! "md_footnote::definitions::FootnoteDefinition",
|
||||
//! "markdown_it::plugins::cmark::block::paragraph::Paragraph",
|
||||
//! "markdown_it::parser::inline::builtin::skip_text::Text"
|
||||
//! ]);
|
||||
//! ```
|
||||
use markdown_it::parser::inline::{InlineRule, InlineState};
|
||||
use markdown_it::{MarkdownIt, Node, NodeValue, Renderer};
|
||||
|
||||
use crate::FootnoteMap;
|
||||
|
||||
/// Add the footnote reference parsing to the markdown parser
|
||||
pub fn add(md: &mut MarkdownIt) {
|
||||
// insert this rule into inline subparser
|
||||
md.inline.add_rule::<FootnoteReferenceScanner>();
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// AST node for footnote reference
|
||||
pub struct FootnoteReference {
|
||||
pub label: Option<String>,
|
||||
pub ref_id: usize,
|
||||
pub def_id: usize,
|
||||
}
|
||||
|
||||
impl NodeValue for FootnoteReference {
|
||||
fn render(&self, node: &Node, fmt: &mut dyn Renderer) {
|
||||
let mut attrs = node.attrs.clone();
|
||||
attrs.push(("class", "footnote-ref".into()));
|
||||
|
||||
fmt.open("sup", &attrs);
|
||||
fmt.open(
|
||||
"a",
|
||||
&[
|
||||
("href", format!("#fn{}", self.def_id)),
|
||||
("id", format!("fnref{}", self.ref_id)),
|
||||
],
|
||||
);
|
||||
fmt.text(&format!("[{}]", self.def_id));
|
||||
fmt.close("a");
|
||||
fmt.close("sup");
|
||||
}
|
||||
}
|
||||
|
||||
// This is an extension for the inline subparser.
|
||||
struct FootnoteReferenceScanner;
|
||||
|
||||
impl InlineRule for FootnoteReferenceScanner {
|
||||
const MARKER: char = '[';
|
||||
|
||||
fn run(state: &mut InlineState<'_, '_>) -> Option<(Node, usize)> {
|
||||
let mut chars = state.src[state.pos..state.pos_max].chars();
|
||||
|
||||
// check line starts with the correct syntax
|
||||
let Some('[') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
let Some('^') = chars.next() else {
|
||||
return None;
|
||||
};
|
||||
|
||||
// gather the label
|
||||
let mut label = String::new();
|
||||
// The labels in footnote references may not contain spaces, tabs, or newlines.
|
||||
// Backslash escapes form part of the label and do not escape anything
|
||||
loop {
|
||||
match chars.next() {
|
||||
None => return None,
|
||||
Some(']') => {
|
||||
break;
|
||||
}
|
||||
Some(' ') => return None,
|
||||
Some(c) => label.push(c),
|
||||
}
|
||||
}
|
||||
if label.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let definitions = state.root_ext.get_or_insert_default::<FootnoteMap>();
|
||||
let (def_id, ref_id) = definitions.add_ref(&label)?;
|
||||
|
||||
let length = label.len() + 3; // 3 for '[^' and ']'
|
||||
|
||||
// return new node and length of this structure
|
||||
Some((
|
||||
Node::new(FootnoteReference {
|
||||
label: Some(label),
|
||||
ref_id,
|
||||
def_id,
|
||||
}),
|
||||
length,
|
||||
))
|
||||
}
|
||||
}
|
||||
15
crates/lib/md-footnote/tests/fixtures.rs
Normal file
15
crates/lib/md-footnote/tests/fixtures.rs
Normal file
@@ -0,0 +1,15 @@
|
||||
use std::path::PathBuf;
|
||||
use testing::fixture;
|
||||
|
||||
#[fixture("tests/fixtures/[!_]*.md")]
|
||||
fn test_html(file: PathBuf) {
|
||||
let f = md_dev::read_fixture_file(file);
|
||||
|
||||
let parser = &mut markdown_it::MarkdownIt::new();
|
||||
markdown_it::plugins::sourcepos::add(parser);
|
||||
markdown_it::plugins::cmark::add(parser);
|
||||
md_footnote::add(parser);
|
||||
let actual = parser.parse(&f.input).render();
|
||||
|
||||
md_dev::assert_no_diff(f, &actual);
|
||||
}
|
||||
31
crates/lib/md-footnote/tests/fixtures/0.md
vendored
Normal file
31
crates/lib/md-footnote/tests/fixtures/0.md
vendored
Normal file
@@ -0,0 +1,31 @@
|
||||
Basic test
|
||||
|
||||
......
|
||||
|
||||
[^a]
|
||||
[^a]
|
||||
|
||||
[^a]: Multi
|
||||
line
|
||||
|
||||
Multi-paragraph
|
||||
|
||||
[^a]: duplicate
|
||||
|
||||
normal paragraph
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-2:4"><sup data-sourcepos="1:1-1:4" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup>
|
||||
<sup data-sourcepos="2:1-2:4" class="footnote-ref"><a href="#fn1" id="fnref2">[1]</a></sup></p>
|
||||
<p data-sourcepos="11:1-11:16">normal paragraph</p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="4:1-8:0" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="4:7-5:4">Multi
|
||||
line</p>
|
||||
<p data-sourcepos="7:5-7:19">Multi-paragraph <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
45
crates/lib/md-footnote/tests/fixtures/1.md
vendored
Normal file
45
crates/lib/md-footnote/tests/fixtures/1.md
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
Pandoc example
|
||||
|
||||
......
|
||||
|
||||
Here is a footnote reference,[^1] and another.[^longnote]
|
||||
|
||||
[^1]: Here is the footnote.
|
||||
|
||||
[^longnote]: Here's one with multiple blocks.
|
||||
|
||||
Subsequent paragraphs are indented to show that they
|
||||
belong to the previous footnote.
|
||||
|
||||
{ some.code }
|
||||
|
||||
The whole paragraph can be indented, or just the first
|
||||
line. In this way, multi-paragraph footnotes work like
|
||||
multi-paragraph list items.
|
||||
|
||||
This paragraph won't be part of the note, because it
|
||||
isn't indented.
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:57">Here is a footnote reference,<sup data-sourcepos="1:30-1:33" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> and another.<sup data-sourcepos="1:47-1:57" class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
|
||||
<p data-sourcepos="16:1-17:15">This paragraph won't be part of the note, because it
|
||||
isn't indented.</p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-4:0" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:7-3:27">Here is the footnote. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
<li data-sourcepos="5:1-15:0" id="fn2" class="footnote-item">
|
||||
<p data-sourcepos="5:14-5:45">Here's one with multiple blocks.</p>
|
||||
<p data-sourcepos="7:5-8:32">Subsequent paragraphs are indented to show that they
|
||||
belong to the previous footnote.</p>
|
||||
<pre><code data-sourcepos="10:9-10:21">{ some.code }
|
||||
</code></pre>
|
||||
<p data-sourcepos="12:5-14:31">The whole paragraph can be indented, or just the first
|
||||
line. In this way, multi-paragraph footnotes work like
|
||||
multi-paragraph list items. <a href="#fnref2" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
19
crates/lib/md-footnote/tests/fixtures/10.md
vendored
Normal file
19
crates/lib/md-footnote/tests/fixtures/10.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Newline after footnote identifier
|
||||
|
||||
......
|
||||
|
||||
[^a]
|
||||
|
||||
[^a]:
|
||||
b
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:4"><sup data-sourcepos="1:1-1:4" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<p data-sourcepos="4:1-4:1">b</p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-3:5" id="fn1" class="footnote-item"> <a href="#fnref1" class="footnote-backref">↩︎</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
27
crates/lib/md-footnote/tests/fixtures/2.md
vendored
Normal file
27
crates/lib/md-footnote/tests/fixtures/2.md
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
They could terminate each other
|
||||
|
||||
......
|
||||
|
||||
[^1][^2][^3]
|
||||
|
||||
[^1]: foo
|
||||
[^2]: bar
|
||||
[^3]: baz
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:12"><sup data-sourcepos="1:1-1:4" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup><sup data-sourcepos="1:5-1:8" class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup><sup data-sourcepos="1:9-1:12" class="footnote-ref"><a href="#fn3" id="fnref3">[3]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-3:9" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:7-3:9">foo <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
<li data-sourcepos="4:1-4:9" id="fn2" class="footnote-item">
|
||||
<p data-sourcepos="4:7-4:9">bar <a href="#fnref2" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
<li data-sourcepos="5:1-5:9" id="fn3" class="footnote-item">
|
||||
<p data-sourcepos="5:7-5:9">baz <a href="#fnref3" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
23
crates/lib/md-footnote/tests/fixtures/3.md
vendored
Normal file
23
crates/lib/md-footnote/tests/fixtures/3.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
||||
They could be inside blockquotes, and are lazy
|
||||
|
||||
......
|
||||
|
||||
[^foo]
|
||||
|
||||
> [^foo]: bar
|
||||
baz
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:6"><sup data-sourcepos="1:1-1:6" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<blockquote data-sourcepos="3:1-4:3">
|
||||
</blockquote>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:3-4:3" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:11-4:3">bar
|
||||
baz <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
14
crates/lib/md-footnote/tests/fixtures/4.md
vendored
Normal file
14
crates/lib/md-footnote/tests/fixtures/4.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
Their labels could not contain spaces or newlines
|
||||
|
||||
......
|
||||
|
||||
[^ foo]: bar baz
|
||||
|
||||
[^foo
|
||||
]: bar baz
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:16">[^ foo]: bar baz</p>
|
||||
<p data-sourcepos="3:1-4:10">[^foo
|
||||
]: bar baz</p>
|
||||
19
crates/lib/md-footnote/tests/fixtures/5.md
vendored
Normal file
19
crates/lib/md-footnote/tests/fixtures/5.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Duplicate footnotes:
|
||||
|
||||
......
|
||||
|
||||
[^xxxxx] [^xxxxx]
|
||||
|
||||
[^xxxxx]: foo
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:17"><sup data-sourcepos="1:1-1:8" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> <sup data-sourcepos="1:10-1:17" class="footnote-ref"><a href="#fn1" id="fnref2">[1]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-3:13" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:11-3:13">foo <a href="#fnref1" class="footnote-backref">↩︎</a> <a href="#fnref2" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
28
crates/lib/md-footnote/tests/fixtures/6.md
vendored
Normal file
28
crates/lib/md-footnote/tests/fixtures/6.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
Indents
|
||||
|
||||
|
||||
......
|
||||
|
||||
[^xxxxx] [^yyyyy]
|
||||
|
||||
[^xxxxx]: foo
|
||||
---
|
||||
|
||||
[^yyyyy]: foo
|
||||
---
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:17"><sup data-sourcepos="1:1-1:8" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> <sup data-sourcepos="1:10-1:17" class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
|
||||
<hr data-sourcepos="7:4-7:6">
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-5:0" id="fn1" class="footnote-item">
|
||||
<h2 data-sourcepos="3:11-4:7">foo</h2>
|
||||
<a href="#fnref1" class="footnote-backref">↩︎</a></li>
|
||||
<li data-sourcepos="6:1-6:13" id="fn2" class="footnote-item">
|
||||
<p data-sourcepos="6:11-6:13">foo <a href="#fnref2" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
19
crates/lib/md-footnote/tests/fixtures/8.md
vendored
Normal file
19
crates/lib/md-footnote/tests/fixtures/8.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Indents for the first line (tabs)
|
||||
|
||||
......
|
||||
|
||||
[^xxxxx]
|
||||
|
||||
[^xxxxx]: foo
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:8"><sup data-sourcepos="1:1-1:8" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-3:14" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:12-3:14">foo <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
37
crates/lib/md-footnote/tests/fixtures/9.md
vendored
Normal file
37
crates/lib/md-footnote/tests/fixtures/9.md
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
Nested blocks
|
||||
|
||||
......
|
||||
|
||||
[^a]
|
||||
|
||||
[^a]: abc
|
||||
|
||||
def
|
||||
hij
|
||||
|
||||
- list
|
||||
|
||||
> block
|
||||
|
||||
terminates here
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:4"><sup data-sourcepos="1:1-1:4" class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<p data-sourcepos="12:1-12:15">terminates here</p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li data-sourcepos="3:1-11:0" id="fn1" class="footnote-item">
|
||||
<p data-sourcepos="3:7-3:9">abc</p>
|
||||
<p data-sourcepos="5:5-6:3">def
|
||||
hij</p>
|
||||
<ul data-sourcepos="8:5-9:0">
|
||||
<li data-sourcepos="8:5-9:0">list</li>
|
||||
</ul>
|
||||
<blockquote data-sourcepos="10:5-10:11">
|
||||
<p data-sourcepos="10:7-10:11">block</p>
|
||||
</blockquote>
|
||||
<a href="#fnref1" class="footnote-backref">↩︎</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
24
crates/lib/md-footnote/tests/fixtures/_7.md
vendored
Normal file
24
crates/lib/md-footnote/tests/fixtures/_7.md
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
Indents for the first line
|
||||
.............
|
||||
|
||||
[^xxxxx] [^yyyyy]
|
||||
|
||||
[^xxxxx]: foo
|
||||
|
||||
[^yyyyy]: foo
|
||||
|
||||
.............
|
||||
|
||||
<p><sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup> <sup class="footnote-ref"><a href="#fn2" id="fnref2">[2]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
<p>foo <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
<li id="fn2" class="footnote-item">
|
||||
<pre><code>foo
|
||||
</code></pre>
|
||||
<a href="#fnref2" class="footnote-backref">↩︎</a></li>
|
||||
</ol>
|
||||
</section>
|
||||
21
crates/lib/md-footnote/tests/fixtures/inline-1.md
vendored
Normal file
21
crates/lib/md-footnote/tests/fixtures/inline-1.md
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
We support inline notes too (pandoc example)
|
||||
|
||||
......
|
||||
|
||||
Here is an inline note.^[Inlines notes are easier to write, since
|
||||
you don't have to pick an identifier and move down to type the
|
||||
note.]
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-3:6">Here is an inline note.<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
<p>Inlines notes are easier to write, since
|
||||
you don't have to pick an identifier and move down to type the
|
||||
note. <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
17
crates/lib/md-footnote/tests/fixtures/inline-2.md
vendored
Normal file
17
crates/lib/md-footnote/tests/fixtures/inline-2.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
Inline footnotes can have arbitrary markup
|
||||
|
||||
......
|
||||
|
||||
foo^[ *bar* ]
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:13">foo<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
<p> <em data-sourcepos="1:7-1:11">bar</em> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
19
crates/lib/md-footnote/tests/fixtures/inline-3.md
vendored
Normal file
19
crates/lib/md-footnote/tests/fixtures/inline-3.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
Should allow links in inline footnotes
|
||||
|
||||
......
|
||||
|
||||
Example^[this is another example [a]]
|
||||
|
||||
[a]: https://github.com
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:37">Example<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
<p>this is another example <a data-sourcepos="1:34-1:36" href="https://github.com">a</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
19
crates/lib/md-footnote/tests/fixtures/inline-4.md
vendored
Normal file
19
crates/lib/md-footnote/tests/fixtures/inline-4.md
vendored
Normal file
@@ -0,0 +1,19 @@
|
||||
nested inline footnotes
|
||||
|
||||
......
|
||||
|
||||
[Example^[this is another example [a]]][a]
|
||||
|
||||
[a]: https://github.com
|
||||
|
||||
......
|
||||
|
||||
<p data-sourcepos="1:1-1:42"><a data-sourcepos="1:1-1:42" href="https://github.com">Example<sup class="footnote-ref"><a href="#fn1" id="fnref1">[1]</a></sup></a></p>
|
||||
<hr class="footnotes-sep">
|
||||
<section class="footnotes">
|
||||
<ol class="footnotes-list">
|
||||
<li id="fn1" class="footnote-item">
|
||||
<p>this is another example <a data-sourcepos="1:35-1:37" href="https://github.com">a</a> <a href="#fnref1" class="footnote-backref">↩︎</a></p>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
Reference in New Issue
Block a user