diff options
author | tjpcc <tjp@ctrl-c.club> | 2023-09-01 08:14:13 -0600 |
---|---|---|
committer | tjpcc <tjp@ctrl-c.club> | 2023-09-01 08:14:13 -0600 |
commit | b8f1e92bfc9e690a318c9adc96370d60bbcdedd7 (patch) | |
tree | e239afb0cdfbe9a6c4004faa0c753c00407a483f | |
parent | 7bb8b79221740dd4a6d9a63c035e46087f4680fb (diff) |
gophermap->html conversion with overridable templates
-rw-r--r-- | gopher/gophermap/htmlconv/convert.go | 196 | ||||
-rw-r--r-- | gopher/gophermap/htmlconv/convert_test.go | 59 |
2 files changed, 255 insertions, 0 deletions
diff --git a/gopher/gophermap/htmlconv/convert.go b/gopher/gophermap/htmlconv/convert.go new file mode 100644 index 0000000..a669601 --- /dev/null +++ b/gopher/gophermap/htmlconv/convert.go @@ -0,0 +1,196 @@ +package htmlconv + +import ( + "bytes" + "html/template" + "io" + "strings" + + "tildegit.org/tjp/sliderule/gopher" + "tildegit.org/tjp/sliderule/internal/types" +) + +// Convert writes html to a writer from the provided gophermap document. +// +// Templates can be provided to override the output for different line types. +// The supported templates are: +// - "header' is called before any lines and is passed the full document. +// - "footer" is called after all lines and is passed the full document. +// - "message" is called for every group of consecutive info-message lines. It is +// passed a string of all the display components of the included lines, joined by +// newline characters. +// - "image" is called for all lines of any of the following types: GifFileType, +// ImageFileType, BitmapType, PngImageFileType. It is passed the gopher.MapItem. +// - "link" is called for all lines of any other type not yet mentioned, and is passed +// the gopher.MapItem. +// +// There are already default implementations of each of these templates, so the "overrides" +// argument can be nil or include just a subset of the supported templates. +func Convert(wr io.Writer, doc gopher.MapDocument, overrides *template.Template) error { + tmpl, err := baseTmpl.Clone() + if err != nil { + return err + } + + tmpl, err = addOverrides(tmpl, overrides) + if err != nil { + return err + } + + for _, item := range renderItems(doc) { + if err := tmpl.ExecuteTemplate(wr, item.template, item.object); err != nil { + return err + } + } + + return nil +} + +type renderItem struct { + template string + object any +} + +type renderRef struct { + Type types.Status + Display string + Selector template.URL + Hostname string + Port string +} + +func refItem(item gopher.MapItem) renderRef { + return renderRef{ + Type: item.Type, + Display: item.Display, + Selector: template.URL(item.Selector), + Hostname: item.Hostname, + Port: item.Port, + } +} + +func renderItems(doc gopher.MapDocument) []renderItem { + out := make([]renderItem, 0, len(doc)) + out = append(out, renderItem{ + template: "header", + object: doc, + }) + inMsg := false + msg := &bytes.Buffer{} + var currentHost, currentPort string + + for _, mapItem := range doc { + switch mapItem.Type { + case gopher.InfoMessageType: + if inMsg { + _, _ = msg.WriteString("\n") + } else { + msg.Reset() + } + _, _ = msg.WriteString(mapItem.Display) + inMsg = true + + if currentHost == "" { + currentHost = mapItem.Hostname + } + if currentPort == "" { + currentPort = mapItem.Port + } + case gopher.GifFileType, gopher.ImageFileType, gopher.BitmapType, gopher.PngImageFileType: + if inMsg { + out = append(out, renderItem{ + template: "message", + object: msg.String(), + }) + inMsg = false + } + out = append(out, renderItem{ + template: "image", + object: refItem(mapItem), + }) + default: + if inMsg { + out = append(out, renderItem{ + template: "message", + object: msg.String(), + }) + inMsg = false + } + out = append(out, renderItem{ + template: "link", + object: refItem(mapItem), + }) + } + } + + if inMsg { + out = append(out, renderItem{ + template: "message", + object: msg.String(), + }) + } + + simplifyLinks(out, currentHost, currentPort) + + return append(out, renderItem{ + template: "footer", + object: doc, + }) +} + +func simplifyLinks(items []renderItem, currentHost, currentPort string) { + for i, item := range items { + if item.template != "link" && item.template != "image" { + continue + } + + m := item.object.(renderRef) + if m.Hostname == currentHost && m.Port == currentPort { + m.Hostname = "" + m.Port = "" + m.Selector = template.URL(strings.TrimPrefix(string(m.Selector), "URL:")) + items[i].object = m + } + } +} + +func addOverrides(base *template.Template, overrides *template.Template) (*template.Template, error) { + if overrides == nil { + return base, nil + } + + tmpl := base + var err error + for _, override := range overrides.Templates() { + tmpl, err = tmpl.AddParseTree(override.Name(), override.Tree) + if err != nil { + return nil, err + } + } + + return tmpl, nil +} + +var baseTmpl = template.Must(template.New("htmlconv").Parse(` +{{ define "header" -}} +<!DOCTYPE html> +<html><body class="gophermap"> +{{- end }} + +{{ define "message" -}} +<pre class="gophermap">{{.}} +</pre> +{{- end }} + +{{ define "link" -}} +<p class="gophermap"><a class="gophermap" {{ if .Hostname | and .Port }}href="gopher://{{.Hostname}}:{{.Port}}{{.Selector}}"{{ else }}href="{{.Selector}}"{{ end }}>{{.Display}}</a></p> +{{- end }} + +{{ define "image" -}} +<p class="gophermap"><img class="gophermap" {{ if .Hostname | and .Port }}src="gopher://{{.Hostname}}:{{.Port}}{{.Selector}}"{{ else }}src="{{.Selector}}"{{ end }} alt="{{.Display}}"/></p> +{{- end }} + +{{ define "footer" -}} +</body></html> +{{ end }} +`)) diff --git a/gopher/gophermap/htmlconv/convert_test.go b/gopher/gophermap/htmlconv/convert_test.go new file mode 100644 index 0000000..1cdc8e2 --- /dev/null +++ b/gopher/gophermap/htmlconv/convert_test.go @@ -0,0 +1,59 @@ +package htmlconv + +import ( + "bytes" + "fmt" + "strings" + "testing" + + "tildegit.org/tjp/sliderule/gopher/gophermap" +) + +func TestConvert(t *testing.T) { + tests := []struct { + name string + input string + output string + }{ + { + name: "basic doc", + input: strings.ReplaceAll(` +iI am informational text localhost 70 +icontinued on this line localhost 70 +i localhost 70 +0this is my text file /file.txt localhost 70 +i localhost 70 +1here's a sub-menu /sub/ localhost 70 +. +`[1:], "\n", "\r\n"), + output: ` +<!DOCTYPE html> +<html><body class="gophermap"><pre class="gophermap">I am informational text +continued on this line + +</pre><p class="gophermap"><a class="gophermap" href="/file.txt">this is my text file</a></p><pre class="gophermap"> +</pre><p class="gophermap"><a class="gophermap" href="/sub/">here's a sub-menu</a></p></body></html> +`[1:], + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + doc, err := gophermap.Parse(bytes.NewBufferString(test.input)) + if err != nil { + t.Fatal(err) + } + + buf := &bytes.Buffer{} + if err := Convert(buf, doc, nil); err != nil { + t.Fatal(err) + } + + if buf.String() != test.output { + fmt.Println(test.output) + fmt.Println(buf.String()) + t.Fatal("html body mismatch") + } + }) + } +} |