fix: even more closing of M0/M1/M2 gaps against the spec

This commit is contained in:
2026-04-05 14:26:26 +02:00
parent 6e34f5de1c
commit 0cf59da467
24 changed files with 1979 additions and 29 deletions

View File

@@ -149,6 +149,123 @@ pub fn remove_category(data_dir: &Path, category: &str) -> EngineResult<()> {
Ok(())
}
// ── update helpers ──────────────────────────────────────────────────
/// Update the blog_languages list in project.json.
/// Per metadata.allium UpdateProjectMetadata: writes ProjectJsonWritten.
pub fn update_blog_languages(data_dir: &Path, languages: Vec<String>) -> EngineResult<()> {
let mut meta = read_project_json(data_dir)?;
meta.blog_languages = languages;
write_project_json(data_dir, &meta)?;
Ok(())
}
/// Update arbitrary fields of project metadata.
/// Per metadata.allium UpdateProjectMetadata rule.
pub fn update_project_metadata(
data_dir: &Path,
changes: &serde_json::Value,
) -> EngineResult<()> {
let mut meta = read_project_json(data_dir)?;
if let Some(name) = changes.get("name").and_then(|v| v.as_str()) {
meta.name = name.to_string();
}
if let Some(desc) = changes.get("description") {
meta.description = desc.as_str().map(|s| s.to_string());
}
if let Some(url) = changes.get("publicUrl") {
meta.public_url = url.as_str().map(|s| s.to_string());
}
if let Some(lang) = changes.get("mainLanguage") {
meta.main_language = lang.as_str().map(|s| s.to_string());
}
if let Some(author) = changes.get("defaultAuthor") {
meta.default_author = author.as_str().map(|s| s.to_string());
}
if let Some(max) = changes.get("maxPostsPerPage").and_then(|v| v.as_i64()) {
meta.max_posts_per_page = max as i32;
}
if let Some(cat) = changes.get("blogmarkCategory") {
meta.blogmark_category = cat.as_str().map(|s| s.to_string());
}
if let Some(theme) = changes.get("picoTheme") {
meta.pico_theme = theme.as_str().map(|s| s.to_string());
}
if let Some(enabled) = changes.get("semanticSimilarityEnabled").and_then(|v| v.as_bool()) {
meta.semantic_similarity_enabled = enabled;
}
if let Some(langs) = changes.get("blogLanguages").and_then(|v| v.as_array()) {
meta.blog_languages = langs
.iter()
.filter_map(|v| v.as_str().map(|s| s.to_string()))
.collect();
}
meta.validate().map_err(|e| crate::engine::EngineError::Validation(e))?;
write_project_json(data_dir, &meta)?;
Ok(())
}
// ── startup sync ────────────────────────────────────────────────────
/// Per metadata.allium StartupSync: loads metadata from filesystem, creating
/// default files if missing. Called on project activation.
pub fn startup_sync(data_dir: &Path) -> EngineResult<()> {
let meta_dir = data_dir.join("meta");
fs::create_dir_all(&meta_dir)?;
// Ensure project.json exists
if !meta_dir.join("project.json").exists() {
let default_meta = ProjectMetadata {
name: "My Blog".to_string(),
description: None,
public_url: None,
main_language: None,
default_author: None,
max_posts_per_page: 50,
blogmark_category: None,
pico_theme: None,
semantic_similarity_enabled: false,
blog_languages: Vec::new(),
};
write_project_json(data_dir, &default_meta)?;
}
// Ensure categories.json exists with defaults
if !meta_dir.join("categories.json").exists() {
let defaults = vec![
"article".to_string(),
"aside".to_string(),
"page".to_string(),
"picture".to_string(),
];
write_categories_json(data_dir, &defaults)?;
}
// Ensure category-meta.json exists
if !meta_dir.join("category-meta.json").exists() {
let empty: HashMap<String, CategorySettings> = HashMap::new();
write_category_meta_json(data_dir, &empty)?;
}
// Ensure publishing.json exists
if !meta_dir.join("publishing.json").exists() {
atomic_write_str(&meta_dir.join("publishing.json"), "{}")?;
}
// Ensure tags.json exists
if !meta_dir.join("tags.json").exists() {
write_tags_json(data_dir, &[])?;
}
// Ensure menu.opml exists
if !meta_dir.join("menu.opml").exists() {
let opml = crate::engine::menu::default_menu_opml();
atomic_write_str(&meta_dir.join("menu.opml"), &opml)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;