use super::doc_path;
use crate::datatypes::values::Value;
use crate::okf::model::{ConceptDoc, Link, DEFAULT_LABEL};
use std::collections::HashMap;
fn doc_aliases(d: &ConceptDoc) -> Vec<&str> {
d.props
.iter()
.filter(|(k, _)| k == "aliases")
.flat_map(|(_, v)| match v {
Value::List(items) => items
.iter()
.filter_map(|x| match x {
Value::String(s) => Some(s.as_str()),
_ => None,
})
.collect::<Vec<_>>(),
Value::String(s) => vec![s.as_str()],
_ => Vec::new(),
})
.filter(|s| !s.is_empty())
.collect()
}
fn normalize_slug(s: &str) -> String {
let mut out = String::with_capacity(s.len());
let mut prev_dash = false;
for ch in s.chars() {
let c = ch.to_ascii_lowercase();
let c = if c == '_' || c == ' ' { '-' } else { c };
if c == '-' {
if prev_dash {
continue;
}
prev_dash = true;
} else {
prev_dash = false;
}
out.push(c);
}
out.trim_matches('-').to_string()
}
pub(super) struct Resolver<'a> {
pub(super) id_to_label: HashMap<&'a str, &'a str>,
path_to_id: HashMap<&'a str, &'a str>,
stem_to_id: HashMap<&'a str, &'a str>,
alias_to_id: HashMap<&'a str, &'a str>,
slug_to_id: HashMap<String, &'a str>,
title_to_id: HashMap<String, &'a str>,
}
impl<'a> Resolver<'a> {
pub(super) fn new(
docs: &'a [ConceptDoc],
profile: &crate::okf::model::Profile,
) -> (Self, Vec<String>) {
let mut id_to_label = HashMap::new();
let mut path_to_id = HashMap::new();
let mut stem_to_id = HashMap::new();
let mut alias_to_id = HashMap::new();
let mut slug_to_id = HashMap::new();
let mut title_to_id = HashMap::new();
let mut warnings = Vec::new();
for d in docs {
id_to_label.insert(d.concept_id.as_str(), d.label.as_str());
let path = doc_path(d);
path_to_id.entry(path).or_insert(d.concept_id.as_str());
let stem = path.rsplit('/').next().unwrap_or(path);
stem_to_id.entry(stem).or_insert(d.concept_id.as_str());
slug_to_id
.entry(normalize_slug(&d.concept_id))
.or_insert(d.concept_id.as_str());
slug_to_id
.entry(normalize_slug(stem))
.or_insert(d.concept_id.as_str());
title_to_id
.entry(normalize_slug(&d.title))
.or_insert(d.concept_id.as_str());
}
if profile.alias_resolution {
for d in docs {
for alias in doc_aliases(d) {
if let Some(&owner) = stem_to_id.get(alias) {
if owner != d.concept_id {
warnings.push(format!(
"alias `{alias}` on {} is already the file stem of `{owner}`; the stem wins",
d.file_path
));
}
continue;
}
if let Some(&owner) = alias_to_id.get(alias) {
warnings.push(format!(
"alias `{alias}` is claimed by both `{owner}` and `{}`; the first wins",
d.concept_id
));
continue;
}
alias_to_id.insert(alias, d.concept_id.as_str());
}
}
}
(
Self {
id_to_label,
path_to_id,
stem_to_id,
alias_to_id,
slug_to_id,
title_to_id,
},
warnings,
)
}
fn label_of(&self, id: &str) -> String {
self.id_to_label
.get(id)
.copied()
.unwrap_or(DEFAULT_LABEL)
.to_string()
}
pub(super) fn resolve(&self, link: &Link, source_dir: &str) -> (String, String) {
let t = link.target.as_str();
if let Some(lbl) = self.id_to_label.get(t) {
return (t.to_string(), lbl.to_string());
}
if let Some(id) = self.path_to_id.get(t) {
return ((*id).to_string(), self.label_of(id));
}
if t.contains('/') && !source_dir.is_empty() {
if let Some(id) = self.path_to_id.get(format!("{source_dir}/{t}").as_str()) {
return ((*id).to_string(), self.label_of(id));
}
}
if let Some(id) = self.stem_to_id.get(t) {
return ((*id).to_string(), self.label_of(id));
}
if let Some(id) = self.alias_to_id.get(t) {
return ((*id).to_string(), self.label_of(id));
}
let norm = normalize_slug(t);
if let Some(id) = self.slug_to_id.get(&norm) {
return ((*id).to_string(), self.label_of(id));
}
if let Some(seg) = t.rsplit('/').next() {
if let Some(id) = self.slug_to_id.get(&normalize_slug(seg)) {
return ((*id).to_string(), self.label_of(id));
}
}
if let Some(id) = self.title_to_id.get(&norm) {
return ((*id).to_string(), self.label_of(id));
}
(t.to_string(), DEFAULT_LABEL.to_string())
}
}
#[cfg(test)]
#[path = "resolver_tests.rs"]
mod resolver_tests;