pub mod builder;
pub mod define;
pub mod label;
pub mod signature;
pub mod xparse;
pub use define::scan_definitions;
pub use label::{LabelDef, LabelId, LabelRef, RefCommand, RefId};
pub use signature::{ArgKind, ArgSpec, CommandSig, EnvironmentSig, SignatureDb, Signatures};
use crate::syntax::SyntaxNode;
#[derive(Debug, Default, PartialEq, Eq)]
pub struct SemanticModel {
pub(crate) labels: Vec<LabelDef>,
pub(crate) refs: Vec<LabelRef>,
}
impl SemanticModel {
pub fn build(root: &SyntaxNode) -> Self {
builder::build(root)
}
pub fn labels(&self) -> &[LabelDef] {
&self.labels
}
pub fn label(&self, id: LabelId) -> &LabelDef {
&self.labels[id.0 as usize]
}
pub fn refs(&self) -> &[LabelRef] {
&self.refs
}
pub fn reference(&self, id: RefId) -> &LabelRef {
&self.refs[id.0 as usize]
}
pub fn unreferenced_labels(&self) -> impl Iterator<Item = LabelId> + '_ {
(0..self.labels.len())
.map(LabelId::from_index)
.filter(move |id| !self.label(*id).referenced)
}
pub fn unresolved_refs(&self) -> impl Iterator<Item = RefId> + '_ {
(0..self.refs.len())
.map(RefId::from_index)
.filter(move |id| !self.reference(*id).resolved)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::parser::parse;
fn model_of(src: &str) -> SemanticModel {
SemanticModel::build(&SyntaxNode::new_root(parse(src).green))
}
#[test]
fn label_creates_def() {
let model = model_of("\\label{sec:intro}\n");
assert_eq!(model.labels().len(), 1);
assert_eq!(model.labels()[0].name, "sec:intro");
assert!(!model.labels()[0].referenced);
}
#[test]
fn ref_creates_use() {
let model = model_of("\\ref{sec:intro}\n");
assert_eq!(model.refs().len(), 1);
assert_eq!(model.refs()[0].name, "sec:intro");
assert_eq!(model.refs()[0].command, RefCommand::Ref);
assert!(!model.refs()[0].resolved);
}
#[test]
fn label_and_ref_resolve() {
let model = model_of("\\label{a}\\ref{a}\n");
assert!(model.labels()[0].referenced);
assert!(model.refs()[0].resolved);
assert_eq!(model.unreferenced_labels().count(), 0);
assert_eq!(model.unresolved_refs().count(), 0);
}
#[test]
fn ref_family_recognized() {
let model = model_of(
"\\pageref{x}\\eqref{x}\\autoref{x}\\nameref{x}\\Cref{x}\\vref{x}\\Vref{x}\\cpageref{x}\n",
);
let kinds: Vec<_> = model.refs().iter().map(|r| r.command).collect();
assert_eq!(
kinds,
vec![
RefCommand::PageRef,
RefCommand::EqRef,
RefCommand::AutoRef,
RefCommand::NameRef,
RefCommand::CrefUpper,
RefCommand::Vref,
RefCommand::VrefUpper,
RefCommand::CpageRef,
]
);
}
#[test]
fn non_ref_commands_ignored() {
let model = model_of("\\textbf{x}\\section{Hi}\\emph{y}\n");
assert_eq!(model.labels().len(), 0);
assert_eq!(model.refs().len(), 0);
}
#[test]
fn cref_splits_comma_list() {
let model = model_of("\\cref{a,b,c}\n");
let names: Vec<_> = model.refs().iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["a", "b", "c"]);
assert!(model.refs().iter().all(|r| r.command == RefCommand::Cref));
let range = model.refs()[0].range;
assert!(model.refs().iter().all(|r| r.range == range));
}
#[test]
fn plain_ref_does_not_split() {
let model = model_of("\\ref{a,b}\n");
assert_eq!(model.refs().len(), 1);
assert_eq!(model.refs()[0].name, "a,b");
}
#[test]
fn cref_empty_and_blank_keys_dropped() {
assert_eq!(model_of("\\cref{}\n").refs().len(), 0);
let model = model_of("\\cref{a,,b}\n");
let names: Vec<_> = model.refs().iter().map(|r| r.name.as_str()).collect();
assert_eq!(names, vec!["a", "b"]);
}
#[test]
fn unresolved_ref_when_no_label() {
let model = model_of("\\ref{missing}\n");
assert!(!model.refs()[0].resolved);
assert_eq!(model.unresolved_refs().count(), 1);
}
#[test]
fn unreferenced_label_reported() {
let model = model_of("\\label{x}\n");
assert_eq!(model.unreferenced_labels().count(), 1);
}
#[test]
fn duplicate_labels_preserved() {
let model = model_of("\\label{x}\\label{x}\\ref{x}\n");
assert_eq!(model.labels().len(), 2);
assert!(model.labels().iter().all(|l| l.referenced));
assert!(model.refs()[0].resolved);
}
#[test]
fn nested_macro_key_skipped() {
let model = model_of("\\label{\\foo}\n");
assert_eq!(model.labels().len(), 0);
}
#[test]
fn label_collected_inside_environment() {
let model = model_of("\\begin{figure}\n\\label{fig:one}\n\\end{figure}\n");
assert_eq!(model.labels().len(), 1);
assert_eq!(model.labels()[0].name, "fig:one");
}
}