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.
|
/// 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!()
|
||||||
}
|
}
|
||||||
|
@ -867,21 +898,25 @@ mod test {
|
||||||
fn parse_heading_multi() {
|
fn parse_heading_multi() {
|
||||||
test_parse!(
|
test_parse!(
|
||||||
concat!(
|
concat!(
|
||||||
"# 2\n",
|
"# 2\n",
|
||||||
"\n",
|
"\n",
|
||||||
" # 8\n",
|
" # 8\n",
|
||||||
" 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)), "#"),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue