impl automatic heading identifiers

This commit is contained in:
Noah Hellman 2023-01-29 15:10:01 +01:00
parent b726580724
commit 60dcf09c1a
5 changed files with 323 additions and 102 deletions

View file

@ -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()

View file

@ -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"),

View file

@ -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>")?,

View file

@ -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() }),
); );
} }

View file

@ -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>,
>; >;