package main
import (
htemplate "html/template"
"net/url"
"os"
"text/template"
"tildegit.org/tjp/sliderule/gemini/gemtext"
"tildegit.org/tjp/sliderule/gemini/gemtext/atomconv"
gem_htmlconv "tildegit.org/tjp/sliderule/gemini/gemtext/htmlconv"
gem_mdconv "tildegit.org/tjp/sliderule/gemini/gemtext/mdconv"
"tildegit.org/tjp/sliderule/gopher/gophermap"
goph_htmlconv "tildegit.org/tjp/sliderule/gopher/gophermap/htmlconv"
goph_mdconv "tildegit.org/tjp/sliderule/gopher/gophermap/mdconv"
)
const usage = `Conversions for small web formats.
Usage:
sw-convert (-h | --help)
sw-convert --list-formats
sw-convert (-i INPUT | --input INPUT) (-o OUTPUT | --output OUTPUT) [-t PATH | --template PATH]
Options:
-h --help Show this screen.
--list-formats List supported input and output formats.
-i --input INPUT Format with which to parse standard input.
-o --output OUTPUT Which format to write to standard output.
-t --template PATH Path to a template file. May be specified more than once.
-l --location URL URL the source came from. Required only for gemtext to atom conversion.
Templates:
Template files provided by the -t/--template option should be in the golang template formats.
When converting to markdown they will be parsed by text/template (https://pkg.go.dev/text/template),
and for html conversions they will be parsed by html/template (https://pkg.go.dev/html/template).
The template names available for override depend on the type of the source document.
Gemtext:
"header" is rendered at the beginning of the document and is passed the full Document.
"footer" is rendered at the end of the document and is passed the full Document.
"textline" is rendered once for each plain text line, and is passed the Line.
"linkline" is rendered for each link line and is passed the Line.
"preformattedtextlines" is rendered for each block of pre-formatted text, and is passed a slice of Lines.
"heading1line", "heading2line", and "heading3line" are rendered for heading lines and are passed the Line.
"listitemlines" is rendered for any contiguous group of list item lines, and is passed a slice of the Lines.
"quoteline" is rendered for each block-quote line and is passed the Line.
The default gemtext->html templates define an HTML5 document with all HTML nodes given a class of "gemtext".
Document:
The header and footer templates are given the full document, which can be iterated over to produce all the lines.
Line:
Line-specific or line-group-specific templates are passed Line objects. These all have Type, Raw, and String methods, and some have type-specific additional methods.
- link lines also have URL() and Label() methods.
- heading, list item, and quote lines have a Body() method which omit the leading prefixes.
Gophermap:
"header" is rendered at the beginning of the document and is passed the full Document.
"footer" is rendered at the end of the document and is passed the full Document.
"message" is rendered for any contiguous group of info-message lines, and is passed a string of the newline-separated lines.
"image" is rendered for any gif file, bitmap file, png file, or image line types, and is passed the Map Item.
"link" is rendered for all other line types and is passed the Map Item.
The default gophermap->html templates define an HTML5 document with all HTML nodes given a class of "gophermap".
Document:
The full gophermap document object is a list of map items, additionally with a String() method which serializes the full document back into the gophermap format.
Map Item:
An individual gophermap item has attributes Type, Display, Selector, Hostname, and Port, and can re-serialize itself into a gophermap CRLF-terminated line with the String() method.
`
const formats = `Inputs:
gemtext
gophermap
Outputs:
markdown
html
atom (with gemtext input only)
`
func main() {
conf := configure()
tover, hover, err := overrides(conf)
if err != nil {
fail("template loading failed")
}
switch conf.input {
case "gemtext":
doc, err := gemtext.Parse(os.Stdin)
if err != nil {
fail("failure reading gemtext")
}
switch conf.output {
case "markdown":
if err := gem_mdconv.Convert(os.Stdout, doc, tover); err != nil {
fail("failure writing markdown")
}
case "html":
if err := gem_htmlconv.Convert(os.Stdout, doc, hover); err != nil {
fail("failure writing html")
}
case "atom":
u, err := url.Parse(conf.location)
if conf.location == "" || err != nil {
fail("-l|--location must be a valid url for gemtext->atom conversion")
}
if err := atomconv.Convert(os.Stdout, doc, u); err != nil {
fail("failure writing atom XML")
}
default:
goto unknown
}
case "gophermap":
doc, err := gophermap.Parse(os.Stdin)
if err != nil {
fail("failure reading gophermap")
}
switch conf.output {
case "markdown":
if err := goph_mdconv.Convert(os.Stdout, doc, tover); err != nil {
fail("failure writing markdown")
}
case "html":
if err := goph_htmlconv.Convert(os.Stdout, doc, hover); err != nil {
fail("failure writing html")
}
default:
goto unknown
}
}
return
unknown:
os.Stderr.WriteString("unsupported input/output combination\n")
fail(formats)
}
type config struct {
input string
output string
location string
template []string
}
func configure() config {
conf := config{}
for i := 1; i < len(os.Args); i += 1 {
switch os.Args[i] {
case "-h", "--help":
os.Stdout.WriteString(usage)
os.Exit(0)
case "--list-formats":
os.Stdout.WriteString(formats)
os.Exit(0)
case "-i", "--input":
if i == len(os.Args) {
fail(usage)
}
i += 1
conf.input = os.Args[i]
case "-o", "--output":
if i == len(os.Args) {
fail(usage)
}
i += 1
conf.output = os.Args[i]
case "-l", "--location":
if i == len(os.Args) {
fail(usage)
}
i += 1
conf.location = os.Args[i]
case "-t", "--template":
if i == len(os.Args) {
fail(usage)
}
i += 1
conf.template = append(conf.template, os.Args[i])
}
}
if conf.input == "" || conf.output == "" {
fail("both -i|--input and -o|--output are required\n")
}
return conf
}
func overrides(conf config) (*template.Template, *htemplate.Template, error) {
if len(conf.template) == 0 {
return nil, nil, nil
}
switch conf.output {
case "markdown":
tmpl, err := template.New("mdconv").ParseFiles(conf.template...)
if err != nil {
return nil, nil, err
}
return tmpl, nil, nil
case "html":
tmpl, err := htemplate.New("htmlconv").ParseFiles(conf.template...)
if err != nil {
return nil, nil, err
}
return nil, tmpl, err
}
return nil, nil, nil
}
func fail(msg string) {
os.Stderr.WriteString(msg)
os.Exit(1)
}