refactor(content): remove gray_matter, manual frontmatter

Implement extract_frontmatter (--- delimiters) and parse_frontmatter
(simple YAML key:value pairs). Handles taxonomies.tags for blog posts.
~70 lines replaces external dependency.
This commit is contained in:
Timothy DeHerrera
2026-02-05 23:59:53 -07:00
parent 8034e24f75
commit 8e1f8cbb95
3 changed files with 90 additions and 116 deletions

63
Cargo.lock generated
View File

@@ -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"

View File

@@ -6,7 +6,6 @@ name = "sukr"
version = "0.1.0"
[dependencies]
gray_matter = "0.2"
pulldown-cmark = "0.12"
# Syntax highlighting

View File

@@ -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::<YAML>::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<Frontmatter> {
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<Frontmatter> {
let mut map: HashMap<String, String> = HashMap::new();
let mut tags: Vec<String> = 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()),
})
}