aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBrian Picciano <me@mediocregopher.com>2024-10-31 17:14:10 +0100
committerBrian Picciano <me@mediocregopher.com>2024-10-31 17:14:10 +0100
commit246a99c28980e985bb4cff99042459bd5729cde1 (patch)
treef9ac2d531acc9edb7664f36317d9fafc52d20742
parent695cd4c58fb146933ed02e61db8f553f7f9bf419 (diff)
Move gemtext translation into its own package
-rw-r--r--http/handlers/templates/functions/gemtext.go120
-rw-r--r--internal/gemtext/gemtext.go145
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
+}