Add frontmatter, markdown, path, xml, rss, and web packages
Sync local packages into the registry repo and update index.json and README.md to include all 9 packages.
This commit is contained in:
168
packages/frontmatter/lib.lux
Normal file
168
packages/frontmatter/lib.lux
Normal file
@@ -0,0 +1,168 @@
|
||||
// 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", ""))
|
||||
Reference in New Issue
Block a user