impl automatic heading identifiers
This commit is contained in:
parent
b726580724
commit
60dcf09c1a
5 changed files with 323 additions and 102 deletions
|
@ -100,6 +100,11 @@ impl<'s> Attributes<'s> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn get(&self, key: &str) -> Option<&str> {
|
||||||
|
self.iter().find(|(k, _)| *k == key).map(|(_, v)| v)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn iter(&self) -> impl Iterator<Item = (&'s str, &str)> + '_ {
|
pub fn iter(&self) -> impl Iterator<Item = (&'s str, &str)> + '_ {
|
||||||
self.0
|
self.0
|
||||||
.iter()
|
.iter()
|
||||||
|
|
64
src/block.rs
64
src/block.rs
|
@ -59,7 +59,9 @@ pub enum Leaf {
|
||||||
|
|
||||||
/// Span is `#` characters.
|
/// Span is `#` characters.
|
||||||
/// Each inline is a line.
|
/// Each inline is a line.
|
||||||
Heading,
|
Heading {
|
||||||
|
has_section: bool,
|
||||||
|
},
|
||||||
|
|
||||||
/// Span is '|'.
|
/// Span is '|'.
|
||||||
/// Has zero or one inline for the cell contents.
|
/// Has zero or one inline for the cell contents.
|
||||||
|
@ -254,7 +256,7 @@ impl<'s> TreeParser<'s> {
|
||||||
|
|
||||||
fn parse_leaf(
|
fn parse_leaf(
|
||||||
&mut self,
|
&mut self,
|
||||||
leaf: Leaf,
|
mut leaf: Leaf,
|
||||||
k: &Kind,
|
k: &Kind,
|
||||||
span: Span,
|
span: Span,
|
||||||
lines: &mut [Span],
|
lines: &mut [Span],
|
||||||
|
@ -300,6 +302,10 @@ impl<'s> TreeParser<'s> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Leaf::Heading { has_section } = &mut leaf {
|
||||||
|
*has_section = top_level;
|
||||||
|
}
|
||||||
|
|
||||||
self.tree.enter(Node::Leaf(leaf), span);
|
self.tree.enter(Node::Leaf(leaf), span);
|
||||||
lines
|
lines
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -573,7 +579,7 @@ impl From<&Kind> for Block {
|
||||||
match kind {
|
match kind {
|
||||||
Kind::Atom(a) => Self::Atom(*a),
|
Kind::Atom(a) => Self::Atom(*a),
|
||||||
Kind::Paragraph => Self::Leaf(Paragraph),
|
Kind::Paragraph => Self::Leaf(Paragraph),
|
||||||
Kind::Heading { .. } => Self::Leaf(Heading),
|
Kind::Heading { .. } => Self::Leaf(Heading { has_section: false }),
|
||||||
Kind::Fenced {
|
Kind::Fenced {
|
||||||
kind: FenceKind::CodeBlock(..),
|
kind: FenceKind::CodeBlock(..),
|
||||||
..
|
..
|
||||||
|
@ -983,13 +989,13 @@ mod test {
|
||||||
"## b\n", //
|
"## b\n", //
|
||||||
),
|
),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "a"),
|
(Inline, "a"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Enter(Container(Section)), "##"),
|
(Enter(Container(Section)), "##"),
|
||||||
(Enter(Leaf(Heading)), "##"),
|
(Enter(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Inline, "b"),
|
(Inline, "b"),
|
||||||
(Exit(Leaf(Heading)), "##"),
|
(Exit(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Exit(Container(Section)), "##"),
|
(Exit(Container(Section)), "##"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
);
|
);
|
||||||
|
@ -1003,9 +1009,9 @@ mod test {
|
||||||
"heading\n", //
|
"heading\n", //
|
||||||
),
|
),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "heading"),
|
(Inline, "heading"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1021,17 +1027,17 @@ mod test {
|
||||||
"15\n", //
|
"15\n", //
|
||||||
),
|
),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "2"),
|
(Inline, "2"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "8\n"),
|
(Inline, "8\n"),
|
||||||
(Inline, "12\n"),
|
(Inline, "12\n"),
|
||||||
(Inline, "15"),
|
(Inline, "15"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1045,11 +1051,11 @@ mod test {
|
||||||
"c\n", //
|
"c\n", //
|
||||||
),
|
),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "a\n"),
|
(Inline, "a\n"),
|
||||||
(Inline, "b\n"),
|
(Inline, "b\n"),
|
||||||
(Inline, "c"),
|
(Inline, "c"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1071,39 +1077,39 @@ mod test {
|
||||||
"# b\n",
|
"# b\n",
|
||||||
),
|
),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "a"),
|
(Inline, "a"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Enter(Container(Section)), "##"),
|
(Enter(Container(Section)), "##"),
|
||||||
(Enter(Leaf(Heading)), "##"),
|
(Enter(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Inline, "aa"),
|
(Inline, "aa"),
|
||||||
(Exit(Leaf(Heading)), "##"),
|
(Exit(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Enter(Container(Section)), "####"),
|
(Enter(Container(Section)), "####"),
|
||||||
(Enter(Leaf(Heading)), "####"),
|
(Enter(Leaf(Heading { has_section: true })), "####"),
|
||||||
(Inline, "aaaa"),
|
(Inline, "aaaa"),
|
||||||
(Exit(Leaf(Heading)), "####"),
|
(Exit(Leaf(Heading { has_section: true })), "####"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Exit(Container(Section)), "####"),
|
(Exit(Container(Section)), "####"),
|
||||||
(Exit(Container(Section)), "##"),
|
(Exit(Container(Section)), "##"),
|
||||||
(Enter(Container(Section)), "##"),
|
(Enter(Container(Section)), "##"),
|
||||||
(Enter(Leaf(Heading)), "##"),
|
(Enter(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Inline, "ab"),
|
(Inline, "ab"),
|
||||||
(Exit(Leaf(Heading)), "##"),
|
(Exit(Leaf(Heading { has_section: true })), "##"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Enter(Container(Section)), "###"),
|
(Enter(Container(Section)), "###"),
|
||||||
(Enter(Leaf(Heading)), "###"),
|
(Enter(Leaf(Heading { has_section: true })), "###"),
|
||||||
(Inline, "aba"),
|
(Inline, "aba"),
|
||||||
(Exit(Leaf(Heading)), "###"),
|
(Exit(Leaf(Heading { has_section: true })), "###"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Exit(Container(Section)), "###"),
|
(Exit(Container(Section)), "###"),
|
||||||
(Exit(Container(Section)), "##"),
|
(Exit(Container(Section)), "##"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
(Enter(Container(Section)), "#"),
|
(Enter(Container(Section)), "#"),
|
||||||
(Enter(Leaf(Heading)), "#"),
|
(Enter(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Inline, "b"),
|
(Inline, "b"),
|
||||||
(Exit(Leaf(Heading)), "#"),
|
(Exit(Leaf(Heading { has_section: true })), "#"),
|
||||||
(Exit(Container(Section)), "#"),
|
(Exit(Container(Section)), "#"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1141,9 +1147,9 @@ mod test {
|
||||||
(Inline, "a"),
|
(Inline, "a"),
|
||||||
(Exit(Leaf(Paragraph)), ""),
|
(Exit(Leaf(Paragraph)), ""),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Enter(Leaf(Heading)), "##"),
|
(Enter(Leaf(Heading { has_section: false })), "##"),
|
||||||
(Inline, "hl"),
|
(Inline, "hl"),
|
||||||
(Exit(Leaf(Heading)), "##"),
|
(Exit(Leaf(Heading { has_section: false })), "##"),
|
||||||
(Atom(Blankline), "\n"),
|
(Atom(Blankline), "\n"),
|
||||||
(Enter(Leaf(Paragraph)), ""),
|
(Enter(Leaf(Paragraph)), ""),
|
||||||
(Inline, "para"),
|
(Inline, "para"),
|
||||||
|
|
20
src/html.rs
20
src/html.rs
|
@ -148,7 +148,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||||
}
|
}
|
||||||
Container::Table => self.out.write_str("<table")?,
|
Container::Table => self.out.write_str("<table")?,
|
||||||
Container::TableRow { .. } => self.out.write_str("<tr")?,
|
Container::TableRow { .. } => self.out.write_str("<tr")?,
|
||||||
Container::Section => self.out.write_str("<section")?,
|
Container::Section { .. } => self.out.write_str("<section")?,
|
||||||
Container::Div { .. } => self.out.write_str("<div")?,
|
Container::Div { .. } => self.out.write_str("<div")?,
|
||||||
Container::Paragraph => {
|
Container::Paragraph => {
|
||||||
if matches!(self.list_tightness.last(), Some(true)) {
|
if matches!(self.list_tightness.last(), Some(true)) {
|
||||||
|
@ -156,7 +156,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||||
}
|
}
|
||||||
self.out.write_str("<p")?;
|
self.out.write_str("<p")?;
|
||||||
}
|
}
|
||||||
Container::Heading { level } => write!(self.out, "<h{}", level)?,
|
Container::Heading { level, .. } => write!(self.out, "<h{}", level)?,
|
||||||
Container::TableCell { head: false, .. } => self.out.write_str("<td")?,
|
Container::TableCell { head: false, .. } => self.out.write_str("<td")?,
|
||||||
Container::TableCell { head: true, .. } => self.out.write_str("<th")?,
|
Container::TableCell { head: true, .. } => self.out.write_str("<th")?,
|
||||||
Container::Caption => self.out.write_str("<caption")?,
|
Container::Caption => self.out.write_str("<caption")?,
|
||||||
|
@ -196,6 +196,18 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||||
write!(self.out, r#" {}="{}""#, a, v)?;
|
write!(self.out, r#" {}="{}""#, a, v)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Container::Heading {
|
||||||
|
id,
|
||||||
|
has_section: false,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| Container::Section { id } = &c
|
||||||
|
{
|
||||||
|
if !attrs.iter().any(|(a, _)| a == "id") {
|
||||||
|
write!(self.out, r#" id="{}""#, id)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if attrs.iter().any(|(a, _)| a == "class")
|
if attrs.iter().any(|(a, _)| a == "class")
|
||||||
|| matches!(
|
|| matches!(
|
||||||
c,
|
c,
|
||||||
|
@ -312,7 +324,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||||
}
|
}
|
||||||
Container::Table => self.out.write_str("</table>")?,
|
Container::Table => self.out.write_str("</table>")?,
|
||||||
Container::TableRow { .. } => self.out.write_str("</tr>")?,
|
Container::TableRow { .. } => self.out.write_str("</tr>")?,
|
||||||
Container::Section => self.out.write_str("</section>")?,
|
Container::Section { .. } => self.out.write_str("</section>")?,
|
||||||
Container::Div { .. } => self.out.write_str("</div>")?,
|
Container::Div { .. } => self.out.write_str("</div>")?,
|
||||||
Container::Paragraph => {
|
Container::Paragraph => {
|
||||||
if matches!(self.list_tightness.last(), Some(true)) {
|
if matches!(self.list_tightness.last(), Some(true)) {
|
||||||
|
@ -333,7 +345,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
||||||
}
|
}
|
||||||
self.out.write_str("</p>")?;
|
self.out.write_str("</p>")?;
|
||||||
}
|
}
|
||||||
Container::Heading { level } => write!(self.out, "</h{}>", level)?,
|
Container::Heading { level, .. } => write!(self.out, "</h{}>", level)?,
|
||||||
Container::TableCell { head: false, .. } => self.out.write_str("</td>")?,
|
Container::TableCell { head: false, .. } => self.out.write_str("</td>")?,
|
||||||
Container::TableCell { head: true, .. } => self.out.write_str("</th>")?,
|
Container::TableCell { head: true, .. } => self.out.write_str("</th>")?,
|
||||||
Container::Caption => self.out.write_str("</caption>")?,
|
Container::Caption => self.out.write_str("</caption>")?,
|
||||||
|
|
332
src/lib.rs
332
src/lib.rs
|
@ -1,3 +1,5 @@
|
||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
pub mod html;
|
pub mod html;
|
||||||
|
|
||||||
mod attr;
|
mod attr;
|
||||||
|
@ -49,13 +51,17 @@ pub enum Container<'s> {
|
||||||
/// A row element of a table.
|
/// A row element of a table.
|
||||||
TableRow { head: bool },
|
TableRow { head: bool },
|
||||||
/// A section belonging to a top level heading.
|
/// A section belonging to a top level heading.
|
||||||
Section,
|
Section { id: CowStr<'s> },
|
||||||
/// A block-level divider element.
|
/// A block-level divider element.
|
||||||
Div { class: Option<&'s str> },
|
Div { class: Option<&'s str> },
|
||||||
/// A paragraph.
|
/// A paragraph.
|
||||||
Paragraph,
|
Paragraph,
|
||||||
/// A heading.
|
/// A heading.
|
||||||
Heading { level: u16 },
|
Heading {
|
||||||
|
level: u16,
|
||||||
|
has_section: bool,
|
||||||
|
id: CowStr<'s>,
|
||||||
|
},
|
||||||
/// A cell element of row within a table.
|
/// A cell element of row within a table.
|
||||||
TableCell { alignment: Alignment, head: bool },
|
TableCell { alignment: Alignment, head: bool },
|
||||||
/// A caption within a table.
|
/// A caption within a table.
|
||||||
|
@ -107,7 +113,7 @@ impl<'s> Container<'s> {
|
||||||
| Self::Footnote { .. }
|
| Self::Footnote { .. }
|
||||||
| Self::Table
|
| Self::Table
|
||||||
| Self::TableRow { .. }
|
| Self::TableRow { .. }
|
||||||
| Self::Section
|
| Self::Section { .. }
|
||||||
| Self::Div { .. }
|
| Self::Div { .. }
|
||||||
| Self::Paragraph
|
| Self::Paragraph
|
||||||
| Self::Heading { .. }
|
| Self::Heading { .. }
|
||||||
|
@ -144,7 +150,7 @@ impl<'s> Container<'s> {
|
||||||
| Self::Footnote { .. }
|
| Self::Footnote { .. }
|
||||||
| Self::Table
|
| Self::Table
|
||||||
| Self::TableRow { .. }
|
| Self::TableRow { .. }
|
||||||
| Self::Section
|
| Self::Section { .. }
|
||||||
| Self::Div { .. } => true,
|
| Self::Div { .. } => true,
|
||||||
Self::Paragraph
|
Self::Paragraph
|
||||||
| Self::Heading { .. }
|
| Self::Heading { .. }
|
||||||
|
@ -321,15 +327,12 @@ impl OrderedListStyle {
|
||||||
pub struct Parser<'s> {
|
pub struct Parser<'s> {
|
||||||
src: &'s str,
|
src: &'s str,
|
||||||
|
|
||||||
/// Link definitions encountered during block parse, written once.
|
/// Block tree parsed at first.
|
||||||
link_definitions: std::collections::HashMap<&'s str, (CowStr<'s>, attr::Attributes<'s>)>,
|
|
||||||
|
|
||||||
/// Block tree cursor.
|
|
||||||
tree: block::Tree,
|
tree: block::Tree,
|
||||||
/// Spans to the inlines in the block currently being parsed.
|
|
||||||
inlines: span::InlineSpans<'s>,
|
/// Contents obtained by the prepass.
|
||||||
/// Inline parser, recreated for each new inline.
|
pre_pass: PrePass<'s>,
|
||||||
inline_parser: Option<inline::Parser<span::InlineCharsIter<'s>>>,
|
|
||||||
/// Last parsed block attributes
|
/// Last parsed block attributes
|
||||||
block_attributes: Attributes<'s>,
|
block_attributes: Attributes<'s>,
|
||||||
|
|
||||||
|
@ -344,47 +347,168 @@ pub struct Parser<'s> {
|
||||||
footnote_index: usize,
|
footnote_index: usize,
|
||||||
/// Currently within a footnote.
|
/// Currently within a footnote.
|
||||||
footnote_active: bool,
|
footnote_active: bool,
|
||||||
|
|
||||||
|
/// Spans to the inlines in the leaf block currently being parsed.
|
||||||
|
inlines: span::InlineSpans<'s>,
|
||||||
|
/// Inline parser, recreated for each new inline.
|
||||||
|
inline_parser: Option<inline::Parser<span::InlineCharsIter<'s>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Heading {
|
||||||
|
/// Location of heading in src.
|
||||||
|
location: usize,
|
||||||
|
/// Automatically generated id from heading text.
|
||||||
|
id_auto: String,
|
||||||
|
/// Overriding id from an explicit attribute on the heading.
|
||||||
|
id_override: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Because of potential future references, an initial pass is required to obtain all definitions.
|
||||||
|
struct PrePass<'s> {
|
||||||
|
/// Link definitions and their attributes.
|
||||||
|
link_definitions: std::collections::HashMap<&'s str, (CowStr<'s>, attr::Attributes<'s>)>,
|
||||||
|
/// Cache of all heading ids.
|
||||||
|
headings: Vec<Heading>,
|
||||||
|
/// Indices to headings sorted lexicographically.
|
||||||
|
headings_lex: Vec<usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'s> PrePass<'s> {
|
||||||
|
#[must_use]
|
||||||
|
fn new(src: &'s str, mut tree: block::Tree) -> Self {
|
||||||
|
let mut link_definitions = std::collections::HashMap::new();
|
||||||
|
let mut headings: Vec<Heading> = Vec::new();
|
||||||
|
|
||||||
|
let mut inlines = span::InlineSpans::new(src);
|
||||||
|
|
||||||
|
let mut attr_prev: Option<Span> = None;
|
||||||
|
while let Some(e) = tree.next() {
|
||||||
|
match e.kind {
|
||||||
|
tree::EventKind::Enter(block::Node::Leaf(block::Leaf::LinkDefinition)) => {
|
||||||
|
// All link definition tags have to be obtained initially, as references can
|
||||||
|
// appear before the definition.
|
||||||
|
let tag = e.span.of(src);
|
||||||
|
let attrs =
|
||||||
|
attr_prev.map_or_else(Attributes::new, |sp| attr::parse(sp.of(src)));
|
||||||
|
let url = match tree.count_children() {
|
||||||
|
0 => "".into(),
|
||||||
|
1 => tree.take_inlines().next().unwrap().of(src).trim().into(),
|
||||||
|
_ => tree.take_inlines().map(|sp| sp.of(src).trim()).collect(),
|
||||||
|
};
|
||||||
|
link_definitions.insert(tag, (url, attrs));
|
||||||
|
}
|
||||||
|
tree::EventKind::Enter(block::Node::Leaf(block::Leaf::Heading { .. })) => {
|
||||||
|
// All headings ids have to be obtained initially, as references can appear
|
||||||
|
// before the heading. Additionally, determining the id requires inline parsing
|
||||||
|
// as formatting must be removed.
|
||||||
|
//
|
||||||
|
// We choose to parse all headers twice instead of caching them.
|
||||||
|
let attrs = attr_prev.map(|sp| attr::parse(sp.of(src)));
|
||||||
|
let id_override = attrs
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|attrs| attrs.get("id"))
|
||||||
|
.map(ToString::to_string);
|
||||||
|
|
||||||
|
inlines.set_spans(tree.take_inlines());
|
||||||
|
let mut id_auto = String::new();
|
||||||
|
inline::Parser::new(inlines.chars()).for_each(|ev| match ev.kind {
|
||||||
|
inline::EventKind::Str => {
|
||||||
|
let mut chars = inlines.slice(ev.span).chars().peekable();
|
||||||
|
while let Some(c) = chars.next() {
|
||||||
|
if c.is_whitespace() {
|
||||||
|
while chars.peek().map_or(false, |c| c.is_whitespace()) {
|
||||||
|
chars.next();
|
||||||
|
}
|
||||||
|
if !id_auto.is_empty() {
|
||||||
|
id_auto.push('-');
|
||||||
|
}
|
||||||
|
} else if !c.is_ascii_punctuation() || matches!(c, '-' | '_') {
|
||||||
|
id_auto.push(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inline::EventKind::Atom(inline::Atom::Softbreak) => {
|
||||||
|
id_auto.push('-');
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
|
id_auto.drain(id_auto.trim_end_matches('-').len()..);
|
||||||
|
|
||||||
|
// ensure id unique
|
||||||
|
if headings.iter().any(|h| h.id_auto == id_auto) || id_auto.is_empty() {
|
||||||
|
if id_auto.is_empty() {
|
||||||
|
id_auto.push('s');
|
||||||
|
}
|
||||||
|
let mut num = 1;
|
||||||
|
id_auto.push('-');
|
||||||
|
let i_num = id_auto.len();
|
||||||
|
write!(id_auto, "{}", num).unwrap();
|
||||||
|
while headings.iter().any(|h| h.id_auto == id_auto) {
|
||||||
|
num += 1;
|
||||||
|
id_auto.drain(i_num..);
|
||||||
|
write!(id_auto, "{}", num).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
headings.push(Heading {
|
||||||
|
location: e.span.start(),
|
||||||
|
id_auto,
|
||||||
|
id_override,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
tree::EventKind::Atom(block::Atom::Attributes) => {
|
||||||
|
attr_prev = Some(e.span);
|
||||||
|
}
|
||||||
|
tree::EventKind::Enter(..)
|
||||||
|
| tree::EventKind::Exit(block::Node::Container(block::Container::Section {
|
||||||
|
..
|
||||||
|
})) => {}
|
||||||
|
_ => {
|
||||||
|
attr_prev = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut headings_lex = (0..headings.len()).collect::<Vec<_>>();
|
||||||
|
headings_lex.sort_by_key(|i| &headings[*i].id_auto);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
link_definitions,
|
||||||
|
headings,
|
||||||
|
headings_lex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading_id(&self, i: usize) -> &str {
|
||||||
|
let h = &self.headings[i];
|
||||||
|
h.id_override.as_ref().unwrap_or(&h.id_auto)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading_id_by_location(&self, location: usize) -> Option<&str> {
|
||||||
|
self.headings
|
||||||
|
.binary_search_by_key(&location, |h| h.location)
|
||||||
|
.ok()
|
||||||
|
.map(|i| self.heading_id(i))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading_id_by_tag(&self, tag: &str) -> Option<&str> {
|
||||||
|
self.headings_lex
|
||||||
|
.binary_search_by_key(&tag, |i| &self.headings[*i].id_auto)
|
||||||
|
.ok()
|
||||||
|
.map(|i| self.heading_id(i))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'s> Parser<'s> {
|
impl<'s> Parser<'s> {
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(src: &'s str) -> Self {
|
pub fn new(src: &'s str) -> Self {
|
||||||
let tree = block::parse(src);
|
let tree = block::parse(src);
|
||||||
|
let pre_pass = PrePass::new(src, tree.clone());
|
||||||
// All link definition tags have to be obtained initially, as references can appear before
|
|
||||||
// the definition.
|
|
||||||
let link_definitions = {
|
|
||||||
let mut branch = tree.clone();
|
|
||||||
let mut defs = std::collections::HashMap::new();
|
|
||||||
let mut attr_prev: Option<Span> = None;
|
|
||||||
while let Some(e) = branch.next() {
|
|
||||||
if let tree::EventKind::Enter(block::Node::Leaf(block::Leaf::LinkDefinition)) =
|
|
||||||
e.kind
|
|
||||||
{
|
|
||||||
let tag = e.span.of(src);
|
|
||||||
let attrs =
|
|
||||||
attr_prev.map_or_else(Attributes::new, |sp| attr::parse(sp.of(src)));
|
|
||||||
let url = match branch.count_children() {
|
|
||||||
0 => "".into(),
|
|
||||||
1 => branch.take_inlines().next().unwrap().of(src).trim().into(),
|
|
||||||
_ => branch.take_inlines().map(|sp| sp.of(src).trim()).collect(),
|
|
||||||
};
|
|
||||||
defs.insert(tag, (url, attrs));
|
|
||||||
} else if let tree::EventKind::Atom(block::Atom::Attributes) = e.kind {
|
|
||||||
attr_prev = Some(e.span);
|
|
||||||
} else {
|
|
||||||
attr_prev = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
defs
|
|
||||||
};
|
|
||||||
|
|
||||||
let branch = tree.clone();
|
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
src,
|
src,
|
||||||
link_definitions,
|
tree,
|
||||||
tree: branch,
|
pre_pass,
|
||||||
block_attributes: Attributes::new(),
|
block_attributes: Attributes::new(),
|
||||||
table_head_row: false,
|
table_head_row: false,
|
||||||
footnote_references: Vec::new(),
|
footnote_references: Vec::new(),
|
||||||
|
@ -453,12 +577,18 @@ impl<'s> Parser<'s> {
|
||||||
CowStr::Owned(s) => s.replace('\n', " ").into(),
|
CowStr::Owned(s) => s.replace('\n', " ").into(),
|
||||||
s @ CowStr::Borrowed(_) => s,
|
s @ CowStr::Borrowed(_) => s,
|
||||||
};
|
};
|
||||||
let (url, attrs_def) = self
|
let link_def =
|
||||||
.link_definitions
|
self.pre_pass.link_definitions.get(tag.as_ref()).cloned();
|
||||||
.get(tag.as_ref())
|
|
||||||
.cloned()
|
let url = if let Some((url, attrs_def)) = link_def {
|
||||||
.unwrap_or_else(|| ("".into(), Attributes::new()));
|
|
||||||
attributes.union(attrs_def);
|
attributes.union(attrs_def);
|
||||||
|
url
|
||||||
|
} else {
|
||||||
|
self.pre_pass
|
||||||
|
.heading_id_by_tag(tag.as_ref())
|
||||||
|
.map_or_else(|| "".into(), |id| format!("#{}", id).into())
|
||||||
|
};
|
||||||
|
|
||||||
if matches!(c, inline::Container::ReferenceLink) {
|
if matches!(c, inline::Container::ReferenceLink) {
|
||||||
Container::Link(url, LinkType::Span(SpanLinkType::Reference))
|
Container::Link(url, LinkType::Span(SpanLinkType::Reference))
|
||||||
} else {
|
} else {
|
||||||
|
@ -561,8 +691,15 @@ impl<'s> Parser<'s> {
|
||||||
}
|
}
|
||||||
match l {
|
match l {
|
||||||
block::Leaf::Paragraph => Container::Paragraph,
|
block::Leaf::Paragraph => Container::Paragraph,
|
||||||
block::Leaf::Heading => Container::Heading {
|
block::Leaf::Heading { has_section } => Container::Heading {
|
||||||
level: content.len().try_into().unwrap(),
|
level: content.len().try_into().unwrap(),
|
||||||
|
has_section,
|
||||||
|
id: self
|
||||||
|
.pre_pass
|
||||||
|
.heading_id_by_location(ev.span.start())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
},
|
},
|
||||||
block::Leaf::CodeBlock => {
|
block::Leaf::CodeBlock => {
|
||||||
if let Some(format) = content.strip_prefix('=') {
|
if let Some(format) = content.strip_prefix('=') {
|
||||||
|
@ -631,7 +768,14 @@ impl<'s> Parser<'s> {
|
||||||
}
|
}
|
||||||
Container::TableRow { head }
|
Container::TableRow { head }
|
||||||
}
|
}
|
||||||
block::Container::Section => Container::Section,
|
block::Container::Section => Container::Section {
|
||||||
|
id: self
|
||||||
|
.pre_pass
|
||||||
|
.heading_id_by_location(ev.span.start())
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string()
|
||||||
|
.into(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
if enter {
|
if enter {
|
||||||
|
@ -751,20 +895,49 @@ mod test {
|
||||||
fn heading() {
|
fn heading() {
|
||||||
test_parse!(
|
test_parse!(
|
||||||
"#\n",
|
"#\n",
|
||||||
Start(Section, Attributes::new()),
|
Start(Section { id: "s-1".into() }, Attributes::new()),
|
||||||
Start(Heading { level: 1 }, Attributes::new()),
|
Start(
|
||||||
End(Heading { level: 1 }),
|
Heading {
|
||||||
End(Section),
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "s-1".into()
|
||||||
|
},
|
||||||
|
Attributes::new()
|
||||||
|
),
|
||||||
|
End(Heading {
|
||||||
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "s-1".into()
|
||||||
|
}),
|
||||||
|
End(Section { id: "s-1".into() }),
|
||||||
);
|
);
|
||||||
test_parse!(
|
test_parse!(
|
||||||
"# abc\ndef\n",
|
"# abc\ndef\n",
|
||||||
Start(Section, Attributes::new()),
|
Start(
|
||||||
Start(Heading { level: 1 }, Attributes::new()),
|
Section {
|
||||||
|
id: "abc-def".into()
|
||||||
|
},
|
||||||
|
Attributes::new()
|
||||||
|
),
|
||||||
|
Start(
|
||||||
|
Heading {
|
||||||
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "abc-def".into()
|
||||||
|
},
|
||||||
|
Attributes::new()
|
||||||
|
),
|
||||||
Str("abc".into()),
|
Str("abc".into()),
|
||||||
Atom(Softbreak),
|
Atom(Softbreak),
|
||||||
Str("def".into()),
|
Str("def".into()),
|
||||||
End(Heading { level: 1 }),
|
End(Heading {
|
||||||
End(Section),
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "abc-def".into(),
|
||||||
|
}),
|
||||||
|
End(Section {
|
||||||
|
id: "abc-def".into()
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -776,16 +949,41 @@ mod test {
|
||||||
"{a=b}\n",
|
"{a=b}\n",
|
||||||
"# def\n", //
|
"# def\n", //
|
||||||
),
|
),
|
||||||
Start(Section, Attributes::new()),
|
Start(Section { id: "abc".into() }, Attributes::new()),
|
||||||
Start(Heading { level: 1 }, Attributes::new()),
|
Start(
|
||||||
|
Heading {
|
||||||
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "abc".into()
|
||||||
|
},
|
||||||
|
Attributes::new()
|
||||||
|
),
|
||||||
Str("abc".into()),
|
Str("abc".into()),
|
||||||
End(Heading { level: 1 }),
|
End(Heading {
|
||||||
End(Section),
|
level: 1,
|
||||||
Start(Section, [("a", "b")].into_iter().collect(),),
|
has_section: true,
|
||||||
Start(Heading { level: 1 }, Attributes::new(),),
|
id: "abc".into(),
|
||||||
|
}),
|
||||||
|
End(Section { id: "abc".into() }),
|
||||||
|
Start(
|
||||||
|
Section { id: "def".into() },
|
||||||
|
[("a", "b")].into_iter().collect(),
|
||||||
|
),
|
||||||
|
Start(
|
||||||
|
Heading {
|
||||||
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "def".into()
|
||||||
|
},
|
||||||
|
Attributes::new(),
|
||||||
|
),
|
||||||
Str("def".into()),
|
Str("def".into()),
|
||||||
End(Heading { level: 1 }),
|
End(Heading {
|
||||||
End(Section),
|
level: 1,
|
||||||
|
has_section: true,
|
||||||
|
id: "def".into(),
|
||||||
|
}),
|
||||||
|
End(Section { id: "def".into() }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -302,7 +302,7 @@ impl<'s, 'i> DiscontinuousString<'s> for InlineSpansSlice<'s, 'i> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type InlineSpansSliceIter<'i> = std::iter::Chain<
|
pub type InlineSpansSliceIter<'i> = std::iter::Chain<
|
||||||
std::iter::Chain<std::iter::Once<Span>, std::iter::Copied<std::slice::Iter<'i, Span>>>,
|
std::iter::Chain<std::iter::Once<Span>, std::iter::Copied<std::slice::Iter<'i, Span>>>,
|
||||||
std::iter::Once<Span>,
|
std::iter::Once<Span>,
|
||||||
>;
|
>;
|
||||||
|
|
Loading…
Reference in a new issue