// frontmatter - YAML-like frontmatter parser for Lux // // Parses documents with --- delimited frontmatter: // --- // title: My Post // date: 2025-01-29 // tags: web blog // --- // Body content here... // A single key-value pair from the frontmatter type Entry = | Entry(String, String) // Parse result: list of key-value entries and the body text type Document = | Document(List, String) // Internal parser state type PState = | PState(Bool, Bool, List, String) fn psInFront(s: PState): Bool = match s { PState(f, _, _, _) => f, } fn psPastFront(s: PState): Bool = match s { PState(_, p, _, _) => p, } fn psEntries(s: PState): List = match s { PState(_, _, e, _) => e, } fn psBody(s: PState): String = match s { PState(_, _, _, b) => b, } // Strip surrounding quotes from a value if present fn stripQuotes(s: String): String = if String.length(s) >= 2 then if String.startsWith(s, "\"") then if String.endsWith(s, "\"") then String.substring(s, 1, String.length(s) - 1) else s else if String.startsWith(s, "'") then if String.endsWith(s, "'") then String.substring(s, 1, String.length(s) - 1) else s else s else s // Parse a single line within the frontmatter block fn parseFrontLine(entries: List, line: String): List = match String.indexOf(line, ": ") { Some(idx) => { let key = String.trim(String.substring(line, 0, idx)) let rawVal = String.trim(String.substring(line, idx + 2, String.length(line))) let val = stripQuotes(rawVal) List.concat(entries, [Entry(key, val)]) }, None => { // Handle "key:" with no value (treat as empty string) let trimmed = String.trim(line) if String.endsWith(trimmed, ":") then { let key = String.substring(trimmed, 0, String.length(trimmed) - 1) List.concat(entries, [Entry(key, "")]) } else entries }, } // Fold function that processes one line at a time fn foldLine(acc: PState, line: String): PState = { let inFront = psInFront(acc) let pastFront = psPastFront(acc) let entries = psEntries(acc) let body = psBody(acc) if pastFront then PState(inFront, pastFront, entries, body + line + "\n") else if String.trim(line) == "---" then if inFront then PState(false, true, entries, body) else PState(true, false, entries, body) else if inFront then PState(inFront, pastFront, parseFrontLine(entries, line), body) else acc } // Parse a document string into frontmatter entries and body content. // // Returns a Document with an empty entry list if no frontmatter is found. pub fn parse(content: String): Document = { let lines = String.lines(content) let init = PState(false, false, [], "") let result = List.fold(lines, init, foldLine) Document(psEntries(result), psBody(result)) } // Get the list of key-value entries from a Document pub fn entries(doc: Document): List = match doc { Document(e, _) => e, } // Get the body text from a Document pub fn body(doc: Document): String = match doc { Document(_, b) => b, } // Look up a value by key. Returns the first matching entry. pub fn get(doc: Document, key: String): Option = { let es = entries(doc) getFromEntries(es, key) } fn getFromEntries(es: List, key: String): Option = match List.head(es) { Some(entry) => match entry { Entry(k, v) => if k == key then Some(v) else match List.tail(es) { Some(rest) => getFromEntries(rest, key), None => None, }, }, None => None, } // Get a value by key, returning a default if not found pub fn getOrDefault(doc: Document, key: String, default: String): String = match get(doc, key) { Some(v) => v, None => default, } // Get the entry key pub fn entryKey(e: Entry): String = match e { Entry(k, _) => k, } // Get the entry value pub fn entryValue(e: Entry): String = match e { Entry(_, v) => v, } // Check if a document has frontmatter (at least one entry) pub fn hasFrontmatter(doc: Document): Bool = List.length(entries(doc)) > 0 // Split a tags string into a list of tags (space-separated) pub fn parseTags(tagsStr: String): List = if tagsStr == "" then [] else String.split(tagsStr, " ") // Convenience: parse and get common blog fields pub fn title(doc: Document): String = getOrDefault(doc, "title", "") pub fn date(doc: Document): String = getOrDefault(doc, "date", "") pub fn description(doc: Document): String = getOrDefault(doc, "description", "") pub fn tags(doc: Document): List = parseTags(getOrDefault(doc, "tags", ""))