Use a Renderer interface instead of rendering fragments

This commit is contained in:
Christian Muehlhaeuser
2019-11-26 03:24:56 +01:00
parent d438c26548
commit 24a6306e1a
10 changed files with 469 additions and 397 deletions
+72
View File
@@ -0,0 +1,72 @@
package gold
import (
"fmt"
"io"
"strconv"
"github.com/logrusorgru/aurora"
bf "gopkg.in/russross/blackfriday.v2"
)
type BaseElement struct {
Token string
Pre string
Post string
Style StyleType
}
func (e *BaseElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
if e.Pre != "" {
fmt.Fprintf(w, "%s", e.Pre)
}
defer func() {
if e.Post != "" {
fmt.Fprintf(w, "%s", e.Post)
}
}()
rules := tr.style[e.Style]
if rules == nil {
fmt.Fprintf(w, "%s", e.Token)
return nil
}
out := aurora.Reset(e.Token)
if rules.Color != "" {
i, err := strconv.Atoi(rules.Color)
if err == nil && i >= 0 && i <= 255 {
out = out.Index(uint8(i))
}
}
if rules.BackgroundColor != "" {
i, err := strconv.Atoi(rules.BackgroundColor)
if err == nil && i >= 0 && i <= 255 {
out = out.Index(uint8(i))
}
}
if rules.Underline {
out = out.Underline()
}
if rules.Bold {
out = out.Bold()
}
if rules.Italic {
out = out.Italic()
}
if rules.CrossedOut {
out = out.CrossedOut()
}
if rules.Overlined {
out = out.Overlined()
}
if rules.Inverse {
out = out.Reverse()
}
if rules.Blink {
out = out.Blink()
}
w.Write([]byte(aurora.Sprintf("%s", out)))
return nil
}
+32
View File
@@ -0,0 +1,32 @@
package gold
import (
"io"
"github.com/alecthomas/chroma/quick"
bf "gopkg.in/russross/blackfriday.v2"
)
type CodeBlockElement struct {
Code string
Language string
}
func (e *CodeBlockElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
var theme string
rules := tr.style[CodeBlock]
if rules != nil {
theme = rules.Theme
}
if len(theme) > 0 {
return quick.Highlight(w, e.Code, e.Language, "terminal16m", theme)
}
// fallback rendering
el := &BaseElement{
Token: string(node.Literal),
Style: CodeBlock,
}
return el.Render(w, node, tr)
}
+171
View File
@@ -0,0 +1,171 @@
package gold
import (
"html"
"io"
"strings"
bf "gopkg.in/russross/blackfriday.v2"
)
type ElementRenderer interface {
Render(w io.Writer, node *bf.Node, tr *TermRenderer) error
}
type Element struct {
Pre string
Post string
Renderer ElementRenderer
}
func (tr *TermRenderer) NewElement(node *bf.Node) Element {
switch node.Type {
case bf.Document:
return Element{
Renderer: &BaseElement{
Style: Document,
},
}
case bf.BlockQuote:
return Element{
Pre: "\n",
Post: "\n",
Renderer: &BaseElement{
Token: string(node.Literal),
Style: BlockQuote,
},
}
case bf.List:
return Element{
Renderer: &ListElement{},
}
case bf.Item:
return Element{
Renderer: &ItemElement{},
}
case bf.Paragraph:
return Element{
Post: "\n",
Renderer: &ParagraphElement{},
}
case bf.Heading:
return Element{
Post: "\n",
Renderer: &HeadingElement{},
}
case bf.HorizontalRule:
return Element{
Pre: "\n",
Post: "\n",
Renderer: &BaseElement{
Token: "---",
Style: HorizontalRule,
},
}
case bf.Emph:
return Element{
Renderer: &BaseElement{
Token: string(node.FirstChild.Literal),
Style: Emph,
},
}
case bf.Strong:
return Element{
Renderer: &BaseElement{
Token: string(node.FirstChild.Literal),
Style: Strong,
},
}
case bf.Del:
return Element{
Renderer: &BaseElement{
Token: string(node.Literal),
Style: Del,
},
}
case bf.Link:
return Element{
Renderer: &LinkElement{},
}
case bf.Image:
return Element{
Renderer: &ImageElement{},
}
case bf.Text:
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(stripper.Sanitize(string(node.Literal))),
Style: Text,
},
}
case bf.HTMLBlock:
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(strings.TrimSpace(stripper.Sanitize(string(node.Literal)))) + "\n",
Style: HTMLBlock,
},
}
case bf.CodeBlock:
return Element{
Pre: "\n",
Post: "\n",
Renderer: &CodeBlockElement{
Code: string(node.Literal),
Language: string(node.CodeBlockData.Info),
},
}
case bf.Softbreak:
return Element{
Post: "\n",
Renderer: &BaseElement{
Token: string(node.Literal),
Style: Softbreak,
},
}
case bf.Hardbreak:
return Element{
Post: "\n",
Renderer: &BaseElement{
Token: string(node.Literal),
Style: Hardbreak,
},
}
case bf.Code:
return Element{
Renderer: &BaseElement{
Token: string(node.Literal),
Style: Code,
},
}
case bf.HTMLSpan:
return Element{
Renderer: &BaseElement{
Token: html.UnescapeString(strings.TrimSpace(stripper.Sanitize(string(node.Literal)))) + "\n",
Style: HTMLSpan,
},
}
case bf.Table:
return Element{
Pre: "\n",
Post: "\n",
}
case bf.TableCell:
return Element{}
case bf.TableHead:
return Element{}
case bf.TableBody:
return Element{}
case bf.TableRow:
return Element{
Pre: "\n",
Post: "\n",
}
default:
return Element{
Renderer: &BaseElement{
Token: string(node.Literal),
},
}
}
}
+23 -397
View File
@@ -3,17 +3,12 @@ package gold
import (
"encoding/json"
"fmt"
"html"
"io"
"io/ioutil"
"log"
"net/url"
"os"
"strconv"
"strings"
"github.com/alecthomas/chroma/quick"
"github.com/logrusorgru/aurora"
"github.com/microcosm-cc/bluemonday"
bf "gopkg.in/russross/blackfriday.v2"
)
@@ -22,19 +17,6 @@ var (
stripper = bluemonday.StrictPolicy()
)
type Fragment struct {
Token string
Pre string
Post string
Style StyleType
}
type Element struct {
Pre string
Post string
Fragments []Fragment
}
type TermRenderer struct {
BaseURL string
style map[StyleType]*ElementStyle
@@ -89,313 +71,6 @@ func NewTermRendererFromBytes(b []byte) (*TermRenderer, error) {
return tr, nil
}
func resolveRelativeURL(baseURL string, rel string) string {
u, err := url.Parse(rel)
if err != nil {
log.Fatal(err)
}
if u.IsAbs() {
return rel
}
if u.Path[0] == '/' {
u.Path = u.Path[1:]
}
base, err := url.Parse(baseURL)
if err != nil {
return rel
}
return base.ResolveReference(u).String()
}
func (tr *TermRenderer) NewElement(node *bf.Node) Element {
switch node.Type {
case bf.Document:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: "",
Style: Document,
}},
}
case bf.BlockQuote:
return Element{
Pre: "\n",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: BlockQuote,
}},
}
case bf.List:
pre := ""
if node.Parent.Type != bf.Item {
pre = "\n"
}
return Element{
Pre: pre,
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: List,
}},
}
case bf.Item:
l := 0
n := node
for n.Parent != nil && (n.Parent.Type == bf.List || n.Parent.Type == bf.Item) {
if n.Parent.Type == bf.List {
l++
}
n = n.Parent
}
return Element{
Pre: strings.Repeat(" ", l-1) + "• ",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Item,
}},
}
case bf.Paragraph:
pre := "\n"
if node.Prev == nil || (node.Parent != nil && node.Parent.Type == bf.Item) {
pre = ""
}
return Element{
Pre: pre,
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Paragraph,
}},
}
case bf.Heading:
var pre string
if node.Prev != nil {
pre = "\n"
}
return Element{
Pre: pre,
Post: "\n",
Fragments: []Fragment{{
Token: fmt.Sprintf("%s %s", strings.Repeat("#", node.HeadingData.Level), node.FirstChild.Literal),
Style: Heading,
}},
}
case bf.HorizontalRule:
return Element{
Pre: "\n",
Post: "\n",
Fragments: []Fragment{{
Token: "---",
Style: HorizontalRule,
}},
}
case bf.Emph:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.FirstChild.Literal),
Style: Emph,
}},
}
case bf.Strong:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.FirstChild.Literal),
Style: Strong,
}},
}
case bf.Del:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Del,
}},
}
case bf.Link:
f := []Fragment{}
if node.LastChild != nil {
if node.LastChild.Type == bf.Image {
el := tr.NewElement(node.LastChild)
f = el.Fragments
}
if len(node.LastChild.Literal) > 0 {
f = append(f, Fragment{
Token: string(node.LastChild.Literal),
Style: LinkText,
})
}
}
if len(node.LinkData.Destination) > 0 {
f = append(f, Fragment{
Token: resolveRelativeURL(tr.BaseURL, string(node.LinkData.Destination)),
Pre: " (",
Post: ")",
Style: Link,
})
}
return Element{
Pre: "",
Post: "",
Fragments: f,
}
case bf.Image:
f := []Fragment{}
if len(node.LastChild.Literal) > 0 {
f = append(f, Fragment{
Token: string(node.LastChild.Literal),
Style: Image,
})
}
if len(node.LinkData.Destination) > 0 {
f = append(f, Fragment{
Token: resolveRelativeURL(tr.BaseURL, string(node.LinkData.Destination)),
Pre: " [Image: ",
Post: "]",
Style: Link,
})
}
return Element{
Pre: "",
Post: "",
Fragments: f,
}
case bf.Text:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: html.UnescapeString(stripper.Sanitize(string(node.Literal))),
Style: Text,
}},
}
case bf.HTMLBlock:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: html.UnescapeString(strings.TrimSpace(stripper.Sanitize(string(node.Literal)))) + "\n",
Style: HTMLBlock,
}},
}
case bf.CodeBlock:
return Element{
Pre: "\n",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: CodeBlock,
}},
}
case bf.Softbreak:
return Element{
Pre: "",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Softbreak,
}},
}
case bf.Hardbreak:
return Element{
Pre: "",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Hardbreak,
}},
}
case bf.Code:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Code,
}},
}
case bf.HTMLSpan:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: html.UnescapeString(strings.TrimSpace(stripper.Sanitize(string(node.Literal)))) + "\n",
Style: HTMLSpan,
}},
}
case bf.Table:
return Element{
Pre: "\n",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: Table,
}},
}
case bf.TableCell:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: TableCell,
}},
}
case bf.TableHead:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: TableHead,
}},
}
case bf.TableBody:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: TableBody,
}},
}
case bf.TableRow:
return Element{
Pre: "\n",
Post: "\n",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: TableRow,
}},
}
default:
return Element{
Pre: "",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
}},
}
}
}
func (tr *TermRenderer) Render(in string) string {
return string(tr.RenderBytes([]byte(in)))
}
@@ -404,51 +79,6 @@ func (tr *TermRenderer) RenderBytes(in []byte) []byte {
return bf.Run(in, bf.WithRenderer(tr))
}
func (tr *TermRenderer) renderFragment(w io.Writer, f Fragment) {
rules := tr.style[f.Style]
if rules == nil {
fmt.Fprintf(w, "%s", f.Token)
return
}
out := aurora.Reset(f.Token)
if rules.Color != "" {
i, err := strconv.Atoi(rules.Color)
if err == nil && i >= 0 && i <= 255 {
out = out.Index(uint8(i))
}
}
if rules.BackgroundColor != "" {
i, err := strconv.Atoi(rules.BackgroundColor)
if err == nil && i >= 0 && i <= 255 {
out = out.Index(uint8(i))
}
}
if rules.Underline {
out = out.Underline()
}
if rules.Bold {
out = out.Bold()
}
if rules.Italic {
out = out.Italic()
}
if rules.CrossedOut {
out = out.CrossedOut()
}
if rules.Overlined {
out = out.Overlined()
}
if rules.Inverse {
out = out.Reverse()
}
if rules.Blink {
out = out.Blink()
}
w.Write([]byte(aurora.Sprintf("%s", out)))
}
func (tr *TermRenderer) RenderNode(w io.Writer, node *bf.Node, entering bool) bf.WalkStatus {
// fmt.Fprintf(w, "%s %t", node.Type, entering)
e := tr.NewElement(node)
@@ -465,33 +95,10 @@ func (tr *TermRenderer) RenderNode(w io.Writer, node *bf.Node, entering bool) bf
return bf.GoToNext
}
for _, f := range e.Fragments {
if node.Type == bf.CodeBlock {
var theme string
if rules, ok := tr.style[f.Style]; ok {
if len(rules.Theme) > 0 {
theme = rules.Theme
}
}
if len(theme) > 0 {
err := quick.Highlight(w, f.Token, string(node.CodeBlockData.Info), "terminal16m", theme)
if err == nil {
continue
}
// if chroma failed, render the fragment as text below
}
}
if f.Token == "" {
continue
}
if f.Pre != "" {
fmt.Fprintf(w, "%s", f.Pre)
}
tr.renderFragment(w, f)
if f.Post != "" {
fmt.Fprintf(w, "%s", f.Post)
if e.Renderer != nil {
err := e.Renderer.Render(w, node, tr)
if err == nil {
return bf.GoToNext
}
}
@@ -515,3 +122,22 @@ func isChild(node *bf.Node) bool {
return false
}
}
func resolveRelativeURL(baseURL string, rel string) string {
u, err := url.Parse(rel)
if err != nil {
log.Fatal(err)
}
if u.IsAbs() {
return rel
}
if u.Path[0] == '/' {
u.Path = u.Path[1:]
}
base, err := url.Parse(baseURL)
if err != nil {
return rel
}
return base.ResolveReference(u).String()
}
+26
View File
@@ -0,0 +1,26 @@
package gold
import (
"fmt"
"io"
"strings"
bf "gopkg.in/russross/blackfriday.v2"
)
type HeadingElement struct {
}
func (e *HeadingElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
var pre string
if node.Prev != nil {
pre = "\n"
}
el := &BaseElement{
Pre: pre,
Token: fmt.Sprintf("%s %s", strings.Repeat("#", node.HeadingData.Level), node.FirstChild.Literal),
Style: Heading,
}
return el.Render(w, node, tr)
}
+31
View File
@@ -0,0 +1,31 @@
package gold
import (
"io"
bf "gopkg.in/russross/blackfriday.v2"
)
type ImageElement struct {
}
func (e *ImageElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
if len(node.LastChild.Literal) > 0 {
el := &BaseElement{
Token: string(node.LastChild.Literal),
Style: Image,
}
el.Render(w, node.LastChild, tr)
}
if len(node.LinkData.Destination) > 0 {
el := &BaseElement{
Token: resolveRelativeURL(tr.BaseURL, string(node.LinkData.Destination)),
Pre: " [Image: ",
Post: "]",
Style: Link,
}
el.Render(w, node, tr)
}
return nil
}
+37
View File
@@ -0,0 +1,37 @@
package gold
import (
"io"
bf "gopkg.in/russross/blackfriday.v2"
)
type LinkElement struct {
}
func (e *LinkElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
if node.LastChild != nil {
if node.LastChild.Type == bf.Image {
el := tr.NewElement(node.LastChild)
el.Renderer.Render(w, node.LastChild, tr)
}
if len(node.LastChild.Literal) > 0 {
el := &BaseElement{
Token: string(node.LastChild.Literal),
Style: LinkText,
}
el.Render(w, node.LastChild, tr)
}
}
if len(node.LinkData.Destination) > 0 {
el := &BaseElement{
Token: resolveRelativeURL(tr.BaseURL, string(node.LinkData.Destination)),
Pre: " (",
Post: ")",
Style: Link,
}
el.Render(w, node, tr)
}
return nil
}
+24
View File
@@ -0,0 +1,24 @@
package gold
import (
"io"
bf "gopkg.in/russross/blackfriday.v2"
)
type ListElement struct {
}
func (e *ListElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
pre := ""
if node.Parent.Type != bf.Item {
pre = "\n"
}
el := &BaseElement{
Pre: pre,
Token: string(node.Literal),
Style: List,
}
return el.Render(w, node, tr)
}
+29
View File
@@ -0,0 +1,29 @@
package gold
import (
"io"
"strings"
bf "gopkg.in/russross/blackfriday.v2"
)
type ItemElement struct {
}
func (e *ItemElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
l := 0
n := node
for n.Parent != nil && (n.Parent.Type == bf.List || n.Parent.Type == bf.Item) {
if n.Parent.Type == bf.List {
l++
}
n = n.Parent
}
el := &BaseElement{
Pre: strings.Repeat(" ", l-1) + "• ",
Token: string(node.Literal),
Style: Item,
}
return el.Render(w, node, tr)
}
+24
View File
@@ -0,0 +1,24 @@
package gold
import (
"io"
bf "gopkg.in/russross/blackfriday.v2"
)
type ParagraphElement struct {
}
func (e *ParagraphElement) Render(w io.Writer, node *bf.Node, tr *TermRenderer) error {
pre := "\n"
if node.Prev == nil || (node.Parent != nil && node.Parent.Type == bf.Item) {
pre = ""
}
el := &BaseElement{
Pre: pre,
Token: string(node.Literal),
Style: Paragraph,
}
return el.Render(w, node, tr)
}