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
192      && semantic_level == 2
193      && !nodes.last().unwrap().children.iter().any(|n| n.level == 1)
194    {
195      depth = 1;
196    }
197
198    while depth > last_level {
199      // we don't push out of sequence sections, shouldn't panic
200      nodes = nodes.last_mut().unwrap().children.as_mut();
201      depth -= 1;
202    }
203    nodes.push(node);
204  }
205}
206
207#[derive(Debug)]
208pub struct PeekedSection<'arena> {
209  pub meta: ChunkMeta<'arena>,
210  pub lines: ContiguousLines<'arena>,
211  pub semantic_level: u8,
212  pub authored_level: u8,
213}
214
215impl PeekedSection<'_> {
216  pub fn is_special_sect(&self) -> bool {
217    self.meta.attrs.special_sect().is_some()
218  }
219
220  pub fn special_sect(&self) -> Option<SpecialSection> {
221    self.meta.attrs.special_sect()
222  }
223}
224
225#[cfg(test)]
226mod tests {
227  use super::*;
228  use pretty_assertions::assert_eq;
229  use test_utils::*;
230
231  #[test]
232  fn test_parse_2_sections() {
233    let input = adoc! {"
234      == one
235
236      foo
237
238      == two
239
240      bar
241    "};
242    let mut parser = test_parser!(input);
243    let section = parser.parse_section().unwrap().unwrap();
244    assert_eq!(
245      section,
246      Section {
247        meta: chunk_meta!(0),
248        level: 1,
249        id: Some(bstr!("_one")),
250        heading: nodes![node!("one"; 3..6)],
251        blocks: vecb![Block {
252          context: BlockContext::Paragraph,
253          content: BlockContent::Simple(nodes![node!("foo"; 8..11)]),
254          loc: (8..11).into(),
255          ..empty_block!(8)
256        }],
257        loc: (0..11).into()
258      }
259    );
260    let section = parser.parse_section().unwrap().unwrap();
261    assert_eq!(
262      section,
263      Section {
264        meta: chunk_meta!(13),
265        level: 1,
266        id: Some(bstr!("_two")),
267        heading: nodes![node!("two"; 16..19)],
268        blocks: vecb![Block {
269          context: BlockContext::Paragraph,
270          content: BlockContent::Simple(nodes![node!("bar"; 21..24)]),
271          loc: (21..24).into(),
272          ..empty_block!(21)
273        }],
274        loc: (13..24).into()
275      }
276    );
277  }
278}