glow/gold.go
2019-11-25 22:24:22 +01:00

497 lines
9.1 KiB
Go

package gold
import (
"encoding/json"
"fmt"
"html"
"io"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/alecthomas/chroma/quick"
"github.com/eliukblau/pixterm/ansimage"
"github.com/logrusorgru/aurora"
"github.com/lucasb-eyer/go-colorful"
"github.com/microcosm-cc/bluemonday"
bf "gopkg.in/russross/blackfriday.v2"
)
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 {
style map[StyleType]*ElementStyle
}
func Render(in string, stylePath string) ([]byte, error) {
return RenderBytes([]byte(in), stylePath)
}
func RenderBytes(in []byte, stylePath string) ([]byte, error) {
r, err := NewTermRenderer(stylePath)
if err != nil {
return nil, err
}
return r.RenderBytes(in), nil
}
func NewPlainTermRenderer() *TermRenderer {
return &TermRenderer{}
}
func NewTermRenderer(stylePath string) (*TermRenderer, error) {
if stylePath == "" {
return NewTermRendererFromBytes([]byte("{}"))
}
f, err := os.Open(stylePath)
if err != nil {
return nil, err
}
defer f.Close()
b, _ := ioutil.ReadAll(f)
return NewTermRendererFromBytes(b)
}
func NewTermRendererFromBytes(b []byte) (*TermRenderer, error) {
e := make(map[string]*ElementStyle)
err := json.Unmarshal(b, &e)
if err != nil {
return nil, err
}
tr := &TermRenderer{}
tr.style = make(map[StyleType]*ElementStyle)
for k, v := range e {
t, err := keyToType(k)
if err != nil {
fmt.Println(err)
continue
}
tr.style[t] = v
}
return tr, nil
}
func 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:
return Element{
Pre: "\n",
Post: "",
Fragments: []Fragment{{
Token: string(node.Literal),
Style: List,
}},
}
case bf.Item:
return Element{
Pre: "• ",
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 := 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: 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: string(node.LinkData.Destination),
// Pre: " [Image: ",
// Post: "]",
Style: Image,
})
}
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: "\n",
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)))
}
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
}
if f.Style == Image {
sm := ansimage.ScaleMode(2)
dm := ansimage.DitheringMode(0)
mc, _ := colorful.Hex("#000000")
pix, err := ansimage.NewScaledFromURL(f.Token, 62, 80, mc, sm, dm)
if err != nil {
return
}
fmt.Fprintf(w, "%s", pix.Render())
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 := NewElement(node)
if entering && e.Pre != "" {
fmt.Fprintf(w, "%s", e.Pre)
}
if !entering && e.Post != "" {
fmt.Fprintf(w, "%s", e.Post)
}
if isChild(node) {
return bf.GoToNext
}
if !entering {
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)
}
}
return bf.GoToNext
}
func (tr *TermRenderer) RenderHeader(w io.Writer, ast *bf.Node) {
}
func (tr *TermRenderer) RenderFooter(w io.Writer, ast *bf.Node) {
}
func isChild(node *bf.Node) bool {
if node.Parent == nil {
return false
}
switch node.Parent.Type {
case bf.Heading, bf.Link, bf.Image, bf.Emph, bf.Strong:
return true
default:
return false
}
}