use smol_str::SmolStr;
use rowan::{TextRange, TextSize};
use crate::ast::{command_name, first_group_range, nth_group_inner};
use crate::semantic::SemanticModel;
use crate::semantic::label::{CitationRef, LabelDef, LabelRef, RefCommand};
use crate::syntax::{SyntaxKind, SyntaxNode};
pub fn build(root: &SyntaxNode) -> SemanticModel {
let mut model = SemanticModel::default();
for command in root
.descendants()
.filter(|node| node.kind() == SyntaxKind::COMMAND)
{
let Some(name) = command_name(&command) else {
continue;
};
if name == "label" {
if let Some((inner_range, inner)) = nth_group_inner(&command, 0) {
for (key, key_range) in key_spans(&inner, inner_range, false) {
model.labels.push(LabelDef {
name: SmolStr::from(key),
range: first_group_range(&command),
key_range,
referenced: false,
});
}
}
} else if let Some(kind) = ref_command(&name)
&& let Some((inner_range, inner)) = nth_group_inner(&command, 0)
{
for (key, key_range) in key_spans(&inner, inner_range, kind.is_key_list()) {
model.refs.push(LabelRef {
name: SmolStr::from(key),
command: kind,
range: command.text_range(),
key_range,
resolved: false,
});
}
} else if is_cite_command(&name)
&& let Some((inner_range, inner)) = nth_group_inner(&command, 0)
{
if name == "nocite" && inner.trim() == "*" {
model.nocite_all = true;
} else {
for (key, key_range) in key_spans(&inner, inner_range, true) {
model.citations.push(CitationRef {
name: SmolStr::from(key),
command: SmolStr::from(name.as_str()),
range: command.text_range(),
key_range,
});
}
}
}
}
resolve(&mut model);
model
}
pub(crate) fn is_cite_command(name: &str) -> bool {
const EXTRA: &[&str] = &[
"parencite",
"Parencite",
"footcite",
"footcitetext",
"textcite",
"Textcite",
"smartcite",
"Smartcite",
"autocite",
"Autocite",
"supercite",
"fullcite",
"footfullcite",
"nocite",
"notecite",
"Notecite",
"pnotecite",
"fnotecite",
];
name.starts_with("cite") || name.starts_with("Cite") || EXTRA.contains(&name)
}
pub(crate) fn ref_command(name: &str) -> Option<RefCommand> {
Some(match name {
"ref" => RefCommand::Ref,
"pageref" => RefCommand::PageRef,
"eqref" => RefCommand::EqRef,
"autoref" => RefCommand::AutoRef,
"nameref" => RefCommand::NameRef,
"cref" => RefCommand::Cref,
"Cref" => RefCommand::CrefUpper,
"vref" => RefCommand::Vref,
"Vref" => RefCommand::VrefUpper,
"cpageref" => RefCommand::CpageRef,
_ => return None,
})
}
fn key_spans(inner: &str, inner_range: TextRange, split: bool) -> Vec<(&str, TextRange)> {
let base = inner_range.start();
let mut out = Vec::new();
if split {
let mut seg_off = 0usize;
for segment in inner.split(',') {
if let Some((key, lo, hi)) = trimmed_span(segment) {
out.push((key, key_range(base, seg_off + lo, seg_off + hi)));
}
seg_off += segment.len() + 1;
}
} else if let Some((key, lo, hi)) = trimmed_span(inner) {
out.push((key, key_range(base, lo, hi)));
}
out
}
fn trimmed_span(segment: &str) -> Option<(&str, usize, usize)> {
let key = segment.trim();
if key.is_empty() {
return None;
}
let lo = segment.len() - segment.trim_start().len();
Some((key, lo, lo + key.len()))
}
fn key_range(base: TextSize, lo: usize, hi: usize) -> TextRange {
TextRange::new(
base + TextSize::from(lo as u32),
base + TextSize::from(hi as u32),
)
}
fn resolve(model: &mut SemanticModel) {
for ref_idx in 0..model.refs.len() {
let name = model.refs[ref_idx].name.clone();
let mut hit = false;
for label in &mut model.labels {
if label.name == name {
label.referenced = true;
hit = true;
}
}
model.refs[ref_idx].resolved = hit;
}
}
#[cfg(test)]
mod tests {
use crate::parser::parse;
use crate::syntax::SyntaxNode;
use super::build;
fn model(src: &str) -> crate::semantic::SemanticModel {
build(&SyntaxNode::new_root(parse(src).green))
}
#[test]
fn label_key_range_excludes_command_and_braces() {
let src = "\\label{ sec:intro }\n";
let model = model(src);
let def = &model.labels()[0];
assert_eq!(def.name, "sec:intro");
assert_eq!(&src[def.key_range], "sec:intro");
}
#[test]
fn cref_list_keys_get_isolated_ranges() {
let src = "\\cref{a,b,c}\n";
let model = model(src);
let keys: Vec<_> = model
.refs()
.iter()
.map(|r| (r.name.as_str(), &src[r.key_range]))
.collect();
assert_eq!(
keys,
vec![("a", "a"), ("b", "b"), ("c", "c")],
"each key in a list command isolates its own span"
);
}
#[test]
fn cite_list_keys_get_isolated_ranges() {
let src = "\\cite{ foo , bar }\n";
let model = model(src);
let keys: Vec<_> = model
.citations()
.iter()
.map(|c| (c.name.as_str(), &src[c.key_range]))
.collect();
assert_eq!(keys, vec![("foo", "foo"), ("bar", "bar")]);
}
}