block: add hierarchical heading sections
This commit is contained in:
parent
5347def13c
commit
530820a04e
3 changed files with 59 additions and 13 deletions
61
src/block.rs
61
src/block.rs
|
@ -102,6 +102,9 @@ pub enum Container {
|
|||
|
||||
/// Span is first '|' character.
|
||||
TableRow { head: bool },
|
||||
|
||||
/// Span is '#' characters of heading.
|
||||
Section,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
@ -132,6 +135,8 @@ struct TreeParser<'s> {
|
|||
prev_blankline: bool,
|
||||
/// Stack of currently open lists.
|
||||
open_lists: Vec<OpenList>,
|
||||
/// Stack of currently open sections.
|
||||
open_sections: Vec<u32>,
|
||||
/// Alignments for each column in for the current table.
|
||||
alignments: Vec<Alignment>,
|
||||
}
|
||||
|
@ -145,6 +150,7 @@ impl<'s> TreeParser<'s> {
|
|||
prev_blankline: false,
|
||||
open_lists: Vec::new(),
|
||||
alignments: Vec::new(),
|
||||
open_sections: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -153,7 +159,7 @@ impl<'s> TreeParser<'s> {
|
|||
let mut lines = lines(self.src).collect::<Vec<_>>();
|
||||
let mut line_pos = 0;
|
||||
while line_pos < lines.len() {
|
||||
let line_count = self.parse_block(&mut lines[line_pos..]);
|
||||
let line_count = self.parse_block(&mut lines[line_pos..], true);
|
||||
if line_count == 0 {
|
||||
break;
|
||||
}
|
||||
|
@ -162,11 +168,14 @@ impl<'s> TreeParser<'s> {
|
|||
for _ in self.open_lists.drain(..) {
|
||||
self.tree.exit(); // list
|
||||
}
|
||||
for _ in self.open_sections.drain(..) {
|
||||
self.tree.exit(); // section
|
||||
}
|
||||
self.tree.finish()
|
||||
}
|
||||
|
||||
/// Recursively parse a block and all of its children. Return number of lines the block uses.
|
||||
fn parse_block(&mut self, lines: &mut [Span]) -> usize {
|
||||
fn parse_block(&mut self, lines: &mut [Span], top_level: bool) -> usize {
|
||||
BlockParser::parse(lines.iter().map(|sp| sp.of(self.src))).map_or(
|
||||
0,
|
||||
|(indent, kind, span, line_count)| {
|
||||
|
@ -236,7 +245,7 @@ impl<'s> TreeParser<'s> {
|
|||
|
||||
match kind {
|
||||
Block::Atom(a) => self.tree.atom(a, span),
|
||||
Block::Leaf(l) => self.parse_leaf(l, lines, span, indent),
|
||||
Block::Leaf(l) => self.parse_leaf(l, lines, span, indent, top_level),
|
||||
Block::Container(Table) => self.parse_table(lines, span),
|
||||
Block::Container(c) => self.parse_container(c, lines, span, indent),
|
||||
}
|
||||
|
@ -246,7 +255,14 @@ impl<'s> TreeParser<'s> {
|
|||
)
|
||||
}
|
||||
|
||||
fn parse_leaf(&mut self, l: Leaf, lines: &mut [Span], span: Span, indent: usize) {
|
||||
fn parse_leaf(
|
||||
&mut self,
|
||||
l: Leaf,
|
||||
lines: &mut [Span],
|
||||
span: Span,
|
||||
indent: usize,
|
||||
top_level: bool,
|
||||
) {
|
||||
if matches!(l, Leaf::CodeBlock) {
|
||||
for line in lines.iter_mut() {
|
||||
let indent_line = line.len() - line.trim_start(self.src).len();
|
||||
|
@ -266,6 +282,21 @@ impl<'s> TreeParser<'s> {
|
|||
}
|
||||
}
|
||||
|
||||
if matches!(l, Leaf::Heading) && top_level {
|
||||
let level = u32::try_from(span.len()).unwrap();
|
||||
let first_close = self
|
||||
.open_sections
|
||||
.iter()
|
||||
.rev()
|
||||
.rposition(|l| *l > level)
|
||||
.map_or(0, |i| i + 1);
|
||||
self.open_sections.drain(first_close..).for_each(|_| {
|
||||
self.tree.exit(); // section
|
||||
});
|
||||
self.open_sections.push(level);
|
||||
self.tree.enter(Node::Container(Section), span);
|
||||
}
|
||||
|
||||
self.tree.enter(Node::Leaf(l), span);
|
||||
lines.iter().for_each(|line| self.tree.inline(*line));
|
||||
self.tree.exit();
|
||||
|
@ -288,7 +319,7 @@ impl<'s> TreeParser<'s> {
|
|||
}
|
||||
}
|
||||
ListItem(..) | Footnote | Div => spaces.min(indent),
|
||||
List { .. } | DescriptionList | Table | TableRow { .. } => {
|
||||
List { .. } | DescriptionList | Table | TableRow { .. } | Section => {
|
||||
panic!()
|
||||
}
|
||||
};
|
||||
|
@ -319,7 +350,7 @@ impl<'s> TreeParser<'s> {
|
|||
self.tree.enter(Node::Container(c), span);
|
||||
let mut l = 0;
|
||||
while l < lines.len() {
|
||||
l += self.parse_block(&mut lines[l..]);
|
||||
l += self.parse_block(&mut lines[l..], false);
|
||||
}
|
||||
|
||||
if let Some(OpenList { depth, .. }) = self.open_lists.last() {
|
||||
|
@ -663,7 +694,7 @@ impl BlockParser {
|
|||
}
|
||||
}
|
||||
}
|
||||
Block::Container(List { .. } | DescriptionList | TableRow { .. })
|
||||
Block::Container(List { .. } | DescriptionList | TableRow { .. } | Section)
|
||||
| Block::Leaf(TableCell(..) | Caption) => {
|
||||
panic!()
|
||||
}
|
||||
|
@ -867,21 +898,25 @@ mod test {
|
|||
fn parse_heading_multi() {
|
||||
test_parse!(
|
||||
concat!(
|
||||
"# 2\n",
|
||||
"\n",
|
||||
" # 8\n",
|
||||
" 12\n",
|
||||
"15\n", //
|
||||
),
|
||||
"# 2\n",
|
||||
"\n",
|
||||
" # 8\n",
|
||||
" 12\n",
|
||||
"15\n", //
|
||||
),
|
||||
(Enter(Container(Section)), "#"),
|
||||
(Enter(Leaf(Heading)), "#"),
|
||||
(Inline, "2"),
|
||||
(Exit(Leaf(Heading)), "#"),
|
||||
(Atom(Blankline), "\n"),
|
||||
(Exit(Container(Section)), "#"),
|
||||
(Enter(Container(Section)), "#"),
|
||||
(Enter(Leaf(Heading)), "#"),
|
||||
(Inline, "8\n"),
|
||||
(Inline, "12\n"),
|
||||
(Inline, "15"),
|
||||
(Exit(Leaf(Heading)), "#"),
|
||||
(Exit(Container(Section)), "#"),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -148,6 +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::TableRow { .. } => self.out.write_str("<tr")?,
|
||||
Container::Section => self.out.write_str("<section")?,
|
||||
Container::Div { .. } => self.out.write_str("<div")?,
|
||||
Container::Paragraph => {
|
||||
if matches!(self.list_tightness.last(), Some(true)) {
|
||||
|
@ -311,6 +312,7 @@ impl<'s, I: Iterator<Item = Event<'s>>, W: std::fmt::Write> Writer<'s, I, W> {
|
|||
}
|
||||
Container::Table => self.out.write_str("</table>")?,
|
||||
Container::TableRow { .. } => self.out.write_str("</tr>")?,
|
||||
Container::Section => self.out.write_str("</section>")?,
|
||||
Container::Div { .. } => self.out.write_str("</div>")?,
|
||||
Container::Paragraph => {
|
||||
if matches!(self.list_tightness.last(), Some(true)) {
|
||||
|
|
|
@ -48,6 +48,8 @@ pub enum Container<'s> {
|
|||
Table,
|
||||
/// A row element of a table.
|
||||
TableRow { head: bool },
|
||||
/// A section belonging to a top level heading.
|
||||
Section,
|
||||
/// A block-level divider element.
|
||||
Div { class: Option<&'s str> },
|
||||
/// A paragraph.
|
||||
|
@ -105,6 +107,7 @@ impl<'s> Container<'s> {
|
|||
| Self::Footnote { .. }
|
||||
| Self::Table
|
||||
| Self::TableRow { .. }
|
||||
| Self::Section
|
||||
| Self::Div { .. }
|
||||
| Self::Paragraph
|
||||
| Self::Heading { .. }
|
||||
|
@ -141,6 +144,7 @@ impl<'s> Container<'s> {
|
|||
| Self::Footnote { .. }
|
||||
| Self::Table
|
||||
| Self::TableRow { .. }
|
||||
| Self::Section
|
||||
| Self::Div { .. } => true,
|
||||
Self::Paragraph
|
||||
| Self::Heading { .. }
|
||||
|
@ -620,6 +624,7 @@ impl<'s> Parser<'s> {
|
|||
}
|
||||
Container::TableRow { head }
|
||||
}
|
||||
block::Container::Section => Container::Section,
|
||||
},
|
||||
};
|
||||
if enter {
|
||||
|
@ -739,16 +744,20 @@ mod test {
|
|||
fn heading() {
|
||||
test_parse!(
|
||||
"#\n",
|
||||
Start(Section, Attributes::new()),
|
||||
Start(Heading { level: 1 }, Attributes::new()),
|
||||
End(Heading { level: 1 }),
|
||||
End(Section),
|
||||
);
|
||||
test_parse!(
|
||||
"# abc\ndef\n",
|
||||
Start(Section, Attributes::new()),
|
||||
Start(Heading { level: 1 }, Attributes::new()),
|
||||
Str("abc".into()),
|
||||
Atom(Softbreak),
|
||||
Str("def".into()),
|
||||
End(Heading { level: 1 }),
|
||||
End(Section),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue