markon-core 0.11.4

markon core - Turn your markdown on.
Documentation
// ── Language registry (auto-generated by build.rs from i18n/*.json5) ────
include!(concat!(env!("OUT_DIR"), "/langs_generated.rs"));

/// Strip `// ...` line comments from JSON5 text so serde_json can parse it.
pub fn strip_json5_comments(s: &str) -> String {
    s.lines()
        .map(|line| {
            if line.trim_start().starts_with("//") {
                ""
            } else {
                line
            }
        })
        .collect::<Vec<_>>()
        .join("\n")
}

fn parse_lang(entry: &LangEntry) -> serde_json::Value {
    serde_json::from_str(&strip_json5_comments(entry.data)).unwrap_or_default()
}

/// Load all i18n data as a single JSON string (for template injection).
pub fn load_i18n() -> String {
    let mut map = serde_json::Map::new();
    for entry in LANGS {
        map.insert(entry.key.to_string(), parse_lang(entry));
    }
    serde_json::Value::Object(map).to_string()
}

/// Resolve a language setting value ("zh_CN", "en", "auto", ...) to an i18n dict key ("zh", "en").
pub fn resolve_lang(language: &str) -> &'static str {
    // Exact match on value or key
    for l in LANGS {
        if language == l.value || language == l.key {
            return l.key;
        }
    }
    // Auto / unknown → sys_locale → best match → default
    let locale = sys_locale::get_locale().unwrap_or_default().to_lowercase();
    for l in LANGS {
        if locale.starts_with(l.key) {
            return l.key;
        }
    }
    DEFAULT_LANG_KEY
}

/// Get parsed i18n data for a given language setting value.
pub fn get_lang_data(language: &str) -> serde_json::Value {
    let key = resolve_lang(language);
    let entry = LANGS
        .iter()
        .find(|l| l.key == key)
        .unwrap_or(&LANGS[LANGS.len() - 1]);
    parse_lang(entry)
}

/// Return all i18n translations plus a `_languages` list derived from the LANGS registry.
pub fn all_i18n_with_languages() -> serde_json::Value {
    let mut result = serde_json::Map::new();
    let mut languages = Vec::new();

    for entry in LANGS {
        let data = parse_lang(entry);
        let label = data["_meta.label"]
            .as_str()
            .unwrap_or(entry.value)
            .to_string();
        languages.push(serde_json::json!({
            "value": entry.value,
            "key": entry.key,
            "label": label,
        }));
        result.insert(entry.key.to_string(), data);
    }

    result.insert(
        "_languages".to_string(),
        serde_json::Value::Array(languages),
    );
    serde_json::Value::Object(result)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn strip_json5_comments_removes_line_comments() {
        let input = "// this is a comment\n{\"key\": \"value\"}";
        let result = strip_json5_comments(input);
        assert_eq!(result, "\n{\"key\": \"value\"}");
    }

    #[test]
    fn strip_json5_comments_preserves_non_comment_lines() {
        let input = "{\"url\": \"https://example.com\"}";
        let result = strip_json5_comments(input);
        assert_eq!(result, input);
    }

    #[test]
    fn strip_json5_comments_handles_indented_comments() {
        let input = "  // indented comment\n  value";
        let result = strip_json5_comments(input);
        assert_eq!(result, "\n  value");
    }

    #[test]
    fn strip_json5_comments_empty_input() {
        assert_eq!(strip_json5_comments(""), "");
    }

    #[test]
    fn strip_json5_comments_only_comments() {
        let input = "// a\n// b\n// c";
        let result = strip_json5_comments(input);
        assert_eq!(result, "\n\n");
    }

    #[test]
    fn resolve_lang_exact_value_match() {
        // "en" is both a key and a value
        assert_eq!(resolve_lang("en"), "en");
    }

    #[test]
    fn resolve_lang_locale_value() {
        // "zh_CN" is a value that maps to key "zh"
        assert_eq!(resolve_lang("zh_CN"), "zh");
    }

    #[test]
    fn resolve_lang_key_match() {
        assert_eq!(resolve_lang("zh"), "zh");
    }

    #[test]
    fn resolve_lang_unknown_falls_back() {
        // Unknown language should fall back (either to sys_locale match or DEFAULT_LANG_KEY)
        let result = resolve_lang("xx_UNKNOWN");
        // Result must be a valid key from LANGS
        assert!(LANGS.iter().any(|l| l.key == result));
    }

    #[test]
    fn get_lang_data_returns_object() {
        let data = get_lang_data("en");
        assert!(data.is_object());
    }

    #[test]
    fn all_i18n_with_languages_contains_languages_list() {
        let result = all_i18n_with_languages();
        let languages = result["_languages"].as_array().unwrap();
        assert!(!languages.is_empty());
        // Each entry should have value, key, label
        for lang in languages {
            assert!(lang["value"].is_string());
            assert!(lang["key"].is_string());
            assert!(lang["label"].is_string());
        }
    }

    #[test]
    fn load_i18n_returns_valid_json() {
        let json_str = load_i18n();
        let parsed: serde_json::Value = serde_json::from_str(&json_str).unwrap();
        assert!(parsed.is_object());
    }
}