diff options
author | Brian Picciano <me@mediocregopher.com> | 2024-10-31 17:14:10 +0100 |
---|---|---|
committer | Brian Picciano <me@mediocregopher.com> | 2024-10-31 17:14:10 +0100 |
commit | 246a99c28980e985bb4cff99042459bd5729cde1 (patch) | |
tree | f9ac2d531acc9edb7664f36317d9fafc52d20742 | |
parent | 695cd4c58fb146933ed02e61db8f553f7f9bf419 (diff) |
Move gemtext translation into its own package
-rw-r--r-- | http/handlers/templates/functions/gemtext.go | 120 | ||||
-rw-r--r-- | internal/gemtext/gemtext.go | 145 |
2 files changed, 157 insertions, 108 deletions
diff --git a/http/handlers/templates/functions/gemtext.go b/http/handlers/templates/functions/gemtext.go index 179d558..68a10ca 100644 --- a/http/handlers/templates/functions/gemtext.go +++ b/http/handlers/templates/functions/gemtext.go @@ -1,9 +1,6 @@ package functions import ( - "bufio" - "bytes" - "errors" "fmt" "html" "io" @@ -11,6 +8,7 @@ import ( "strings" "text/template" + "dev.mediocregopher.com/mediocre-caddy-plugins.git/internal/gemtext" "github.com/caddyserver/caddy/v2" "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile" "github.com/caddyserver/caddy/v2/caddyconfig/httpcaddyfile" @@ -67,120 +65,26 @@ func sanitizeText(str string) string { return html.EscapeString(strings.TrimSpace(str)) } -type gemtextResult struct { - Title string - Body string -} - -func (g *Gemtext) funcGemtext(input any) (gemtextResult, error) { +func (g *Gemtext) funcGemtext(input any) (gemtext.HTML, error) { var ( - r = bufio.NewReader(strings.NewReader(caddy.ToString(input))) - w = new(bytes.Buffer) - title string - pft, list bool - writeErr error + r = strings.NewReader(caddy.ToString(input)) + translator gemtext.HTMLTranslator ) - write := func(fmtStr string, args ...any) { - if writeErr != nil { - return - } - fmt.Fprintf(w, fmtStr, args...) - } - -loop: - for { - if writeErr != nil { - return gemtextResult{}, fmt.Errorf("writing line: %w", writeErr) - } - - line, err := r.ReadString('\n') - - switch { - case errors.Is(err, io.EOF): - break loop - - case err != nil: - return gemtextResult{}, fmt.Errorf("reading next line: %w", err) - - case strings.HasPrefix(line, "```"): - if !pft { - write("<pre>\n") - pft = true - } else { - write("</pre>\n") - pft = false - } - continue - - case pft: - write(line) - continue - - case len(strings.TrimSpace(line)) == 0: - continue - } - - // list case is special, because it requires a prefix and suffix tag - if strings.HasPrefix(line, "*") { - if !list { - write("<ul>\n") + if g.GatewayURL != "" { + translator.RenderLink = func(w io.Writer, urlStr, label string) error { + if u, err := url.Parse(urlStr); err == nil && u.Scheme == "gemini" { + urlStr = g.GatewayURL + u.Host + u.Path } - write("<li>%s</li>\n", sanitizeText(line[1:])) - list = true - continue - } else if list { - write("</ul>\n") - list = false - } - switch { - case strings.HasPrefix(line, "=>"): - // TODO convert gemini:// links ? - var ( - line = strings.TrimSpace(line[2:]) - urlStr = line - label = urlStr + _, err := fmt.Fprintf( + w, "<p><a href=\"%s\">%s (proxied)</a></p>\n", urlStr, label, ) - - if i := strings.IndexAny(urlStr, " \t"); i > -1 { - urlStr, label = urlStr[:i], sanitizeText(urlStr[i:]) - } - - if g.GatewayURL != "" { - if u, err := url.Parse(urlStr); err == nil && u.Scheme == "gemini" { - urlStr = g.GatewayURL + u.Host + u.Path - } - } - - write("<p><a href=\"%s\">%s</a></p>\n", urlStr, label) - - case strings.HasPrefix(line, "###"): - write("<h3>%s</h3>\n", sanitizeText(line[3:])) - - case strings.HasPrefix(line, "##"): - write("<h2>%s</h2>\n", sanitizeText(line[2:])) - - case strings.HasPrefix(line, "#"): - line = sanitizeText(line[1:]) - if title == "" { - title = line - } - write("<h1>%s</h1>\n", line) - - case strings.HasPrefix(line, ">"): - write("<blockquote>%s</blockquote>\n", sanitizeText(line[1:])) - - default: - line = strings.TrimSpace(line) - write("<p>%s</p>\n", line) + return err } } - return gemtextResult{ - Title: title, - Body: w.String(), - }, nil + return translator.Translate(r) } // UnmarshalCaddyfile implements caddyfile.Unmarshaler. diff --git a/internal/gemtext/gemtext.go b/internal/gemtext/gemtext.go new file mode 100644 index 0000000..83a1be3 --- /dev/null +++ b/internal/gemtext/gemtext.go @@ -0,0 +1,145 @@ +// Package gemtext implements shared logic related to gemtext files. +package gemtext + +import ( + "bufio" + "bytes" + "errors" + "fmt" + "html" + "io" + "strings" +) + +// HTMLTranslator is used to translate a gemtext file into equivalent HTML DOM +// elements. +type HTMLTranslator struct { + // RenderLink, if given, can be used to override how links are rendered. + RenderLink func(w io.Writer, url, label string) error +} + +// HTML contains the result of a translation from gemtext. The Body will be the +// translated body itself, and Title will correspond to the first primary header +// of the gemtext file, if there was one. +type HTML struct { + Title string + Body string +} + +// Translate will read a gemtext file from the Reader and return it as an HTML +// document. +func (t HTMLTranslator) Translate(src io.Reader) (HTML, error) { + var ( + r = bufio.NewReader(src) + w = new(bytes.Buffer) + title string + pft, list bool + writeErr error + ) + + sanitizeText := func(str string) string { + return html.EscapeString(strings.TrimSpace(str)) + } + + write := func(fmtStr string, args ...any) { + if writeErr != nil { + return + } + fmt.Fprintf(w, fmtStr, args...) + } + +loop: + for { + if writeErr != nil { + return HTML{}, fmt.Errorf("writing line: %w", writeErr) + } + + line, err := r.ReadString('\n') + + switch { + case errors.Is(err, io.EOF): + break loop + + case err != nil: + return HTML{}, fmt.Errorf("reading next line: %w", err) + + case strings.HasPrefix(line, "```"): + if !pft { + write("<pre>\n") + pft = true + } else { + write("</pre>\n") + pft = false + } + continue + + case pft: + write(line) + continue + + case len(strings.TrimSpace(line)) == 0: + continue + } + + // list case is special, because it requires a prefix and suffix tag + if strings.HasPrefix(line, "*") { + if !list { + write("<ul>\n") + } + write("<li>%s</li>\n", sanitizeText(line[1:])) + list = true + continue + } else if list { + write("</ul>\n") + list = false + } + + switch { + case strings.HasPrefix(line, "=>"): + var ( + line = strings.TrimSpace(line[2:]) + urlStr = line + label = urlStr + ) + + if i := strings.IndexAny(urlStr, " \t"); i > -1 { + urlStr, label = urlStr[:i], sanitizeText(urlStr[i:]) + } + + if t.RenderLink == nil { + write("<p><a href=\"%s\">%s</a></p>\n", urlStr, label) + } else { + if err := t.RenderLink(w, urlStr, label); err != nil { + return HTML{}, fmt.Errorf( + "rendering link %q (label:%q): %w", urlStr, label, err, + ) + } + } + + case strings.HasPrefix(line, "###"): + write("<h3>%s</h3>\n", sanitizeText(line[3:])) + + case strings.HasPrefix(line, "##"): + write("<h2>%s</h2>\n", sanitizeText(line[2:])) + + case strings.HasPrefix(line, "#"): + line = sanitizeText(line[1:]) + if title == "" { + title = line + } + write("<h1>%s</h1>\n", line) + + case strings.HasPrefix(line, ">"): + write("<blockquote>%s</blockquote>\n", sanitizeText(line[1:])) + + default: + line = strings.TrimSpace(line) + write("<p>%s</p>\n", line) + } + } + + return HTML{ + Title: title, + Body: w.String(), + }, nil +} |