mirror of
https://github.com/rwinkhart/glamour-temp-MUTN.git
synced 2026-09-03 07:27:19 -04:00
Since BlackFriday's Nodes are limiting us a bit in rendering flexibility, I've introduced Gold's own ElementStyles (a bit of code repetition here, but alas). Nodes can now be divided into multiple fragments, which means we are able to display the link's URL and text in different styles. Updated the dark theme to demo the newly gained flexibility.
549 lines
9.7 KiB
Go
549 lines
9.7 KiB
Go
package gold
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/logrusorgru/aurora"
|
|
bf "gopkg.in/russross/blackfriday.v2"
|
|
)
|
|
|
|
type StyleType int
|
|
|
|
const (
|
|
Document StyleType = iota
|
|
BlockQuote
|
|
List
|
|
Item
|
|
Paragraph
|
|
Heading
|
|
HorizontalRule
|
|
Emph
|
|
Strong
|
|
Del
|
|
Link
|
|
LinkText
|
|
Image
|
|
Text
|
|
HTMLBlock
|
|
CodeBlock
|
|
Softbreak
|
|
Hardbreak
|
|
Code
|
|
HTMLSpan
|
|
Table
|
|
TableCell
|
|
TableHead
|
|
TableBody
|
|
TableRow
|
|
)
|
|
|
|
type ElementStyle struct {
|
|
Color string `json:"color"`
|
|
BackgroundColor string `json:"background_color"`
|
|
Underline bool `json:"underline"`
|
|
Bold bool `json:"bold"`
|
|
Italic bool `json:"italic"`
|
|
CrossedOut bool `json:"crossed_out"`
|
|
Faint bool `json:"faint"`
|
|
Conceal bool `json:"conceal"`
|
|
Overlined bool `json:"overlined"`
|
|
Inverse bool `json:"inverse"`
|
|
Blink bool `json:"blink"`
|
|
}
|
|
|
|
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 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 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: Link,
|
|
})
|
|
}
|
|
|
|
return Element{
|
|
Pre: "",
|
|
Post: "",
|
|
Fragments: f,
|
|
}
|
|
case bf.Text:
|
|
return Element{
|
|
Pre: "",
|
|
Post: "",
|
|
Fragments: []Fragment{{
|
|
Token: string(node.Literal),
|
|
Style: Text,
|
|
}},
|
|
}
|
|
case bf.HTMLBlock:
|
|
return Element{
|
|
Pre: "\n",
|
|
Post: "\n",
|
|
Fragments: []Fragment{{
|
|
Token: string(node.Literal),
|
|
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: string(node.Literal),
|
|
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
|
|
}
|
|
|
|
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 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
|
|
}
|
|
}
|
|
|
|
func keyToType(key string) (StyleType, error) {
|
|
switch key {
|
|
case "document":
|
|
return Document, nil
|
|
case "block_quote":
|
|
return BlockQuote, nil
|
|
case "list":
|
|
return List, nil
|
|
case "item":
|
|
return Item, nil
|
|
case "paragraph":
|
|
return Paragraph, nil
|
|
case "heading":
|
|
return Heading, nil
|
|
case "hr":
|
|
return HorizontalRule, nil
|
|
case "emph":
|
|
return Emph, nil
|
|
case "strong":
|
|
return Strong, nil
|
|
case "del":
|
|
return Del, nil
|
|
case "link":
|
|
return Link, nil
|
|
case "link_text":
|
|
return LinkText, nil
|
|
case "image":
|
|
return Image, nil
|
|
case "text":
|
|
return Text, nil
|
|
case "html_block":
|
|
return HTMLBlock, nil
|
|
case "code_block":
|
|
return CodeBlock, nil
|
|
case "softbreak":
|
|
return Softbreak, nil
|
|
case "hardbreak":
|
|
return Hardbreak, nil
|
|
case "code":
|
|
return Code, nil
|
|
case "html_span":
|
|
return HTMLSpan, nil
|
|
case "table":
|
|
return Table, nil
|
|
case "table_cel":
|
|
return TableCell, nil
|
|
case "table_head":
|
|
return TableHead, nil
|
|
case "table_body":
|
|
return TableBody, nil
|
|
case "table_row":
|
|
return TableRow, nil
|
|
|
|
default:
|
|
return 0, fmt.Errorf("Invalid style element type: %s", key)
|
|
}
|
|
}
|