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) +}