use crate::facts::{Declaration, DeclarationKind, Facts, Import, Span};
use crate::syntax::Language;
#[must_use]
pub fn extract(source: &str, language: Language) -> Facts {
let mut state = Extractor {
facts: Facts::default(),
headings: Vec::new(),
offset: 0,
};
state.run(source, language);
state.facts
}
struct Heading {
name: String,
level: usize,
}
struct Extractor {
facts: Facts,
headings: Vec<Heading>,
offset: usize,
}
impl Extractor {
fn run(&mut self, source: &str, language: Language) {
let lines = source.lines().collect::<Vec<_>>();
let mut fenced = false;
let mut script = String::new();
for (number, line) in lines.iter().enumerate() {
let start = self.offset;
self.offset += line.len() + 1;
let trimmed = line.trim();
if trimmed.starts_with("```") || trimmed.starts_with("~~~") {
fenced = !fenced;
continue;
}
if fenced {
continue;
}
let span = Span {
start,
end: start + line.len(),
line: u32::try_from(number + 1).unwrap_or(u32::MAX),
column: 1,
end_line: u32::try_from(number + 1).unwrap_or(u32::MAX),
end_column: u32::try_from(line.len() + 1).unwrap_or(u32::MAX),
};
if language == Language::Mdx
&& (trimmed.starts_with("import ") || trimmed.starts_with("export "))
{
script.push_str(line);
script.push('\n');
continue;
}
self.heading(trimmed, lines.get(number + 1).copied(), language, span);
self.targets(line, language, span);
}
if !script.is_empty() {
let inner = crate::script::extract(&script, Language::TypeScript);
self.facts.imports.extend(inner.imports);
}
}
fn heading(&mut self, line: &str, next: Option<&str>, language: Language, span: Span) {
let Some((level, name)) = read_heading(line, next, language) else {
return;
};
while self
.headings
.last()
.is_some_and(|heading| heading.level >= level)
{
self.headings.pop();
}
self.facts.declarations.push(Declaration {
name: name.clone(),
kind: DeclarationKind::Heading,
span,
extent: span,
owner: self.headings.last().map(|heading| heading.name.clone()),
exported: true,
});
self.headings.push(Heading { name, level });
}
fn targets(&mut self, line: &str, language: Language, span: Span) {
for target in link_targets(line, language) {
if !is_repository_path(&target) {
continue;
}
self.facts.imports.push(Import {
specifier: target,
span,
type_only: false,
reexport: false,
names: Vec::new(),
bindings: Vec::new(),
});
}
}
}
mod formats;
use formats::{is_repository_path, link_targets, read_heading};
#[cfg(test)]
mod tests;