mirror of
https://github.com/rwinkhart/glamour-temp-MUTN.git
synced 2026-09-05 16:37:19 -04:00
Move ANSIRenderer into a separate package
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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{}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user