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:
2026-02-24 21:04:20 -05:00
parent c5a2276f6e
commit cbb66fbb73
42 changed files with 3844 additions and 0 deletions

3
packages/rss/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
_site/
.lux_packages/
*.bak

101
packages/rss/lib.lux Normal file
View File

@@ -0,0 +1,101 @@
// rss - RSS 2.0 feed generator for Lux
//
// Generates valid RSS 2.0 XML feeds from structured data.
import xml
// An RSS feed item
type Item =
| Item(String, String, String, String, String)
// Item(title, link, description, pubDate, guid)
// An RSS channel/feed
type Feed =
| Feed(String, String, String, String, List<Item>)
// Feed(title, link, description, language, items)
// Create a feed item
pub fn item(title: String, link: String, description: String, pubDate: String): Item =
Item(title, link, description, pubDate, link)
// Create a feed item with custom GUID
pub fn itemWithGuid(title: String, link: String, description: String, pubDate: String, guid: String): Item =
Item(title, link, description, pubDate, guid)
// Create an RSS feed
pub fn feed(title: String, link: String, description: String, items: List<Item>): Feed =
Feed(title, link, description, "en", items)
// Create an RSS feed with language
pub fn feedWithLang(title: String, link: String, description: String, language: String, items: List<Item>): Feed =
Feed(title, link, description, language, items)
// Access item fields
pub fn itemTitle(i: Item): String = match i { Item(t, _, _, _, _) => t, }
pub fn itemLink(i: Item): String = match i { Item(_, l, _, _, _) => l, }
pub fn itemDescription(i: Item): String = match i { Item(_, _, d, _, _) => d, }
pub fn itemPubDate(i: Item): String = match i { Item(_, _, _, p, _) => p, }
pub fn itemGuid(i: Item): String = match i { Item(_, _, _, _, g) => g, }
// Convert ISO date (2025-01-29) to RFC 822 format (29 Jan 2025 00:00:00 GMT)
pub fn isoToRfc822(isoDate: String): String =
if String.length(isoDate) < 10 then isoDate
else {
let year = String.substring(isoDate, 0, 4)
let month = String.substring(isoDate, 5, 7)
let day = String.substring(isoDate, 8, 10)
let monthName = if month == "01" then "Jan"
else if month == "02" then "Feb"
else if month == "03" then "Mar"
else if month == "04" then "Apr"
else if month == "05" then "May"
else if month == "06" then "Jun"
else if month == "07" then "Jul"
else if month == "08" then "Aug"
else if month == "09" then "Sep"
else if month == "10" then "Oct"
else if month == "11" then "Nov"
else "Dec"
day + " " + monthName + " " + year + " 00:00:00 GMT"
}
// Render a single item to XML string
fn renderItemXml(i: Item): String =
match i {
Item(title, link, desc, pubDate, guid) =>
xml.render(xml.el("item", [
xml.textEl("title", title),
xml.textEl("link", link),
xml.textEl("description", desc),
xml.textEl("pubDate", isoToRfc822(pubDate)),
xml.textElAttr("guid", [xml.attr("isPermaLink", "true")], guid)
])),
}
// Render items to concatenated XML string
fn renderItemsXml(items: List<Item>): String =
match List.head(items) {
None => "",
Some(i) => renderItemXml(i) + match List.tail(items) {
Some(rest) => renderItemsXml(rest),
None => "",
},
}
// Render an RSS feed to XML string
pub fn render(f: Feed): String =
match f {
Feed(title, link, desc, lang, items) => {
let itemsXml = renderItemsXml(items)
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
"<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">" +
"<channel>" +
"<title>" + xml.escape(title) + "</title>" +
"<link>" + xml.escape(link) + "</link>" +
"<description>" + xml.escape(desc) + "</description>" +
"<language>" + lang + "</language>" +
"<generator>Lux RSS Package</generator>" +
itemsXml +
"</channel></rss>"
},
}

7
packages/rss/lux.lock Normal file
View File

@@ -0,0 +1,7 @@
# This file is auto-generated by lux pkg. Do not edit manually.
[[package]]
name = "xml"
version = "0.1.0"
source = "path:../xml"

9
packages/rss/lux.toml Normal file
View File

@@ -0,0 +1,9 @@
[project]
name = "rss"
version = "0.1.0"
description = "RSS 2.0 feed generator for Lux"
authors = ["Brandon Lucas"]
license = "MIT"
[dependencies]
xml = { version = "0.1.0", path = "../xml" }

View File

@@ -0,0 +1,106 @@
import lib
fn contains(haystack: String, needle: String): Bool =
match String.indexOf(haystack, needle) {
Some(_) => true,
None => false,
}
// Build a complete blog RSS feed with multiple posts
fn test_full_blog_feed(): Unit with {Test} = {
let items = [
lib.item("Getting Started with Lux", "https://blog.example.com/getting-started", "Learn the basics of the Lux programming language", "2025-01-15"),
lib.item("Advanced Effects in Lux", "https://blog.example.com/advanced-effects", "Deep dive into algebraic effects", "2025-02-01"),
lib.item("Building a Static Site", "https://blog.example.com/static-site", "Create a blog with Lux", "2025-02-15")
]
let f = lib.feed("Lux Programming Blog", "https://blog.example.com", "Tips and tutorials about Lux", items)
let xml = lib.render(f)
Test.assert(contains(xml, "<?xml version=\"1.0\""), "has XML declaration")
Test.assert(contains(xml, "<rss version=\"2.0\""), "has RSS 2.0 tag")
Test.assert(contains(xml, "<title>Lux Programming Blog</title>"), "feed title")
Test.assert(contains(xml, "<link>https://blog.example.com</link>"), "feed link")
Test.assert(contains(xml, "<title>Getting Started with Lux</title>"), "first item title")
Test.assert(contains(xml, "<title>Advanced Effects in Lux</title>"), "second item title")
Test.assert(contains(xml, "<title>Building a Static Site</title>"), "third item title")
Test.assert(contains(xml, "15 Jan 2025 00:00:00 GMT"), "first item date in RFC 822")
Test.assert(contains(xml, "01 Feb 2025 00:00:00 GMT"), "second item date in RFC 822")
Test.assert(contains(xml, "<generator>Lux RSS Package</generator>"), "has generator tag")
}
// Feed with items containing custom GUIDs
fn test_feed_with_custom_guids(): Unit with {Test} = {
let items = [
lib.itemWithGuid("Post 1", "https://example.com/1", "Desc 1", "2025-01-01", "urn:uuid:1234"),
lib.itemWithGuid("Post 2", "https://example.com/2", "Desc 2", "2025-02-01", "urn:uuid:5678")
]
let f = lib.feed("Blog", "https://example.com", "A blog", items)
let xml = lib.render(f)
Test.assert(contains(xml, "urn:uuid:1234"), "first custom guid")
Test.assert(contains(xml, "urn:uuid:5678"), "second custom guid")
}
// Feed with special characters in all fields
fn test_feed_with_special_chars(): Unit with {Test} = {
let i = lib.item(
"Tom & Jerry's <Adventure>",
"https://example.com/tom&jerry",
"A \"great\" show with <characters>",
"2025-01-01"
)
let f = lib.feed(
"Kids' TV & More",
"https://example.com",
"Shows for <everyone>",
[i]
)
let xml = lib.render(f)
Test.assert(contains(xml, "&amp;"), "ampersand escaped somewhere")
Test.assert(contains(xml, "&lt;"), "angle bracket escaped somewhere")
}
// Multilingual feed
fn test_multilingual_feed(): Unit with {Test} = {
let f = lib.feedWithLang(
"Mon Blog",
"https://exemple.fr",
"Un blog en francais",
"fr",
[lib.item("Premier Article", "https://exemple.fr/premier", "Le debut", "2025-01-01")]
)
let xml = lib.render(f)
Test.assert(contains(xml, "<language>fr</language>"), "french language tag")
Test.assert(contains(xml, "<title>Premier Article</title>"), "french item title")
}
// Empty feed is still valid XML
fn test_empty_feed_structure(): Unit with {Test} = {
let f = lib.feed("Empty Blog", "https://example.com", "Nothing here yet", [])
let xml = lib.render(f)
Test.assert(contains(xml, "<?xml"), "has declaration")
Test.assert(contains(xml, "<rss"), "has rss tag")
Test.assert(contains(xml, "<channel>"), "has channel open")
Test.assert(contains(xml, "</channel>"), "has channel close")
Test.assert(contains(xml, "</rss>"), "has rss close")
Test.assert(contains(xml, "<title>Empty Blog</title>"), "has title")
}
// Feed with many items (stress test)
fn test_feed_many_items(): Unit with {Test} = {
let items = [
lib.item("Post 1", "https://example.com/1", "D1", "2025-01-01"),
lib.item("Post 2", "https://example.com/2", "D2", "2025-01-02"),
lib.item("Post 3", "https://example.com/3", "D3", "2025-01-03"),
lib.item("Post 4", "https://example.com/4", "D4", "2025-01-04"),
lib.item("Post 5", "https://example.com/5", "D5", "2025-01-05"),
lib.item("Post 6", "https://example.com/6", "D6", "2025-01-06"),
lib.item("Post 7", "https://example.com/7", "D7", "2025-01-07"),
lib.item("Post 8", "https://example.com/8", "D8", "2025-01-08"),
lib.item("Post 9", "https://example.com/9", "D9", "2025-01-09"),
lib.item("Post 10", "https://example.com/10", "D10", "2025-01-10")
]
let f = lib.feed("Big Blog", "https://example.com", "Many posts", items)
let xml = lib.render(f)
Test.assert(contains(xml, "<title>Post 1</title>"), "first item")
Test.assert(contains(xml, "<title>Post 10</title>"), "last item")
Test.assert(contains(xml, "10 Jan 2025"), "last item date")
}

67
packages/rss/test_rss.lux Normal file
View File

@@ -0,0 +1,67 @@
import lib
fn contains(haystack: String, needle: String): Bool =
match String.indexOf(haystack, needle) {
Some(_) => true,
None => false,
}
fn test_create_item(): Unit with {Test} = {
let i = lib.item("My Post", "https://example.com/post", "A description", "2025-01-29")
Test.assertEqualMsg("My Post", lib.itemTitle(i), "item title")
Test.assertEqualMsg("https://example.com/post", lib.itemLink(i), "item link")
Test.assertEqualMsg("A description", lib.itemDescription(i), "item description")
Test.assertEqualMsg("2025-01-29", lib.itemPubDate(i), "item pubDate")
Test.assertEqualMsg("https://example.com/post", lib.itemGuid(i), "item guid defaults to link")
}
fn test_item_with_guid(): Unit with {Test} = {
let i = lib.itemWithGuid("Post", "https://example.com", "Desc", "2025-01-29", "custom-guid-123")
Test.assertEqualMsg("custom-guid-123", lib.itemGuid(i), "item custom guid")
}
fn test_date_conversion(): Unit with {Test} = {
Test.assertEqualMsg("29 Jan 2025 00:00:00 GMT", lib.isoToRfc822("2025-01-29"), "date jan")
Test.assertEqualMsg("31 Dec 2025 00:00:00 GMT", lib.isoToRfc822("2025-12-31"), "date dec")
Test.assertEqualMsg("15 Jun 2025 00:00:00 GMT", lib.isoToRfc822("2025-06-15"), "date jun")
Test.assertEqualMsg("bad", lib.isoToRfc822("bad"), "date short string passthrough")
}
fn test_empty_feed(): Unit with {Test} = {
let emptyFeed = lib.feed("My Blog", "https://example.com", "Blog about things", [])
let xml = lib.render(emptyFeed)
Test.assert(contains(xml, "<?xml version=\"1.0\""), "xml declaration")
Test.assert(contains(xml, "<rss version=\"2.0\""), "rss tag")
Test.assert(contains(xml, "<title>My Blog</title>"), "feed title")
Test.assert(contains(xml, "<link>https://example.com</link>"), "feed link")
Test.assert(contains(xml, "<description>Blog about things</description>"), "feed description")
Test.assert(contains(xml, "<language>en</language>"), "feed language")
}
fn test_feed_with_items(): Unit with {Test} = {
let items = [
lib.item("First Post", "https://example.com/first", "First!", "2025-01-29"),
lib.item("Second Post", "https://example.com/second", "Second!", "2025-02-01")
]
let fullFeed = lib.feed("Blog", "https://example.com", "A blog", items)
let xml = lib.render(fullFeed)
Test.assert(contains(xml, "<item>"), "has item tag")
Test.assert(contains(xml, "<title>First Post</title>"), "first item title")
Test.assert(contains(xml, "<title>Second Post</title>"), "second item title")
Test.assert(contains(xml, "29 Jan 2025 00:00:00 GMT"), "rfc822 date in feed")
Test.assert(contains(xml, "<guid isPermaLink=\"true\">"), "guid with permalink")
}
fn test_feed_with_language(): Unit with {Test} = {
let frFeed = lib.feedWithLang("Mon Blog", "https://example.fr", "Un blog", "fr", [])
let xml = lib.render(frFeed)
Test.assert(contains(xml, "<language>fr</language>"), "custom language")
}
fn test_special_characters(): Unit with {Test} = {
let specialItem = lib.item("A & B <C>", "https://example.com", "\"quoted\" & <tagged>", "2025-01-01")
let specialFeed = lib.feed("Test", "https://example.com", "Test", [specialItem])
let xml = lib.render(specialFeed)
Test.assert(contains(xml, "&amp;"), "ampersand escaped")
Test.assert(contains(xml, "&lt;"), "angle bracket escaped")
}

View File

@@ -0,0 +1,103 @@
import lib
fn contains(haystack: String, needle: String): Bool =
match String.indexOf(haystack, needle) {
Some(_) => true,
None => false,
}
// Snapshot: complete single-item feed
fn test_snapshot_single_item_feed(): Unit with {Test} = {
let f = lib.feed(
"My Blog",
"https://example.com",
"A personal blog",
[lib.item("Hello World", "https://example.com/hello", "My first post", "2025-01-29")]
)
let xml = lib.render(f)
// Verify exact structure of the feed
Test.assert(String.startsWith(xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"), "snap: XML declaration")
Test.assert(contains(xml, "<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\">"), "snap: RSS opening tag")
Test.assert(contains(xml, "<channel>"), "snap: channel open")
Test.assert(contains(xml, "<title>My Blog</title>"), "snap: feed title")
Test.assert(contains(xml, "<link>https://example.com</link>"), "snap: feed link")
Test.assert(contains(xml, "<description>A personal blog</description>"), "snap: feed desc")
Test.assert(contains(xml, "<language>en</language>"), "snap: language")
Test.assert(contains(xml, "<generator>Lux RSS Package</generator>"), "snap: generator")
Test.assert(contains(xml, "<item>"), "snap: item open")
Test.assert(contains(xml, "<title>Hello World</title>"), "snap: item title")
Test.assert(contains(xml, "<link>https://example.com/hello</link>"), "snap: item link")
Test.assert(contains(xml, "<description>My first post</description>"), "snap: item desc")
Test.assert(contains(xml, "<pubDate>29 Jan 2025 00:00:00 GMT</pubDate>"), "snap: item pubDate")
Test.assert(contains(xml, "<guid isPermaLink=\"true\">https://example.com/hello</guid>"), "snap: item guid")
Test.assert(contains(xml, "</item>"), "snap: item close")
Test.assert(contains(xml, "</channel></rss>"), "snap: channel and rss close")
}
// Snapshot: empty feed
fn test_snapshot_empty_feed(): Unit with {Test} = {
let f = lib.feed("Empty", "https://example.com", "Nothing", [])
let xml = lib.render(f)
Test.assert(String.startsWith(xml, "<?xml version=\"1.0\""), "snap: starts with declaration")
// Empty feed should have no <item> tags
Test.assertEqualMsg(false, contains(xml, "<item>"), "snap: no item tags")
Test.assert(contains(xml, "<title>Empty</title>"), "snap: title present")
Test.assert(String.endsWith(xml, "</channel></rss>"), "snap: ends properly")
}
// Snapshot: feed with custom language
fn test_snapshot_french_feed(): Unit with {Test} = {
let f = lib.feedWithLang("Blog Francais", "https://exemple.fr", "Un blog", "fr", [
lib.item("Bonjour", "https://exemple.fr/bonjour", "Premier article", "2025-06-15")
])
let xml = lib.render(f)
Test.assert(contains(xml, "<language>fr</language>"), "snap: french language")
Test.assert(contains(xml, "<title>Bonjour</title>"), "snap: french item title")
Test.assert(contains(xml, "15 Jun 2025 00:00:00 GMT"), "snap: french item date")
}
// Snapshot: feed with escaped content
fn test_snapshot_escaped_feed(): Unit with {Test} = {
let f = lib.feed(
"Tom & Jerry",
"https://example.com",
"A <fun> show",
[lib.item("Episode: \"The Chase\"", "https://example.com/1", "Tom & Jerry's adventure", "2025-01-01")]
)
let xml = lib.render(f)
Test.assert(contains(xml, "<title>Tom &amp; Jerry</title>"), "snap: escaped feed title")
Test.assert(contains(xml, "<description>A &lt;fun&gt; show</description>"), "snap: escaped feed desc")
}
// Snapshot: multi-item feed order
fn test_snapshot_multi_item_order(): Unit with {Test} = {
let f = lib.feed("Blog", "https://example.com", "A blog", [
lib.item("First", "https://example.com/1", "D1", "2025-01-01"),
lib.item("Second", "https://example.com/2", "D2", "2025-02-01"),
lib.item("Third", "https://example.com/3", "D3", "2025-03-01")
])
let xml = lib.render(f)
// Items should appear in order
let firstIdx = match String.indexOf(xml, "<title>First</title>") { Some(i) => i, None => -1 }
let secondIdx = match String.indexOf(xml, "<title>Second</title>") { Some(i) => i, None => -1 }
let thirdIdx = match String.indexOf(xml, "<title>Third</title>") { Some(i) => i, None => -1 }
Test.assert(firstIdx > 0, "snap: first item found")
Test.assert(secondIdx > firstIdx, "snap: second after first")
Test.assert(thirdIdx > secondIdx, "snap: third after second")
}
// Snapshot: date conversion across all months
fn test_snapshot_all_month_dates(): Unit with {Test} = {
Test.assertEqualMsg("01 Jan 2025 00:00:00 GMT", lib.isoToRfc822("2025-01-01"), "snap: Jan")
Test.assertEqualMsg("01 Feb 2025 00:00:00 GMT", lib.isoToRfc822("2025-02-01"), "snap: Feb")
Test.assertEqualMsg("01 Mar 2025 00:00:00 GMT", lib.isoToRfc822("2025-03-01"), "snap: Mar")
Test.assertEqualMsg("01 Apr 2025 00:00:00 GMT", lib.isoToRfc822("2025-04-01"), "snap: Apr")
Test.assertEqualMsg("01 May 2025 00:00:00 GMT", lib.isoToRfc822("2025-05-01"), "snap: May")
Test.assertEqualMsg("01 Jun 2025 00:00:00 GMT", lib.isoToRfc822("2025-06-01"), "snap: Jun")
Test.assertEqualMsg("01 Jul 2025 00:00:00 GMT", lib.isoToRfc822("2025-07-01"), "snap: Jul")
Test.assertEqualMsg("01 Aug 2025 00:00:00 GMT", lib.isoToRfc822("2025-08-01"), "snap: Aug")
Test.assertEqualMsg("01 Sep 2025 00:00:00 GMT", lib.isoToRfc822("2025-09-01"), "snap: Sep")
Test.assertEqualMsg("01 Oct 2025 00:00:00 GMT", lib.isoToRfc822("2025-10-01"), "snap: Oct")
Test.assertEqualMsg("01 Nov 2025 00:00:00 GMT", lib.isoToRfc822("2025-11-01"), "snap: Nov")
Test.assertEqualMsg("01 Dec 2025 00:00:00 GMT", lib.isoToRfc822("2025-12-01"), "snap: Dec")
}

111
packages/rss/test_unit.lux Normal file
View File

@@ -0,0 +1,111 @@
import lib
// --- item creation ---
fn test_item_defaults_guid_to_link(): Unit with {Test} = {
let i = lib.item("Title", "https://example.com/post", "Desc", "2025-01-01")
Test.assertEqualMsg("https://example.com/post", lib.itemGuid(i), "guid defaults to link")
}
fn test_item_with_custom_guid(): Unit with {Test} = {
let i = lib.itemWithGuid("Title", "https://example.com", "Desc", "2025-01-01", "custom-123")
Test.assertEqualMsg("custom-123", lib.itemGuid(i), "custom guid preserved")
}
fn test_item_empty_fields(): Unit with {Test} = {
let i = lib.item("", "", "", "")
Test.assertEqualMsg("", lib.itemTitle(i), "empty title")
Test.assertEqualMsg("", lib.itemLink(i), "empty link")
Test.assertEqualMsg("", lib.itemDescription(i), "empty description")
Test.assertEqualMsg("", lib.itemPubDate(i), "empty pubDate")
}
fn test_item_special_chars(): Unit with {Test} = {
let i = lib.item("A & B <C>", "https://example.com?a=1&b=2", "\"quotes\" & <tags>", "2025-01-01")
Test.assertEqualMsg("A & B <C>", lib.itemTitle(i), "special chars in title preserved")
Test.assertEqualMsg("\"quotes\" & <tags>", lib.itemDescription(i), "special chars in desc preserved")
}
// --- all accessor functions ---
fn test_item_accessors(): Unit with {Test} = {
let i = lib.item("T", "L", "D", "P")
Test.assertEqualMsg("T", lib.itemTitle(i), "itemTitle")
Test.assertEqualMsg("L", lib.itemLink(i), "itemLink")
Test.assertEqualMsg("D", lib.itemDescription(i), "itemDescription")
Test.assertEqualMsg("P", lib.itemPubDate(i), "itemPubDate")
Test.assertEqualMsg("L", lib.itemGuid(i), "itemGuid defaults to link")
}
// --- date conversion for all months ---
fn test_date_january(): Unit with {Test} =
Test.assertEqualMsg("15 Jan 2025 00:00:00 GMT", lib.isoToRfc822("2025-01-15"), "January")
fn test_date_february(): Unit with {Test} =
Test.assertEqualMsg("28 Feb 2025 00:00:00 GMT", lib.isoToRfc822("2025-02-28"), "February")
fn test_date_march(): Unit with {Test} =
Test.assertEqualMsg("01 Mar 2025 00:00:00 GMT", lib.isoToRfc822("2025-03-01"), "March")
fn test_date_april(): Unit with {Test} =
Test.assertEqualMsg("30 Apr 2025 00:00:00 GMT", lib.isoToRfc822("2025-04-30"), "April")
fn test_date_may(): Unit with {Test} =
Test.assertEqualMsg("01 May 2025 00:00:00 GMT", lib.isoToRfc822("2025-05-01"), "May")
fn test_date_june(): Unit with {Test} =
Test.assertEqualMsg("15 Jun 2025 00:00:00 GMT", lib.isoToRfc822("2025-06-15"), "June")
fn test_date_july(): Unit with {Test} =
Test.assertEqualMsg("04 Jul 2025 00:00:00 GMT", lib.isoToRfc822("2025-07-04"), "July")
fn test_date_august(): Unit with {Test} =
Test.assertEqualMsg("20 Aug 2025 00:00:00 GMT", lib.isoToRfc822("2025-08-20"), "August")
fn test_date_september(): Unit with {Test} =
Test.assertEqualMsg("10 Sep 2025 00:00:00 GMT", lib.isoToRfc822("2025-09-10"), "September")
fn test_date_october(): Unit with {Test} =
Test.assertEqualMsg("31 Oct 2025 00:00:00 GMT", lib.isoToRfc822("2025-10-31"), "October")
fn test_date_november(): Unit with {Test} =
Test.assertEqualMsg("11 Nov 2025 00:00:00 GMT", lib.isoToRfc822("2025-11-11"), "November")
fn test_date_december(): Unit with {Test} =
Test.assertEqualMsg("25 Dec 2025 00:00:00 GMT", lib.isoToRfc822("2025-12-25"), "December")
fn test_date_short_string(): Unit with {Test} =
Test.assertEqualMsg("bad", lib.isoToRfc822("bad"), "short string passthrough")
fn test_date_empty(): Unit with {Test} =
Test.assertEqualMsg("", lib.isoToRfc822(""), "empty date passthrough")
fn test_date_partial(): Unit with {Test} =
Test.assertEqualMsg("2025-01", lib.isoToRfc822("2025-01"), "partial date passthrough")
// --- feed creation ---
fn test_feed_default_language(): Unit with {Test} = {
let f = lib.feed("Title", "https://example.com", "Desc", [])
let xml = lib.render(f)
Test.assert(String.contains(xml, "<language>en</language>"), "default language is en")
}
fn test_feed_custom_language(): Unit with {Test} = {
let f = lib.feedWithLang("Title", "https://example.com", "Desc", "fr", [])
let xml = lib.render(f)
Test.assert(String.contains(xml, "<language>fr</language>"), "custom language fr")
}
fn test_feed_escapes_title(): Unit with {Test} = {
let f = lib.feed("A & B", "https://example.com", "Desc", [])
let xml = lib.render(f)
Test.assert(String.contains(xml, "<title>A &amp; B</title>"), "feed title is escaped")
}
fn test_feed_escapes_description(): Unit with {Test} = {
let f = lib.feed("Title", "https://example.com", "<b>Bold</b> desc", [])
let xml = lib.render(f)
Test.assert(String.contains(xml, "&lt;b&gt;Bold&lt;/b&gt;"), "feed description is escaped")
}