weavatrix_parse/docs/
mod.rs1use crate::facts::{Declaration, DeclarationKind, Facts, Import, Span};
15use crate::syntax::Language;
16
17#[must_use]
19pub fn extract(source: &str, language: Language) -> Facts {
20 let mut state = Extractor {
21 facts: Facts::default(),
22 headings: Vec::new(),
23 offset: 0,
24 };
25 state.run(source, language);
26 state.facts
27}
28
29struct Heading {
31 name: String,
32 level: usize,
33}
34
35struct Extractor {
36 facts: Facts,
37 headings: Vec<Heading>,
38 offset: usize,
39}
40
41impl Extractor {
42 fn run(&mut self, source: &str, language: Language) {
43 let lines = source.lines().collect::<Vec<_>>();
44 let mut fenced = false;
45 let mut script = String::new();
46 for (number, line) in lines.iter().enumerate() {
47 let start = self.offset;
48 self.offset += line.len() + 1;
49 let trimmed = line.trim();
50 if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
53 fenced = !fenced;
54 continue;
55 }
56 if fenced {
57 continue;
58 }
59 let span = Span {
60 start,
61 end: start + line.len(),
62 line: u32::try_from(number + 1).unwrap_or(u32::MAX),
63 column: 1,
64 end_line: u32::try_from(number + 1).unwrap_or(u32::MAX),
65 end_column: u32::try_from(line.len() + 1).unwrap_or(u32::MAX),
66 };
67 if language == Language::Mdx
70 && (trimmed.starts_with("import ") || trimmed.starts_with("export "))
71 {
72 script.push_str(line);
73 script.push('\n');
74 continue;
75 }
76 self.heading(trimmed, lines.get(number + 1).copied(), language, span);
77 self.targets(line, language, span);
78 }
79 if !script.is_empty() {
80 let inner = crate::script::extract(&script, Language::TypeScript);
81 self.facts.imports.extend(inner.imports);
82 }
83 }
84
85 fn heading(&mut self, line: &str, next: Option<&str>, language: Language, span: Span) {
87 let Some((level, name)) = read_heading(line, next, language) else {
88 return;
89 };
90 while self
91 .headings
92 .last()
93 .is_some_and(|heading| heading.level >= level)
94 {
95 self.headings.pop();
96 }
97 self.facts.declarations.push(Declaration {
98 name: name.clone(),
99 kind: DeclarationKind::Heading,
100 span,
101 extent: span,
102 owner: self.headings.last().map(|heading| heading.name.clone()),
103 exported: true,
105 });
106 self.headings.push(Heading { name, level });
107 }
108
109 fn targets(&mut self, line: &str, language: Language, span: Span) {
111 for target in link_targets(line, language) {
112 if !is_repository_path(&target) {
113 continue;
114 }
115 self.facts.imports.push(Import {
116 specifier: target,
117 span,
118 type_only: false,
119 reexport: false,
120 names: Vec::new(),
121 bindings: Vec::new(),
122 });
123 }
124 }
125}
126
127mod formats;
128
129use formats::{is_repository_path, link_targets, read_heading};
130
131#[cfg(test)]
132mod tests;