1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-12-04 14:46:29 -05:00

Switched from Write to ReadFrom for all renderers and finished gemtext renderer

This commit is contained in:
makeworld 2021-03-04 20:09:51 -05:00
parent d2d5846b0e
commit fedc1fd11a
2 changed files with 388 additions and 59 deletions

View File

@ -2,37 +2,42 @@ package render
import ( import (
"bufio" "bufio"
"fmt"
"io" "io"
urlPkg "net/url"
"strconv"
"strings"
"github.com/makeworld-the-better-one/amfora/config"
"github.com/spf13/viper"
"gitlab.com/tslocum/cview"
) )
// Renderer for gemtext. Other Renderers are in renderer.go. // Renderer for gemtext. Other Renderers are in renderer.go.
type GemtextRenderer struct { type GemtextRenderer struct {
r *io.PipeReader // Buffers and I/O
w *io.PipeWriter
// scanner is used to process line by line.
scanner *bufio.Scanner
// scanWriter is used to send data to the scanner, which reads out of the other
// end of the pipe.
scanWriter *io.PipeWriter
// lineEnd holds the rest of line when the Read call cuts off the line being returned.
lineEnd []byte
r *io.PipeReader
w *io.PipeWriter
links chan string links chan string
// numLinks is the number of links that exist so far. // Configurable options
numLinks int
// width is the number of columns to wrap to. // width is the number of columns to wrap to.
width int width int
// proxied is whether the request is through the gemini:// scheme. // proxied is whether the request is through the gemini:// scheme.
proxied bool proxied bool
ansiEnabled bool
colorEnabled bool
// State
// pre indicates whether the renderer is currently in a preformatted block // pre indicates whether the renderer is currently in a preformatted block
// or not. // or not.
pre bool pre bool
// numLinks is the number of links that exist so far.
numLinks int
} }
// NewGemtextRenderer. // NewGemtextRenderer.
@ -43,29 +48,268 @@ type GemtextRenderer struct {
// If it's not a gemini:// page, set this to true. // If it's not a gemini:// page, set this to true.
func NewGemtextRenderer(width int, proxied bool) *GemtextRenderer { func NewGemtextRenderer(width int, proxied bool) *GemtextRenderer {
pr, pw := io.Pipe() pr, pw := io.Pipe()
scanReader, scanWriter := io.Pipe()
scanner := bufio.NewScanner(scanReader)
links := make(chan string, 10) links := make(chan string, 10)
return &GemtextRenderer{ ansiEnabled := false
r: pr, if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
w: pw, ansiEnabled = true
scanner: scanner,
scanWriter: scanWriter,
lineEnd: make([]byte, 0),
links: links,
numLinks: 0,
width: width,
proxied: proxied,
pre: false,
} }
colorEnabled := false
if viper.GetBool("a-general.color") {
colorEnabled = true
}
return &GemtextRenderer{
r: pr,
w: pw,
links: links,
width: width,
proxied: proxied,
ansiEnabled: ansiEnabled,
colorEnabled: colorEnabled,
}
}
// renderLine handles all lines except preformatted markings. The input line
// should not end with any line delimiters, but the output line does.
func (r *GemtextRenderer) renderLine(line string) string {
if r.pre {
if r.ansiEnabled {
line = cview.TranslateANSI(line)
// The TranslateANSI function injects tags like [-:-:-]
// but this will reset the background to use the user's terminal color.
// These tags need to be replaced with resets that use the theme color.
line = strings.ReplaceAll(line, "[-:-:-]",
fmt.Sprintf("[%s:%s:-]", config.GetColorString("preformatted_text"), config.GetColorString("bg")),
)
// Set color at beginning and end of line to prevent background glitches
// where the terminal background color slips through. This only happens on
// preformatted blocks with ANSI characters.
line = fmt.Sprintf("[%s]", config.GetColorString("preformatted_text")) +
line + fmt.Sprintf("[%s:%s:-]", config.GetColorString("regular_text"), config.GetColorString("bg"))
} else {
line = ansiRegex.ReplaceAllString(line, "")
}
return line + "\n"
}
// Not preformatted, regular lines
wrappedLines := make([]string, 0) // Final result
// ANSI not allowed in regular text - see #59
line = ansiRegex.ReplaceAllString(line, "")
if strings.HasPrefix(line, "#") {
// Headings
var tag string
if viper.GetBool("a-general.color") {
if strings.HasPrefix(line, "###") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_3"))
} else if strings.HasPrefix(line, "##") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_2"))
} else if strings.HasPrefix(line, "#") {
tag = fmt.Sprintf("[%s::b]", config.GetColorString("hdg_1"))
}
wrappedLines = append(wrappedLines, wrapLine(line, r.width, tag, "[-::-]", true)...)
} else {
// Just bold, no colors
wrappedLines = append(wrappedLines, wrapLine(line, r.width, "[::b]", "[-::-]", true)...)
}
// Links
} else if strings.HasPrefix(line, "=>") && len([]rune(line)) >= 3 {
// Trim whitespace and separate link from link text
line = strings.Trim(line[2:], " \t") // Remove `=>` part too
delim := strings.IndexAny(line, " \t") // Whitespace between link and link text
var url string
var linkText string
if delim == -1 {
// No link text
url = line
linkText = url
} else {
// There is link text
url = line[:delim]
linkText = strings.Trim(line[delim:], " \t")
if viper.GetBool("a-general.show_link") {
linkText += " (" + url + ")"
}
}
if strings.TrimSpace(line) == "" || strings.TrimSpace(url) == "" {
// Link was just whitespace, return it
return "=>\n"
}
r.links <- url
r.numLinks++
num := r.numLinks // Visible link number, one-indexed
var indent int
if num > 99 {
// Indent link text by 3 or more spaces
indent = len(strconv.Itoa(num)) + 4 // +4 indent for spaces and brackets
} else {
// One digit and two digit links have the same spacing - see #60
indent = 5 // +4 indent for spaces and brackets, and 1 for link number
}
// Spacing after link number: 1 or 2 spaces?
var spacing string
if num > 9 {
// One space to keep it in line with other links - see #60
spacing = " "
} else {
// One digit numbers use two spaces
spacing = " "
}
// Wrap and add link text
// Wrap the link text, but add some spaces to indent the wrapped lines past the link number
// Set the style tags
// Add them to the first line
var wrappedLink []string
if viper.GetBool("a-general.color") {
pU, err := urlPkg.Parse(url)
if !r.proxied && err == nil &&
(pU.Scheme == "" || pU.Scheme == "gemini" || pU.Scheme == "about") {
// A gemini link
// Add the link text in blue (in a region), and a gray link number to the left of it
// Those are the default colors, anyway
wrappedLink = wrapLine(linkText, r.width,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("amfora_link")+`]`,
`[-][""]`,
false, // Don't indent the first line, it's the one with link number
)
// Add special stuff to first line, like the link number
wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
strconv.Itoa(num) + "[]" + "[-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("amfora_link") + `]` +
wrappedLink[0] + `[-][""]`
} else {
// Not a gemini link
wrappedLink = wrapLine(linkText, r.width,
strings.Repeat(" ", indent)+
`["`+strconv.Itoa(num-1)+`"][`+config.GetColorString("foreign_link")+`]`,
`[-][""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = fmt.Sprintf(`[%s::b][`, config.GetColorString("link_number")) +
strconv.Itoa(num) + "[]" + "[-::-]" + spacing +
`["` + strconv.Itoa(num-1) + `"][` + config.GetColorString("foreign_link") + `]` +
wrappedLink[0] + `[-][""]`
}
} else {
// No colors allowed
wrappedLink = wrapLine(linkText, r.width,
strings.Repeat(" ", len(strconv.Itoa(num))+4)+ // +4 for spaces and brackets
`["`+strconv.Itoa(num-1)+`"]`,
`[""]`,
false, // Don't indent the first line, it's the one with link number
)
wrappedLink[0] = `[::b][` + strconv.Itoa(num) + "[][::-] " +
`["` + strconv.Itoa(num-1) + `"]` +
wrappedLink[0] + `[""]`
}
wrappedLines = append(wrappedLines, wrappedLink...)
// Lists
} else if strings.HasPrefix(line, "* ") {
if viper.GetBool("a-general.bullets") {
// Wrap list item, and indent wrapped lines past the bullet
wrappedItem := wrapLine(line[1:], r.width,
fmt.Sprintf(" [%s]", config.GetColorString("list_text")),
"[-]", false)
// Add bullet
wrappedItem[0] = fmt.Sprintf(" [%s]\u2022", config.GetColorString("list_text")) +
wrappedItem[0] + "[-]"
wrappedLines = append(wrappedLines, wrappedItem...)
}
// Optionally list lines could be colored here too, if color is enabled
} else if strings.HasPrefix(line, ">") {
// It's a quote line, add extra quote symbols and italics to the start of each wrapped line
if len(line) == 1 {
// Just an empty quote line
wrappedLines = append(wrappedLines, fmt.Sprintf("[%s::i]>[-::-]", config.GetColorString("quote_text")))
} else {
// Remove beginning quote and maybe space
line = strings.TrimPrefix(line, ">")
line = strings.TrimPrefix(line, " ")
wrappedLines = append(wrappedLines,
wrapLine(line, r.width, fmt.Sprintf("[%s::i]> ", config.GetColorString("quote_text")),
"[-::-]", true)...,
)
}
} else if strings.TrimSpace(line) == "" {
// Just add empty line without processing
wrappedLines = append(wrappedLines, "")
} else {
// Regular line, just wrap it
wrappedLines = append(wrappedLines, wrapLine(line, r.width,
fmt.Sprintf("[%s]", config.GetColorString("regular_text")),
"[-]", true)...)
}
return strings.Join(wrappedLines, "\n") + "\n"
}
func (ren *GemtextRenderer) ReadFrom(r io.Reader) (int64, error) {
// Go through lines, render, and write each line
// TODO: Should writes be buffered?
var n int64
scanner := bufio.NewScanner(r)
scanner.Split(ScanLines)
for scanner.Scan() {
n += int64(len(scanner.Bytes()))
line := scanner.Text()
if strings.HasPrefix(line, "```") {
ren.pre = !ren.pre
continue
}
// Render line and write it
//nolint:errcheck
ren.w.Write([]byte(
ren.renderLine(strings.TrimRight(line, "\r\n")),
))
}
return n, scanner.Err()
}
// Write will panic, use ReadFrom instead.
func (r *GemtextRenderer) Write(p []byte) (n int, err error) {
// This renderer is line based, and so it can't process arbitrary bytes.
// One solution would be to handle rendering on the other end of the pipe,
// the Read call, but it's simpler to just implement ReadFrom.
panic("func Write not allowed for GemtextRenderer")
}
func (r *GemtextRenderer) Read(p []byte) (n int, err error) {
return r.r.Read(p)
} }
func (r *GemtextRenderer) Links() <-chan string { func (r *GemtextRenderer) Links() <-chan string {
return r.links return r.links
} }
func (r *GemtextRenderer) Write(p []byte) (n int, err error) {
// Just write to the scanner, all logic is in Read()
return r.scanWriter.Write(p)
}

View File

@ -1,6 +1,7 @@
package render package render
import ( import (
"bufio"
"bytes" "bytes"
"fmt" "fmt"
"io" "io"
@ -12,17 +13,45 @@ import (
// Renderer renderers network bytes into something that can be displayed on a // Renderer renderers network bytes into something that can be displayed on a
// cview.TextView. // cview.TextView.
//
// Write calls may block if the Lines channel buffer is full.
//
// Current implementations don't actually implement io.Writer, and calling Write
// will panic. ReadFrom should be used instead.
type Renderer interface { type Renderer interface {
io.ReadWriter io.ReadWriter
io.ReaderFrom
// Links returns a channel that yields Link URLs as they are parsed. // Links returns a channel that yields Link URLs as they are parsed.
// It is buffered. The channel might be closed to indicate links are supported // It is buffered. The channel might be closed to indicate links aren't supported
// for this renderer. // for this renderer.
Links() <-chan string Links() <-chan string
} }
// ScanLines is copied from bufio.ScanLines and is used with bufio.Scanner.
// The only difference is that this func doesn't get rid of the end-of-line marker.
// This is so that the number of read bytes can be counted correctly in ReadFrom.
//
// It also simplifes code by no longer having to append a newline character.
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, data[0 : i+1], nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), data, nil
}
// Request more data.
return 0, nil, nil
}
// PlaintextRenderer escapes text for cview usage and does nothing else.
type PlaintextRenderer struct { type PlaintextRenderer struct {
*io.PipeReader r *io.PipeReader
w *io.PipeWriter w *io.PipeWriter
} }
@ -31,10 +60,33 @@ func NewPlaintextRenderer() *PlaintextRenderer {
return &PlaintextRenderer{pr, pw} return &PlaintextRenderer{pr, pw}
} }
func (ren *PlaintextRenderer) ReadFrom(r io.Reader) (int64, error) {
// Go through lines and escape bytes and write each line
// TODO: Should writes be buffered?
var n int64
scanner := bufio.NewScanner(r)
scanner.Split(ScanLines)
for scanner.Scan() {
n += int64(len(scanner.Bytes()))
//nolint:errcheck
ren.w.Write(cview.EscapeBytes(scanner.Bytes()))
}
return n, scanner.Err()
}
// Write will panic, use ReadFrom instead.
func (r *PlaintextRenderer) Write(p []byte) (n int, err error) { func (r *PlaintextRenderer) Write(p []byte) (n int, err error) {
// TODO: The escaping will fail if the Write bytes end in the middle of a tag // This function would normally use cview.EscapeBytes
// How can this be avoided by users of this func? // But the escaping will fail if the Write bytes end in the middle of a tag
return r.w.Write(cview.EscapeBytes(p)) // So instead it just panics, because it should never be used.
panic("func Write not allowed for PlaintextRenderer")
}
func (r *PlaintextRenderer) Read(p []byte) (n int, err error) {
return r.r.Read(p)
} }
func (r *PlaintextRenderer) Links() <-chan string { func (r *PlaintextRenderer) Links() <-chan string {
@ -43,11 +95,13 @@ func (r *PlaintextRenderer) Links() <-chan string {
return ch return ch
} }
// ANSIRenderer escapes text for cview usage, as well as converting ANSI codes
// into cview tags if the config allows it.
type ANSIRenderer struct { type ANSIRenderer struct {
*io.PipeReader r *io.PipeReader
pw *io.PipeWriter w *io.PipeWriter
ansiWriter io.Writer // cview.ANSIWriter ansiWriter io.Writer // cview.ANSIWriter
buf bytes.Buffer buf bytes.Buffer // Where ansiWriter writes to
} }
func NewANSIRenderer() *ANSIRenderer { func NewANSIRenderer() *ANSIRenderer {
@ -63,25 +117,56 @@ func NewANSIRenderer() *ANSIRenderer {
return &ANSIRenderer{pr, pw, ansiWriter, buf} return &ANSIRenderer{pr, pw, ansiWriter, buf}
} }
// Write will panic, use ReadFrom instead.
func (r *ANSIRenderer) Write(p []byte) (n int, err error) { func (r *ANSIRenderer) Write(p []byte) (n int, err error) {
if r.ansiWriter == nil { // This function would normally use cview.EscapeBytes among other things.
// ANSI disabled // But the escaping will fail if the Write bytes end in the middle of a tag
return r.pw.Write(ansiRegex.ReplaceAll(p, []byte{})) // So instead it just panics, because it should never be used.
} panic("func Write not allowed for ANSIRenderer")
// ANSI enabled }
r.buf.Reset() func (ren *ANSIRenderer) ReadFrom(r io.Reader) (int64, error) {
r.ansiWriter.Write(p) // Shouldn't error because everything it writes to are all bytes.Buffer // Go through lines, render, and write each line
return r.pw.Write( // TODO: Should writes be buffered?
// The ANSIWriter injects tags like [-:-:-]
// but this will reset the background to use the user's terminal color. var n int64
// These tags need to be replaced with resets that use the theme color. scanner := bufio.NewScanner(r)
bytes.ReplaceAll( scanner.Split(ScanLines)
r.buf.Bytes(),
[]byte("[-:-:-]"), for scanner.Scan() {
[]byte(fmt.Sprintf("[-:%s:-]", config.GetColorString("bg"))), n += int64(len(scanner.Bytes()))
), line := scanner.Bytes()
) line = cview.EscapeBytes(line)
if ren.ansiWriter == nil {
// ANSI disabled
line = ansiRegex.ReplaceAll(scanner.Bytes(), nil)
} else {
// ANSI enabled
ren.buf.Reset()
// Shouldn't error because everything it writes to are all bytes.Buffer
ren.ansiWriter.Write(line) //nolint:errcheck
// The ANSIWriter injects tags like [-:-:-]
// but this will reset the background to use the user's terminal color.
// These tags need to be replaced with resets that use the theme color.
line = bytes.ReplaceAll(
ren.buf.Bytes(),
[]byte("[-:-:-]"),
[]byte(fmt.Sprintf("[-:%s:-]", config.GetColorString("bg"))),
)
}
ren.w.Write(line) //nolint:errcheck
}
return n, scanner.Err()
}
func (r *ANSIRenderer) Read(p []byte) (n int, err error) {
return r.r.Read(p)
} }
func (r *ANSIRenderer) Links() <-chan string { func (r *ANSIRenderer) Links() <-chan string {