asciidork_parser/tasks/
parse_section.rs

1use crate::internal::*;
2
3impl<'arena> Parser<'arena> {
4  pub(crate) fn parse_section(&mut self) -> Result<Option<Section<'arena>>> {
5    let Some(peeked) = self.peek_section()? else {
6      return Ok(None);
7    };
8
9    if peeked.semantic_level == 0 && self.document.meta.get_doctype() != DocType::Book {
10      self.err_line(
11        "Level 0 section allowed only in doctype=book",
12        peeked.lines.current().unwrap(),
13      )?;
14    } else if peeked.semantic_level == 0 {
15      self.restore_peeked_section(peeked);
16      return Ok(None);
17    }
18    Ok(Some(self.parse_peeked_section(peeked)?))
19  }
20
21  pub(crate) fn peek_section(&mut self) -> Result<Option<PeekedSection<'arena>>> {
22    let Some(mut lines) = self.read_lines()? else {
23      return Ok(None);
24    };
25
26    lines.discard_leading_comment_lines();
27    if lines.is_empty() {
28      return self.peek_section();
29    }
30
31    let meta = self.parse_chunk_meta(&mut lines)?;
32
33    let Some(line) = lines.current() else {
34      if !meta.is_empty() {
35        self.err_line_starting("Unattached block metadata", meta.start_loc)?;
36      }
37      self.restore_peeked_meta(meta);
38      return Ok(None);
39    };
40
41    let Some(level) = self.line_heading_level(line) else {
42      self.restore_peeked(lines, meta);
43      return Ok(None);
44    };
45
46    Ok(Some(PeekedSection {
47      meta,
48      lines,
49      semantic_level: level,
50      authored_level: level,
51    }))
52  }
53
54  pub(crate) fn parse_peeked_section(
55    &mut self,
56    peeked: PeekedSection<'arena>,
57  ) -> Result<Section<'arena>> {
58    let special_sect = peeked.special_sect();
59    let PeekedSection {
60      meta,
61      mut lines,
62      semantic_level,
63      authored_level,
64    } = peeked;
65    let last_level = self.ctx.section_level;
66    self.ctx.section_level = semantic_level;
67    let mut heading_line = lines.consume_current().unwrap();
68    let mut loc: MultiSourceLocation = heading_line.loc().unwrap().into();
69    let equals = heading_line.consume_current().unwrap();
70    heading_line.discard_assert(TokenKind::Whitespace);
71    let id = self.section_id(&heading_line, &meta.attrs);
72
73    let out_of_sequence = semantic_level > last_level && semantic_level - last_level > 1;
74    if out_of_sequence {
75      self.err_token_full(
76        format!(
77          "Section title out of sequence: expected level {} `{}`",
78          last_level + 1,
79          "=".repeat((last_level + 2) as usize)
80        ),
81        &equals,
82      )?;
83    }
84
85    let heading = self.parse_inlines(&mut heading_line.into_lines())?;
86    if !out_of_sequence {
87      self.push_toc_node(
88        authored_level,
89        semantic_level,
90        &heading,
91        id.as_ref(),
92        meta.attrs.special_sect(),
93      );
94    }
95
96    if let Some(id) = &id {
97      let reftext = meta
98        .attrs
99        .iter()
100        .find_map(|a| a.named.get("reftext"))
101        .cloned();
102      self.document.anchors.borrow_mut().insert(
103        id.clone(),
104        Anchor {
105          reftext,
106          title: heading.clone(),
107          source_loc: None,
108          source_idx: self.lexer.source_idx(),
109          is_biblio: false,
110        },
111      );
112    }
113
114    if meta.attrs.str_positional_at(0) == Some("bibliography") {
115      self.ctx.bibliography_ctx = BiblioContext::Section;
116    }
117
118    self.restore_lines(lines);
119    let mut blocks = BumpVec::new_in(self.bump);
120    while let Some(inner) = self.parse_block()? {
121      loc.extend_end(&inner.loc);
122      blocks.push(inner);
123    }
124
125    if let Some(special_sect) = special_sect {
126      if !special_sect.supports_subsections() {
127        for block in &blocks {
128          if let BlockContent::Section(subsection) = &block.content {
129            self.err_line_starting(
130              format!(
131                "{} sections do not support nested sections",
132                special_sect.to_str()
133              ),
134              subsection.heading.first_loc().unwrap(),
135            )?;
136          }
137        }
138      }
139    }
140
141    self.ctx.bibliography_ctx = BiblioContext::None;
142    self.ctx.section_level = last_level;
143    Ok(Section {
144      meta,
145      level: semantic_level,
146      id,
147      heading,
148      blocks,
149      loc,
150    })
151  }
152
153  pub(crate) fn restore_peeked_section(&mut self, peeked: PeekedSection<'arena>) {
154    self.restore_peeked(peeked.lines, peeked.meta);
155  }
156
157  pub fn push_toc_node(
158    &mut self,
159    authored_level: u8,
160    semantic_level: u8,
161    heading: &InlineNodes<'arena>,
162    as_ref: Option<&BumpString<'arena>>,
163    special_sect: Option<SpecialSection>,
164  ) {
165    let Some(toc) = self.document.toc.as_mut() else {
166      return;
167    };
168    if authored_level > self.document.meta.u8_or("toclevels", 2) {
169      return;
170    }
171    let node = TocNode {
172      level: authored_level,
173      title: heading.clone(),
174      id: as_ref.cloned(),
175      special_sect,
176      children: BumpVec::new_in(self.bump),
177    };
178    let mut nodes: &mut BumpVec<'_, TocNode<'_>> = toc.nodes.as_mut();
179    let Some(last_level) = nodes.last().map(|n| n.level) else {
180      nodes.push(node);
181      return;
182    };
183    if authored_level < last_level || authored_level == 0 {
184      nodes.push(node);
185      return;
186    }
187
188    let mut depth = semantic_level;
189
190    // special case: book special sections can go from 0 to 2
191    if last_level == 0 && semantic_level == 2 && !nodes.iter().any(|n| n.level == 1) {
192      depth = 1;
193    }
194
195    while depth > last_level {
196      // we don't push out of sequence sections, shouldn't panic
197      nodes = nodes.last_mut().unwrap().children.as_mut();
198      depth -= 1;
199    }
200    nodes.push(node);
201  }
202}
203
204#[derive(Debug)]
205pub struct PeekedSection<'arena> {
206  pub meta: ChunkMeta<'arena>,
207  pub lines: ContiguousLines<'arena>,
208  pub semantic_level: u8,
209  pub authored_level: u8,
210}
211
212impl PeekedSection<'_> {
213  pub fn is_special_sect(&self) -> bool {
214    self.meta.attrs.special_sect().is_some()
215  }
216
217  pub fn special_sect(&self) -> Option<SpecialSection> {
218    self.meta.attrs.special_sect()
219  }
220}
221
222#[cfg(test)]
223mod tests {
224  use super::*;
225  use pretty_assertions::assert_eq;
226  use test_utils::*;
227
228  #[test]
229  fn test_parse_2_sections() {
230    let input = adoc! {"
231      == one
232
233      foo
234
235      == two
236
237      bar
238    "};
239    let mut parser = test_parser!(input);
240    let section = parser.parse_section().unwrap().unwrap();
241    assert_eq!(
242      section,
243      Section {
244        meta: chunk_meta!(0),
245        level: 1,
246        id: Some(bstr!("_one")),
247        heading: nodes![node!("one"; 3..6)],
248        blocks: vecb![Block {
249          context: BlockContext::Paragraph,
250          content: BlockContent::Simple(nodes![node!("foo"; 8..11)]),
251          loc: (8..11).into(),
252          ..empty_block!(8)
253        }],
254        loc: (0..11).into()
255      }
256    );
257    let section = parser.parse_section().unwrap().unwrap();
258    assert_eq!(
259      section,
260      Section {
261        meta: chunk_meta!(13),
262        level: 1,
263        id: Some(bstr!("_two")),
264        heading: nodes![node!("two"; 16..19)],
265        blocks: vecb![Block {
266          context: BlockContext::Paragraph,
267          content: BlockContent::Simple(nodes![node!("bar"; 21..24)]),
268          loc: (21..24).into(),
269          ..empty_block!(21)
270        }],
271        loc: (13..24).into()
272      }
273    );
274  }
275}