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:
parent
d2d5846b0e
commit
fedc1fd11a
@ -2,37 +2,42 @@ package render
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"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.
|
||||
|
||||
type GemtextRenderer struct {
|
||||
r *io.PipeReader
|
||||
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
|
||||
// Buffers and I/O
|
||||
|
||||
r *io.PipeReader
|
||||
w *io.PipeWriter
|
||||
links chan string
|
||||
|
||||
// numLinks is the number of links that exist so far.
|
||||
numLinks int
|
||||
// Configurable options
|
||||
|
||||
// width is the number of columns to wrap to.
|
||||
width int
|
||||
// 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
|
||||
// or not.
|
||||
pre bool
|
||||
// numLinks is the number of links that exist so far.
|
||||
numLinks int
|
||||
}
|
||||
|
||||
// NewGemtextRenderer.
|
||||
@ -43,29 +48,268 @@ type GemtextRenderer struct {
|
||||
// If it's not a gemini:// page, set this to true.
|
||||
func NewGemtextRenderer(width int, proxied bool) *GemtextRenderer {
|
||||
pr, pw := io.Pipe()
|
||||
scanReader, scanWriter := io.Pipe()
|
||||
scanner := bufio.NewScanner(scanReader)
|
||||
links := make(chan string, 10)
|
||||
|
||||
return &GemtextRenderer{
|
||||
r: pr,
|
||||
w: pw,
|
||||
scanner: scanner,
|
||||
scanWriter: scanWriter,
|
||||
lineEnd: make([]byte, 0),
|
||||
links: links,
|
||||
numLinks: 0,
|
||||
width: width,
|
||||
proxied: proxied,
|
||||
pre: false,
|
||||
ansiEnabled := false
|
||||
if viper.GetBool("a-general.color") && viper.GetBool("a-general.ansi") {
|
||||
ansiEnabled = true
|
||||
}
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package render
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -12,17 +13,45 @@ import (
|
||||
|
||||
// Renderer renderers network bytes into something that can be displayed on a
|
||||
// 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 {
|
||||
io.ReadWriter
|
||||
io.ReaderFrom
|
||||
|
||||
// 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.
|
||||
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 {
|
||||
*io.PipeReader
|
||||
r *io.PipeReader
|
||||
w *io.PipeWriter
|
||||
}
|
||||
|
||||
@ -31,10 +60,33 @@ func NewPlaintextRenderer() *PlaintextRenderer {
|
||||
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) {
|
||||
// TODO: The escaping will fail if the Write bytes end in the middle of a tag
|
||||
// How can this be avoided by users of this func?
|
||||
return r.w.Write(cview.EscapeBytes(p))
|
||||
// This function would normally use cview.EscapeBytes
|
||||
// But the escaping will fail if the Write bytes end in the middle of a tag
|
||||
// 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 {
|
||||
@ -43,11 +95,13 @@ func (r *PlaintextRenderer) Links() <-chan string {
|
||||
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 {
|
||||
*io.PipeReader
|
||||
pw *io.PipeWriter
|
||||
ansiWriter io.Writer // cview.ANSIWriter
|
||||
buf bytes.Buffer
|
||||
r *io.PipeReader
|
||||
w *io.PipeWriter
|
||||
ansiWriter io.Writer // cview.ANSIWriter
|
||||
buf bytes.Buffer // Where ansiWriter writes to
|
||||
}
|
||||
|
||||
func NewANSIRenderer() *ANSIRenderer {
|
||||
@ -63,25 +117,56 @@ func NewANSIRenderer() *ANSIRenderer {
|
||||
return &ANSIRenderer{pr, pw, ansiWriter, buf}
|
||||
}
|
||||
|
||||
// Write will panic, use ReadFrom instead.
|
||||
func (r *ANSIRenderer) Write(p []byte) (n int, err error) {
|
||||
if r.ansiWriter == nil {
|
||||
// ANSI disabled
|
||||
return r.pw.Write(ansiRegex.ReplaceAll(p, []byte{}))
|
||||
}
|
||||
// ANSI enabled
|
||||
// This function would normally use cview.EscapeBytes among other things.
|
||||
// But the escaping will fail if the Write bytes end in the middle of a tag
|
||||
// So instead it just panics, because it should never be used.
|
||||
panic("func Write not allowed for ANSIRenderer")
|
||||
}
|
||||
|
||||
r.buf.Reset()
|
||||
r.ansiWriter.Write(p) // Shouldn't error because everything it writes to are all bytes.Buffer
|
||||
return r.pw.Write(
|
||||
// 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.
|
||||
bytes.ReplaceAll(
|
||||
r.buf.Bytes(),
|
||||
[]byte("[-:-:-]"),
|
||||
[]byte(fmt.Sprintf("[-:%s:-]", config.GetColorString("bg"))),
|
||||
),
|
||||
)
|
||||
func (ren *ANSIRenderer) 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.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 {
|
||||
|
Loading…
Reference in New Issue
Block a user