summaryrefslogtreecommitdiff
path: root/gopher
diff options
context:
space:
mode:
authortjpcc <tjp@ctrl-c.club>2023-09-01 08:14:13 -0600
committertjpcc <tjp@ctrl-c.club>2023-09-01 08:14:13 -0600
commitb8f1e92bfc9e690a318c9adc96370d60bbcdedd7 (patch)
treee239afb0cdfbe9a6c4004faa0c753c00407a483f /gopher
parent7bb8b79221740dd4a6d9a63c035e46087f4680fb (diff)
gophermap->html conversion with overridable templates
Diffstat (limited to 'gopher')
-rw-r--r--gopher/gophermap/htmlconv/convert.go196
-rw-r--r--gopher/gophermap/htmlconv/convert_test.go59
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&#39;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")
+ }
+ })
+ }
+}