Skip to main content

varar_core/
structurer.rs

1//! Groups the flat scanner output into [`Example`]s, tracking a heading scope
2//! stack — port of `structurer.ts` / `Structurer.java`.
3
4use crate::ast::{Block, Example, TableOrFence, VarDoc};
5use crate::span::Span;
6
7/// Groups `blocks` (scanned from `source`) into a [`VarDoc`].
8///
9/// This is pure syntax — it does NOT decide where one example ends and the next
10/// begins. Instead each candidate records `preceded_by_delimiter` (a heading or
11/// `---` sits before it), and the planner groups adjacent matching candidates
12/// into examples using that flag plus which candidates match a step. See ADR
13/// 0012.
14pub fn structure(path: &str, source: &str, blocks: Vec<Block>) -> VarDoc {
15    let mut examples: Vec<Example> = Vec::new();
16    let mut orphan_attachments: Vec<TableOrFence> = Vec::new();
17    let mut scope_stack: Vec<(usize, String)> = Vec::new();
18    let mut last_example_idx: Option<usize> = None;
19    let mut attachment_open = false;
20    // A heading or thematic break seen since the previous candidate — the next
21    // candidate is then delimiter-preceded. Starts true so the first candidate in
22    // the file counts as delimiter-preceded (nothing to merge into).
23    let mut delimiter_pending = true;
24
25    for block in blocks {
26        match &block {
27            Block::Heading(heading) => {
28                // Pop deeper-or-equal-level entries before pushing the new heading.
29                while scope_stack.last().is_some_and(|e| e.0 >= heading.level) {
30                    scope_stack.pop();
31                }
32                scope_stack.push((heading.level, heading.text.clone()));
33                attachment_open = false;
34                delimiter_pending = true;
35            }
36            Block::Paragraph(_) | Block::ListItem(_) | Block::Blockquote(_) => {
37                let block_span = block.span();
38                examples.push(Example {
39                    scope_stack: scope_texts(&scope_stack),
40                    span: block_span,
41                    body: vec![block],
42                    preceded_by_delimiter: delimiter_pending,
43                });
44                last_example_idx = Some(examples.len() - 1);
45                attachment_open = true;
46                delimiter_pending = false;
47            }
48            Block::Table(_) | Block::Fence(_) => {
49                let target = if attachment_open {
50                    last_example_idx
51                } else {
52                    None
53                };
54                if let Some(idx) = target {
55                    let block_span = block.span();
56                    examples[idx].span = Span::from_offsets(
57                        source,
58                        examples[idx].span.start_offset,
59                        block_span.end_offset,
60                    );
61                    examples[idx].body.push(block);
62                } else {
63                    orphan_attachments.push(match block {
64                        Block::Table(t) => TableOrFence::Table(t),
65                        Block::Fence(f) => TableOrFence::Fence(f),
66                        _ => unreachable!(),
67                    });
68                }
69            }
70            Block::ThematicBreak(_) => {
71                attachment_open = false;
72                delimiter_pending = true;
73            }
74        }
75    }
76
77    VarDoc {
78        path: path.to_string(),
79        source: source.to_string(),
80        examples,
81        orphan_attachments,
82    }
83}
84
85fn scope_texts(scope_stack: &[(usize, String)]) -> Vec<String> {
86    scope_stack.iter().map(|e| e.1.clone()).collect()
87}