From 246a99c28980e985bb4cff99042459bd5729cde1 Mon Sep 17 00:00:00 2001 From: Brian Picciano Date: Thu, 31 Oct 2024 17:14:10 +0100 Subject: Move gemtext translation into its own package --- internal/gemtext/gemtext.go | 145 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 internal/gemtext/gemtext.go (limited to 'internal/gemtext/gemtext.go') 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("
\n")
+				pft = true
+			} else {
+				write("
\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("\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("

%s

\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("

%s

\n", sanitizeText(line[3:])) + + case strings.HasPrefix(line, "##"): + write("

%s

\n", sanitizeText(line[2:])) + + case strings.HasPrefix(line, "#"): + line = sanitizeText(line[1:]) + if title == "" { + title = line + } + write("

%s

\n", line) + + case strings.HasPrefix(line, ">"): + write("
%s
\n", sanitizeText(line[1:])) + + default: + line = strings.TrimSpace(line) + write("

%s

\n", line) + } + } + + return HTML{ + Title: title, + Body: w.String(), + }, nil +} -- cgit v1.2.3