fmj/fmj.tm
2026-01-29 19:12:05 -06:00

178 lines
5.1 KiB
Tcl

enum Writable(
Stdout,
File(path:Path),
)
func write(self:Writable, text:Text, newline:Bool=no)
when self is Stdout
say(text, newline)
return
is File(path)
_ := path.append(text)
if newline
_ := path.append("\n")
return
struct Option(option: Text, value: Text)
func parse(line: Text, expected: Text -> Option?)
val : Text = ""
if not line.starts_with(expected, &val)
return none
if val == ""
return none
foundequals : Bool = no
cursor := 1
while yes
if val[cursor] == " "
cursor += 1
continue
if not foundequals and val[cursor] == "="
foundequals = yes
cursor += 1
continue
if foundequals and cursor <= val.length
return Option(expected, val.slice(cursor))
return none
return none
fmjconfpath := ".fmjconf"
fmj_extension := "fmj"
description := "description"
title := "title"
link := "link"
default_author := "default_author"
author := "author"
category := "category"
pub_date := "pub_date"
default_author_value : Text? = none
func write_feed_header(out: Writable, config: Text?)
out.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?><rss version=\"2.0\"><channel>")
found_title : Bool = no
found_link : Bool = no
found_description : Bool = no
found_default_author : Bool = no
if config
for line in config.lines()
opt : Option? = none
opt = Option.parse(line, title)
if not found_title and opt
out.write("<title>$(opt!.value)</title>")
found_title = yes
continue
opt = Option.parse(line, link)
if not found_link and opt
out.write("<link>$(opt!.value)</link>")
found_link = yes
continue
opt = Option.parse(line, description)
if not found_description and opt
out.write("<description>$(opt!.value)</description>")
found_description = yes
continue
opt = Option.parse(line, default_author)
if not found_default_author and opt
default_author_value = opt!.value
found_default_author = yes
continue
if not found_title
out.write("<title>fmj generated feed</title>")
if not found_link
out.write("<link>http://example.com</link>")
if not found_description
out.write("<description>an rss feed generated by fmj</description>")
out.write("<generator>fmj</generator>")
func write_feed_footer(out : Writable)
out.write("</channel></rss>")
func write_feed_item(out : Writable, path : Path)
if not (path.extension() == fmj_extension)
return
lines := path.read()!.lines()
has_requirements : Bool = no
for line in lines
if Option.parse(line, title) or Option.parse(line, description)
has_requirements = yes
break
if not has_requirements
return
found_title : Bool = no
found_description : Bool = no
found_link : Bool = no
found_pub_date : Bool = no
found_author : Bool = no
out.write("<item>")
for line in lines
opt : Option? = none
opt = Option.parse(line, title)
if not found_title and opt
out.write("<title>$(opt!.value)</title>")
found_title = yes
continue
opt = Option.parse(line, link)
if not found_link and opt
out.write("<link>$(opt!.value)</link>")
found_link = yes
continue
opt = Option.parse(line, description)
if not found_description and opt
out.write("<description>$(opt!.value)</description>")
found_description = yes
continue
opt = Option.parse(line, author)
if not found_author and opt
out.write("<author>$(opt!.value)</description>")
found_author = yes
continue
opt = Option.parse(line, pub_date)
if not found_pub_date and opt
out.write("<pubDate>$(opt!.value)</pubDate>")
found_pub_date = yes
continue
opt = Option.parse(line, category)
if opt
out.write("<category>$(opt.value)</category>")
continue
if not found_author and default_author_value
out.write("<author>$(default_author_value)</author>")
out.write("</item>")
func main(dir: Path, output: Text = "rss.xml")
usingStdout : Bool = no
outfile : Path? = none
if output == "-"
usingStdout = yes
else
outfile = Path.from_text(output)
if not usingStdout and outfile!.exists()
_ := outfile!.remove()
out : Writable = if usingStdout
Writable.Stdout
else
Writable.File(outfile!)
config := dir.child(fmjconfpath).read()
out.write_feed_header(config)
for path in dir.children()
out.write_feed_item(path)
out.write_feed_footer()