// SSG Generators
//
// Pure functions to generate sitemap.xml, RSS feed, and llms.txt from DocumentTree.
// These generators are shared between Sol SSG and Sol (SSR).
// =============================================================================
// Sitemap Generator
// =============================================================================
///|
/// Generate sitemap.xml from DocumentTree
pub fn generate_sitemap(tree : DocumentTree) -> String {
let mut urls = ""
for page in tree.pages {
urls = urls + generate_sitemap_url(page, tree.site.base_url)
}
"\n\n" +
urls +
"\n"
}
///|
fn generate_sitemap_url(page : PageInfo, base_url : String) -> String {
let url = page.canonical_url(base_url)
let lastmod = iso_to_date(page.updated_at)
" \n " +
escape_xml(url) +
"\n" +
(if lastmod != "" { " " + lastmod + "\n" } else { "" }) +
" \n"
}
///|
/// Convert ISO 8601 datetime to date only
/// "2024-12-22T12:00:00.000Z" -> "2024-12-22"
fn iso_to_date(iso_date : String) -> String {
if iso_date.length() >= 10 {
iso_date[:10].to_owned()
} else {
iso_date
}
}
// =============================================================================
// RSS Feed Generator
// =============================================================================
///|
/// Generate RSS 2.0 feed from DocumentTree
pub fn generate_rss(tree : DocumentTree, limit? : Int = 20) -> String {
let pages = tree.get_pages_sorted_by_date(limit~)
let mut items = ""
for page in pages {
items = items + generate_rss_item(page, tree.site.base_url)
}
let rfc822_date = iso_to_rfc822(tree.site.updated_at)
"\n\n \n " +
escape_xml(tree.site.title) +
"\n " +
escape_xml(tree.site.base_url) +
"\n " +
escape_xml(tree.site.description) +
"\n " +
tree.site.language +
"\n " +
rfc822_date +
"\n \n" +
items +
" \n\n"
}
///|
fn generate_rss_item(page : PageInfo, base_url : String) -> String {
let url = page.canonical_url(base_url)
let rfc822_date = iso_to_rfc822(page.updated_at)
" - \n " +
escape_xml(page.title) +
"\n " +
escape_xml(url) +
"\n " +
escape_xml(page.description) +
"\n " +
rfc822_date +
"\n " +
escape_xml(url) +
"\n
\n"
}
///|
/// Convert ISO 8601 datetime to RFC 822 format
/// "2024-12-22T12:00:00.000Z" -> "Sun, 22 Dec 2024 12:00:00 GMT"
fn iso_to_rfc822(iso_date : String) -> String {
if iso_date == "" {
return ""
}
// Simple conversion (not a complete implementation)
// Format: YYYY-MM-DDTHH:MM:SS
if iso_date.length() < 10 {
return iso_date
}
let year = iso_date[:4].to_owned()
let month = iso_date[5:7].to_owned()
let day = iso_date[8:10].to_owned()
let time = if iso_date.length() >= 19 {
iso_date[11:19].to_owned()
} else {
"00:00:00"
}
let month_name = month_to_name(month)
// RFC 822: "22 Dec 2024 12:00:00 GMT"
day + " " + month_name + " " + year + " " + time + " GMT"
}
///|
fn month_to_name(month : String) -> String {
match month {
"01" => "Jan"
"02" => "Feb"
"03" => "Mar"
"04" => "Apr"
"05" => "May"
"06" => "Jun"
"07" => "Jul"
"08" => "Aug"
"09" => "Sep"
"10" => "Oct"
"11" => "Nov"
"12" => "Dec"
_ => "Jan"
}
}
// =============================================================================
// llms.txt Generator
// =============================================================================
///|
/// Generate llms.txt from DocumentTree
/// Reference: https://llmstxt.org/
pub fn generate_llms_txt(tree : DocumentTree) -> String {
let mut result = ""
// Header
result = result + "# " + tree.site.title + "\n\n"
if tree.site.description != "" {
result = result + "> " + tree.site.description + "\n\n"
}
// Table of contents
result = result + "## Table of Contents\n\n"
result = result + generate_toc(tree)
result = result + "\n---\n\n"
// Content for each page
for page in tree.pages {
if page.locale == tree.site.language {
result = result + generate_page_content(page)
}
}
result
}
///|
/// Generate table of contents
fn generate_toc(tree : DocumentTree) -> String {
let mut result = ""
result = result + generate_toc_node(tree.root, tree, 0)
result
}
///|
fn generate_toc_node(
node : TreeNode,
tree : DocumentTree,
depth : Int,
) -> String {
let indent = " ".repeat(depth)
match node {
TreeNode::Page(page_id~, ..) =>
match tree.get_page(page_id) {
Some(page) => indent + "- [" + page.title + "](" + page.url_path + ")\n"
None => ""
}
TreeNode::Section(name~, children~, ..) => {
let mut result = ""
if name != "" {
result = result + indent + "- " + name + "\n"
}
for child in children {
result = result + generate_toc_node(child, tree, depth + 1)
}
result
}
}
}
///|
/// Generate page content
fn generate_page_content(page : PageInfo) -> String {
let mut result = ""
// Page title
result = result + "# " + page.title + "\n\n"
// URL
result = result + "URL: " + page.url_path + "\n\n"
// Description
if page.description != "" {
result = result + "> " + page.description + "\n\n"
}
// Heading structure
if page.headings.length() > 0 {
result = result + "## Outline\n\n"
for heading in page.headings {
let indent = " ".repeat(heading.level - 1)
result = result + indent + "- " + heading.text + "\n"
}
result = result + "\n"
}
// Markdown content (strip frontmatter)
let content = strip_frontmatter(page.content_md)
if content != "" {
result = result + "## Content\n\n"
result = result + content + "\n"
}
result = result + "\n---\n\n"
result
}
///|
/// Strip frontmatter
fn strip_frontmatter(content : String) -> String {
if !content.has_prefix("---") {
return content
}
// Find the second "---"
let after_first = content[3:].to_owned()
match find_substring(after_first, "---") {
Some(idx) => after_first[idx + 3:].trim(chars=" \t\n\r").to_owned()
None => content
}
}
///|
/// Find substring
fn find_substring(haystack : String, needle : String) -> Int? {
let needle_chars = needle.to_array()
let haystack_chars = haystack.to_array()
let needle_len = needle_chars.length()
let haystack_len = haystack_chars.length()
if needle_len > haystack_len {
return None
}
for i = 0; i <= haystack_len - needle_len; i = i + 1 {
let mut found = true
for j = 0; j < needle_len; j = j + 1 {
if haystack_chars[i + j] != needle_chars[j] {
found = false
break
}
}
if found {
return Some(i)
}
}
None
}
// =============================================================================
// Utility Functions
// =============================================================================
///|
/// Escape XML special characters
fn escape_xml(s : String) -> String {
let mut result = ""
for c in s {
match c {
'&' => result = result + "&"
'<' => result = result + "<"
'>' => result = result + ">"
'"' => result = result + """
'\'' => result = result + "'"
_ => result = result + c.to_string()
}
}
result
}