From 24a6306e1a2e4b513e5e721b039d89bb84c66c0b Mon Sep 17 00:00:00 2001 From: Christian Muehlhaeuser Date: Tue, 26 Nov 2019 03:07:00 +0100 Subject: [PATCH] Use a Renderer interface instead of rendering fragments --- baselement.go | 72 +++++++++ codeblock.go | 32 ++++ elements.go | 171 ++++++++++++++++++++ gold.go | 420 +++----------------------------------------------- heading.go | 26 ++++ image.go | 31 ++++ link.go | 37 +++++ list.go | 24 +++ listitem.go | 29 ++++ paragraph.go | 24 +++ 10 files changed, 469 insertions(+), 397 deletions(-) create mode 100644 baselement.go create mode 100644 codeblock.go create mode 100644 elements.go create mode 100644 heading.go create mode 100644 image.go create mode 100644 link.go create mode 100644 list.go create mode 100644 listitem.go create mode 100644 paragraph.go diff --git a/baselement.go b/baselement.go new file mode 100644 index 0000000..40510e4 --- /dev/null +++ b/baselement.go @@ -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 +} diff --git a/codeblock.go b/codeblock.go new file mode 100644 index 0000000..d2db79a --- /dev/null +++ b/codeblock.go @@ -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) +} diff --git a/elements.go b/elements.go new file mode 100644 index 0000000..d3fa9e0 --- /dev/null +++ b/elements.go @@ -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), + }, + } + } +} diff --git a/gold.go b/gold.go index ebca1e0..5e44dd6 100644 --- a/gold.go +++ b/gold.go @@ -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() +} diff --git a/heading.go b/heading.go new file mode 100644 index 0000000..c6a654f --- /dev/null +++ b/heading.go @@ -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) +} diff --git a/image.go b/image.go new file mode 100644 index 0000000..1d2f538 --- /dev/null +++ b/image.go @@ -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 +} diff --git a/link.go b/link.go new file mode 100644 index 0000000..9f8366c --- /dev/null +++ b/link.go @@ -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 +} diff --git a/list.go b/list.go new file mode 100644 index 0000000..0967318 --- /dev/null +++ b/list.go @@ -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) +} diff --git a/listitem.go b/listitem.go new file mode 100644 index 0000000..227ed2b --- /dev/null +++ b/listitem.go @@ -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) +} diff --git a/paragraph.go b/paragraph.go new file mode 100644 index 0000000..c850917 --- /dev/null +++ b/paragraph.go @@ -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) +}