diff --git a/Cargo.lock b/Cargo.lock index 954456f..85ad857 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -65,12 +65,6 @@ dependencies = [ "rustversion", ] -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - [[package]] name = "autocfg" version = "1.5.0" @@ -449,15 +443,6 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -573,18 +558,6 @@ dependencies = [ "ordered_hashmap", ] -[[package]] -name = "gray_matter" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8666976c40b8633f918783969b6681a3ddb205f29150348617de425d85a3e3bd" -dependencies = [ - "serde", - "serde_json", - "toml 0.5.11", - "yaml-rust2", -] - [[package]] name = "hashbrown" version = "0.12.3" @@ -599,10 +572,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash 0.8.12", - "allocator-api2", -] [[package]] name = "hashbrown" @@ -621,15 +590,6 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -[[package]] -name = "hashlink" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" -dependencies = [ - "hashbrown 0.14.5", -] - [[package]] name = "heck" version = "0.5.0" @@ -1598,7 +1558,6 @@ dependencies = [ name = "sukr" version = "0.1.0" dependencies = [ - "gray_matter", "katex-rs", "lightningcss", "mermaid-rs-renderer", @@ -1607,7 +1566,7 @@ dependencies = [ "serde", "tempfile", "tera", - "toml 0.8.23", + "toml", "tree-house", "tree-house-bindings", "tree-sitter", @@ -1745,15 +1704,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" -[[package]] -name = "toml" -version = "0.5.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" -dependencies = [ - "serde", -] - [[package]] name = "toml" version = "0.8.23" @@ -2222,17 +2172,6 @@ dependencies = [ "tap", ] -[[package]] -name = "yaml-rust2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" -dependencies = [ - "arraydeque", - "encoding_rs", - "hashlink", -] - [[package]] name = "zerocopy" version = "0.8.33" diff --git a/Cargo.toml b/Cargo.toml index 5c78ed9..c2e9dc9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,7 +6,6 @@ name = "sukr" version = "0.1.0" [dependencies] -gray_matter = "0.2" pulldown-cmark = "0.12" # Syntax highlighting diff --git a/src/content.rs b/src/content.rs index b7af579..d0cb4e7 100644 --- a/src/content.rs +++ b/src/content.rs @@ -1,8 +1,8 @@ //! Content discovery and frontmatter parsing. use crate::error::{Error, Result}; -use gray_matter::{Matter, engine::YAML}; use serde::Serialize; +use std::collections::HashMap; use std::fs; use std::path::{Path, PathBuf}; @@ -82,10 +82,8 @@ impl Content { source: e, })?; - let matter = Matter::::new(); - let parsed = matter.parse(&raw); - - let frontmatter = parse_frontmatter(path, &parsed)?; + let (yaml_block, body) = extract_frontmatter(&raw, path)?; + let frontmatter = parse_frontmatter(path, &yaml_block)?; // Derive slug from filename (without extension) let slug = path @@ -97,7 +95,7 @@ impl Content { Ok(Content { kind, frontmatter, - body: parsed.content, + body, source_path: path.to_path_buf(), slug, }) @@ -126,64 +124,102 @@ impl Content { } } -fn parse_frontmatter(path: &Path, parsed: &gray_matter::ParsedEntity) -> Result { - let data = parsed.data.as_ref().ok_or_else(|| Error::Frontmatter { - path: path.to_path_buf(), - message: "missing frontmatter".to_string(), - })?; +/// Extract YAML frontmatter block and body from raw content. +/// Frontmatter must be delimited by `---` at start and end. +fn extract_frontmatter(raw: &str, path: &Path) -> Result<(String, String)> { + let trimmed = raw.trim_start(); - let pod = data.as_hashmap().map_err(|_| Error::Frontmatter { - path: path.to_path_buf(), - message: "frontmatter is not a valid map".to_string(), - })?; + if !trimmed.starts_with("---") { + return Err(Error::Frontmatter { + path: path.to_path_buf(), + message: "missing frontmatter delimiter".to_string(), + }); + } - let title = pod + // Find the closing --- + let after_first = &trimmed[3..].trim_start_matches(['\r', '\n']); + let end_idx = after_first + .find("\n---") + .ok_or_else(|| Error::Frontmatter { + path: path.to_path_buf(), + message: "missing closing frontmatter delimiter".to_string(), + })?; + + let yaml_block = after_first[..end_idx].to_string(); + let body = after_first[end_idx + 4..].trim_start().to_string(); + + Ok((yaml_block, body)) +} + +/// Parse simple YAML frontmatter into structured fields. +/// Supports: key: value, key: "quoted value", and nested taxonomies.tags +fn parse_frontmatter(path: &Path, yaml: &str) -> Result { + let mut map: HashMap = HashMap::new(); + let mut tags: Vec = Vec::new(); + let mut in_taxonomies = false; + let mut in_tags = false; + + for line in yaml.lines() { + let trimmed = line.trim(); + + // Skip empty lines and comments + if trimmed.is_empty() || trimmed.starts_with('#') { + continue; + } + + // Handle nested structure for taxonomies.tags + if trimmed == "taxonomies:" { + in_taxonomies = true; + continue; + } + + if in_taxonomies && trimmed == "tags:" { + in_tags = true; + continue; + } + + // Collect tag list items + if in_tags && trimmed.starts_with("- ") { + let tag = trimmed[2..].trim().trim_matches('"').to_string(); + tags.push(tag); + continue; + } + + // Exit nested context on non-indented line + if !line.starts_with(' ') && !line.starts_with('\t') { + in_taxonomies = false; + in_tags = false; + } + + // Parse key: value + if let Some((key, value)) = trimmed.split_once(':') { + let key = key.trim().to_string(); + let value = value.trim().trim_matches('"').to_string(); + if !value.is_empty() { + map.insert(key, value); + } + } + } + + let title = map .get("title") - .and_then(|v| v.as_string().ok()) + .cloned() .ok_or_else(|| Error::Frontmatter { path: path.to_path_buf(), message: "missing required 'title' field".to_string(), })?; - let description = pod.get("description").and_then(|v| v.as_string().ok()); - let date = pod.get("date").and_then(|v| v.as_string().ok()); - let weight = pod.get("weight").and_then(|v| v.as_i64().ok()); - let link_to = pod.get("link_to").and_then(|v| v.as_string().ok()); - let nav_label = pod.get("nav_label").and_then(|v| v.as_string().ok()); - let section_type = pod.get("section_type").and_then(|v| v.as_string().ok()); - let template = pod.get("template").and_then(|v| v.as_string().ok()); - let toc = pod.get("toc").and_then(|v| v.as_bool().ok()); - - // Handle nested taxonomies.tags structure - let tags = if let Some(taxonomies) = pod.get("taxonomies") { - if let Ok(tax_map) = taxonomies.as_hashmap() { - if let Some(tags_pod) = tax_map.get("tags") { - if let Ok(tags_vec) = tags_pod.as_vec() { - tags_vec.iter().filter_map(|v| v.as_string().ok()).collect() - } else { - Vec::new() - } - } else { - Vec::new() - } - } else { - Vec::new() - } - } else { - Vec::new() - }; - Ok(Frontmatter { title, - description, - date, + description: map.get("description").cloned(), + date: map.get("date").cloned(), tags, - weight, - link_to, - nav_label, - section_type, - template, - toc, + weight: map.get("weight").and_then(|v| v.parse().ok()), + link_to: map.get("link_to").cloned(), + nav_label: map.get("nav_label").cloned(), + section_type: map.get("section_type").cloned(), + template: map.get("template").cloned(), + toc: map.get("toc").and_then(|v| v.parse().ok()), }) }