use anyhow::{anyhow, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CrossrefKind {
Fig,
Tbl,
Lst,
}
const PREFIXES: [(&str, CrossrefKind); 3] = [
("fig-", CrossrefKind::Fig),
("tbl-", CrossrefKind::Tbl),
("lst-", CrossrefKind::Lst),
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CrossrefLabel {
pub kind: CrossrefKind,
pub name: String,
}
impl CrossrefKind {
pub fn as_str(&self) -> &'static str {
match self {
CrossrefKind::Fig => "fig",
CrossrefKind::Tbl => "tbl",
CrossrefKind::Lst => "lst",
}
}
}
impl CrossrefLabel {
pub fn to_doc(&self) -> crate::typst::model::CrossrefLabelDoc {
crate::typst::model::CrossrefLabelDoc {
kind: self.kind.as_str().to_string(),
name: self.name.clone(),
}
}
}
pub fn classify_label(name: &str) -> Result<CrossrefLabel> {
for (prefix, kind) in PREFIXES {
if name.starts_with(prefix) && name.len() > prefix.len() {
return Ok(CrossrefLabel {
kind,
name: name.to_string(),
});
}
}
Err(anyhow!(
"label `{}` has no recognized cross-reference prefix (expected one of: fig-, tbl-, lst-)",
name
))
}
pub fn has_crossref_prefix(name: &str) -> bool {
PREFIXES
.iter()
.any(|(prefix, _)| name.starts_with(prefix) && name.len() > prefix.len())
}
pub fn parse_label_names(names: &[String]) -> Result<Vec<CrossrefLabel>> {
if names.is_empty() {
return Err(anyhow!("label list is empty"));
}
let mut labels = Vec::with_capacity(names.len());
let mut seen_kinds: Vec<CrossrefKind> = Vec::new();
for name in names {
let label = classify_label(name)?;
if seen_kinds.contains(&label.kind) {
return Err(anyhow!(
"label list has more than one `{}` entry; use one label per kind",
&name[..name.find('-').map(|i| i + 1).unwrap_or(name.len())]
));
}
seen_kinds.push(label.kind);
labels.push(label);
}
Ok(labels)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classifies_recognized_prefixes() {
assert_eq!(classify_label("fig-x").unwrap().kind, CrossrefKind::Fig);
assert_eq!(classify_label("tbl-x").unwrap().kind, CrossrefKind::Tbl);
assert_eq!(classify_label("lst-x").unwrap().kind, CrossrefKind::Lst);
}
#[test]
fn keeps_full_name_including_prefix() {
assert_eq!(classify_label("fig-plot").unwrap().name, "fig-plot");
}
#[test]
fn rejects_unprefixed_label() {
let err = classify_label("myplot").unwrap_err().to_string();
assert!(err.contains("myplot"), "{err}");
assert!(err.contains("fig-"), "{err}");
}
#[test]
fn parses_single_string_into_one_label() {
let labels = parse_label_names(&["fig-x".to_string()]).unwrap();
assert_eq!(labels.len(), 1);
assert_eq!(labels[0].kind, CrossrefKind::Fig);
}
#[test]
fn parses_distinct_kinds_list() {
let labels = parse_label_names(&["fig-x".to_string(), "lst-y".to_string()]).unwrap();
assert_eq!(labels.len(), 2);
}
#[test]
fn rejects_duplicate_kinds() {
let err = parse_label_names(&["fig-a".to_string(), "fig-b".to_string()])
.unwrap_err()
.to_string();
assert!(err.contains("fig"), "{err}");
}
}