initial commit

This commit is contained in:
moss 2026-01-29 19:12:05 -06:00
commit e43f252e44
8 changed files with 215 additions and 0 deletions

177
fmj.tm Normal file
View File

@ -0,0 +1,177 @@
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()

24
readme.md Normal file
View File

@ -0,0 +1,24 @@
# fmj
fmj is an rss feed generator. it takes a directory of config files that describe the posts and outputs xml.
the file format is more concise than handwriting xml:
```fmj
title = s/discord/???
description = on replacing discord
link = https://bear.o7moon.dev/sdiscord/
anything that isnt one of the defined options is a comment
```
## usage examples:
- `fmj tests/examples \\-` will output to stdout
- `fmj tests/noconf filename.xml` will output to `filename.xml`
- `fmj tests/defaultauthor` will output to `rss.xml`
## building / platform support:
this repo is a port of the [previous c implementation](https://git.tilde.town/moss/fmj) to [tomo](https://github.com/bruce-hill/tomo), which is a relatively recent language and has some rough edges (the requirement to write \\- instead of - for stdout comes from a conflict with the language's built-in argument parsing) but ive found it to be nicer than c for this small program and i used it as a chance to both learn a new language and make a small improvement to the c implementation (this one handles utf-8! due to tomo's Text type also handling it).
`fmj.tm` can be built with the tomo compiler like this, if you have it installed:
`````
```
tomo -e fmj.tm
```

View File

@ -0,0 +1,3 @@
title = default author test case
description = contains a post with an author option and one without, to test the default author option from the .fmjconf file
default_author = alice

View File

@ -0,0 +1,2 @@
title = alice's post
description = this post implicitly has alice as the author due to the .fmjconf

View File

@ -0,0 +1,3 @@
title = bob's post
author = bob
description = bob wrote this post.

2
tests/example/.fmjconf Normal file
View File

@ -0,0 +1,2 @@
title = example rss feed !!!!!!!!!
description = this is an example generated by fmj as a test case

3
tests/example/test1.fmj Normal file
View File

@ -0,0 +1,3 @@
title = TEST TITLE !!!!!!
this is a comment

1
tests/noconf/test1.fmj Normal file
View File

@ -0,0 +1 @@
title = this is a test where the config file doesnt exist