Files
tolaria/src-tauri/src/frontmatter/ops.rs
2026-05-02 02:16:13 +02:00

165 lines
5.0 KiB
Rust

use super::keys::{frontmatter_key_rule, frontmatter_keys_match, FrontmatterKey};
use super::yaml::{format_yaml_field, FrontmatterValue};
/// Check if a line continues the previous key's value (indented list item,
/// block scalar content, or blank line inside a block scalar).
fn is_value_continuation(line: FrontmatterLine<'_>) -> bool {
line.0.is_empty() || line.0.starts_with(" ") || line.0.starts_with('\t')
}
#[derive(Clone, Copy)]
enum KeyMatchMode {
Exact,
Canonical,
}
#[derive(Clone, Copy)]
struct DocumentText<'a>(&'a str);
#[derive(Clone, Copy)]
struct FrontmatterLine<'a>(&'a str);
#[derive(Clone, Copy)]
struct PropertyKey<'a>(&'a str);
impl<'a> PropertyKey<'a> {
fn as_str(self) -> &'a str {
self.0
}
fn matches(self, candidate: &str, mode: KeyMatchMode) -> bool {
match mode {
KeyMatchMode::Exact => candidate == self.as_str(),
KeyMatchMode::Canonical => frontmatter_keys_match(
FrontmatterKey::new(candidate),
FrontmatterKey::new(self.as_str()),
),
}
}
}
impl<'a> FrontmatterLine<'a> {
fn key(self) -> Option<&'a str> {
let trimmed = self.0.trim_start();
if let Some(raw) = trimmed.strip_prefix('"') {
return quoted_yaml_key(raw, '"');
}
if let Some(raw) = trimmed.strip_prefix('\'') {
return quoted_yaml_key(raw, '\'');
}
trimmed
.split_once(':')
.map(|(key, _)| key.trim())
.filter(|key| !key.is_empty())
}
}
fn quoted_yaml_key(raw: &str, quote: char) -> Option<&str> {
let (key, rest) = raw.split_once(quote)?;
rest.trim_start().starts_with(':').then_some(key)
}
#[derive(Clone, Copy)]
struct FieldUpdate<'a> {
key: PropertyKey<'a>,
value: Option<&'a FrontmatterValue>,
match_mode: KeyMatchMode,
}
impl<'a> FieldUpdate<'a> {
fn matches_line(self, line: FrontmatterLine<'_>) -> bool {
line.key()
.is_some_and(|candidate| self.key.matches(candidate, self.match_mode))
}
fn prepend_to(self, content: DocumentText<'_>) -> String {
let field_lines =
format_yaml_field(self.key.as_str(), self.value.expect("value must exist"));
format!("---\n{}\n---\n{}", field_lines.join("\n"), content.0)
}
fn apply_to_lines(self, lines: &[FrontmatterLine<'_>]) -> Vec<String> {
let mut new_lines: Vec<String> = Vec::new();
let mut found_key = false;
let mut i = 0;
while i < lines.len() {
if !self.matches_line(lines[i]) {
new_lines.push(lines[i].0.to_string());
i += 1;
continue;
}
found_key = true;
i += 1;
while i < lines.len() && is_value_continuation(lines[i]) {
i += 1;
}
if let Some(v) = self.value {
new_lines.extend(format_yaml_field(self.key.as_str(), v));
}
}
if let (false, Some(v)) = (found_key, self.value) {
new_lines.extend(format_yaml_field(self.key.as_str(), v));
}
new_lines
}
fn apply_to_content(self, content: DocumentText<'_>) -> Result<String, String> {
if !content.0.starts_with("---\n") {
return match self.value {
Some(_) => Ok(self.prepend_to(content)),
None => Ok(content.0.to_string()),
};
}
let after_open = &content.0[4..];
let (fm_content, rest) = if let Some(stripped) = after_open.strip_prefix("---") {
("", stripped)
} else {
let fm_end = after_open
.find("\n---")
.map(|i| i + 4)
.ok_or_else(|| "Malformed frontmatter: no closing ---".to_string())?;
(&content.0[4..fm_end], &content.0[fm_end + 4..])
};
let lines: Vec<FrontmatterLine<'_>> = fm_content.lines().map(FrontmatterLine).collect();
let new_fm = self.apply_to_lines(&lines).join("\n");
Ok(format!("---\n{}\n---{}", new_fm, rest))
}
}
/// Internal function to update frontmatter content
pub fn update_frontmatter_content(
content: &str,
key: &str,
value: Option<FrontmatterValue>,
) -> Result<String, String> {
let update = FieldUpdate {
key: PropertyKey(key),
value: value.as_ref(),
match_mode: KeyMatchMode::Exact,
};
let Some(rule) = frontmatter_key_rule(FrontmatterKey::new(update.key.as_str()))
.filter(|rule| rule.canonicalizes_on_write())
else {
return update.apply_to_content(DocumentText(content));
};
let updated = FieldUpdate {
key: PropertyKey(rule.write_key()),
value: None,
match_mode: KeyMatchMode::Canonical,
}
.apply_to_content(DocumentText(content))?;
FieldUpdate {
key: PropertyKey(rule.write_key()),
value: update.value,
match_mode: KeyMatchMode::Exact,
}
.apply_to_content(DocumentText(&updated))
}