From cec3718bdd089bcf58575740c5ae4f86b27226d1 Mon Sep 17 00:00:00 2001 From: tjpcc Date: Sat, 14 Jan 2023 19:59:12 -0700 Subject: markdown converter --- README.gmi | 114 +++++++++++++++++++++++++++++++++++++++++ README.md | 3 +- examples/gmi2md/main.go | 20 ++++++++ gemtext/mdconv/convert.go | 72 ++++++++++++++++++++++++++ gemtext/mdconv/convert_test.go | 102 ++++++++++++++++++++++++++++++++++++ gemtext/types.go | 10 ++++ 6 files changed, 319 insertions(+), 2 deletions(-) create mode 100644 README.gmi create mode 100644 examples/gmi2md/main.go create mode 100644 gemtext/mdconv/convert.go create mode 100644 gemtext/mdconv/convert_test.go diff --git a/README.gmi b/README.gmi new file mode 100644 index 0000000..35211cb --- /dev/null +++ b/README.gmi @@ -0,0 +1,114 @@ +# Gus: The small web server toolkit + +Gus is the toolkit for working with the small web in Go. + +Think of it as a net/http for small web protocols. You still have to write your server, but you can focus on the logic you want to implement knowing the protocol is already dealt with. It's been said of gemini that you can write your server in a day. Now you can write it in under an hour. + +## gus/gemini + +Gus is determined to be structured as composable building blocks, and the gemini package mainly just defines the structure that holds the blocks together. + +The package contains: +* a request type +* a response type +* a "Handler" abstraction +* a "Middleware" abstraction +* some useful Handler wrappers: filtering, falling through a list of handlers +* helpers for building a gemini-suitable TLS config +* a Server that can run your handler(s) + +## gus/gemtext + +The gemtext package today provides a parser for gemtext documents. In the future, there will be conversions for the in-memory parsed representation into markdown, html, and whatever else people come up with. + +## gus/contrib/* + +This is where useful building blocks themselves start to come in. Sub-packages of contrib include Handler and Middleware implementations which accomplish the things your servers actually need to do. + +So far there are at least 3 packages: +* log contains a simple request-logging middleware +* fs has handlers that make file servers possible: serve files, build directory listings, etc +* cgi includes handlers which can execute CGI programs +* ...with more to come + +## Get it + +### Using gus in your project + +To add it to your own go project: +```shell command to add a dependency on gus in a go module +$ go get tildegit.org/tjp/gus +``` + +### Straight to the code please + +=> https://tildegit.org/tjp/gus The code is hosted here on tildegit. +=> https://pkg.go.dev/tildegit.org/tjp/gus The generated documentation is on the go package index. + +## Contribute + +There's lots still to do, and contributions are very welcome! + +=> https://tildegit.org/tjp/gus submit an issue or pull request on the tildegit repository, +=> mailto:tjp@ctrl-c.club send me an email directly, +or poke me on IRC: I'm @tjp on irc.tilde.chat, and you'll find me in #gemini + + +------------------------ + +>Step 2: draw the rest of the owl +```An ASCII art owl +;;;;;:::::::::::::::::::::::;;;;;;;,,,,,,,''''''''''',,,,,,,,,,,;;;;;;;;;;,,,,,,,,,,,,,,,,,,;;;;:::::::::::::;; +;;;;;;:::::::::::::::;;;;;;;;;;;,,,,,,,,'''''''''''''',,,,,,,,;;;;;;;;;;;;;;;;;;;,,,,,,,,,,,,,;;;;::::::::::::; +;;;;;;;:::::::::::;;;;;;;;;;;,,,,,,,,,''''''''''''''',,,,,,,,;;;;;;;;::clooodddddddooooooodollc::;;:::::::::::; +;;;;;;;;;;;;;:::;;;;;;;;;;;;,,,,,,,,''''''''''''''''',,,,,,,,;;;;;:clodxkdodxdddooxxkxxxxkxxkxxxdlc:::::::::::: +;;;;;;;;;;;;;;;;;;;;;;;;;;;,,,,,,,''''''''''''''''''',,,,,,,;;;;:lxkOkkkdolldl:c::loooolloclccolddolc:::::::::; +;;;;;;;;;;;;;;;;;;;;;;;;;;,,,,,,,''''''''''''''''''''',,,,,;;::oxOOOOkxxdoodkxlcc;;clcc:lccllddclloddol::::::;; +;;;;;;;;;;;;;;;;;;;;;;;;;,,,,,,,,,'''''''''''''''''''',,,,;;cdxxkkxdollllloodoodoc:::;:lddxxxOkdlldxkdolc;::;;; +;;;;;;,,,;;;;;;;;;;;;;;;;,,,,,,,,,,'''''''''''''''''''',,,;cdkkkxdollc::;,''',;lodd:;;cxkxdolllolc::ldool:;;;;; +,,,,,,,,,,;;;;;;;;;;;;;;;,,,,,,,,,,,,'''''''''''''''''',,,cdkkxdlc:cc:;........':odo::lolc;'...';:::::lool:;;;; +,,,,,,,,,,,,;;;;;;;;;;;;;,,,,,,,,,,,,,'''''''''''''''',,,:dkxxdolcccc::'':'.....,loollol,.........,;;::cll:;;;; +,,,,,,,,,,,,,,,,,;;;;;;;;;,,,,,,,,,,,,,,'''''''''',,,,,,;oxxxdllccccloo;'c:'';:,;codxdlc,'. ,,.,::::::lc:;;;; +,,,,,,,,,,,,,,,,,,;;;;;;;;;;;;;,,,,,,,,,,,,,,,,,,,,,,,,;lddddlc:;;:cldxdcclc::::cclddooc,';,',:,,clc::;:lc;;;;; +,,,,,,,,,,,,,,,,,,,,,;;;;;;;;;;;;;,,,,,,,,,,,,,,,,,,,,,:odddolc:;;::cloxxddocclllc::::ll:;;;:c::odoc;;;:lc;;;;; +;,,,,,,,,,,,,,,,,,,,,,,;;;;;;;;;;;;;;;;;;;;;,,,,,,,,,,,cdolloool:;;::cloddoooodoc;'''.:lllollddddl::;;;;::;;;;; +;;;,,,,,,,,,,,,,,,,,,,,,,,;;;;;;;;;;;;;;;;;;;;;;;;;;;,:odooxxooolc:::codxdlcllll:'....;ccccodddlc:;,,;;:c:,,,,, +;;;;,,,,,,,,,,,,,,,,,,,,,,,,;;;;;;;;;;::::::;;;;;;;;;:oxkO0KX0Oxolccccllodddddolc:'..;clcccclddl;,',,',::;,,,,, +;;;;;,,,,,,,,,,,,,,,,,,,,,,,;;;;;;;;;::::::::::::;:clodddooxOkxdol:coollodxkdl:,,'..';:looddool:;,'.';;::,,,,,; +;;;;;,,,,,,,,,,,,,,,,,,,,,,,;;;;;;;;::::::::::::ccloollloollc::::;;:lccodxkxlco:.......',cdxxdl;,,''';::;;;;;;; +;;;;;;,,,,,,,,,,,,,,,,,,,,,,,,;;;;;;;::::::::ccllccoolcclc:::::clc;;:;,;;cc,,cxd;,'......,:oxdol:;,;loc;;;;;;;; +;;;;;;,,,,,,,,,,,,,,,,,,,,,,,,,;;;;;;:::::cclllccccc::;;;;:ooc:oOxloxl'....,collddlc;'..';:lddoxdlldxdc;;;;:::: +;;;;;;,,,,,,,,,,,,,,,,,,,,,,,,,,;;;;;:odooolcccccc::;:odlcoxdc;cldOKx;''''';:;,;loc;,'',,,,:clodkkkOkl:;::::::: +;;;;;;,,,,,,,,,,,,,,,,,,,,,,,,,,,;;:lxOxolllllccllc::x0xc;;c:''':kN0c,,;;;;;;,,',,,,''''''..,::lododdc:::::cccc +;;;;;,,,,,,,,,,,,,,,,,,,,,,,,,,,,;lxdolc:codoolcllc;:cc:,''''.'';lxo::::;;;;;;,''''.........:l;;::;:ol:::cccccc +;;;;;,,,,,,,,,,,,,,,,,,''',,,,,,:dxc'':loOKkdoccll:,,;;;,',,;:::;;;;::;;;;;;;;,,,...........,;,,',,;c::cccccccc +,,,,,,,,,,,,,,,,,,,,,,,'''''';:lol,':dxx0Xkolc::lc;;;;::;,;;:::;;;::::;;;;,,,',,,...........,;''''',:::ccccllll +,,,,,,,,,,,,,,,,,,,,,,,'''';ldl,..'cOKkdkdccccc:::::::::;;;:::;;:c:;;;,,,,'''''','........,;;'.',;;;:::ccllllll +,,,,,,,,,,,,,,,,,,,,,,,',;clc,.. .ckOkdlllccccc:c::::::::::;;;:::;;,,'''''...';,''.....';cl:'',;:ll:::ccccllccc +,,,,,,,,,,,,,,,,,,,,,,,;cl;......:loollcccccc::::::::c:;;,;;;;;;;,,,:l:'......'......,:llc;'',:codl::cccccccccc +,,,,,,,,,,,,,,,,,,,,;ccl:.. .':ldollc::::::::;;;;;;,;:,',;;;;;;;,',ox:..........',::::;,,,'';cllcccccccccccccc +',,,,,,,,,,,,,,,,,:ll:;,. .';cclllll:::::;;,,,,,,'..''',,,,;::;,'..........'..',;c:'.',;,''':ccccccccccccccccc +',,,,,,,,,,,,,;;;::'.. .,:cccc::cc:::;;,''',,'....,cc,'',;,'............'',;c:,,''',:c,,;,;ccccccclllllllcccc +',,,,,,,,,,;;;;,.. ..,:::::ccclolcc;;,,'''.',,''',::;,'..........',,;;:::::;'.,,,,cc,,;;:cccllllllllllllllll +'',,,,,,,,;;'.. .';::::clllolcc:;,;;;;,;;:;,'','................,:ccc::,''',;;,;::;,;::ccclllllllllllllllll +''',,,,,;:,.. .,;;;:clolc::;;;cc:;;;,;cc;,'..''..............',;;;;;;,',;::;;;::;;::::ccclllllllllllllllll +'''',,,,;'. ..',;::cc:;;,;;:ccc:;,,:lol;'''........''''..'',,;:ccc;,',:lc;,,,;;;;:::::ccclllllllllllllllll +'''''',;,.. ....,;:c::;:::::;:cc:,'''',,,'',,'....'';ccc::::::;;;:::,'':cc;,,,',;;;;;::::ccclllllllllllllllll +''''''':c,....',,:llllllc::;,:lo:,'',;;,,,,;;,,,'',;;cdxdllclllccccl:;,,::;,'''',,,,;;;;::::cccclllllcccccccccc +'''''',;lc'..,:ccllllcc:;,,;;clc::::cccccccc::::::clldxdoolloolcclc:;,,;,,;,,,,,,,,,,;;;;;:::cccccccccccccccccc +''''';lolc:ccllllc::::;;:cloollllllccclllooollooooooodolcclloc:;,,,;;,',,;,,,,,,,,,,,;;;;;;;::::ccccccccccc:::: +'''';odl::lolc;,'...,codxxxxxddolcccllloooddddxxxxxdddlcccllc;,,;:::;,,;,,,,,,,,,,,,,,,,;;;;;;::::::::::::::::; +''',colc::;,......,coxxxdxxxol:;;;;:cloodddddddxxxxxddoooddlcccll:loo:;::,,,,,,,,,,,,,,,,,,,,;;;;;;:::::::;;;;; +'',co:,,,'...''..,:cccc:;::;,,,,,,;;:lodxddddooddddoooolcoxxdolc;,:ldxodoc:c:,,,,,,,,,,,,,,,,,,;;;;;;;;;;;;;;;, +''cOxcc;,,,;,'.''',,'',,,,,,,,,,,,,,;:clooddollooolllll;,cxkxol:,:,';llllodddo:,,,,,,,,,,,,,,,,,,,;;;;,,,,,,,,, +'';xkxoc,,,'..''',,,,,,,,,,,,,,,,,,,,,;;;:ccc::ccc:;::,';;;:cloodl;;,,:::lodOOl,',,,,,,,,,,,,,,,,,,,,,,,,,,,,,' +''',:c:,'';:;;;,,,,,,,,,,,,,,,,,,,,,,,,,,,;;,,;;;;,;;;'';;',oxddxocc:;clloxxk00kddc;,,,,,,,,,,,,,,,,,,,,,'''''' +,''''''''';::,''''',,,,,,,,,,,,,,,,,,,,,,,;::cclccccc:,,,,.'loc;,:lllodoodkOO0KKKKOl;,;,,,,,,,,,,,,,,,,,,'''''' +'''''''''''''''''''',,,,,,,,,,,,,,,,,,,,;clllcc::;,'...'...,lc:'.,c;:oook00OddO00KKd,,;;,;;;;,,,,,,,,,,,,,,,,'' +'''''''''''''''''''',,,,,,,,,,,,,,,,;;::c:::;;,,''.....'''.:dl:'.,ll;',oKXXd;;:lod00dc::;;,;,,,,,,,,,,,,,,,,,,, +'''''''''''''''''',,,,,,,,;;;;,,,,;::ccccc::;;;,,,'..'''''';dc....:xxl;cxkkl:;;;::lddl:coc;,,,,,,,,,,,,,,,,,,,, +'''''''''''''''',,,,,;;;;;;;;;;;:clc:ccc::;;;;;,,'',,,,,,,,;lc'''',oOOxo;';cllccc:;:l:'':loc;,,,,,,,,,,,,,,,,,, +,''''''''''',,,,,,;;;;;;;::;;;::llc::::::;;:;;;:;;;;;;;;;;;;:cc;,,':k000x:;lddddoolodd:.'ldxo:,,,,,,,,,,,,,,''' +,'''''''''',,,,,;;;:::::::::cllllcccc::c:::::::cc::;;;;;;;;;;:cc:;,,lOKKK0dodxxxdollccl:,,codl;''''',,,,,''','' +,,''''''',,,,;;;;:::::::::cloooolcccc:::;;:::cc::;;,,;;;:;;;;;;:cc:,,oKKKKOddxxo:;:c::;:;',;:cc;''''''''''',,,, +``` diff --git a/README.md b/README.md index a7a95d8..cf26de7 100644 --- a/README.md +++ b/README.md @@ -51,13 +51,12 @@ There's lots still to do, and contributions are very welcome! * => [submit an issue or pull request on the tildegit repository,](https://tildegit.org/tjp/gus) * => [send me an email directly,](mailto:tjp@ctrl-c.club) -* or poke me on IRC: I'm @tjp on irc.tilde.chat, and you'll find me in #gemini +or poke me on IRC: I'm @tjp on irc.tilde.chat, and you'll find me in #gemini ------------------------ > Step 2: draw the rest of the owl - ``` ;;;;;:::::::::::::::::::::::;;;;;;;,,,,,,,''''''''''',,,,,,,,,,,;;;;;;;;;;,,,,,,,,,,,,,,,,,,;;;;:::::::::::::;; ;;;;;;:::::::::::::::;;;;;;;;;;;,,,,,,,,'''''''''''''',,,,,,,,;;;;;;;;;;;;;;;;;;;,,,,,,,,,,,,,;;;;::::::::::::; diff --git a/examples/gmi2md/main.go b/examples/gmi2md/main.go new file mode 100644 index 0000000..386983a --- /dev/null +++ b/examples/gmi2md/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "log" + "os" + + "tildegit.org/tjp/gus/gemtext" + "tildegit.org/tjp/gus/gemtext/mdconv" +) + +func main() { + gmiDoc, err := gemtext.Parse(os.Stdin) + if err != nil { + log.Fatal(err) + } + + if err := mdconv.Convert(os.Stdout, gmiDoc, nil); err != nil { + log.Fatal(err) + } +} diff --git a/gemtext/mdconv/convert.go b/gemtext/mdconv/convert.go new file mode 100644 index 0000000..0c92f9f --- /dev/null +++ b/gemtext/mdconv/convert.go @@ -0,0 +1,72 @@ +package mdconv + +import ( + "fmt" + "io" + "text/template" + + "tildegit.org/tjp/gus/gemtext" +) + +func Convert(wr io.Writer, doc gemtext.Document, overrides *template.Template) error { + tmpl, err := baseTmpl.Clone() + if err != nil { + return err + } + + if overrides != nil { + for _, override := range overrides.Templates() { + tmpl, err = tmpl.AddParseTree(override.Name(), override.Tree) + if err != nil { + return err + } + } + } + + return tmpl.ExecuteTemplate(wr, "mdconv", doc) +} + +var baseTmpl = template.Must(template.New("mdconv").Parse(fmt.Sprintf((` +{{block "header" .}}{{end -}} +{{range . -}} +{{if .Type | eq %d}}{{block "textline" . -}} + {{. -}} +{{end -}} +{{else if .Type | eq %d}}{{block "linkline" . -}} + => [{{if eq .Label ""}}{{.URL}}{{else}}{{.Label}}{{end}}]({{.URL}}) +{{end -}} +{{else if .Type | eq %d}}{{block "preformattoggleline" . -}} + ` + "```" + ` +{{end -}} +{{else if .Type | eq %d}}{{block "preformattedtextline" . -}} + {{. -}} +{{end -}} +{{else if .Type | eq %d}}{{block "heading1line" . -}} + # {{.Body}} +{{end -}} +{{else if .Type | eq %d}}{{block "heading2line" . -}} + ## {{.Body}} +{{end -}} +{{else if .Type | eq %d}}{{block "heading3line" . -}} + ### {{.Body}} +{{end -}} +{{else if .Type | eq %d}}{{block "listitemline" . -}} + * {{.Body}} +{{end -}} +{{else if .Type | eq %d}}{{block "quoteline" . -}} + > {{.Body}} +{{end -}} +{{end -}} +{{end -}} +{{block "footer" .}}{{end -}} +`)[1:], + gemtext.LineTypeText, + gemtext.LineTypeLink, + gemtext.LineTypePreformatToggle, + gemtext.LineTypePreformattedText, + gemtext.LineTypeHeading1, + gemtext.LineTypeHeading2, + gemtext.LineTypeHeading3, + gemtext.LineTypeListItem, + gemtext.LineTypeQuote, +))) diff --git a/gemtext/mdconv/convert_test.go b/gemtext/mdconv/convert_test.go new file mode 100644 index 0000000..6cde08b --- /dev/null +++ b/gemtext/mdconv/convert_test.go @@ -0,0 +1,102 @@ +package mdconv_test + +import ( + "bytes" + "testing" + "text/template" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "tildegit.org/tjp/gus/gemtext" + "tildegit.org/tjp/gus/gemtext/mdconv" +) + +var gmiDoc = ` +# top-level header line + +## subtitle + +This is some non-blank regular text. + +* an +* unordered +* list + +=> gemini://google.com/ as if +=> https://google.com/ + +> this is a quote +> -tjp + +`[1:] + "```pre-formatted code\ndoc := gemtext.Parse(req.Body)\n```ignored closing alt-text\n" + +func TestConvert(t *testing.T) { + mdDoc := ` +# top-level header line + +## subtitle + +This is some non-blank regular text. + +* an +* unordered +* list + +=> [as if](gemini://google.com/) +=> [https://google.com/](https://google.com/) + +> this is a quote +> -tjp + +`[1:] + "```\ndoc := gemtext.Parse(req.Body)\n```\n" + + doc, err := gemtext.Parse(bytes.NewBufferString(gmiDoc)) + require.Nil(t, err) + + buf := &bytes.Buffer{} + require.Nil(t, mdconv.Convert(buf, doc, nil)) + + assert.Equal(t, mdDoc, buf.String()) +} + +func TestConvertWithOverrides(t *testing.T) { + mdDoc := ` +# h1: top-level header line +text: +## h2: subtitle +text: +text: This is some non-blank regular text. +text: +* li: an +* li: unordered +* li: list +text: +=> link: [as if](gemini://google.com/) +=> link: [https://google.com/](https://google.com/) +text: +> quote: this is a quote +> quote: -tjp +text: +`[1:] + "pftoggle: ```\npf: doc := gemtext.Parse(req.Body)\npftoggle: ```\n" + + overrides := template.Must(template.New("overrides").Parse((` + {{define "textline"}}text: {{.}}{{end}} + {{define "linkline"}}=> link: [{{if eq .Label ""}}{{.URL}}{{else}}{{.Label}}{{end}}]({{.URL}})` + "\n" + `{{end}} + {{define "preformattoggleline"}}pftoggle: ` + "```\n" + `{{end}} + {{define "preformattedtextline"}}pf: {{.}}{{end}} + {{define "heading1line"}}# h1: {{.Body}}` + "\n" + `{{end}} + {{define "heading2line"}}## h2: {{.Body}}` + "\n" + `{{end}} + {{define "heading3line"}}### h3: {{.Body}}` + "\n" + `{{end}} + {{define "listitemline"}}* li: {{.Body}}` + "\n" + `{{end}} + {{define "quoteline"}}> quote: {{.Body}}` + "\n" + `{{end}} + `)[1:])) + + doc, err := gemtext.Parse(bytes.NewBufferString(gmiDoc)) + require.Nil(t, err) + + buf := &bytes.Buffer{} + require.Nil(t, mdconv.Convert(buf, doc, overrides)) + + assert.Equal(t, mdDoc, buf.String()) +} diff --git a/gemtext/types.go b/gemtext/types.go index fefbece..440fed4 100644 --- a/gemtext/types.go +++ b/gemtext/types.go @@ -75,6 +75,9 @@ type Line interface { // Raw reproduces the original bytes from the source reader. Raw() []byte + + // String represents the original bytes from the source reader as a string. + String() string } // Document is the list of lines that make up a full text/gemini resource. @@ -87,6 +90,7 @@ type TextLine struct { func (tl TextLine) Type() LineType { return LineTypeText } func (tl TextLine) Raw() []byte { return tl.raw } +func (tl TextLine) String() string { return string(tl.raw) } // LinkLine is a line of LineTypeLink. type LinkLine struct { @@ -97,6 +101,7 @@ type LinkLine struct { func (ll LinkLine) Type() LineType { return LineTypeLink } func (ll LinkLine) Raw() []byte { return ll.raw } +func (ll LinkLine) String() string { return string(ll.raw) } // URL returns the original url portion of the line. // @@ -114,6 +119,7 @@ type PreformatToggleLine struct { func (tl PreformatToggleLine) Type() LineType { return LineTypePreformatToggle } func (tl PreformatToggleLine) Raw() []byte { return tl.raw } +func (tl PreformatToggleLine) String() string { return string(tl.raw) } // AltText returns the alt-text portion of the line. // @@ -135,6 +141,7 @@ type PreformattedTextLine struct { func (tl PreformattedTextLine) Type() LineType { return LineTypePreformattedText } func (tl PreformattedTextLine) Raw() []byte { return tl.raw } +func (tl PreformattedTextLine) String() string { return string(tl.raw) } // HeadingLine is a line of LineTypeHeading[1,2,3]. type HeadingLine struct { @@ -145,6 +152,7 @@ type HeadingLine struct { func (hl HeadingLine) Type() LineType { return hl.lineType } func (hl HeadingLine) Raw() []byte { return hl.raw } +func (hl HeadingLine) String() string { return string(hl.raw) } // Body returns the portion of the line with the header text. func (hl HeadingLine) Body() string { return string(hl.body) } @@ -157,6 +165,7 @@ type ListItemLine struct { func (li ListItemLine) Type() LineType { return LineTypeListItem } func (li ListItemLine) Raw() []byte { return li.raw } +func (li ListItemLine) String() string { return string(li.raw) } // Body returns the text of the list item. func (li ListItemLine) Body() string { return string(li.body) } @@ -169,6 +178,7 @@ type QuoteLine struct { func (ql QuoteLine) Type() LineType { return LineTypeQuote } func (ql QuoteLine) Raw() []byte { return ql.raw } +func (ql QuoteLine) String() string { return string(ql.raw) } // Body returns the text of the quote. func (ql QuoteLine) Body() string { return string(ql.body) } -- cgit v1.2.3