Sync local packages into the registry repo and update index.json and README.md to include all 9 packages.
169 lines
4.8 KiB
Plaintext
169 lines
4.8 KiB
Plaintext
// 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<Entry>, String)
|
|
|
|
// Internal parser state
|
|
type PState =
|
|
| PState(Bool, Bool, List<Entry>, 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<Entry> =
|
|
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<Entry>, line: String): List<Entry> =
|
|
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<Entry> =
|
|
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<String> = {
|
|
let es = entries(doc)
|
|
getFromEntries(es, key)
|
|
}
|
|
|
|
fn getFromEntries(es: List<Entry>, key: String): Option<String> =
|
|
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<String> =
|
|
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<String> = parseTags(getOrDefault(doc, "tags", ""))
|