block: add hierarchical heading sections

This commit is contained in:
Noah Hellman 2023-01-28 18:12:45 +01:00
parent 5347def13c
commit 530820a04e
3 changed files with 59 additions and 13 deletions

View file

@ -102,6 +102,9 @@ pub enum Container {
/// Span is first '|' character. /// Span is first '|' character.
TableRow { head: bool }, TableRow { head: bool },
/// Span is '#' characters of heading.
Section,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -132,6 +135,8 @@ struct TreeParser<'s> {
prev_blankline: bool, prev_blankline: bool,
/// Stack of currently open lists. /// Stack of currently open lists.
open_lists: Vec<OpenList>, open_lists: Vec<OpenList>,
/// Stack of currently open sections.
open_sections: Vec<u32>,
/// Alignments for each column in for the current table. /// Alignments for each column in for the current table.
alignments: Vec<Alignment>, alignments: Vec<Alignment>,
} }
@ -145,6 +150,7 @@ impl<'s> TreeParser<'s> {
prev_blankline: false, prev_blankline: false,
open_lists: Vec::new(), open_lists: Vec::new(),
alignments: 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 lines = lines(self.src).collect::<Vec<_>>();
let mut line_pos = 0; let mut line_pos = 0;
while line_pos < lines.len() { 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 { if line_count == 0 {
break; break;
} }
@ -162,11 +168,14 @@ impl<'s> TreeParser<'s> {
for _ in self.open_lists.drain(..) { for _ in self.open_lists.drain(..) {
self.tree.exit(); // list self.tree.exit(); // list
} }
for _ in self.open_sections.drain(..) {
self.tree.exit(); // section
}
self.tree.finish() self.tree.finish()
} }
/// Recursively parse a block and all of its children. Return number of lines the block uses. /// 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( BlockParser::parse(lines.iter().map(|sp| sp.of(self.src))).map_or(
0, 0,
|(indent, kind, span, line_count)| { |(indent, kind, span, line_count)| {
@ -236,7 +245,7 @@ impl<'s> TreeParser<'s> {
match kind { match kind {
Block::Atom(a) => self.tree.atom(a, span), 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(Table) => self.parse_table(lines, span),
Block::Container(c) => self.parse_container(c, lines, span, indent), 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) { if matches!(l, Leaf::CodeBlock) {
for line in lines.iter_mut() { for line in lines.iter_mut() {
let indent_line = line.len() - line.trim_start(self.src).len(); 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); self.tree.enter(Node::Leaf(l), span);
lines.iter().for_each(|line| self.tree.inline(*line)); lines.iter().for_each(|line| self.tree.inline(*line));
self.tree.exit(); self.tree.exit();
@ -288,7 +319,7 @@ impl<'s> TreeParser<'s> {
} }
} }
ListItem(..) | Footnote | Div => spaces.min(indent), ListItem(..) | Footnote | Div => spaces.min(indent),
List { .. } | DescriptionList | Table | TableRow { .. } => { List { .. } | DescriptionList | Table | TableRow { .. } | Section => {
panic!() panic!()
} }
}; };
@ -319,7 +350,7 @@ impl<'s> TreeParser<'s> {
self.tree.enter(Node::Container(c), span); self.tree.enter(Node::Container(c), span);
let mut l = 0; let mut l = 0;
while l < lines.len() { 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() { 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) => { | Block::Leaf(TableCell(..) | Caption) => {
panic!() panic!()
} }
@ -873,15 +904,19 @@ mod test {
" 12\n", " 12\n",
"15\n", // "15\n", //
), ),
(Enter(Container(Section)), "#"),
(Enter(Leaf(Heading)), "#"), (Enter(Leaf(Heading)), "#"),
(Inline, "2"), (Inline, "2"),
(Exit(Leaf(Heading)), "#"), (Exit(Leaf(Heading)), "#"),
(Atom(Blankline), "\n"), (Atom(Blankline), "\n"),
(Exit(Container(Section)), "#"),
(Enter(Container(Section)), "#"),
(Enter(Leaf(Heading)), "#"), (Enter(Leaf(Heading)), "#"),
(Inline, "8\n"), (Inline, "8\n"),
(Inline, "12\n"), (Inline, "12\n"),
(Inline, "15"), (Inline, "15"),
(Exit(Leaf(Heading)), "#"), (Exit(Leaf(Heading)), "#"),
(Exit(Container(Section)), "#"),
); );
} }

View file

@ -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::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::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)) {
@ -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::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::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)) {

View file

@ -48,6 +48,8 @@ pub enum Container<'s> {
Table, Table,
/// 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.
Section,
/// A block-level divider element. /// A block-level divider element.
Div { class: Option<&'s str> }, Div { class: Option<&'s str> },
/// A paragraph. /// A paragraph.
@ -105,6 +107,7 @@ impl<'s> Container<'s> {
| Self::Footnote { .. } | Self::Footnote { .. }
| Self::Table | Self::Table
| Self::TableRow { .. } | Self::TableRow { .. }
| Self::Section
| Self::Div { .. } | Self::Div { .. }
| Self::Paragraph | Self::Paragraph
| Self::Heading { .. } | Self::Heading { .. }
@ -141,6 +144,7 @@ impl<'s> Container<'s> {
| Self::Footnote { .. } | Self::Footnote { .. }
| Self::Table | Self::Table
| Self::TableRow { .. } | Self::TableRow { .. }
| Self::Section
| Self::Div { .. } => true, | Self::Div { .. } => true,
Self::Paragraph Self::Paragraph
| Self::Heading { .. } | Self::Heading { .. }
@ -620,6 +624,7 @@ impl<'s> Parser<'s> {
} }
Container::TableRow { head } Container::TableRow { head }
} }
block::Container::Section => Container::Section,
}, },
}; };
if enter { if enter {
@ -739,16 +744,20 @@ mod test {
fn heading() { fn heading() {
test_parse!( test_parse!(
"#\n", "#\n",
Start(Section, Attributes::new()),
Start(Heading { level: 1 }, Attributes::new()), Start(Heading { level: 1 }, Attributes::new()),
End(Heading { level: 1 }), End(Heading { level: 1 }),
End(Section),
); );
test_parse!( test_parse!(
"# abc\ndef\n", "# abc\ndef\n",
Start(Section, Attributes::new()),
Start(Heading { level: 1 }, Attributes::new()), Start(Heading { level: 1 }, Attributes::new()),
Str("abc".into()), Str("abc".into()),
Atom(Softbreak), Atom(Softbreak),
Str("def".into()), Str("def".into()),
End(Heading { level: 1 }), End(Heading { level: 1 }),
End(Section),
); );
} }