Move ANSIRenderer into a separate package

This commit is contained in:
Christian Muehlhaeuser
2019-12-14 00:11:29 +01:00
parent 3018ad9a32
commit 4b65ed22c2
21 changed files with 245 additions and 227 deletions
+65
View File
@@ -0,0 +1,65 @@
package ansi
import (
"io"
"strings"
)
type AnsiWriter struct {
Forward io.Writer
ansi bool
ansiseq string
lastseq string
seqchanged bool
}
// Write is used to write content to the ANSI buffer.
func (w *AnsiWriter) Write(b []byte) (int, error) {
for _, c := range string(b) {
if c == '\x1B' {
// ANSI escape sequence
w.ansi = true
w.seqchanged = true
w.ansiseq += string(c)
} else if w.ansi {
w.ansiseq += string(c)
if (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) {
// ANSI sequence terminated
w.ansi = false
_, _ = w.Forward.Write([]byte(w.ansiseq))
if strings.HasSuffix(w.ansiseq, "[0m") {
// reset sequence
w.lastseq = ""
} else if strings.HasSuffix(w.ansiseq, "m") {
// color code
w.lastseq = w.ansiseq
}
w.ansiseq = ""
}
} else {
_, err := w.Forward.Write([]byte(string(c)))
if err != nil {
return 0, err
}
}
}
return len(b), nil
}
func (w *AnsiWriter) LastSequence() string {
return w.lastseq
}
func (w *AnsiWriter) ResetAnsi() {
if !w.seqchanged {
return
}
_, _ = w.Forward.Write([]byte("\x1b[0m"))
}
func (w *AnsiWriter) RestoreAnsi() {
_, _ = w.Forward.Write([]byte(w.lastseq))
}
+170
View File
@@ -0,0 +1,170 @@
package ansi
import (
"bytes"
"errors"
"fmt"
"io"
"os"
"strconv"
"text/template"
"github.com/logrusorgru/aurora"
"github.com/lucasb-eyer/go-colorful"
)
type BaseElement struct {
Token string
Prefix string
Suffix string
Style StylePrimitive
}
func color(c *string) (uint8, error) {
if c == nil || len(*c) == 0 {
return 0, errors.New("invalid color")
}
if (*c)[0] == '#' {
i, err := hexToANSIColor(*c)
return uint8(i), err
}
i, err := strconv.Atoi(*c)
return uint8(i), err
}
func colorSeq(fg *string, bg *string) (string, error) {
fc := ""
bc := ""
if fg != nil {
fc = *fg
}
if bg != nil {
bc = *bg
}
fs := ""
bs := ""
f, err := colorful.Hex(fc)
if err == nil {
fs = fmt.Sprintf("38;2;%d;%d;%d", uint8(f.R*255), uint8(f.G*255), uint8(f.B*255))
}
b, err := colorful.Hex(bc)
if err == nil {
bs = fmt.Sprintf("48;2;%d;%d;%d", uint8(b.R*255), uint8(b.G*255), uint8(b.B*255))
}
if len(fs) > 0 || len(bs) > 0 {
seq := "\x1b[" + fs
if len(fs) > 0 {
seq += ";"
}
return seq + bs + "m", nil
}
return "", errors.New("invalid color")
}
func formatToken(format string, token string) (string, error) {
var b bytes.Buffer
v := make(map[string]interface{})
v["text"] = token
tmpl, err := template.New(format).Funcs(TemplateFuncMap).Parse(format)
if err != nil {
return "", err
}
err = tmpl.Execute(&b, v)
return b.String(), err
}
func renderText(w io.Writer, rules StylePrimitive, s string) {
if len(s) == 0 {
return
}
truecolor := os.Getenv("COLORTERM") == "truecolor"
// FIXME: ugly true-color ANSI support hack
if truecolor {
seq, err := colorSeq(rules.Color, rules.BackgroundColor)
if err == nil {
s = seq + s
} else {
truecolor = false
}
}
out := aurora.Reset(s)
if !truecolor {
if rules.Color != nil {
i, err := color(rules.Color)
if err == nil {
out = out.Index(i)
}
}
if rules.BackgroundColor != nil {
i, err := color(rules.BackgroundColor)
if err == nil {
out = out.BgIndex(i)
}
}
}
if rules.Underline != nil && *rules.Underline {
out = out.Underline()
}
if rules.Bold != nil && *rules.Bold {
out = out.Bold()
}
if rules.Italic != nil && *rules.Italic {
out = out.Italic()
}
if rules.CrossedOut != nil && *rules.CrossedOut {
out = out.CrossedOut()
}
if rules.Overlined != nil && *rules.Overlined {
out = out.Overlined()
}
if rules.Inverse != nil && *rules.Inverse {
out = out.Reverse()
}
if rules.Blink != nil && *rules.Blink {
out = out.Blink()
}
_, _ = w.Write([]byte(out.String()))
}
func (e *BaseElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
renderText(w, bs.Current().Style.StylePrimitive, e.Prefix)
defer func() {
renderText(w, bs.Current().Style.StylePrimitive, e.Suffix)
}()
rules := bs.With(e.Style)
// render unstyled prefix/suffix
renderText(w, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
defer func() {
renderText(w, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
}()
// render styled prefix/suffix
renderText(w, rules, rules.Prefix)
defer func() {
renderText(w, rules, rules.Suffix)
}()
s := e.Token
if len(rules.Format) > 0 {
var err error
s, err = formatToken(rules.Format, s)
if err != nil {
return err
}
}
renderText(w, rules, s)
return nil
}
+56
View File
@@ -0,0 +1,56 @@
package ansi
import (
"bytes"
"io"
"github.com/muesli/reflow"
)
type BlockElement struct {
Block *bytes.Buffer
Style StyleBlock
Margin bool
Newline bool
}
func (e *BlockElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
bs.Push(*e)
renderText(w, bs.Parent().Style.StylePrimitive, e.Style.BlockPrefix)
renderText(bs.Current().Block, bs.Current().Style.StylePrimitive, e.Style.Prefix)
return nil
}
func (e *BlockElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
if e.Margin {
mw := NewMarginWriter(ctx, w, bs.Current().Style)
_, err := mw.Write(
reflow.Bytes(bs.Current().Block.Bytes(), int(bs.Width(ctx))))
if err != nil {
return err
}
if e.Newline {
_, err = mw.Write([]byte("\n"))
if err != nil {
return err
}
}
} else {
_, err := bs.Parent().Block.Write(bs.Current().Block.Bytes())
if err != nil {
return err
}
}
renderText(w, bs.Current().Style.StylePrimitive, e.Style.Suffix)
renderText(w, bs.Parent().Style.StylePrimitive, e.Style.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
+84
View File
@@ -0,0 +1,84 @@
package ansi
import (
"bytes"
)
type BlockStack []BlockElement
func (s *BlockStack) Len() int {
return len(*s)
}
func (s *BlockStack) Push(e BlockElement) {
*s = append(*s, e)
}
func (s *BlockStack) Pop() {
stack := *s
if len(stack) == 0 {
return
}
stack = stack[0 : len(stack)-1]
*s = stack
}
func (s BlockStack) Indent() uint {
var i uint
for _, v := range s {
if v.Style.Indent == nil {
continue
}
i += *v.Style.Indent
}
return i
}
func (s BlockStack) Margin() uint {
var i uint
for _, v := range s {
if v.Style.Margin == nil {
continue
}
i += *v.Style.Margin
}
return i
}
func (s BlockStack) Width(ctx RenderContext) uint {
if s.Indent()+s.Margin()*2 > uint(ctx.options.WordWrap) {
return 0
}
return uint(ctx.options.WordWrap) - s.Indent() - s.Margin()*2
}
func (s BlockStack) Parent() BlockElement {
if len(s) == 1 {
return BlockElement{
Block: &bytes.Buffer{},
}
}
return s[len(s)-2]
}
func (s BlockStack) Current() BlockElement {
if len(s) == 0 {
return BlockElement{
Block: &bytes.Buffer{},
}
}
return s[len(s)-1]
}
func (s BlockStack) With(child StylePrimitive) StylePrimitive {
sb := StyleBlock{}
sb.StylePrimitive = child
return cascadeStyle(s.Current().Style, sb, true).StylePrimitive
}
+52
View File
@@ -0,0 +1,52 @@
package ansi
import (
"io"
"github.com/alecthomas/chroma/quick"
)
type CodeBlockElement struct {
Code string
Language string
}
func (e *CodeBlockElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
var indent uint
var margin uint
rules := ctx.options.Styles.CodeBlock
if rules.Indent != nil {
indent = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
theme := rules.Theme
iw := &IndentWriter{
Indent: indent + margin,
IndentFunc: func(wr io.Writer) {
renderText(w, bs.Current().Style.StylePrimitive, " ")
},
Forward: &AnsiWriter{
Forward: w,
},
}
if len(theme) > 0 {
renderText(iw, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
err := quick.Highlight(iw, e.Code, e.Language, "terminal16m", theme)
renderText(iw, bs.Current().Style.StylePrimitive, rules.BlockSuffix)
return err
}
// fallback rendering
el := &BaseElement{
Token: e.Code,
Style: rules.StylePrimitive,
}
return el.Render(iw, ctx)
}
+35
View File
@@ -0,0 +1,35 @@
package ansi
import (
"html"
"strings"
"github.com/microcosm-cc/bluemonday"
)
type RenderContext struct {
options Options
blockStack *BlockStack
table *TableElement
stripper *bluemonday.Policy
}
func NewRenderContext(options Options) RenderContext {
return RenderContext{
options: options,
blockStack: &BlockStack{},
table: &TableElement{},
stripper: bluemonday.StrictPolicy(),
}
}
func (ctx RenderContext) SanitizeHTML(s string, trimSpaces bool) string {
s = ctx.stripper.Sanitize(s)
if trimSpaces {
s = strings.TrimSpace(s)
}
return html.UnescapeString(s)
}
+373
View File
@@ -0,0 +1,373 @@
package ansi
import (
"bytes"
"fmt"
"io"
"strings"
"github.com/yuin/goldmark/ast"
astext "github.com/yuin/goldmark/extension/ast"
)
type ElementRenderer interface {
Render(w io.Writer, ctx RenderContext) error
}
type ElementFinisher interface {
Finish(w io.Writer, ctx RenderContext) error
}
type Element struct {
Entering string
Exiting string
Renderer ElementRenderer
Finisher ElementFinisher
}
func (tr *ANSIRenderer) NewElement(node ast.Node, source []byte) Element {
ctx := tr.context
// fmt.Print(strings.Repeat(" ", ctx.blockStack.Len()), node.Type(), node.Kind())
// defer fmt.Println()
switch node.Kind() {
// Document
case ast.KindDocument:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: ctx.options.Styles.Document,
Margin: true,
}
return Element{
Renderer: e,
Finisher: e,
}
// Heading
case ast.KindHeading:
n := node.(*ast.Heading)
he := &HeadingElement{
Level: n.Level,
First: node.PreviousSibling() == nil,
}
return Element{
Exiting: "",
Renderer: he,
Finisher: he,
}
// Paragraph
case ast.KindParagraph:
if node.Parent() != nil && node.Parent().Kind() == ast.KindListItem {
return Element{}
}
return Element{
Renderer: &ParagraphElement{},
Finisher: &ParagraphElement{},
}
// Blockquote
case ast.KindBlockquote:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.BlockQuote, true),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
// Lists
case ast.KindList:
s := ctx.options.Styles.List.StyleBlock
if s.Indent == nil {
var i uint
s.Indent = &i
}
n := node.Parent()
for n != nil {
if n.Kind() == ast.KindList {
i := ctx.options.Styles.List.LevelIndent
s.Indent = &i
break
}
n = n.Parent()
}
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, s, true),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
case ast.KindListItem:
var l uint
var e uint
l = 1
n := node
for n.PreviousSibling() != nil && (n.PreviousSibling().Kind() == ast.KindListItem) {
l++
n = n.PreviousSibling()
}
if node.Parent().(*ast.List).IsOrdered() {
e = l
}
post := "\n"
if node.LastChild().Kind() == ast.KindList || node.NextSibling() == nil {
post = ""
}
if node.FirstChild().FirstChild().Kind() == astext.KindTaskCheckBox {
nc := node.FirstChild().FirstChild().(*astext.TaskCheckBox)
return Element{
Exiting: post,
Renderer: &TaskElement{
Checked: nc.IsChecked,
},
}
}
return Element{
Exiting: post,
Renderer: &ItemElement{
Enumeration: e,
},
}
// Text Elements
case ast.KindText:
n := node.(*ast.Text)
s := string(n.Segment.Value(source))
if n.HardLineBreak() || (n.SoftLineBreak()) {
s += "\n"
}
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(s, false),
Style: ctx.options.Styles.Text,
},
}
case ast.KindEmphasis:
n := node.(*ast.Emphasis)
s := string(n.Text(source))
style := ctx.options.Styles.Emph
if n.Level > 1 {
style = ctx.options.Styles.Strong
}
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(s, false),
Style: style,
},
}
case astext.KindStrikethrough:
n := node.(*astext.Strikethrough)
s := string(n.Text(source))
style := ctx.options.Styles.Strikethrough
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(s, false),
Style: style,
},
}
case ast.KindThematicBreak:
return Element{
Entering: "",
Exiting: "",
Renderer: &BaseElement{
Style: ctx.options.Styles.HorizontalRule,
},
}
// Links
case ast.KindLink:
n := node.(*ast.Link)
text := string(n.Text(source))
return Element{
Renderer: &LinkElement{
Text: text,
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
},
}
case ast.KindAutoLink:
n := node.(*ast.AutoLink)
u := string(n.URL(source))
label := string(n.Label(source))
if n.AutoLinkType == ast.AutoLinkEmail && !strings.HasPrefix(strings.ToLower(u), "mailto:") {
u = "mailto:" + u
}
return Element{
Renderer: &LinkElement{
Text: label,
BaseURL: ctx.options.BaseURL,
URL: u,
},
}
// Images
case ast.KindImage:
n := node.(*ast.Image)
text := string(n.Text(source))
return Element{
Renderer: &ImageElement{
Text: text,
BaseURL: ctx.options.BaseURL,
URL: string(n.Destination),
},
}
// Code
case ast.KindFencedCodeBlock:
n := node.(*ast.FencedCodeBlock)
l := n.Lines().Len()
s := ""
for i := 0; i < l; i++ {
line := n.Lines().At(i)
s += string(line.Value(source))
}
return Element{
Entering: "\n",
Renderer: &CodeBlockElement{
Code: s,
Language: string(n.Language(source)),
},
}
case ast.KindCodeBlock:
n := node.(*ast.CodeBlock)
l := n.Lines().Len()
s := ""
for i := 0; i < l; i++ {
line := n.Lines().At(i)
s += string(line.Value(source))
}
return Element{
Entering: "\n",
Renderer: &CodeBlockElement{
Code: s,
},
}
case ast.KindCodeSpan:
// n := node.(*ast.CodeSpan)
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.Code, true),
}
return Element{
Renderer: e,
Finisher: e,
}
// Tables
case astext.KindTable:
te := &TableElement{}
return Element{
Entering: "\n",
Renderer: te,
Finisher: te,
}
case astext.KindTableCell:
s := ""
n := node.FirstChild()
for n != nil {
s += string(n.Text(source))
// s += string(n.LinkData.Destination)
n = n.NextSibling()
}
return Element{
Renderer: &TableCellElement{
Text: s,
Head: node.Parent().Kind() == astext.KindTableHeader,
},
}
case astext.KindTableHeader:
return Element{
Finisher: &TableHeadElement{},
}
case astext.KindTableRow:
return Element{
Finisher: &TableRowElement{},
}
// HTML Elements
case ast.KindHTMLBlock:
n := node.(*ast.HTMLBlock)
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(string(n.Text(source)), true) + "\n",
Style: ctx.options.Styles.HTMLBlock.StylePrimitive,
},
}
case ast.KindRawHTML:
n := node.(*ast.RawHTML)
return Element{
Renderer: &BaseElement{
Token: ctx.SanitizeHTML(string(n.Text(source)), true) + "\n",
Style: ctx.options.Styles.HTMLSpan.StylePrimitive,
},
}
// Definition Lists
case astext.KindDefinitionList:
e := &BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(ctx.blockStack.Current().Style, ctx.options.Styles.DefinitionList, true),
Margin: true,
Newline: true,
}
return Element{
Entering: "\n",
Renderer: e,
Finisher: e,
}
case astext.KindDefinitionTerm:
return Element{
Renderer: &BaseElement{
Style: ctx.options.Styles.DefinitionTerm,
},
}
case astext.KindDefinitionDescription:
return Element{
Renderer: &BaseElement{
Style: ctx.options.Styles.DefinitionDescription,
},
}
// Handled by parents
case astext.KindTaskCheckBox:
// handled by KindListItem
return Element{}
case ast.KindTextBlock:
return Element{}
// Unknown case
default:
fmt.Println("Warning: unhandled element", node.Kind().String())
return Element{}
}
}
+90
View File
@@ -0,0 +1,90 @@
package ansi
import (
"bytes"
"io"
"github.com/muesli/reflow"
)
type HeadingElement struct {
Level int
First bool
}
func (e *HeadingElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := ctx.options.Styles.Heading
switch e.Level {
case 1:
rules = cascadeStyles(false, rules, ctx.options.Styles.H1)
case 2:
rules = cascadeStyles(false, rules, ctx.options.Styles.H2)
case 3:
rules = cascadeStyles(false, rules, ctx.options.Styles.H3)
case 4:
rules = cascadeStyles(false, rules, ctx.options.Styles.H4)
case 5:
rules = cascadeStyles(false, rules, ctx.options.Styles.H5)
case 6:
rules = cascadeStyles(false, rules, ctx.options.Styles.H6)
}
if !e.First {
renderText(w, bs.Current().Style.StylePrimitive, "\n")
}
be := BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(bs.Current().Style, rules, true),
}
bs.Push(be)
renderText(w, bs.Parent().Style.StylePrimitive, rules.BlockPrefix)
renderText(bs.Current().Block, bs.Current().Style.StylePrimitive, rules.Prefix)
return nil
}
func (e *HeadingElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := bs.Current().Style
var indent uint
var margin uint
if rules.Indent != nil {
indent = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
iw := &IndentWriter{
Indent: indent + margin,
IndentFunc: func(wr io.Writer) {
renderText(w, bs.Parent().Style.StylePrimitive, " ")
},
Forward: &AnsiWriter{
Forward: w,
},
}
flow := reflow.NewReflow(int(bs.Width(ctx) - indent - margin*2))
_, err := flow.Write(bs.Current().Block.Bytes())
if err != nil {
return err
}
flow.Close()
_, err = iw.Write(flow.Bytes())
if err != nil {
return err
}
renderText(w, bs.Current().Style.StylePrimitive, rules.Suffix)
renderText(w, bs.Parent().Style.StylePrimitive, rules.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
+38
View File
@@ -0,0 +1,38 @@
package ansi
import (
"io"
)
type ImageElement struct {
Text string
BaseURL string
URL string
Child ElementRenderer // FIXME
}
func (e *ImageElement) Render(w io.Writer, ctx RenderContext) error {
if len(e.Text) > 0 {
el := &BaseElement{
Token: e.Text,
Style: ctx.options.Styles.ImageText,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
if len(e.URL) > 0 {
el := &BaseElement{
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: " ",
Style: ctx.options.Styles.Image,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
return nil
}
+61
View File
@@ -0,0 +1,61 @@
package ansi
import (
"io"
"strings"
)
type IndentFunc = func(w io.Writer)
type IndentWriter struct {
Forward *AnsiWriter
Indent uint
IndentFunc IndentFunc
skipIndent bool
ansi bool
}
// Write is used to write content to the indent buffer.
func (w *IndentWriter) Write(b []byte) (int, error) {
for _, c := range string(b) {
if c == '\x1B' {
// ANSI escape sequence
w.ansi = true
} else if w.ansi {
if (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) {
// ANSI sequence terminated
w.ansi = false
}
} else {
if !w.skipIndent {
w.Forward.ResetAnsi()
if w.IndentFunc != nil {
for i := 0; i < int(w.Indent); i++ {
w.IndentFunc(w.Forward)
}
} else {
_, err := w.Forward.Write([]byte(strings.Repeat(" ", int(w.Indent))))
if err != nil {
return 0, err
}
}
w.skipIndent = true
w.Forward.RestoreAnsi()
}
if c == '\n' {
// end of current line
w.skipIndent = false
}
}
_, err := w.Forward.Write([]byte(string(c)))
if err != nil {
return 0, err
}
}
return len(b), nil
}
+75
View File
@@ -0,0 +1,75 @@
package ansi
import (
"io"
)
type LinkElement struct {
Text string
BaseURL string
URL string
Child ElementRenderer // FIXME
}
func (e *LinkElement) Render(w io.Writer, ctx RenderContext) error {
var textRendered bool
if len(e.Text) > 0 &&
e.Text != e.URL {
textRendered = true
el := &BaseElement{
Token: e.Text,
Style: ctx.options.Styles.LinkText,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
/*
if node.LastChild != nil {
if node.LastChild.Type == bf.Image {
el := tr.NewElement(node.LastChild)
err := el.Renderer.Render(w, node.LastChild, tr)
if err != nil {
return err
}
}
if len(node.LastChild.Literal) > 0 &&
string(node.LastChild.Literal) != string(node.LinkData.Destination) {
textRendered = true
el := &BaseElement{
Token: string(node.LastChild.Literal),
Style: ctx.style[LinkText],
}
err := el.Render(w, node.LastChild, tr)
if err != nil {
return err
}
}
}
*/
if len(e.URL) > 0 {
pre := " "
style := ctx.options.Styles.Link
if !textRendered {
pre = ""
style.BlockPrefix = ""
style.BlockSuffix = ""
}
el := &BaseElement{
Token: resolveRelativeURL(e.BaseURL, e.URL),
Prefix: pre,
Style: style,
}
err := el.Render(w, ctx)
if err != nil {
return err
}
}
return nil
}
+26
View File
@@ -0,0 +1,26 @@
package ansi
import (
"io"
"strconv"
)
type ItemElement struct {
Enumeration uint
}
func (e *ItemElement) Render(w io.Writer, ctx RenderContext) error {
var el *BaseElement
if e.Enumeration > 0 {
el = &BaseElement{
Style: ctx.options.Styles.Enumeration,
Prefix: strconv.FormatInt(int64(e.Enumeration), 10),
}
} else {
el = &BaseElement{
Style: ctx.options.Styles.Item,
}
}
return el.Render(w, ctx)
}
+53
View File
@@ -0,0 +1,53 @@
package ansi
import (
"io"
)
type MarginWriter struct {
w io.Writer
pw *PaddingWriter
iw *IndentWriter
}
func NewMarginWriter(ctx RenderContext, w io.Writer, rules StyleBlock) *MarginWriter {
bs := ctx.blockStack
var indent uint
var margin uint
if rules.Indent != nil {
indent = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
pw := &PaddingWriter{
Padding: bs.Width(ctx),
PadFunc: func(wr io.Writer) {
renderText(w, rules.StylePrimitive, " ")
},
Forward: &AnsiWriter{
Forward: w,
},
}
iw := &IndentWriter{
Indent: indent + margin,
IndentFunc: func(wr io.Writer) {
renderText(w, bs.Parent().Style.StylePrimitive, " ")
},
Forward: &AnsiWriter{
Forward: pw,
},
}
return &MarginWriter{
w: w,
pw: pw,
iw: iw,
}
}
func (w *MarginWriter) Write(b []byte) (int, error) {
return w.iw.Write(b)
}
+53
View File
@@ -0,0 +1,53 @@
package ansi
import (
"io"
"github.com/mattn/go-runewidth"
)
type PaddingFunc = func(w io.Writer)
type PaddingWriter struct {
Forward *AnsiWriter
Padding uint
PadFunc PaddingFunc
lineLen int
ansi bool
}
// Write is used to write content to the padding buffer.
func (w *PaddingWriter) Write(b []byte) (int, error) {
for _, c := range string(b) {
if c == '\x1B' {
// ANSI escape sequence
w.ansi = true
} else if w.ansi {
if (c >= 0x41 && c <= 0x5a) || (c >= 0x61 && c <= 0x7a) {
// ANSI sequence terminated
w.ansi = false
}
} else {
w.lineLen += runewidth.StringWidth(string(c))
if c == '\n' {
// end of current line
if w.Padding > 0 && uint(w.lineLen) < w.Padding {
for i := 0; i < int(w.Padding)-w.lineLen; i++ {
w.PadFunc(w.Forward)
}
}
w.Forward.ResetAnsi()
w.lineLen = 0
}
}
_, err := w.Forward.Write([]byte(string(c)))
if err != nil {
return 0, err
}
}
return len(b), nil
}
+54
View File
@@ -0,0 +1,54 @@
package ansi
import (
"bytes"
"io"
"strings"
"github.com/muesli/reflow"
)
type ParagraphElement struct {
}
func (e *ParagraphElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := ctx.options.Styles.Paragraph
_, _ = w.Write([]byte("\n"))
be := BlockElement{
Block: &bytes.Buffer{},
Style: cascadeStyle(bs.Current().Style, rules, true),
}
bs.Push(be)
renderText(w, bs.Parent().Style.StylePrimitive, rules.BlockPrefix)
renderText(bs.Current().Block, bs.Current().Style.StylePrimitive, rules.Prefix)
return nil
}
func (e *ParagraphElement) Finish(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
rules := bs.Current().Style
mw := NewMarginWriter(ctx, w, rules)
if len(strings.TrimSpace(bs.Current().Block.String())) > 0 {
flow := reflow.NewReflow(int(bs.Width(ctx)))
flow.KeepNewlines = false
_, _ = flow.Write(bs.Current().Block.Bytes())
flow.Close()
_, err := mw.Write(flow.Bytes())
if err != nil {
return err
}
_, _ = mw.Write([]byte("\n"))
}
renderText(w, bs.Current().Style.StylePrimitive, rules.Suffix)
renderText(w, bs.Parent().Style.StylePrimitive, rules.BlockSuffix)
bs.Current().Block.Reset()
bs.Pop()
return nil
}
+158
View File
@@ -0,0 +1,158 @@
package ansi
import (
"io"
"log"
"net/url"
"strings"
"github.com/yuin/goldmark/ast"
astext "github.com/yuin/goldmark/extension/ast"
"github.com/yuin/goldmark/renderer"
"github.com/yuin/goldmark/util"
)
type Options struct {
BaseURL string
WordWrap int
Styles StyleConfig
}
type ANSIRenderer struct {
context RenderContext
}
// NewANSIRenderer returns a new ANSIRenderer with style and options set.
func NewRenderer(options Options) *ANSIRenderer {
return &ANSIRenderer{
context: NewRenderContext(options),
}
}
// RegisterFuncs implements NodeRenderer.RegisterFuncs.
func (r *ANSIRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
// blocks
reg.Register(ast.KindDocument, r.renderNode)
reg.Register(ast.KindHeading, r.renderNode)
reg.Register(ast.KindBlockquote, r.renderNode)
reg.Register(ast.KindCodeBlock, r.renderNode)
reg.Register(ast.KindFencedCodeBlock, r.renderNode)
reg.Register(ast.KindHTMLBlock, r.renderNode)
reg.Register(ast.KindList, r.renderNode)
reg.Register(ast.KindListItem, r.renderNode)
reg.Register(ast.KindParagraph, r.renderNode)
reg.Register(ast.KindTextBlock, r.renderNode)
reg.Register(ast.KindThematicBreak, r.renderNode)
// inlines
reg.Register(ast.KindAutoLink, r.renderNode)
reg.Register(ast.KindCodeSpan, r.renderNode)
reg.Register(ast.KindEmphasis, r.renderNode)
reg.Register(ast.KindImage, r.renderNode)
reg.Register(ast.KindLink, r.renderNode)
reg.Register(ast.KindRawHTML, r.renderNode)
reg.Register(ast.KindText, r.renderNode)
reg.Register(ast.KindString, r.renderNode)
// tables
reg.Register(astext.KindTable, r.renderNode)
reg.Register(astext.KindTableHeader, r.renderNode)
reg.Register(astext.KindTableRow, r.renderNode)
reg.Register(astext.KindTableCell, r.renderNode)
// definitions
reg.Register(astext.KindDefinitionList, r.renderNode)
reg.Register(astext.KindDefinitionTerm, r.renderNode)
reg.Register(astext.KindDefinitionDescription, r.renderNode)
// footnotes
reg.Register(astext.KindFootnote, r.renderNode)
reg.Register(astext.KindFootnoteList, r.renderNode)
reg.Register(astext.KindFootnoteLink, r.renderNode)
reg.Register(astext.KindFootnoteBackLink, r.renderNode)
// checkboxes
reg.Register(astext.KindTaskCheckBox, r.renderNode)
// strikethrough
reg.Register(astext.KindStrikethrough, r.renderNode)
}
func (tr *ANSIRenderer) renderNode(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
// _, _ = w.Write([]byte(node.Type.String()))
writeTo := io.Writer(w)
bs := tr.context.blockStack
// children get rendered by their parent
if isChild(node) {
return ast.WalkContinue, nil
}
e := tr.NewElement(node, source)
if entering {
// everything below the Document element gets rendered into a block buffer
if bs.Len() > 0 {
writeTo = io.Writer(bs.Current().Block)
}
_, _ = writeTo.Write([]byte(e.Entering))
if e.Renderer != nil {
err := e.Renderer.Render(writeTo, tr.context)
if err != nil {
return ast.WalkStop, err
}
}
} else {
// everything below the Document element gets rendered into a block buffer
if bs.Len() > 0 {
writeTo = io.Writer(bs.Parent().Block)
}
// if we're finished rendering the entire document,
// flush to the real writer
if node.Type() == ast.TypeDocument {
writeTo = w
}
if e.Finisher != nil {
err := e.Finisher.Finish(writeTo, tr.context)
if err != nil {
return ast.WalkStop, err
}
}
_, _ = bs.Current().Block.Write([]byte(e.Exiting))
}
return ast.WalkContinue, nil
}
func isChild(node ast.Node) bool {
if node.Parent() == nil {
return false
}
// These types are already rendered by their parent
switch node.Parent().Kind() {
case ast.KindLink, ast.KindImage, ast.KindEmphasis, astext.KindStrikethrough, ast.KindBlockquote, astext.KindTableCell:
return true
default:
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
}
u.Path = strings.TrimPrefix(u.Path, "/")
base, err := url.Parse(baseURL)
if err != nil {
return rel
}
return base.ResolveReference(u).String()
}
+231
View File
@@ -0,0 +1,231 @@
package ansi
import (
"github.com/lucasb-eyer/go-colorful"
)
type StylePrimitive struct {
BlockPrefix string `json:"block_prefix"`
BlockSuffix string `json:"block_suffix"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
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"`
Format string `json:"format"`
}
type StyleTask struct {
StyleBlock
Ticked string `json:"ticked"`
Unticked string `json:"unticked"`
}
type StyleBlock struct {
StylePrimitive
Indent *uint `json:"indent"`
Margin *uint `json:"margin"`
}
type StyleCodeBlock struct {
StyleBlock
Theme string `json:"theme"`
}
type StyleList struct {
StyleBlock
LevelIndent uint `json:"level_indent"`
}
type StyleConfig struct {
Document StyleBlock `json:"document"`
BlockQuote StyleBlock `json:"block_quote"`
Paragraph StyleBlock `json:"paragraph"`
List StyleList `json:"list"`
Heading StyleBlock `json:"heading"`
H1 StyleBlock `json:"h1"`
H2 StyleBlock `json:"h2"`
H3 StyleBlock `json:"h3"`
H4 StyleBlock `json:"h4"`
H5 StyleBlock `json:"h5"`
H6 StyleBlock `json:"h6"`
Text StylePrimitive `json:"text"`
Strikethrough StylePrimitive `json:"strike_through"`
Emph StylePrimitive `json:"emph"`
Strong StylePrimitive `json:"strong"`
HorizontalRule StylePrimitive `json:"hr"`
Item StylePrimitive `json:"item"`
Enumeration StylePrimitive `json:"enumeration"`
Task StyleTask `json:"task"`
Link StylePrimitive `json:"link"`
LinkText StylePrimitive `json:"link_text"`
Image StylePrimitive `json:"image"`
ImageText StylePrimitive `json:"image_text"`
Code StyleBlock `json:"code"`
CodeBlock StyleCodeBlock `json:"code_block"`
Table StyleBlock `json:"table"`
DefinitionList StyleBlock `json:"definition_list"`
DefinitionTerm StylePrimitive `json:"definition_term"`
DefinitionDescription StylePrimitive `json:"definition_description"`
HTMLBlock StyleBlock `json:"html_block"`
HTMLSpan StyleBlock `json:"html_span"`
}
func cascadeStyles(onlyColors bool, s ...StyleBlock) StyleBlock {
var r StyleBlock
for _, v := range s {
r = cascadeStyle(r, v, onlyColors)
}
return r
}
func cascadeStyle(parent StyleBlock, child StyleBlock, onlyColors bool) StyleBlock {
s := child
s.Color = parent.Color
s.BackgroundColor = parent.BackgroundColor
if !onlyColors {
s.Indent = parent.Indent
s.Margin = parent.Margin
s.Underline = parent.Underline
s.Bold = parent.Bold
s.Italic = parent.Italic
s.CrossedOut = parent.CrossedOut
s.Faint = parent.Faint
s.Conceal = parent.Conceal
s.Overlined = parent.Overlined
s.Inverse = parent.Inverse
s.Blink = parent.Blink
s.BlockPrefix = parent.BlockPrefix
s.BlockSuffix = parent.BlockSuffix
s.Prefix = parent.Prefix
s.Suffix = parent.Suffix
s.Format = parent.Format
}
if child.Color != nil {
s.Color = child.Color
}
if child.BackgroundColor != nil {
s.BackgroundColor = child.BackgroundColor
}
if child.Indent != nil {
s.Indent = child.Indent
}
if child.Margin != nil {
s.Margin = child.Margin
}
if child.Underline != nil {
s.Underline = child.Underline
}
if child.Bold != nil {
s.Bold = child.Bold
}
if child.Italic != nil {
s.Italic = child.Italic
}
if child.CrossedOut != nil {
s.CrossedOut = child.CrossedOut
}
if child.Faint != nil {
s.Faint = child.Faint
}
if child.Conceal != nil {
s.Conceal = child.Conceal
}
if child.Overlined != nil {
s.Overlined = child.Overlined
}
if child.Inverse != nil {
s.Inverse = child.Inverse
}
if child.Blink != nil {
s.Blink = child.Blink
}
if child.BlockPrefix != "" {
s.BlockPrefix = child.BlockPrefix
}
if child.BlockSuffix != "" {
s.BlockSuffix = child.BlockSuffix
}
if child.Prefix != "" {
s.Prefix = child.Prefix
}
if child.Suffix != "" {
s.Suffix = child.Suffix
}
if child.Format != "" {
s.Format = child.Format
}
return s
}
func hexToANSIColor(h string) (int, error) {
c, err := colorful.Hex(h)
if err != nil {
return 0, err
}
v2ci := func(v float64) int {
if v < 48 {
return 0
}
if v < 115 {
return 1
}
return int((v - 35) / 40)
}
// Calculate the nearest 0-based color index at 16..231
r := v2ci(c.R * 255.0) // 0..5 each
g := v2ci(c.G * 255.0)
b := v2ci(c.B * 255.0)
ci := 36*r + 6*g + b /* 0..215 */
// Calculate the represented colors back from the index
i2cv := [6]int{0, 0x5f, 0x87, 0xaf, 0xd7, 0xff}
cr := i2cv[r] // r/g/b, 0..255 each
cg := i2cv[g]
cb := i2cv[b]
// Calculate the nearest 0-based gray index at 232..255
var grayIdx int
average := (r + g + b) / 3
if average > 238 {
grayIdx = 23
} else {
grayIdx = (average - 3) / 10 // 0..23
}
gv := 8 + 10*grayIdx // same value for r/g/b, 0..255
// Return the one which is nearer to the original input rgb value
c2 := colorful.Color{R: float64(cr) / 255.0, G: float64(cg) / 255.0, B: float64(cb) / 255.0}
g2 := colorful.Color{R: float64(gv) / 255.0, G: float64(gv) / 255.0, B: float64(gv) / 255.0}
colorDist := c.DistanceLab(c2)
grayDist := c.DistanceLab(g2)
if colorDist <= grayDist {
return 16 + ci, nil
}
return 232 + grayIdx, nil
}
+84
View File
@@ -0,0 +1,84 @@
package ansi
import (
"io"
"github.com/olekukonko/tablewriter"
)
type TableElement struct {
writer *tablewriter.Table
indentWriter io.Writer
header []string
cell []string
}
type TableRowElement struct {
}
type TableHeadElement struct {
}
type TableCellElement struct {
Text string
Head bool
}
func (e *TableElement) Render(w io.Writer, ctx RenderContext) error {
bs := ctx.blockStack
var indent uint
var margin uint
rules := ctx.options.Styles.Table
if rules.Indent != nil {
indent = *rules.Indent
}
if rules.Margin != nil {
margin = *rules.Margin
}
ctx.table.indentWriter = &IndentWriter{
Indent: indent + margin,
IndentFunc: func(wr io.Writer) {
renderText(w, bs.Current().Style.StylePrimitive, " ")
},
Forward: &AnsiWriter{
Forward: w,
},
}
renderText(ctx.table.indentWriter, bs.Current().Style.StylePrimitive, rules.BlockPrefix)
ctx.table.writer = tablewriter.NewWriter(ctx.table.indentWriter)
return nil
}
func (e *TableElement) Finish(w io.Writer, ctx RenderContext) error {
ctx.table.writer.Render()
ctx.table.writer = nil
rules := ctx.options.Styles.Table
renderText(ctx.table.indentWriter, ctx.blockStack.Current().Style.StylePrimitive, rules.BlockSuffix)
return nil
}
func (e *TableRowElement) Finish(w io.Writer, ctx RenderContext) error {
ctx.table.writer.Append(ctx.table.cell)
ctx.table.cell = []string{}
return nil
}
func (e *TableHeadElement) Finish(w io.Writer, ctx RenderContext) error {
ctx.table.writer.SetHeader(ctx.table.header)
ctx.table.header = []string{}
return nil
}
func (e *TableCellElement) Render(w io.Writer, ctx RenderContext) error {
if e.Head {
ctx.table.header = append(ctx.table.header, e.Text)
} else {
ctx.table.cell = append(ctx.table.cell, e.Text)
}
return nil
}
+25
View File
@@ -0,0 +1,25 @@
package ansi
import (
"io"
)
type TaskElement struct {
Checked bool
}
func (e *TaskElement) Render(w io.Writer, ctx RenderContext) error {
var el *BaseElement
pre := ctx.options.Styles.Task.Unticked
if e.Checked {
pre = ctx.options.Styles.Task.Ticked
}
el = &BaseElement{
Prefix: pre,
Style: ctx.options.Styles.Task.StylePrimitive,
}
return el.Render(w, ctx)
}
+83
View File
@@ -0,0 +1,83 @@
package ansi
import (
"regexp"
"strings"
"text/template"
)
// TemplateFuncMap contains a few useful template helpers
var (
TemplateFuncMap = template.FuncMap{
"Left": func(values ...interface{}) string {
s := values[0].(string)
n := values[1].(int)
if n > len(s) {
n = len(s)
}
return s[:n]
},
"Matches": func(values ...interface{}) bool {
ok, _ := regexp.MatchString(values[1].(string), values[0].(string))
return ok
},
"Mid": func(values ...interface{}) string {
s := values[0].(string)
l := values[1].(int)
if l > len(s) {
l = len(s)
}
if len(values) > 2 {
r := values[2].(int)
if r > len(s) {
r = len(s)
}
return s[l:r]
}
return s[l:]
},
"Right": func(values ...interface{}) string {
s := values[0].(string)
n := len(s) - values[1].(int)
if n < 0 {
n = 0
}
return s[n:]
},
"Last": func(values ...interface{}) string {
return values[0].([]string)[len(values[0].([]string))-1]
},
// strings functions
"Compare": strings.Compare, // 1.5+ only
"Contains": strings.Contains,
"ContainsAny": strings.ContainsAny,
"Count": strings.Count,
"EqualFold": strings.EqualFold,
"HasPrefix": strings.HasPrefix,
"HasSuffix": strings.HasSuffix,
"Index": strings.Index,
"IndexAny": strings.IndexAny,
"Join": strings.Join,
"LastIndex": strings.LastIndex,
"LastIndexAny": strings.LastIndexAny,
"Repeat": strings.Repeat,
"Replace": strings.Replace,
"Split": strings.Split,
"SplitAfter": strings.SplitAfter,
"SplitAfterN": strings.SplitAfterN,
"SplitN": strings.SplitN,
"Title": strings.Title,
"ToLower": strings.ToLower,
"ToTitle": strings.ToTitle,
"ToUpper": strings.ToUpper,
"Trim": strings.Trim,
"TrimLeft": strings.TrimLeft,
"TrimPrefix": strings.TrimPrefix,
"TrimRight": strings.TrimRight,
"TrimSpace": strings.TrimSpace,
"TrimSuffix": strings.TrimSuffix,
}
)