use crate::check::context::Ctx;
use crate::model::Doc;
pub const COMMON_KEYS: &[&str] = &[
"id",
"title",
"status",
"created",
"updated",
"owners",
"tags",
"aliases",
"rel",
"files",
"generated_by",
"check_skip",
"verified_at",
"lang",
"source_rev",
"review_by",
"pins",
];
pub const REQUIRED_KEYS: &[&str] = &["id", "title", "status", "created", "updated"];
pub fn key_line(ctx: &Ctx, path: &str, key: &str) -> usize {
let Some(text) = ctx.current(path) else { return 1 };
let prefix = format!("{key}:");
text.lines().position(|l| l.starts_with(&prefix)).map(|i| i + 1).unwrap_or(1)
}
pub fn skipped(doc: &Doc, rule: &str) -> bool {
doc.list_field("check_skip").iter().any(|r| r == rule)
}
pub use crate::shell::quote;
pub(super) fn distance(a: &str, b: &str) -> usize {
let b: Vec<char> = b.chars().collect();
let mut row: Vec<usize> = (0..=b.len()).collect();
for (i, ca) in a.chars().enumerate() {
let mut prev = row[0];
row[0] = i + 1;
for j in 0..b.len() {
let cur = row[j + 1];
row[j + 1] = (prev + usize::from(ca != b[j])).min(row[j] + 1).min(cur + 1);
prev = cur;
}
}
row[b.len()]
}
pub(crate) fn nearest<'a>(want: &str, values: impl Iterator<Item = &'a str>) -> Option<&'a str> {
let mut scored: Vec<(usize, &str)> = values
.map(|v| (distance(&want.to_lowercase(), &v.to_lowercase()), v))
.filter(|(d, v)| *d > 0 && *d <= 2.max(v.chars().count() / 3))
.collect();
scored.sort();
match scored.as_slice() {
[(d, v), rest @ ..] if rest.first().is_none_or(|(e, _)| e > d) => Some(v),
_ => None,
}
}