// 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("
%s\n", sanitizeText(line[1:])) default: line = strings.TrimSpace(line) write("
%s
\n", line) } } return HTML{ Title: title, Body: w.String(), }, nil }