mirror of
https://github.com/makeworld-the-better-one/amfora.git
synced 2024-11-25 22:53:02 +03:00
243 lines
7.9 KiB
Go
243 lines
7.9 KiB
Go
// Package renderer provides functions to convert various data into a cview primitive.
|
|
// Example objects include a Gemini response, and an error.
|
|
//
|
|
// Rendered lines always end with \r\n, in an effort to be Window compatible.
|
|
package renderer
|
|
|
|
import (
|
|
urlPkg "net/url"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/spf13/viper"
|
|
"gitlab.com/tslocum/cview"
|
|
)
|
|
|
|
// RenderPlainText should be used to format plain text pages.
|
|
func RenderPlainText(s string, leftMargin int) string {
|
|
var shifted string
|
|
lines := strings.Split(cview.Escape(s), "\n")
|
|
for i := range lines {
|
|
shifted += strings.Repeat(" ", leftMargin) + lines[i] + "\n"
|
|
}
|
|
return shifted
|
|
}
|
|
|
|
// wrapLine wraps a line to the provided width, and adds the provided prefix and suffix to each wrapped line.
|
|
// It recovers from wrapping panics and should never cause a panic.
|
|
// It returns a slice of lines, without newlines at the end.
|
|
//
|
|
// Set includeFirst to true if the prefix and suffix should be applied to the first wrapped line as well
|
|
func wrapLine(line string, width int, prefix, suffix string, includeFirst bool) []string {
|
|
// Anonymous function to allow recovery from potential WordWrap panic
|
|
var ret []string
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
// Use unwrapped line instead
|
|
if includeFirst {
|
|
ret = []string{prefix + line + suffix}
|
|
} else {
|
|
ret = []string{line}
|
|
}
|
|
}
|
|
}()
|
|
|
|
wrapped := cview.WordWrap(line, width)
|
|
for i := range wrapped {
|
|
if !includeFirst && i == 0 {
|
|
continue
|
|
}
|
|
wrapped[i] = prefix + wrapped[i] + suffix
|
|
}
|
|
ret = wrapped
|
|
}()
|
|
return ret
|
|
}
|
|
|
|
// convertRegularGemini converts non-preformatted blocks of text/gemini
|
|
// into a cview-compatible format.
|
|
// It also returns a slice of link URLs.
|
|
// numLinks is the number of links that exist so far.
|
|
// width is the number of columns to wrap to.
|
|
//
|
|
// Since this only works on non-preformatted blocks, RenderGemini
|
|
// should always be used instead.
|
|
func convertRegularGemini(s string, numLinks, width int) (string, []string) {
|
|
links := make([]string, 0)
|
|
lines := strings.Split(s, "\n")
|
|
wrappedLines := make([]string, 0) // Final result
|
|
|
|
for i := range lines {
|
|
lines[i] = strings.TrimRight(lines[i], " \r\t\n")
|
|
|
|
if strings.HasPrefix(lines[i], "#") {
|
|
// Headings
|
|
if viper.GetBool("a-general.color") {
|
|
if strings.HasPrefix(lines[i], "###") {
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[fuchsia::b]", "[-::-]", true)...)
|
|
} else if strings.HasPrefix(lines[i], "##") {
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[lime::b]", "[-::-]", true)...)
|
|
} else if strings.HasPrefix(lines[i], "#") {
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[red::b]", "[-::-]", true)...)
|
|
}
|
|
} else {
|
|
// Just bold, no colors
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "[::b]", "[-::-]", true)...)
|
|
}
|
|
|
|
// Links
|
|
} else if strings.HasPrefix(lines[i], "=>") && len([]rune(lines[i])) >= 3 {
|
|
// Trim whitespace and separate link from link text
|
|
|
|
lines[i] = strings.Trim(lines[i][2:], " \t") // Remove `=>` part too
|
|
delim := strings.IndexAny(lines[i], " \t") // Whitespace between link and link text
|
|
|
|
var url string
|
|
var linkText string
|
|
if delim == -1 {
|
|
// No link text
|
|
url = lines[i]
|
|
linkText = url
|
|
} else {
|
|
// There is link text
|
|
url = lines[i][:delim]
|
|
linkText = strings.Trim(lines[i][delim:], " \t")
|
|
}
|
|
|
|
if strings.TrimSpace(lines[i]) == "" || strings.TrimSpace(url) == "" {
|
|
// Link was just whitespace, reset it and move on
|
|
lines[i] = "=>"
|
|
wrappedLines = append(wrappedLines, lines[i])
|
|
continue
|
|
}
|
|
|
|
links = append(links, url)
|
|
|
|
// Wrap and add link text
|
|
|
|
// Wrap the link text, but add some spaces to indent the wrapped lines past the link number
|
|
wrappedLink := wrapLine(linkText, width,
|
|
strings.Repeat(" ", len(strconv.Itoa(numLinks+len(links)))+4), // +4 for spaces and brackets
|
|
"",
|
|
false, // Don't indent the first line, it's the one with link number
|
|
)
|
|
|
|
// Set the style tags
|
|
// Add them to the first line
|
|
|
|
if viper.GetBool("a-general.color") {
|
|
pU, err := urlPkg.Parse(url)
|
|
if 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
|
|
wrappedLink[0] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " +
|
|
`[dodgerblue]["` + strconv.Itoa(numLinks+len(links)-1) + `"]` +
|
|
wrappedLink[0]
|
|
} else {
|
|
// Not a gemini link, use purple instead
|
|
wrappedLink[0] = `[silver::b][` + strconv.Itoa(numLinks+len(links)) + "[]" + "[-::-] " +
|
|
`[#8700d7]["` + strconv.Itoa(numLinks+len(links)-1) + `"]` +
|
|
wrappedLink[0]
|
|
}
|
|
} else {
|
|
// No colours allowed
|
|
wrappedLink[0] = `[::b][` + strconv.Itoa(numLinks+len(links)) + "[][::-] " +
|
|
`["` + strconv.Itoa(numLinks+len(links)-1) + `"]` +
|
|
wrappedLink[0]
|
|
}
|
|
|
|
wrappedLink[len(wrappedLink)-1] += `[""][-]` // Close region and formatting at the end
|
|
|
|
wrappedLines = append(wrappedLines, wrappedLink...)
|
|
|
|
// Lists
|
|
} else if strings.HasPrefix(lines[i], "* ") {
|
|
if viper.GetBool("a-general.bullets") {
|
|
// Wrap list item, and indent wrapped lines past the bullet
|
|
wrappedItem := wrapLine(lines[i][1:], width, " ", "", false)
|
|
// Add bullet
|
|
wrappedItem[0] = " \u2022" + wrappedItem[0]
|
|
wrappedLines = append(wrappedLines, wrappedItem...)
|
|
}
|
|
// Optionally list lines could be colored here too, if color is enabled
|
|
} else if strings.HasPrefix(lines[i], ">") {
|
|
// It's a quote line, add extra quote symbols and italics to the start of each wrapped line
|
|
|
|
// Remove beginning quote and maybe space
|
|
lines[i] = strings.TrimPrefix(lines[i], ">")
|
|
lines[i] = strings.TrimPrefix(lines[i], " ")
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "> [::i]", "[::-]", true)...)
|
|
|
|
} else if strings.TrimSpace(lines[i]) == "" {
|
|
// Just add empty line without processing
|
|
wrappedLines = append(wrappedLines, "")
|
|
} else {
|
|
// Regular line, just wrap it
|
|
wrappedLines = append(wrappedLines, wrapLine(lines[i], width, "", "", true)...)
|
|
}
|
|
}
|
|
|
|
return strings.Join(wrappedLines, "\r\n"), links
|
|
}
|
|
|
|
// RenderGemini converts text/gemini into a cview displayable format.
|
|
// It also returns a slice of link URLs.
|
|
//
|
|
// width is the number of columns to wrap to.
|
|
// leftMargin is the number of blank spaces to prepend to each line.
|
|
func RenderGemini(s string, width, leftMargin int) (string, []string) {
|
|
s = cview.Escape(s)
|
|
if viper.GetBool("a-general.color") {
|
|
s = cview.TranslateANSI(s)
|
|
}
|
|
lines := strings.Split(s, "\n")
|
|
|
|
links := make([]string, 0)
|
|
|
|
// Process and wrap non preformatted lines
|
|
rendered := "" // Final result
|
|
pre := false
|
|
buf := "" // Block of regular or preformatted lines
|
|
for i := range lines {
|
|
if strings.HasPrefix(lines[i], "```") {
|
|
if pre {
|
|
// In a preformatted block, so add the text as is
|
|
// Don't add the current line with backticks
|
|
rendered += buf
|
|
} else {
|
|
// Not preformatted, regular text
|
|
ren, lks := convertRegularGemini(buf, len(links), width)
|
|
links = append(links, lks...)
|
|
rendered += ren
|
|
}
|
|
buf = "" // Clear buffer for next block
|
|
pre = !pre
|
|
continue
|
|
}
|
|
// Lines always end with \r\n for Windows compatibility
|
|
buf += strings.TrimSuffix(lines[i], "\r") + "\r\n"
|
|
}
|
|
// Gone through all the lines, but there still is likely a block in the buffer
|
|
if pre {
|
|
// File ended without closing the preformatted block
|
|
rendered += buf
|
|
} else {
|
|
// Not preformatted, regular text
|
|
// Same code as in the loop above
|
|
ren, lks := convertRegularGemini(buf, len(links), width)
|
|
links = append(links, lks...)
|
|
rendered += ren
|
|
}
|
|
|
|
if leftMargin > 0 {
|
|
renLines := strings.Split(rendered, "\n")
|
|
for i := range renLines {
|
|
renLines[i] = strings.Repeat(" ", leftMargin) + renLines[i]
|
|
}
|
|
return strings.Join(renLines, "\n"), links
|
|
}
|
|
|
|
return rendered, links
|
|
}
|