Added formatters
This commit is contained in:
parent
f9f64b0558
commit
02da7f1fdc
10 changed files with 1766 additions and 0 deletions
57
formatters/api.go
Normal file
57
formatters/api.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2/formatters/html"
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2/formatters/svg"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// NoOp formatter.
|
||||||
|
NoOp = Register("noop", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, iterator chroma.Iterator) error {
|
||||||
|
for t := iterator(); t != chroma.EOF; t = iterator() {
|
||||||
|
if _, err := io.WriteString(w, t.Value); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
||||||
|
// Default HTML formatter outputs self-contained HTML.
|
||||||
|
htmlFull = Register("html", html.New(html.Standalone(true), html.WithClasses(true))) // nolint
|
||||||
|
SVG = Register("svg", svg.New(svg.EmbedFont("Liberation Mono", svg.FontLiberationMono, svg.WOFF)))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fallback formatter.
|
||||||
|
var Fallback = NoOp
|
||||||
|
|
||||||
|
// Registry of Formatters.
|
||||||
|
var Registry = map[string]chroma.Formatter{}
|
||||||
|
|
||||||
|
// Names of registered formatters.
|
||||||
|
func Names() []string {
|
||||||
|
out := []string{}
|
||||||
|
for name := range Registry {
|
||||||
|
out = append(out, name)
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get formatter by name.
|
||||||
|
//
|
||||||
|
// If the given formatter is not found, the Fallback formatter will be returned.
|
||||||
|
func Get(name string) chroma.Formatter {
|
||||||
|
if f, ok := Registry[name]; ok {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
return Fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a named formatter.
|
||||||
|
func Register(name string, formatter chroma.Formatter) chroma.Formatter {
|
||||||
|
Registry[name] = formatter
|
||||||
|
return formatter
|
||||||
|
}
|
633
formatters/html/html.go
Normal file
633
formatters/html/html.go
Normal file
|
@ -0,0 +1,633 @@
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"html"
|
||||||
|
"io"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option sets an option of the HTML formatter.
|
||||||
|
type Option func(f *Formatter)
|
||||||
|
|
||||||
|
// Standalone configures the HTML formatter for generating a standalone HTML document.
|
||||||
|
func Standalone(b bool) Option { return func(f *Formatter) { f.standalone = b } }
|
||||||
|
|
||||||
|
// ClassPrefix sets the CSS class prefix.
|
||||||
|
func ClassPrefix(prefix string) Option { return func(f *Formatter) { f.prefix = prefix } }
|
||||||
|
|
||||||
|
// WithClasses emits HTML using CSS classes, rather than inline styles.
|
||||||
|
func WithClasses(b bool) Option { return func(f *Formatter) { f.Classes = b } }
|
||||||
|
|
||||||
|
// WithAllClasses disables an optimisation that omits redundant CSS classes.
|
||||||
|
func WithAllClasses(b bool) Option { return func(f *Formatter) { f.allClasses = b } }
|
||||||
|
|
||||||
|
// WithCustomCSS sets user's custom CSS styles.
|
||||||
|
func WithCustomCSS(css map[chroma.TokenType]string) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.customCSS = css
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TabWidth sets the number of characters for a tab. Defaults to 8.
|
||||||
|
func TabWidth(width int) Option { return func(f *Formatter) { f.tabWidth = width } }
|
||||||
|
|
||||||
|
// PreventSurroundingPre prevents the surrounding pre tags around the generated code.
|
||||||
|
func PreventSurroundingPre(b bool) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.preventSurroundingPre = b
|
||||||
|
|
||||||
|
if b {
|
||||||
|
f.preWrapper = nopPreWrapper
|
||||||
|
} else {
|
||||||
|
f.preWrapper = defaultPreWrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithNopPreWrapper() Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.preWrapper = nopPreWrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InlineCode creates inline code wrapped in a code tag.
|
||||||
|
func InlineCode(b bool) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.inlineCode = b
|
||||||
|
f.preWrapper = preWrapper{
|
||||||
|
start: func(code bool, styleAttr string) string {
|
||||||
|
if code {
|
||||||
|
return fmt.Sprintf(`<code%s>`, styleAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ``
|
||||||
|
},
|
||||||
|
end: func(code bool) string {
|
||||||
|
if code {
|
||||||
|
return `</code>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ``
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPreWrapper allows control of the surrounding pre tags.
|
||||||
|
func WithPreWrapper(wrapper PreWrapper) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.preWrapper = wrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WrapLongLines wraps long lines.
|
||||||
|
func WrapLongLines(b bool) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.wrapLongLines = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLineNumbers formats output with line numbers.
|
||||||
|
func WithLineNumbers(b bool) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.lineNumbers = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LineNumbersInTable will, when combined with WithLineNumbers, separate the line numbers
|
||||||
|
// and code in table td's, which make them copy-and-paste friendly.
|
||||||
|
func LineNumbersInTable(b bool) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.lineNumbersInTable = b
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithLinkableLineNumbers decorates the line numbers HTML elements with an "id"
|
||||||
|
// attribute so they can be linked.
|
||||||
|
func WithLinkableLineNumbers(b bool, prefix string) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.linkableLineNumbers = b
|
||||||
|
f.lineNumbersIDPrefix = prefix
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HighlightLines higlights the given line ranges with the Highlight style.
|
||||||
|
//
|
||||||
|
// A range is the beginning and ending of a range as 1-based line numbers, inclusive.
|
||||||
|
func HighlightLines(ranges [][2]int) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.highlightRanges = ranges
|
||||||
|
sort.Sort(f.highlightRanges)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseLineNumber sets the initial number to start line numbering at. Defaults to 1.
|
||||||
|
func BaseLineNumber(n int) Option {
|
||||||
|
return func(f *Formatter) {
|
||||||
|
f.baseLineNumber = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// New HTML formatter.
|
||||||
|
func New(options ...Option) *Formatter {
|
||||||
|
f := &Formatter{
|
||||||
|
baseLineNumber: 1,
|
||||||
|
preWrapper: defaultPreWrapper,
|
||||||
|
}
|
||||||
|
f.styleCache = newStyleCache(f)
|
||||||
|
for _, option := range options {
|
||||||
|
option(f)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreWrapper defines the operations supported in WithPreWrapper.
|
||||||
|
type PreWrapper interface {
|
||||||
|
// Start is called to write a start <pre> element.
|
||||||
|
// The code flag tells whether this block surrounds
|
||||||
|
// highlighted code. This will be false when surrounding
|
||||||
|
// line numbers.
|
||||||
|
Start(code bool, styleAttr string) string
|
||||||
|
|
||||||
|
// End is called to write the end </pre> element.
|
||||||
|
End(code bool) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type preWrapper struct {
|
||||||
|
start func(code bool, styleAttr string) string
|
||||||
|
end func(code bool) string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p preWrapper) Start(code bool, styleAttr string) string {
|
||||||
|
return p.start(code, styleAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p preWrapper) End(code bool) string {
|
||||||
|
return p.end(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
nopPreWrapper = preWrapper{
|
||||||
|
start: func(code bool, styleAttr string) string { return "" },
|
||||||
|
end: func(code bool) string { return "" },
|
||||||
|
}
|
||||||
|
defaultPreWrapper = preWrapper{
|
||||||
|
start: func(code bool, styleAttr string) string {
|
||||||
|
if code {
|
||||||
|
return fmt.Sprintf(`<pre%s><code>`, styleAttr)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(`<pre%s>`, styleAttr)
|
||||||
|
},
|
||||||
|
end: func(code bool) string {
|
||||||
|
if code {
|
||||||
|
return `</code></pre>`
|
||||||
|
}
|
||||||
|
|
||||||
|
return `</pre>`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Formatter that generates HTML.
|
||||||
|
type Formatter struct {
|
||||||
|
styleCache *styleCache
|
||||||
|
standalone bool
|
||||||
|
prefix string
|
||||||
|
Classes bool // Exported field to detect when classes are being used
|
||||||
|
allClasses bool
|
||||||
|
customCSS map[chroma.TokenType]string
|
||||||
|
preWrapper PreWrapper
|
||||||
|
inlineCode bool
|
||||||
|
preventSurroundingPre bool
|
||||||
|
tabWidth int
|
||||||
|
wrapLongLines bool
|
||||||
|
lineNumbers bool
|
||||||
|
lineNumbersInTable bool
|
||||||
|
linkableLineNumbers bool
|
||||||
|
lineNumbersIDPrefix string
|
||||||
|
highlightRanges highlightRanges
|
||||||
|
baseLineNumber int
|
||||||
|
}
|
||||||
|
|
||||||
|
type highlightRanges [][2]int
|
||||||
|
|
||||||
|
func (h highlightRanges) Len() int { return len(h) }
|
||||||
|
func (h highlightRanges) Swap(i, j int) { h[i], h[j] = h[j], h[i] }
|
||||||
|
func (h highlightRanges) Less(i, j int) bool { return h[i][0] < h[j][0] }
|
||||||
|
|
||||||
|
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
|
||||||
|
return f.writeHTML(w, style, iterator.Tokens())
|
||||||
|
}
|
||||||
|
|
||||||
|
// We deliberately don't use html/template here because it is two orders of magnitude slower (benchmarked).
|
||||||
|
//
|
||||||
|
// OTOH we need to be super careful about correct escaping...
|
||||||
|
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []chroma.Token) (err error) { // nolint: gocyclo
|
||||||
|
css := f.styleCache.get(style, true)
|
||||||
|
if f.standalone {
|
||||||
|
fmt.Fprint(w, "<html>\n")
|
||||||
|
if f.Classes {
|
||||||
|
fmt.Fprint(w, "<style type=\"text/css\">\n")
|
||||||
|
err = f.WriteCSS(w, style)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "body { %s; }\n", css[chroma.Background])
|
||||||
|
fmt.Fprint(w, "</style>")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "<body%s>\n", f.styleAttr(css, chroma.Background))
|
||||||
|
}
|
||||||
|
|
||||||
|
wrapInTable := f.lineNumbers && f.lineNumbersInTable
|
||||||
|
|
||||||
|
lines := chroma.SplitTokensIntoLines(tokens)
|
||||||
|
lineDigits := len(strconv.Itoa(f.baseLineNumber + len(lines) - 1))
|
||||||
|
highlightIndex := 0
|
||||||
|
|
||||||
|
if wrapInTable {
|
||||||
|
// List line numbers in its own <td>
|
||||||
|
fmt.Fprintf(w, "<div%s>\n", f.styleAttr(css, chroma.PreWrapper))
|
||||||
|
fmt.Fprintf(w, "<table%s><tr>", f.styleAttr(css, chroma.LineTable))
|
||||||
|
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD))
|
||||||
|
fmt.Fprintf(w, "%s", f.preWrapper.Start(false, f.styleAttr(css, chroma.PreWrapper)))
|
||||||
|
for index := range lines {
|
||||||
|
line := f.baseLineNumber + index
|
||||||
|
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||||
|
if next {
|
||||||
|
highlightIndex++
|
||||||
|
}
|
||||||
|
if highlight {
|
||||||
|
fmt.Fprintf(w, "<span%s>", f.styleAttr(css, chroma.LineHighlight))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "<span%s%s>%s\n</span>", f.styleAttr(css, chroma.LineNumbersTable), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
|
||||||
|
|
||||||
|
if highlight {
|
||||||
|
fmt.Fprintf(w, "</span>")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, f.preWrapper.End(false))
|
||||||
|
fmt.Fprint(w, "</td>\n")
|
||||||
|
fmt.Fprintf(w, "<td%s>\n", f.styleAttr(css, chroma.LineTableTD, "width:100%"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s", f.preWrapper.Start(true, f.styleAttr(css, chroma.PreWrapper)))
|
||||||
|
|
||||||
|
highlightIndex = 0
|
||||||
|
for index, tokens := range lines {
|
||||||
|
// 1-based line number.
|
||||||
|
line := f.baseLineNumber + index
|
||||||
|
highlight, next := f.shouldHighlight(highlightIndex, line)
|
||||||
|
if next {
|
||||||
|
highlightIndex++
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||||
|
// Start of Line
|
||||||
|
fmt.Fprint(w, `<span`)
|
||||||
|
|
||||||
|
fmt.Fprintf(w, " %s%s", f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
|
||||||
|
|
||||||
|
if highlight {
|
||||||
|
// Line + LineHighlight
|
||||||
|
if f.Classes {
|
||||||
|
fmt.Fprintf(w, ` class="%s %s"`, f.class(chroma.Line), f.class(chroma.LineHighlight))
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, ` style="%s %s"`, css[chroma.Line], css[chroma.LineHighlight])
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, `>`)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(w, "%s>", f.styleAttr(css, chroma.Line))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Line number
|
||||||
|
// if f.lineNumbers && !wrapInTable {
|
||||||
|
// fmt.Fprintf(w, "<span%s%s>%s</span>", f.styleAttr(css, chroma.LineNumbers), f.lineIDAttribute(line), f.lineTitleWithLinkIfNeeded(css, lineDigits, line))
|
||||||
|
// }
|
||||||
|
|
||||||
|
fmt.Fprintf(w, `<span%s>`, f.styleAttr(css, chroma.CodeLine))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range tokens {
|
||||||
|
html := html.EscapeString(token.String())
|
||||||
|
attr := f.styleAttr(css, token.Type)
|
||||||
|
if attr != "" {
|
||||||
|
html = fmt.Sprintf("<span%s>%s</span>", attr, html)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, html)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !(f.preventSurroundingPre || f.inlineCode) {
|
||||||
|
fmt.Fprint(w, `</span>`) // End of CodeLine
|
||||||
|
|
||||||
|
fmt.Fprint(w, `</span>`) // End of Line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "%s", f.preWrapper.End(true))
|
||||||
|
|
||||||
|
if wrapInTable {
|
||||||
|
fmt.Fprint(w, "</td></tr></table>\n")
|
||||||
|
fmt.Fprint(w, "</div>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.standalone {
|
||||||
|
fmt.Fprint(w, "\n</body>\n")
|
||||||
|
fmt.Fprint(w, "</html>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) lineIDAttribute(line int) string {
|
||||||
|
if !f.linkableLineNumbers {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(" id=\"%s\"", f.lineID(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) lineTitleWithLinkIfNeeded(css map[chroma.TokenType]string, lineDigits, line int) string {
|
||||||
|
if !f.linkableLineNumbers {
|
||||||
|
return f.styleAttr(css, chroma.LineNumbers)
|
||||||
|
}
|
||||||
|
|
||||||
|
class := f.styleAttr(css, chroma.LineNumbers)
|
||||||
|
classes := class[:len(class)-1] + " " + f.styleAttr(css, chroma.Line)[8:]
|
||||||
|
return fmt.Sprintf("%s href=\"#%s\"", classes, f.lineID(line))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) lineID(line int) string {
|
||||||
|
return fmt.Sprintf("%s%d", f.lineNumbersIDPrefix, line)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) shouldHighlight(highlightIndex, line int) (bool, bool) {
|
||||||
|
next := false
|
||||||
|
for highlightIndex < len(f.highlightRanges) && line > f.highlightRanges[highlightIndex][1] {
|
||||||
|
highlightIndex++
|
||||||
|
next = true
|
||||||
|
}
|
||||||
|
if highlightIndex < len(f.highlightRanges) {
|
||||||
|
hrange := f.highlightRanges[highlightIndex]
|
||||||
|
if line >= hrange[0] && line <= hrange[1] {
|
||||||
|
return true, next
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) class(t chroma.TokenType) string {
|
||||||
|
for t != 0 {
|
||||||
|
if cls, ok := chroma.StandardTypes[t]; ok {
|
||||||
|
if cls != "" {
|
||||||
|
return f.prefix + cls
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
t = t.Parent()
|
||||||
|
}
|
||||||
|
if cls := chroma.StandardTypes[t]; cls != "" {
|
||||||
|
return f.prefix + cls
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType, extraCSS ...string) string {
|
||||||
|
if f.Classes {
|
||||||
|
cls := f.class(tt)
|
||||||
|
if cls == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(` class="%s"`, cls)
|
||||||
|
}
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
tt = tt.SubCategory()
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
tt = tt.Category()
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
css := []string{styles[tt]}
|
||||||
|
css = append(css, extraCSS...)
|
||||||
|
return fmt.Sprintf(` style="%s"`, strings.Join(css, ";"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) tabWidthStyle() string {
|
||||||
|
if f.tabWidth != 0 && f.tabWidth != 8 {
|
||||||
|
return fmt.Sprintf("-moz-tab-size: %[1]d; -o-tab-size: %[1]d; tab-size: %[1]d;", f.tabWidth)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteCSS writes CSS style definitions (without any surrounding HTML).
|
||||||
|
func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||||
|
css := f.styleCache.get(style, false)
|
||||||
|
// Special-case background as it is mapped to the outer ".chroma" class.
|
||||||
|
if _, err := fmt.Fprintf(w, "/* %s */ .%sbg { %s }\n", chroma.Background, f.prefix, css[chroma.Background]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Special-case PreWrapper as it is the ".chroma" class.
|
||||||
|
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma { %s }\n", chroma.PreWrapper, f.prefix, css[chroma.PreWrapper]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Special-case code column of table to expand width.
|
||||||
|
if f.lineNumbers && f.lineNumbersInTable {
|
||||||
|
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s:last-child { width: 100%%; }",
|
||||||
|
chroma.LineTableTD, f.prefix, f.class(chroma.LineTableTD)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Special-case line number highlighting when targeted.
|
||||||
|
if f.lineNumbers || f.lineNumbersInTable {
|
||||||
|
targetedLineCSS := StyleEntryToCSS(style.Get(chroma.LineHighlight))
|
||||||
|
for _, tt := range []chroma.TokenType{chroma.LineNumbers, chroma.LineNumbersTable} {
|
||||||
|
fmt.Fprintf(w, "/* %s targeted by URL anchor */ .%schroma .%s:target { %s }\n", tt, f.prefix, f.class(tt), targetedLineCSS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tts := []int{}
|
||||||
|
for tt := range css {
|
||||||
|
tts = append(tts, int(tt))
|
||||||
|
}
|
||||||
|
sort.Ints(tts)
|
||||||
|
for _, ti := range tts {
|
||||||
|
tt := chroma.TokenType(ti)
|
||||||
|
switch tt {
|
||||||
|
case chroma.Background, chroma.PreWrapper:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
class := f.class(tt)
|
||||||
|
if class == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
styles := css[tt]
|
||||||
|
if _, err := fmt.Fprintf(w, "/* %s */ .%schroma .%s { %s }\n", tt, f.prefix, class, styles); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
|
||||||
|
classes := map[chroma.TokenType]string{}
|
||||||
|
bg := style.Get(chroma.Background)
|
||||||
|
// Convert the style.
|
||||||
|
for t := range chroma.StandardTypes {
|
||||||
|
entry := style.Get(t)
|
||||||
|
if t != chroma.Background {
|
||||||
|
entry = entry.Sub(bg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inherit from custom CSS provided by user
|
||||||
|
tokenCategory := t.Category()
|
||||||
|
tokenSubCategory := t.SubCategory()
|
||||||
|
if t != tokenCategory {
|
||||||
|
if css, ok := f.customCSS[tokenCategory]; ok {
|
||||||
|
classes[t] = css
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tokenCategory != tokenSubCategory {
|
||||||
|
if css, ok := f.customCSS[tokenSubCategory]; ok {
|
||||||
|
classes[t] += css
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add custom CSS provided by user
|
||||||
|
if css, ok := f.customCSS[t]; ok {
|
||||||
|
classes[t] += css
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.allClasses && entry.IsZero() && classes[t] == `` {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
styleEntryCSS := StyleEntryToCSS(entry)
|
||||||
|
if styleEntryCSS != `` && classes[t] != `` {
|
||||||
|
styleEntryCSS += `;`
|
||||||
|
}
|
||||||
|
classes[t] = styleEntryCSS + classes[t]
|
||||||
|
}
|
||||||
|
classes[chroma.Background] += `;` + f.tabWidthStyle()
|
||||||
|
classes[chroma.PreWrapper] += classes[chroma.Background]
|
||||||
|
// Make PreWrapper a grid to show highlight style with full width.
|
||||||
|
if len(f.highlightRanges) > 0 && f.customCSS[chroma.PreWrapper] == `` {
|
||||||
|
classes[chroma.PreWrapper] += `display: grid;`
|
||||||
|
}
|
||||||
|
// Make PreWrapper wrap long lines.
|
||||||
|
if f.wrapLongLines {
|
||||||
|
classes[chroma.PreWrapper] += `white-space: pre-wrap; word-break: break-word;`
|
||||||
|
}
|
||||||
|
lineNumbersStyle := `white-space: pre; margin-right: 0.4em; padding: 0 0.4em 0 0.4em;`
|
||||||
|
// All rules begin with default rules followed by user provided rules
|
||||||
|
classes[chroma.Line] = `display: flex; text-decoration: none; color: inherit; outline: none; cursor: text;` + classes[chroma.Line]
|
||||||
|
classes[chroma.LineNumbers] = lineNumbersStyle + classes[chroma.LineNumbers]
|
||||||
|
classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
|
||||||
|
classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
|
||||||
|
classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
|
||||||
|
classes[chroma.LineLink] = "outline: none; text-decoration: none; color: inherit" + classes[chroma.LineLink]
|
||||||
|
return classes
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
|
||||||
|
func StyleEntryToCSS(e chroma.StyleEntry) string {
|
||||||
|
styles := []string{}
|
||||||
|
if e.Colour.IsSet() {
|
||||||
|
styles = append(styles, "color: "+e.Colour.String())
|
||||||
|
}
|
||||||
|
if e.Background.IsSet() {
|
||||||
|
styles = append(styles, "background-color: "+e.Background.String())
|
||||||
|
}
|
||||||
|
if e.Bold == chroma.Yes {
|
||||||
|
styles = append(styles, "font-weight: bold")
|
||||||
|
}
|
||||||
|
if e.Italic == chroma.Yes {
|
||||||
|
styles = append(styles, "font-style: italic")
|
||||||
|
}
|
||||||
|
if e.Underline == chroma.Yes {
|
||||||
|
styles = append(styles, "text-decoration: underline")
|
||||||
|
}
|
||||||
|
return strings.Join(styles, "; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compress CSS attributes - remove spaces, transform 6-digit colours to 3.
|
||||||
|
func compressStyle(s string) string {
|
||||||
|
parts := strings.Split(s, ";")
|
||||||
|
out := []string{}
|
||||||
|
for _, p := range parts {
|
||||||
|
p = strings.Join(strings.Fields(p), " ")
|
||||||
|
p = strings.Replace(p, ": ", ":", 1)
|
||||||
|
if strings.Contains(p, "#") {
|
||||||
|
c := p[len(p)-6:]
|
||||||
|
if c[0] == c[1] && c[2] == c[3] && c[4] == c[5] {
|
||||||
|
p = p[:len(p)-6] + c[0:1] + c[2:3] + c[4:5]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, p)
|
||||||
|
}
|
||||||
|
return strings.Join(out, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
const styleCacheLimit = 32
|
||||||
|
|
||||||
|
type styleCacheEntry struct {
|
||||||
|
style *chroma.Style
|
||||||
|
compressed bool
|
||||||
|
cache map[chroma.TokenType]string
|
||||||
|
}
|
||||||
|
|
||||||
|
type styleCache struct {
|
||||||
|
mu sync.Mutex
|
||||||
|
// LRU cache of compiled (and possibly compressed) styles. This is a slice
|
||||||
|
// because the cache size is small, and a slice is sufficiently fast for
|
||||||
|
// small N.
|
||||||
|
cache []styleCacheEntry
|
||||||
|
f *Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStyleCache(f *Formatter) *styleCache {
|
||||||
|
return &styleCache{f: f}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *styleCache) get(style *chroma.Style, compress bool) map[chroma.TokenType]string {
|
||||||
|
l.mu.Lock()
|
||||||
|
defer l.mu.Unlock()
|
||||||
|
|
||||||
|
// Look for an existing entry.
|
||||||
|
for i := len(l.cache) - 1; i >= 0; i-- {
|
||||||
|
entry := l.cache[i]
|
||||||
|
if entry.style == style && entry.compressed == compress {
|
||||||
|
// Top of the cache, no need to adjust the order.
|
||||||
|
if i == len(l.cache)-1 {
|
||||||
|
return entry.cache
|
||||||
|
}
|
||||||
|
// Move this entry to the end of the LRU
|
||||||
|
copy(l.cache[i:], l.cache[i+1:])
|
||||||
|
l.cache[len(l.cache)-1] = entry
|
||||||
|
return entry.cache
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No entry, create one.
|
||||||
|
cached := l.f.styleToCSS(style)
|
||||||
|
if !l.f.Classes {
|
||||||
|
for t, style := range cached {
|
||||||
|
cached[t] = compressStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if compress {
|
||||||
|
for t, style := range cached {
|
||||||
|
cached[t] = compressStyle(style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Evict the oldest entry.
|
||||||
|
if len(l.cache) >= styleCacheLimit {
|
||||||
|
l.cache = l.cache[0:copy(l.cache, l.cache[1:])]
|
||||||
|
}
|
||||||
|
l.cache = append(l.cache, styleCacheEntry{style: style, cache: cached, compressed: compress})
|
||||||
|
return cached
|
||||||
|
}
|
375
formatters/html/html_test.go
Normal file
375
formatters/html/html_test.go
Normal file
|
@ -0,0 +1,375 @@
|
||||||
|
package html
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2/lexers"
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2/styles"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCompressStyle(t *testing.T) {
|
||||||
|
style := "color: #888888; background-color: #faffff"
|
||||||
|
actual := compressStyle(style)
|
||||||
|
expected := "color:#888;background-color:#faffff"
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkHTMLFormatter(b *testing.B) {
|
||||||
|
formatter := New()
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
|
||||||
|
assert.NoError(b, err)
|
||||||
|
err = formatter.Format(io.Discard, styles.Fallback, it)
|
||||||
|
assert.NoError(b, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitTokensIntoLines(t *testing.T) {
|
||||||
|
in := []chroma.Token{
|
||||||
|
{Value: "hello", Type: chroma.NameKeyword},
|
||||||
|
{Value: " world\nwhat?\n", Type: chroma.NameKeyword},
|
||||||
|
}
|
||||||
|
expected := [][]chroma.Token{
|
||||||
|
{
|
||||||
|
{Type: chroma.NameKeyword, Value: "hello"},
|
||||||
|
{Type: chroma.NameKeyword, Value: " world\n"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
{Type: chroma.NameKeyword, Value: "what?\n"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
actual := chroma.SplitTokensIntoLines(in)
|
||||||
|
assert.Equal(t, expected, actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFormatterStyleToCSS(t *testing.T) {
|
||||||
|
builder := styles.Get("github").Builder()
|
||||||
|
builder.Add(chroma.LineHighlight, "bg:#ffffcc")
|
||||||
|
builder.Add(chroma.LineNumbers, "bold")
|
||||||
|
style, err := builder.Build()
|
||||||
|
if err != nil {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
formatter := New(WithClasses(true))
|
||||||
|
css := formatter.styleToCSS(style)
|
||||||
|
for _, s := range css {
|
||||||
|
if strings.HasPrefix(strings.TrimSpace(s), ";") {
|
||||||
|
t.Errorf("rule starts with semicolon - expected valid css rule without semicolon: %v", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassPrefix(t *testing.T) {
|
||||||
|
wantPrefix := "some-prefix-"
|
||||||
|
withPrefix := New(WithClasses(true), ClassPrefix(wantPrefix))
|
||||||
|
noPrefix := New(WithClasses(true))
|
||||||
|
for st := range chroma.StandardTypes {
|
||||||
|
if noPrefix.class(st) == "" {
|
||||||
|
if got := withPrefix.class(st); got != "" {
|
||||||
|
t.Errorf("Formatter.class(%v): prefix shouldn't be added to empty classes", st)
|
||||||
|
}
|
||||||
|
} else if got := withPrefix.class(st); !strings.HasPrefix(got, wantPrefix) {
|
||||||
|
t.Errorf("Formatter.class(%v): %q should have a class prefix", st, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var styleBuf bytes.Buffer
|
||||||
|
err := withPrefix.WriteCSS(&styleBuf, styles.Fallback)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if !strings.Contains(styleBuf.String(), ".some-prefix-chroma ") {
|
||||||
|
t.Error("Stylesheets should have a class prefix")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableLineNumberNewlines(t *testing.T) {
|
||||||
|
f := New(WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true))
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Don't bother testing the whole output, just verify it's got line numbers
|
||||||
|
// in a <pre>-friendly format.
|
||||||
|
// Note: placing the newlines inside the <span> lets browser selections look
|
||||||
|
// better, instead of "skipping" over the span margin.
|
||||||
|
assert.Contains(t, buf.String(), `<span class="lnt">2
|
||||||
|
</span><span class="lnt">3
|
||||||
|
</span><span class="lnt">4
|
||||||
|
</span>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTabWidthStyle(t *testing.T) {
|
||||||
|
f := New(TabWidth(4), WithClasses(false))
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, regexp.MustCompile(`<pre.*style=".*background-color:[^;]+;-moz-tab-size:4;-o-tab-size:4;tab-size:4;[^"]*".+`).MatchString(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCustomCSS(t *testing.T) {
|
||||||
|
f := New(WithClasses(false), WithCustomCSS(map[chroma.TokenType]string{chroma.Line: `display: inline;`}))
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, regexp.MustCompile(`<span style="display:flex;display:inline;"><span><span style=".*">echo</span> FOO</span></span>`).MatchString(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithCustomCSSStyleInheritance(t *testing.T) {
|
||||||
|
f := New(WithClasses(false), WithCustomCSS(map[chroma.TokenType]string{
|
||||||
|
chroma.String: `background: blue;`,
|
||||||
|
chroma.LiteralStringDouble: `color: tomato;`,
|
||||||
|
}))
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, `echo "FOO"`)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, regexp.MustCompile(` <span style=".*;background:blue;color:tomato;">"FOO"</span>`).MatchString(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWrapLongLines(t *testing.T) {
|
||||||
|
f := New(WithClasses(false), WrapLongLines(true))
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, regexp.MustCompile(`<pre.*style=".*white-space:pre-wrap;word-break:break-word;`).MatchString(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHighlightLines(t *testing.T) {
|
||||||
|
f := New(WithClasses(true), HighlightLines([][2]int{{4, 5}}))
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, buf.String(), `<span class="line hl"><span class="cl">`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLineNumbers(t *testing.T) {
|
||||||
|
f := New(WithClasses(true), WithLineNumbers(true))
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, buf.String(), `<span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> FOO</span></span>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPreWrapper(t *testing.T) {
|
||||||
|
f := New(Standalone(true), WithClasses(true))
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.True(t, regexp.MustCompile("<body class=\"bg\">\n<pre.*class=\"chroma\"><code><span class=\"line\"><span class=\"cl\"><span class=\"nb\">echo</span> FOO</span></span></code></pre>\n</body>\n</html>").MatchString(buf.String()))
|
||||||
|
assert.True(t, regexp.MustCompile(`\.bg { .+ }`).MatchString(buf.String()))
|
||||||
|
assert.True(t, regexp.MustCompile(`\.chroma { .+ }`).MatchString(buf.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLinkeableLineNumbers(t *testing.T) {
|
||||||
|
f := New(WithClasses(true), WithLineNumbers(true), WithLinkableLineNumbers(true, "line"), WithClasses(false))
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(\"hello world\")\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, buf.String(), `id="line1"><a style="outline:none;text-decoration:none;color:inherit" href="#line1">1</a>`)
|
||||||
|
assert.Contains(t, buf.String(), `id="line5"><a style="outline:none;text-decoration:none;color:inherit" href="#line5">5</a>`)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableLinkeableLineNumbers(t *testing.T) {
|
||||||
|
f := New(Standalone(true), WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true), WithLinkableLineNumbers(true, "line"))
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Contains(t, buf.String(), `id="line1"><a class="lnlinks" href="#line1">1</a>`)
|
||||||
|
assert.Contains(t, buf.String(), `id="line5"><a class="lnlinks" href="#line5">5</a>`)
|
||||||
|
assert.Contains(t, buf.String(), `/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }`, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTableLineNumberSpacing(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
baseLineNumber int
|
||||||
|
expectedBuf string
|
||||||
|
}{{
|
||||||
|
7,
|
||||||
|
`<span class="lnt"> 7
|
||||||
|
</span><span class="lnt"> 8
|
||||||
|
</span><span class="lnt"> 9
|
||||||
|
</span><span class="lnt">10
|
||||||
|
</span><span class="lnt">11
|
||||||
|
</span>`,
|
||||||
|
}, {
|
||||||
|
6,
|
||||||
|
`<span class="lnt"> 6
|
||||||
|
</span><span class="lnt"> 7
|
||||||
|
</span><span class="lnt"> 8
|
||||||
|
</span><span class="lnt"> 9
|
||||||
|
</span><span class="lnt">10
|
||||||
|
</span>`,
|
||||||
|
}, {
|
||||||
|
5,
|
||||||
|
`<span class="lnt">5
|
||||||
|
</span><span class="lnt">6
|
||||||
|
</span><span class="lnt">7
|
||||||
|
</span><span class="lnt">8
|
||||||
|
</span><span class="lnt">9
|
||||||
|
</span>`,
|
||||||
|
}}
|
||||||
|
for i, testCase := range testCases {
|
||||||
|
f := New(
|
||||||
|
WithClasses(true),
|
||||||
|
WithLineNumbers(true),
|
||||||
|
LineNumbersInTable(true),
|
||||||
|
BaseLineNumber(testCase.baseLineNumber),
|
||||||
|
)
|
||||||
|
it, err := lexers.Get("go").Tokenise(nil, "package main\nfunc main()\n{\nprintln(`hello world`)\n}\n")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err, "Test Case %d", i)
|
||||||
|
assert.Contains(t, buf.String(), testCase.expectedBuf, "Test Case %d", i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithPreWrapper(t *testing.T) {
|
||||||
|
wrapper := preWrapper{
|
||||||
|
start: func(code bool, styleAttr string) string {
|
||||||
|
return fmt.Sprintf("<foo%s id=\"code-%t\">", styleAttr, code)
|
||||||
|
},
|
||||||
|
end: func(code bool) string {
|
||||||
|
return "</foo>"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
format := func(f *Formatter) string {
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
return buf.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("Regular", func(t *testing.T) {
|
||||||
|
s := format(New(WithClasses(true)))
|
||||||
|
assert.Equal(t, s, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("PreventSurroundingPre", func(t *testing.T) {
|
||||||
|
s := format(New(PreventSurroundingPre(true), WithClasses(true)))
|
||||||
|
assert.Equal(t, s, `<span class="nb">echo</span> FOO`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InlineCode", func(t *testing.T) {
|
||||||
|
s := format(New(InlineCode(true), WithClasses(true)))
|
||||||
|
assert.Equal(t, s, `<code class="chroma"><span class="nb">echo</span> FOO</code>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("InlineCode, inline styles", func(t *testing.T) {
|
||||||
|
s := format(New(InlineCode(true)))
|
||||||
|
assert.True(t, regexp.MustCompile(`<code style=".+?"><span style=".+?">echo</span> FOO</code>`).MatchString(s))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Wrapper", func(t *testing.T) {
|
||||||
|
s := format(New(WithPreWrapper(wrapper), WithClasses(true)))
|
||||||
|
assert.Equal(t, s, `<foo class="chroma" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Wrapper, LineNumbersInTable", func(t *testing.T) {
|
||||||
|
s := format(New(WithPreWrapper(wrapper), WithClasses(true), WithLineNumbers(true), LineNumbersInTable(true)))
|
||||||
|
|
||||||
|
assert.Equal(t, s, `<div class="chroma">
|
||||||
|
<table class="lntable"><tr><td class="lntd">
|
||||||
|
<foo class="chroma" id="code-false"><span class="lnt">1
|
||||||
|
</span></foo></td>
|
||||||
|
<td class="lntd">
|
||||||
|
<foo class="chroma" id="code-true"><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></foo></td></tr></table>
|
||||||
|
</div>
|
||||||
|
`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReconfigureOptions(t *testing.T) {
|
||||||
|
options := []Option{
|
||||||
|
WithClasses(true),
|
||||||
|
WithLineNumbers(true),
|
||||||
|
}
|
||||||
|
|
||||||
|
options = append(options, WithLineNumbers(false))
|
||||||
|
|
||||||
|
f := New(options...)
|
||||||
|
|
||||||
|
it, err := lexers.Get("bash").Tokenise(nil, "echo FOO")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = f.Format(&buf, styles.Fallback, it)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, `<pre class="chroma"><code><span class="line"><span class="cl"><span class="nb">echo</span> FOO</span></span></code></pre>`, buf.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWriteCssWithAllClasses(t *testing.T) {
|
||||||
|
formatter := New(WithAllClasses(true))
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := formatter.WriteCSS(&buf, styles.Fallback)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotContains(t, buf.String(), ".chroma . {", "Generated css doesn't contain invalid css")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStyleCache(t *testing.T) {
|
||||||
|
f := New()
|
||||||
|
|
||||||
|
assert.True(t, len(styles.Registry) > styleCacheLimit)
|
||||||
|
|
||||||
|
for _, style := range styles.Registry {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := f.WriteCSS(&buf, style)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, styleCacheLimit, len(f.styleCache.cache))
|
||||||
|
}
|
31
formatters/json.go
Normal file
31
formatters/json.go
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// JSON formatter outputs the raw token structures as JSON.
|
||||||
|
var JSON = Register("json", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, it chroma.Iterator) error {
|
||||||
|
fmt.Fprintln(w, "[")
|
||||||
|
i := 0
|
||||||
|
for t := it(); t != chroma.EOF; t = it() {
|
||||||
|
if i > 0 {
|
||||||
|
fmt.Fprintln(w, ",")
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
bytes, err := json.Marshal(t)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := fmt.Fprint(w, " "+string(bytes)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Fprintln(w)
|
||||||
|
fmt.Fprintln(w, "]")
|
||||||
|
return nil
|
||||||
|
}))
|
51
formatters/svg/font_liberation_mono.go
Normal file
51
formatters/svg/font_liberation_mono.go
Normal file
File diff suppressed because one or more lines are too long
232
formatters/svg/svg.go
Normal file
232
formatters/svg/svg.go
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
// Package svg contains an SVG formatter.
|
||||||
|
package svg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option sets an option of the SVG formatter.
|
||||||
|
type Option func(f *Formatter)
|
||||||
|
|
||||||
|
// FontFamily sets the font-family.
|
||||||
|
func FontFamily(fontFamily string) Option { return func(f *Formatter) { f.fontFamily = fontFamily } }
|
||||||
|
|
||||||
|
// EmbedFontFile embeds given font file
|
||||||
|
func EmbedFontFile(fontFamily string, fileName string) (option Option, err error) {
|
||||||
|
var format FontFormat
|
||||||
|
switch path.Ext(fileName) {
|
||||||
|
case ".woff":
|
||||||
|
format = WOFF
|
||||||
|
case ".woff2":
|
||||||
|
format = WOFF2
|
||||||
|
case ".ttf":
|
||||||
|
format = TRUETYPE
|
||||||
|
default:
|
||||||
|
return nil, errors.New("unexpected font file suffix")
|
||||||
|
}
|
||||||
|
|
||||||
|
var content []byte
|
||||||
|
if content, err = os.ReadFile(fileName); err == nil {
|
||||||
|
option = EmbedFont(fontFamily, base64.StdEncoding.EncodeToString(content), format)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// EmbedFont embeds given base64 encoded font
|
||||||
|
func EmbedFont(fontFamily string, font string, format FontFormat) Option {
|
||||||
|
return func(f *Formatter) { f.fontFamily = fontFamily; f.embeddedFont = font; f.fontFormat = format }
|
||||||
|
}
|
||||||
|
|
||||||
|
// New SVG formatter.
|
||||||
|
func New(options ...Option) *Formatter {
|
||||||
|
f := &Formatter{fontFamily: "Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace"}
|
||||||
|
for _, option := range options {
|
||||||
|
option(f)
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatter that generates SVG.
|
||||||
|
type Formatter struct {
|
||||||
|
fontFamily string
|
||||||
|
embeddedFont string
|
||||||
|
fontFormat FontFormat
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
|
||||||
|
f.writeSVG(w, style, iterator.Tokens())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var svgEscaper = strings.NewReplacer(
|
||||||
|
`&`, "&",
|
||||||
|
`<`, "<",
|
||||||
|
`>`, ">",
|
||||||
|
`"`, """,
|
||||||
|
` `, " ",
|
||||||
|
` `, "    ",
|
||||||
|
)
|
||||||
|
|
||||||
|
// EscapeString escapes special characters.
|
||||||
|
func escapeString(s string) string {
|
||||||
|
return svgEscaper.Replace(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo
|
||||||
|
svgStyles := f.styleToSVG(style)
|
||||||
|
lines := chroma.SplitTokensIntoLines(tokens)
|
||||||
|
|
||||||
|
fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
|
||||||
|
fmt.Fprint(w, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
|
||||||
|
width := 18 + int(8.8*float64(maxLineWidth(lines)+1))
|
||||||
|
if width < 300 {
|
||||||
|
width = 300
|
||||||
|
}
|
||||||
|
fmt.Fprintf(w, "<svg width=\"%dpx\" height=\"%dpx\" xmlns=\"http://www.w3.org/2000/svg\">\n", width, int(18*float64(len(lines)+3)))
|
||||||
|
|
||||||
|
if f.embeddedFont != "" {
|
||||||
|
f.writeFontStyle(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "<rect width=\"100%%\" height=\"100%%\" border-radius=\"1em\" fill=\"%s\"/>\n", style.Get(chroma.Background).Background.String())
|
||||||
|
|
||||||
|
// add 3 svg circles like macos windows
|
||||||
|
fmt.Fprintf(w, "<circle cx=\"1.5em\" cy=\"1.45em\" r=\"0.5em\" fill=\"#fa6153\"/>\n")
|
||||||
|
fmt.Fprintf(w, "<circle cx=\"3em\" cy=\"1.45em\" r=\"0.5em\" fill=\"#f8c120\"/>\n")
|
||||||
|
fmt.Fprintf(w, "<circle cx=\"4.5em\" cy=\"1.45em\" r=\"0.5em\" fill=\"#2cc640\"/>\n")
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "<g font-family=\"%s\" font-size=\"14px\" font-weight=\"600\" fill=\"%s\">\n", f.fontFamily, style.Get(chroma.Text).Colour.String())
|
||||||
|
|
||||||
|
f.writeTokenBackgrounds(w, lines, style)
|
||||||
|
|
||||||
|
for index, tokens := range lines {
|
||||||
|
fmt.Fprintf(w, "<text x=\"1em\" y=\"%fem\" xml:space=\"preserve\">", 1.2*float64(index+1)+3)
|
||||||
|
|
||||||
|
for _, token := range tokens {
|
||||||
|
text := escapeString(token.String())
|
||||||
|
attr := f.styleAttr(svgStyles, token.Type)
|
||||||
|
if attr != "" {
|
||||||
|
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, text)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, "</text>")
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(w, "\n</g>\n")
|
||||||
|
fmt.Fprint(w, "</svg>\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func maxLineWidth(lines [][]chroma.Token) int {
|
||||||
|
maxWidth := 0
|
||||||
|
for _, tokens := range lines {
|
||||||
|
length := 0
|
||||||
|
for _, token := range tokens {
|
||||||
|
length += len(strings.ReplaceAll(token.String(), ` `, " "))
|
||||||
|
}
|
||||||
|
if length > maxWidth {
|
||||||
|
maxWidth = length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return maxWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is no background attribute for text in SVG so simply calculate the position and text
|
||||||
|
// of tokens with a background color that differs from the default and add a rectangle for each before
|
||||||
|
// adding the token.
|
||||||
|
func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) {
|
||||||
|
for index, tokens := range lines {
|
||||||
|
lineLength := 0
|
||||||
|
for _, token := range tokens {
|
||||||
|
length := len(strings.ReplaceAll(token.String(), ` `, " "))
|
||||||
|
tokenBackground := style.Get(token.Type).Background
|
||||||
|
if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
|
||||||
|
fmt.Fprintf(w, "<rect id=\"%s\" x=\"%dch\" y=\"%fem\" width=\"%dch\" height=\"1.2em\" fill=\"%s\" />\n", escapeString(token.String()), lineLength, 1.2*float64(index)+0.25, length, style.Get(token.Type).Background.String())
|
||||||
|
}
|
||||||
|
lineLength += length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type FontFormat int
|
||||||
|
|
||||||
|
// https://transfonter.org/formats
|
||||||
|
const (
|
||||||
|
WOFF FontFormat = iota
|
||||||
|
WOFF2
|
||||||
|
TRUETYPE
|
||||||
|
)
|
||||||
|
|
||||||
|
var fontFormats = [...]string{
|
||||||
|
"woff",
|
||||||
|
"woff2",
|
||||||
|
"truetype",
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) writeFontStyle(w io.Writer) {
|
||||||
|
fmt.Fprintf(w, `<style>
|
||||||
|
@font-face {
|
||||||
|
font-family: '%s';
|
||||||
|
src: url(data:application/x-font-%s;charset=utf-8;base64,%s) format('%s');'
|
||||||
|
font-weight: normal;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
</style>`, f.fontFamily, fontFormats[f.fontFormat], f.embeddedFont, fontFormats[f.fontFormat])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
tt = tt.SubCategory()
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
tt = tt.Category()
|
||||||
|
if _, ok := styles[tt]; !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return styles[tt]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string {
|
||||||
|
converted := map[chroma.TokenType]string{}
|
||||||
|
bg := style.Get(chroma.Background)
|
||||||
|
// Convert the style.
|
||||||
|
for t := range chroma.StandardTypes {
|
||||||
|
entry := style.Get(t)
|
||||||
|
if t != chroma.Background {
|
||||||
|
entry = entry.Sub(bg)
|
||||||
|
}
|
||||||
|
if entry.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
converted[t] = StyleEntryToSVG(entry)
|
||||||
|
}
|
||||||
|
return converted
|
||||||
|
}
|
||||||
|
|
||||||
|
// StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes.
|
||||||
|
func StyleEntryToSVG(e chroma.StyleEntry) string {
|
||||||
|
var styles []string
|
||||||
|
|
||||||
|
if e.Colour.IsSet() {
|
||||||
|
styles = append(styles, "fill=\""+e.Colour.String()+"\"")
|
||||||
|
}
|
||||||
|
if e.Bold == chroma.Yes {
|
||||||
|
styles = append(styles, "font-weight=\"bold\"")
|
||||||
|
}
|
||||||
|
if e.Italic == chroma.Yes {
|
||||||
|
styles = append(styles, "font-style=\"italic\"")
|
||||||
|
}
|
||||||
|
if e.Underline == chroma.Yes {
|
||||||
|
styles = append(styles, "text-decoration=\"underline\"")
|
||||||
|
}
|
||||||
|
return strings.Join(styles, " ")
|
||||||
|
}
|
18
formatters/tokens.go
Normal file
18
formatters/tokens.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Tokens formatter outputs the raw token structures.
|
||||||
|
var Tokens = Register("tokens", chroma.FormatterFunc(func(w io.Writer, s *chroma.Style, it chroma.Iterator) error {
|
||||||
|
for t := it(); t != chroma.EOF; t = it() {
|
||||||
|
if _, err := fmt.Fprintln(w, t.GoString()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}))
|
291
formatters/tty_indexed.go
Normal file
291
formatters/tty_indexed.go
Normal file
|
@ -0,0 +1,291 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ttyTable struct {
|
||||||
|
foreground map[chroma.Colour]string
|
||||||
|
background map[chroma.Colour]string
|
||||||
|
}
|
||||||
|
|
||||||
|
var c = chroma.MustParseColour
|
||||||
|
|
||||||
|
var ttyTables = map[int]*ttyTable{
|
||||||
|
8: {
|
||||||
|
foreground: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[30m", c("#7f0000"): "\033[31m", c("#007f00"): "\033[32m", c("#7f7fe0"): "\033[33m",
|
||||||
|
c("#00007f"): "\033[34m", c("#7f007f"): "\033[35m", c("#007f7f"): "\033[36m", c("#e5e5e5"): "\033[37m",
|
||||||
|
c("#555555"): "\033[1m\033[30m", c("#ff0000"): "\033[1m\033[31m", c("#00ff00"): "\033[1m\033[32m", c("#ffff00"): "\033[1m\033[33m",
|
||||||
|
c("#0000ff"): "\033[1m\033[34m", c("#ff00ff"): "\033[1m\033[35m", c("#00ffff"): "\033[1m\033[36m", c("#ffffff"): "\033[1m\033[37m",
|
||||||
|
},
|
||||||
|
background: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[40m", c("#7f0000"): "\033[41m", c("#007f00"): "\033[42m", c("#7f7fe0"): "\033[43m",
|
||||||
|
c("#00007f"): "\033[44m", c("#7f007f"): "\033[45m", c("#007f7f"): "\033[46m", c("#e5e5e5"): "\033[47m",
|
||||||
|
c("#555555"): "\033[1m\033[40m", c("#ff0000"): "\033[1m\033[41m", c("#00ff00"): "\033[1m\033[42m", c("#ffff00"): "\033[1m\033[43m",
|
||||||
|
c("#0000ff"): "\033[1m\033[44m", c("#ff00ff"): "\033[1m\033[45m", c("#00ffff"): "\033[1m\033[46m", c("#ffffff"): "\033[1m\033[47m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
16: {
|
||||||
|
foreground: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[30m", c("#7f0000"): "\033[31m", c("#007f00"): "\033[32m", c("#7f7fe0"): "\033[33m",
|
||||||
|
c("#00007f"): "\033[34m", c("#7f007f"): "\033[35m", c("#007f7f"): "\033[36m", c("#e5e5e5"): "\033[37m",
|
||||||
|
c("#555555"): "\033[90m", c("#ff0000"): "\033[91m", c("#00ff00"): "\033[92m", c("#ffff00"): "\033[93m",
|
||||||
|
c("#0000ff"): "\033[94m", c("#ff00ff"): "\033[95m", c("#00ffff"): "\033[96m", c("#ffffff"): "\033[97m",
|
||||||
|
},
|
||||||
|
background: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[40m", c("#7f0000"): "\033[41m", c("#007f00"): "\033[42m", c("#7f7fe0"): "\033[43m",
|
||||||
|
c("#00007f"): "\033[44m", c("#7f007f"): "\033[45m", c("#007f7f"): "\033[46m", c("#e5e5e5"): "\033[47m",
|
||||||
|
c("#555555"): "\033[100m", c("#ff0000"): "\033[101m", c("#00ff00"): "\033[102m", c("#ffff00"): "\033[103m",
|
||||||
|
c("#0000ff"): "\033[104m", c("#ff00ff"): "\033[105m", c("#00ffff"): "\033[106m", c("#ffffff"): "\033[107m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
256: {
|
||||||
|
foreground: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[38;5;0m", c("#800000"): "\033[38;5;1m", c("#008000"): "\033[38;5;2m", c("#808000"): "\033[38;5;3m",
|
||||||
|
c("#000080"): "\033[38;5;4m", c("#800080"): "\033[38;5;5m", c("#008080"): "\033[38;5;6m", c("#c0c0c0"): "\033[38;5;7m",
|
||||||
|
c("#808080"): "\033[38;5;8m", c("#ff0000"): "\033[38;5;9m", c("#00ff00"): "\033[38;5;10m", c("#ffff00"): "\033[38;5;11m",
|
||||||
|
c("#0000ff"): "\033[38;5;12m", c("#ff00ff"): "\033[38;5;13m", c("#00ffff"): "\033[38;5;14m", c("#ffffff"): "\033[38;5;15m",
|
||||||
|
c("#000000"): "\033[38;5;16m", c("#00005f"): "\033[38;5;17m", c("#000087"): "\033[38;5;18m", c("#0000af"): "\033[38;5;19m",
|
||||||
|
c("#0000d7"): "\033[38;5;20m", c("#0000ff"): "\033[38;5;21m", c("#005f00"): "\033[38;5;22m", c("#005f5f"): "\033[38;5;23m",
|
||||||
|
c("#005f87"): "\033[38;5;24m", c("#005faf"): "\033[38;5;25m", c("#005fd7"): "\033[38;5;26m", c("#005fff"): "\033[38;5;27m",
|
||||||
|
c("#008700"): "\033[38;5;28m", c("#00875f"): "\033[38;5;29m", c("#008787"): "\033[38;5;30m", c("#0087af"): "\033[38;5;31m",
|
||||||
|
c("#0087d7"): "\033[38;5;32m", c("#0087ff"): "\033[38;5;33m", c("#00af00"): "\033[38;5;34m", c("#00af5f"): "\033[38;5;35m",
|
||||||
|
c("#00af87"): "\033[38;5;36m", c("#00afaf"): "\033[38;5;37m", c("#00afd7"): "\033[38;5;38m", c("#00afff"): "\033[38;5;39m",
|
||||||
|
c("#00d700"): "\033[38;5;40m", c("#00d75f"): "\033[38;5;41m", c("#00d787"): "\033[38;5;42m", c("#00d7af"): "\033[38;5;43m",
|
||||||
|
c("#00d7d7"): "\033[38;5;44m", c("#00d7ff"): "\033[38;5;45m", c("#00ff00"): "\033[38;5;46m", c("#00ff5f"): "\033[38;5;47m",
|
||||||
|
c("#00ff87"): "\033[38;5;48m", c("#00ffaf"): "\033[38;5;49m", c("#00ffd7"): "\033[38;5;50m", c("#00ffff"): "\033[38;5;51m",
|
||||||
|
c("#5f0000"): "\033[38;5;52m", c("#5f005f"): "\033[38;5;53m", c("#5f0087"): "\033[38;5;54m", c("#5f00af"): "\033[38;5;55m",
|
||||||
|
c("#5f00d7"): "\033[38;5;56m", c("#5f00ff"): "\033[38;5;57m", c("#5f5f00"): "\033[38;5;58m", c("#5f5f5f"): "\033[38;5;59m",
|
||||||
|
c("#5f5f87"): "\033[38;5;60m", c("#5f5faf"): "\033[38;5;61m", c("#5f5fd7"): "\033[38;5;62m", c("#5f5fff"): "\033[38;5;63m",
|
||||||
|
c("#5f8700"): "\033[38;5;64m", c("#5f875f"): "\033[38;5;65m", c("#5f8787"): "\033[38;5;66m", c("#5f87af"): "\033[38;5;67m",
|
||||||
|
c("#5f87d7"): "\033[38;5;68m", c("#5f87ff"): "\033[38;5;69m", c("#5faf00"): "\033[38;5;70m", c("#5faf5f"): "\033[38;5;71m",
|
||||||
|
c("#5faf87"): "\033[38;5;72m", c("#5fafaf"): "\033[38;5;73m", c("#5fafd7"): "\033[38;5;74m", c("#5fafff"): "\033[38;5;75m",
|
||||||
|
c("#5fd700"): "\033[38;5;76m", c("#5fd75f"): "\033[38;5;77m", c("#5fd787"): "\033[38;5;78m", c("#5fd7af"): "\033[38;5;79m",
|
||||||
|
c("#5fd7d7"): "\033[38;5;80m", c("#5fd7ff"): "\033[38;5;81m", c("#5fff00"): "\033[38;5;82m", c("#5fff5f"): "\033[38;5;83m",
|
||||||
|
c("#5fff87"): "\033[38;5;84m", c("#5fffaf"): "\033[38;5;85m", c("#5fffd7"): "\033[38;5;86m", c("#5fffff"): "\033[38;5;87m",
|
||||||
|
c("#870000"): "\033[38;5;88m", c("#87005f"): "\033[38;5;89m", c("#870087"): "\033[38;5;90m", c("#8700af"): "\033[38;5;91m",
|
||||||
|
c("#8700d7"): "\033[38;5;92m", c("#8700ff"): "\033[38;5;93m", c("#875f00"): "\033[38;5;94m", c("#875f5f"): "\033[38;5;95m",
|
||||||
|
c("#875f87"): "\033[38;5;96m", c("#875faf"): "\033[38;5;97m", c("#875fd7"): "\033[38;5;98m", c("#875fff"): "\033[38;5;99m",
|
||||||
|
c("#878700"): "\033[38;5;100m", c("#87875f"): "\033[38;5;101m", c("#878787"): "\033[38;5;102m", c("#8787af"): "\033[38;5;103m",
|
||||||
|
c("#8787d7"): "\033[38;5;104m", c("#8787ff"): "\033[38;5;105m", c("#87af00"): "\033[38;5;106m", c("#87af5f"): "\033[38;5;107m",
|
||||||
|
c("#87af87"): "\033[38;5;108m", c("#87afaf"): "\033[38;5;109m", c("#87afd7"): "\033[38;5;110m", c("#87afff"): "\033[38;5;111m",
|
||||||
|
c("#87d700"): "\033[38;5;112m", c("#87d75f"): "\033[38;5;113m", c("#87d787"): "\033[38;5;114m", c("#87d7af"): "\033[38;5;115m",
|
||||||
|
c("#87d7d7"): "\033[38;5;116m", c("#87d7ff"): "\033[38;5;117m", c("#87ff00"): "\033[38;5;118m", c("#87ff5f"): "\033[38;5;119m",
|
||||||
|
c("#87ff87"): "\033[38;5;120m", c("#87ffaf"): "\033[38;5;121m", c("#87ffd7"): "\033[38;5;122m", c("#87ffff"): "\033[38;5;123m",
|
||||||
|
c("#af0000"): "\033[38;5;124m", c("#af005f"): "\033[38;5;125m", c("#af0087"): "\033[38;5;126m", c("#af00af"): "\033[38;5;127m",
|
||||||
|
c("#af00d7"): "\033[38;5;128m", c("#af00ff"): "\033[38;5;129m", c("#af5f00"): "\033[38;5;130m", c("#af5f5f"): "\033[38;5;131m",
|
||||||
|
c("#af5f87"): "\033[38;5;132m", c("#af5faf"): "\033[38;5;133m", c("#af5fd7"): "\033[38;5;134m", c("#af5fff"): "\033[38;5;135m",
|
||||||
|
c("#af8700"): "\033[38;5;136m", c("#af875f"): "\033[38;5;137m", c("#af8787"): "\033[38;5;138m", c("#af87af"): "\033[38;5;139m",
|
||||||
|
c("#af87d7"): "\033[38;5;140m", c("#af87ff"): "\033[38;5;141m", c("#afaf00"): "\033[38;5;142m", c("#afaf5f"): "\033[38;5;143m",
|
||||||
|
c("#afaf87"): "\033[38;5;144m", c("#afafaf"): "\033[38;5;145m", c("#afafd7"): "\033[38;5;146m", c("#afafff"): "\033[38;5;147m",
|
||||||
|
c("#afd700"): "\033[38;5;148m", c("#afd75f"): "\033[38;5;149m", c("#afd787"): "\033[38;5;150m", c("#afd7af"): "\033[38;5;151m",
|
||||||
|
c("#afd7d7"): "\033[38;5;152m", c("#afd7ff"): "\033[38;5;153m", c("#afff00"): "\033[38;5;154m", c("#afff5f"): "\033[38;5;155m",
|
||||||
|
c("#afff87"): "\033[38;5;156m", c("#afffaf"): "\033[38;5;157m", c("#afffd7"): "\033[38;5;158m", c("#afffff"): "\033[38;5;159m",
|
||||||
|
c("#d70000"): "\033[38;5;160m", c("#d7005f"): "\033[38;5;161m", c("#d70087"): "\033[38;5;162m", c("#d700af"): "\033[38;5;163m",
|
||||||
|
c("#d700d7"): "\033[38;5;164m", c("#d700ff"): "\033[38;5;165m", c("#d75f00"): "\033[38;5;166m", c("#d75f5f"): "\033[38;5;167m",
|
||||||
|
c("#d75f87"): "\033[38;5;168m", c("#d75faf"): "\033[38;5;169m", c("#d75fd7"): "\033[38;5;170m", c("#d75fff"): "\033[38;5;171m",
|
||||||
|
c("#d78700"): "\033[38;5;172m", c("#d7875f"): "\033[38;5;173m", c("#d78787"): "\033[38;5;174m", c("#d787af"): "\033[38;5;175m",
|
||||||
|
c("#d787d7"): "\033[38;5;176m", c("#d787ff"): "\033[38;5;177m", c("#d7af00"): "\033[38;5;178m", c("#d7af5f"): "\033[38;5;179m",
|
||||||
|
c("#d7af87"): "\033[38;5;180m", c("#d7afaf"): "\033[38;5;181m", c("#d7afd7"): "\033[38;5;182m", c("#d7afff"): "\033[38;5;183m",
|
||||||
|
c("#d7d700"): "\033[38;5;184m", c("#d7d75f"): "\033[38;5;185m", c("#d7d787"): "\033[38;5;186m", c("#d7d7af"): "\033[38;5;187m",
|
||||||
|
c("#d7d7d7"): "\033[38;5;188m", c("#d7d7ff"): "\033[38;5;189m", c("#d7ff00"): "\033[38;5;190m", c("#d7ff5f"): "\033[38;5;191m",
|
||||||
|
c("#d7ff87"): "\033[38;5;192m", c("#d7ffaf"): "\033[38;5;193m", c("#d7ffd7"): "\033[38;5;194m", c("#d7ffff"): "\033[38;5;195m",
|
||||||
|
c("#ff0000"): "\033[38;5;196m", c("#ff005f"): "\033[38;5;197m", c("#ff0087"): "\033[38;5;198m", c("#ff00af"): "\033[38;5;199m",
|
||||||
|
c("#ff00d7"): "\033[38;5;200m", c("#ff00ff"): "\033[38;5;201m", c("#ff5f00"): "\033[38;5;202m", c("#ff5f5f"): "\033[38;5;203m",
|
||||||
|
c("#ff5f87"): "\033[38;5;204m", c("#ff5faf"): "\033[38;5;205m", c("#ff5fd7"): "\033[38;5;206m", c("#ff5fff"): "\033[38;5;207m",
|
||||||
|
c("#ff8700"): "\033[38;5;208m", c("#ff875f"): "\033[38;5;209m", c("#ff8787"): "\033[38;5;210m", c("#ff87af"): "\033[38;5;211m",
|
||||||
|
c("#ff87d7"): "\033[38;5;212m", c("#ff87ff"): "\033[38;5;213m", c("#ffaf00"): "\033[38;5;214m", c("#ffaf5f"): "\033[38;5;215m",
|
||||||
|
c("#ffaf87"): "\033[38;5;216m", c("#ffafaf"): "\033[38;5;217m", c("#ffafd7"): "\033[38;5;218m", c("#ffafff"): "\033[38;5;219m",
|
||||||
|
c("#ffd700"): "\033[38;5;220m", c("#ffd75f"): "\033[38;5;221m", c("#ffd787"): "\033[38;5;222m", c("#ffd7af"): "\033[38;5;223m",
|
||||||
|
c("#ffd7d7"): "\033[38;5;224m", c("#ffd7ff"): "\033[38;5;225m", c("#ffff00"): "\033[38;5;226m", c("#ffff5f"): "\033[38;5;227m",
|
||||||
|
c("#ffff87"): "\033[38;5;228m", c("#ffffaf"): "\033[38;5;229m", c("#ffffd7"): "\033[38;5;230m", c("#ffffff"): "\033[38;5;231m",
|
||||||
|
c("#080808"): "\033[38;5;232m", c("#121212"): "\033[38;5;233m", c("#1c1c1c"): "\033[38;5;234m", c("#262626"): "\033[38;5;235m",
|
||||||
|
c("#303030"): "\033[38;5;236m", c("#3a3a3a"): "\033[38;5;237m", c("#444444"): "\033[38;5;238m", c("#4e4e4e"): "\033[38;5;239m",
|
||||||
|
c("#585858"): "\033[38;5;240m", c("#626262"): "\033[38;5;241m", c("#6c6c6c"): "\033[38;5;242m", c("#767676"): "\033[38;5;243m",
|
||||||
|
c("#808080"): "\033[38;5;244m", c("#8a8a8a"): "\033[38;5;245m", c("#949494"): "\033[38;5;246m", c("#9e9e9e"): "\033[38;5;247m",
|
||||||
|
c("#a8a8a8"): "\033[38;5;248m", c("#b2b2b2"): "\033[38;5;249m", c("#bcbcbc"): "\033[38;5;250m", c("#c6c6c6"): "\033[38;5;251m",
|
||||||
|
c("#d0d0d0"): "\033[38;5;252m", c("#dadada"): "\033[38;5;253m", c("#e4e4e4"): "\033[38;5;254m", c("#eeeeee"): "\033[38;5;255m",
|
||||||
|
},
|
||||||
|
background: map[chroma.Colour]string{
|
||||||
|
c("#000000"): "\033[48;5;0m", c("#800000"): "\033[48;5;1m", c("#008000"): "\033[48;5;2m", c("#808000"): "\033[48;5;3m",
|
||||||
|
c("#000080"): "\033[48;5;4m", c("#800080"): "\033[48;5;5m", c("#008080"): "\033[48;5;6m", c("#c0c0c0"): "\033[48;5;7m",
|
||||||
|
c("#808080"): "\033[48;5;8m", c("#ff0000"): "\033[48;5;9m", c("#00ff00"): "\033[48;5;10m", c("#ffff00"): "\033[48;5;11m",
|
||||||
|
c("#0000ff"): "\033[48;5;12m", c("#ff00ff"): "\033[48;5;13m", c("#00ffff"): "\033[48;5;14m", c("#ffffff"): "\033[48;5;15m",
|
||||||
|
c("#000000"): "\033[48;5;16m", c("#00005f"): "\033[48;5;17m", c("#000087"): "\033[48;5;18m", c("#0000af"): "\033[48;5;19m",
|
||||||
|
c("#0000d7"): "\033[48;5;20m", c("#0000ff"): "\033[48;5;21m", c("#005f00"): "\033[48;5;22m", c("#005f5f"): "\033[48;5;23m",
|
||||||
|
c("#005f87"): "\033[48;5;24m", c("#005faf"): "\033[48;5;25m", c("#005fd7"): "\033[48;5;26m", c("#005fff"): "\033[48;5;27m",
|
||||||
|
c("#008700"): "\033[48;5;28m", c("#00875f"): "\033[48;5;29m", c("#008787"): "\033[48;5;30m", c("#0087af"): "\033[48;5;31m",
|
||||||
|
c("#0087d7"): "\033[48;5;32m", c("#0087ff"): "\033[48;5;33m", c("#00af00"): "\033[48;5;34m", c("#00af5f"): "\033[48;5;35m",
|
||||||
|
c("#00af87"): "\033[48;5;36m", c("#00afaf"): "\033[48;5;37m", c("#00afd7"): "\033[48;5;38m", c("#00afff"): "\033[48;5;39m",
|
||||||
|
c("#00d700"): "\033[48;5;40m", c("#00d75f"): "\033[48;5;41m", c("#00d787"): "\033[48;5;42m", c("#00d7af"): "\033[48;5;43m",
|
||||||
|
c("#00d7d7"): "\033[48;5;44m", c("#00d7ff"): "\033[48;5;45m", c("#00ff00"): "\033[48;5;46m", c("#00ff5f"): "\033[48;5;47m",
|
||||||
|
c("#00ff87"): "\033[48;5;48m", c("#00ffaf"): "\033[48;5;49m", c("#00ffd7"): "\033[48;5;50m", c("#00ffff"): "\033[48;5;51m",
|
||||||
|
c("#5f0000"): "\033[48;5;52m", c("#5f005f"): "\033[48;5;53m", c("#5f0087"): "\033[48;5;54m", c("#5f00af"): "\033[48;5;55m",
|
||||||
|
c("#5f00d7"): "\033[48;5;56m", c("#5f00ff"): "\033[48;5;57m", c("#5f5f00"): "\033[48;5;58m", c("#5f5f5f"): "\033[48;5;59m",
|
||||||
|
c("#5f5f87"): "\033[48;5;60m", c("#5f5faf"): "\033[48;5;61m", c("#5f5fd7"): "\033[48;5;62m", c("#5f5fff"): "\033[48;5;63m",
|
||||||
|
c("#5f8700"): "\033[48;5;64m", c("#5f875f"): "\033[48;5;65m", c("#5f8787"): "\033[48;5;66m", c("#5f87af"): "\033[48;5;67m",
|
||||||
|
c("#5f87d7"): "\033[48;5;68m", c("#5f87ff"): "\033[48;5;69m", c("#5faf00"): "\033[48;5;70m", c("#5faf5f"): "\033[48;5;71m",
|
||||||
|
c("#5faf87"): "\033[48;5;72m", c("#5fafaf"): "\033[48;5;73m", c("#5fafd7"): "\033[48;5;74m", c("#5fafff"): "\033[48;5;75m",
|
||||||
|
c("#5fd700"): "\033[48;5;76m", c("#5fd75f"): "\033[48;5;77m", c("#5fd787"): "\033[48;5;78m", c("#5fd7af"): "\033[48;5;79m",
|
||||||
|
c("#5fd7d7"): "\033[48;5;80m", c("#5fd7ff"): "\033[48;5;81m", c("#5fff00"): "\033[48;5;82m", c("#5fff5f"): "\033[48;5;83m",
|
||||||
|
c("#5fff87"): "\033[48;5;84m", c("#5fffaf"): "\033[48;5;85m", c("#5fffd7"): "\033[48;5;86m", c("#5fffff"): "\033[48;5;87m",
|
||||||
|
c("#870000"): "\033[48;5;88m", c("#87005f"): "\033[48;5;89m", c("#870087"): "\033[48;5;90m", c("#8700af"): "\033[48;5;91m",
|
||||||
|
c("#8700d7"): "\033[48;5;92m", c("#8700ff"): "\033[48;5;93m", c("#875f00"): "\033[48;5;94m", c("#875f5f"): "\033[48;5;95m",
|
||||||
|
c("#875f87"): "\033[48;5;96m", c("#875faf"): "\033[48;5;97m", c("#875fd7"): "\033[48;5;98m", c("#875fff"): "\033[48;5;99m",
|
||||||
|
c("#878700"): "\033[48;5;100m", c("#87875f"): "\033[48;5;101m", c("#878787"): "\033[48;5;102m", c("#8787af"): "\033[48;5;103m",
|
||||||
|
c("#8787d7"): "\033[48;5;104m", c("#8787ff"): "\033[48;5;105m", c("#87af00"): "\033[48;5;106m", c("#87af5f"): "\033[48;5;107m",
|
||||||
|
c("#87af87"): "\033[48;5;108m", c("#87afaf"): "\033[48;5;109m", c("#87afd7"): "\033[48;5;110m", c("#87afff"): "\033[48;5;111m",
|
||||||
|
c("#87d700"): "\033[48;5;112m", c("#87d75f"): "\033[48;5;113m", c("#87d787"): "\033[48;5;114m", c("#87d7af"): "\033[48;5;115m",
|
||||||
|
c("#87d7d7"): "\033[48;5;116m", c("#87d7ff"): "\033[48;5;117m", c("#87ff00"): "\033[48;5;118m", c("#87ff5f"): "\033[48;5;119m",
|
||||||
|
c("#87ff87"): "\033[48;5;120m", c("#87ffaf"): "\033[48;5;121m", c("#87ffd7"): "\033[48;5;122m", c("#87ffff"): "\033[48;5;123m",
|
||||||
|
c("#af0000"): "\033[48;5;124m", c("#af005f"): "\033[48;5;125m", c("#af0087"): "\033[48;5;126m", c("#af00af"): "\033[48;5;127m",
|
||||||
|
c("#af00d7"): "\033[48;5;128m", c("#af00ff"): "\033[48;5;129m", c("#af5f00"): "\033[48;5;130m", c("#af5f5f"): "\033[48;5;131m",
|
||||||
|
c("#af5f87"): "\033[48;5;132m", c("#af5faf"): "\033[48;5;133m", c("#af5fd7"): "\033[48;5;134m", c("#af5fff"): "\033[48;5;135m",
|
||||||
|
c("#af8700"): "\033[48;5;136m", c("#af875f"): "\033[48;5;137m", c("#af8787"): "\033[48;5;138m", c("#af87af"): "\033[48;5;139m",
|
||||||
|
c("#af87d7"): "\033[48;5;140m", c("#af87ff"): "\033[48;5;141m", c("#afaf00"): "\033[48;5;142m", c("#afaf5f"): "\033[48;5;143m",
|
||||||
|
c("#afaf87"): "\033[48;5;144m", c("#afafaf"): "\033[48;5;145m", c("#afafd7"): "\033[48;5;146m", c("#afafff"): "\033[48;5;147m",
|
||||||
|
c("#afd700"): "\033[48;5;148m", c("#afd75f"): "\033[48;5;149m", c("#afd787"): "\033[48;5;150m", c("#afd7af"): "\033[48;5;151m",
|
||||||
|
c("#afd7d7"): "\033[48;5;152m", c("#afd7ff"): "\033[48;5;153m", c("#afff00"): "\033[48;5;154m", c("#afff5f"): "\033[48;5;155m",
|
||||||
|
c("#afff87"): "\033[48;5;156m", c("#afffaf"): "\033[48;5;157m", c("#afffd7"): "\033[48;5;158m", c("#afffff"): "\033[48;5;159m",
|
||||||
|
c("#d70000"): "\033[48;5;160m", c("#d7005f"): "\033[48;5;161m", c("#d70087"): "\033[48;5;162m", c("#d700af"): "\033[48;5;163m",
|
||||||
|
c("#d700d7"): "\033[48;5;164m", c("#d700ff"): "\033[48;5;165m", c("#d75f00"): "\033[48;5;166m", c("#d75f5f"): "\033[48;5;167m",
|
||||||
|
c("#d75f87"): "\033[48;5;168m", c("#d75faf"): "\033[48;5;169m", c("#d75fd7"): "\033[48;5;170m", c("#d75fff"): "\033[48;5;171m",
|
||||||
|
c("#d78700"): "\033[48;5;172m", c("#d7875f"): "\033[48;5;173m", c("#d78787"): "\033[48;5;174m", c("#d787af"): "\033[48;5;175m",
|
||||||
|
c("#d787d7"): "\033[48;5;176m", c("#d787ff"): "\033[48;5;177m", c("#d7af00"): "\033[48;5;178m", c("#d7af5f"): "\033[48;5;179m",
|
||||||
|
c("#d7af87"): "\033[48;5;180m", c("#d7afaf"): "\033[48;5;181m", c("#d7afd7"): "\033[48;5;182m", c("#d7afff"): "\033[48;5;183m",
|
||||||
|
c("#d7d700"): "\033[48;5;184m", c("#d7d75f"): "\033[48;5;185m", c("#d7d787"): "\033[48;5;186m", c("#d7d7af"): "\033[48;5;187m",
|
||||||
|
c("#d7d7d7"): "\033[48;5;188m", c("#d7d7ff"): "\033[48;5;189m", c("#d7ff00"): "\033[48;5;190m", c("#d7ff5f"): "\033[48;5;191m",
|
||||||
|
c("#d7ff87"): "\033[48;5;192m", c("#d7ffaf"): "\033[48;5;193m", c("#d7ffd7"): "\033[48;5;194m", c("#d7ffff"): "\033[48;5;195m",
|
||||||
|
c("#ff0000"): "\033[48;5;196m", c("#ff005f"): "\033[48;5;197m", c("#ff0087"): "\033[48;5;198m", c("#ff00af"): "\033[48;5;199m",
|
||||||
|
c("#ff00d7"): "\033[48;5;200m", c("#ff00ff"): "\033[48;5;201m", c("#ff5f00"): "\033[48;5;202m", c("#ff5f5f"): "\033[48;5;203m",
|
||||||
|
c("#ff5f87"): "\033[48;5;204m", c("#ff5faf"): "\033[48;5;205m", c("#ff5fd7"): "\033[48;5;206m", c("#ff5fff"): "\033[48;5;207m",
|
||||||
|
c("#ff8700"): "\033[48;5;208m", c("#ff875f"): "\033[48;5;209m", c("#ff8787"): "\033[48;5;210m", c("#ff87af"): "\033[48;5;211m",
|
||||||
|
c("#ff87d7"): "\033[48;5;212m", c("#ff87ff"): "\033[48;5;213m", c("#ffaf00"): "\033[48;5;214m", c("#ffaf5f"): "\033[48;5;215m",
|
||||||
|
c("#ffaf87"): "\033[48;5;216m", c("#ffafaf"): "\033[48;5;217m", c("#ffafd7"): "\033[48;5;218m", c("#ffafff"): "\033[48;5;219m",
|
||||||
|
c("#ffd700"): "\033[48;5;220m", c("#ffd75f"): "\033[48;5;221m", c("#ffd787"): "\033[48;5;222m", c("#ffd7af"): "\033[48;5;223m",
|
||||||
|
c("#ffd7d7"): "\033[48;5;224m", c("#ffd7ff"): "\033[48;5;225m", c("#ffff00"): "\033[48;5;226m", c("#ffff5f"): "\033[48;5;227m",
|
||||||
|
c("#ffff87"): "\033[48;5;228m", c("#ffffaf"): "\033[48;5;229m", c("#ffffd7"): "\033[48;5;230m", c("#ffffff"): "\033[48;5;231m",
|
||||||
|
c("#080808"): "\033[48;5;232m", c("#121212"): "\033[48;5;233m", c("#1c1c1c"): "\033[48;5;234m", c("#262626"): "\033[48;5;235m",
|
||||||
|
c("#303030"): "\033[48;5;236m", c("#3a3a3a"): "\033[48;5;237m", c("#444444"): "\033[48;5;238m", c("#4e4e4e"): "\033[48;5;239m",
|
||||||
|
c("#585858"): "\033[48;5;240m", c("#626262"): "\033[48;5;241m", c("#6c6c6c"): "\033[48;5;242m", c("#767676"): "\033[48;5;243m",
|
||||||
|
c("#808080"): "\033[48;5;244m", c("#8a8a8a"): "\033[48;5;245m", c("#949494"): "\033[48;5;246m", c("#9e9e9e"): "\033[48;5;247m",
|
||||||
|
c("#a8a8a8"): "\033[48;5;248m", c("#b2b2b2"): "\033[48;5;249m", c("#bcbcbc"): "\033[48;5;250m", c("#c6c6c6"): "\033[48;5;251m",
|
||||||
|
c("#d0d0d0"): "\033[48;5;252m", c("#dadada"): "\033[48;5;253m", c("#e4e4e4"): "\033[48;5;254m", c("#eeeeee"): "\033[48;5;255m",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func entryToEscapeSequence(table *ttyTable, entry chroma.StyleEntry) string {
|
||||||
|
out := ""
|
||||||
|
if entry.Bold == chroma.Yes {
|
||||||
|
out += "\033[1m"
|
||||||
|
}
|
||||||
|
if entry.Underline == chroma.Yes {
|
||||||
|
out += "\033[4m"
|
||||||
|
}
|
||||||
|
if entry.Italic == chroma.Yes {
|
||||||
|
out += "\033[3m"
|
||||||
|
}
|
||||||
|
if entry.Colour.IsSet() {
|
||||||
|
out += table.foreground[findClosest(table, entry.Colour)]
|
||||||
|
}
|
||||||
|
if entry.Background.IsSet() {
|
||||||
|
out += table.background[findClosest(table, entry.Background)]
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func findClosest(table *ttyTable, seeking chroma.Colour) chroma.Colour {
|
||||||
|
closestColour := chroma.Colour(0)
|
||||||
|
closest := float64(math.MaxFloat64)
|
||||||
|
for colour := range table.foreground {
|
||||||
|
distance := colour.Distance(seeking)
|
||||||
|
if distance < closest {
|
||||||
|
closest = distance
|
||||||
|
closestColour = colour
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closestColour
|
||||||
|
}
|
||||||
|
|
||||||
|
func styleToEscapeSequence(table *ttyTable, style *chroma.Style) map[chroma.TokenType]string {
|
||||||
|
style = clearBackground(style)
|
||||||
|
out := map[chroma.TokenType]string{}
|
||||||
|
for _, ttype := range style.Types() {
|
||||||
|
entry := style.Get(ttype)
|
||||||
|
out[ttype] = entryToEscapeSequence(table, entry)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear the background colour.
|
||||||
|
func clearBackground(style *chroma.Style) *chroma.Style {
|
||||||
|
builder := style.Builder()
|
||||||
|
bg := builder.Get(chroma.Background)
|
||||||
|
bg.Background = 0
|
||||||
|
bg.NoInherit = true
|
||||||
|
builder.AddEntry(chroma.Background, bg)
|
||||||
|
style, _ = builder.Build()
|
||||||
|
return style
|
||||||
|
}
|
||||||
|
|
||||||
|
type indexedTTYFormatter struct {
|
||||||
|
table *ttyTable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *indexedTTYFormatter) Format(w io.Writer, style *chroma.Style, it chroma.Iterator) (err error) {
|
||||||
|
theme := styleToEscapeSequence(c.table, style)
|
||||||
|
for token := it(); token != chroma.EOF; token = it() {
|
||||||
|
clr, ok := theme[token.Type]
|
||||||
|
|
||||||
|
// This search mimics how styles.Get() is used in tty_truecolour.go.
|
||||||
|
if !ok {
|
||||||
|
clr, ok = theme[token.Type.SubCategory()]
|
||||||
|
if !ok {
|
||||||
|
clr, ok = theme[token.Type.Category()]
|
||||||
|
if !ok {
|
||||||
|
clr, ok = theme[chroma.Text]
|
||||||
|
if !ok {
|
||||||
|
clr = theme[chroma.Background]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if clr != "" {
|
||||||
|
fmt.Fprint(w, clr)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, token.Value)
|
||||||
|
if clr != "" {
|
||||||
|
fmt.Fprintf(w, "\033[0m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TTY is an 8-colour terminal formatter.
|
||||||
|
//
|
||||||
|
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||||
|
var TTY = Register("terminal", &indexedTTYFormatter{ttyTables[8]})
|
||||||
|
|
||||||
|
// TTY8 is an 8-colour terminal formatter.
|
||||||
|
//
|
||||||
|
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||||
|
var TTY8 = Register("terminal8", &indexedTTYFormatter{ttyTables[8]})
|
||||||
|
|
||||||
|
// TTY16 is a 16-colour terminal formatter.
|
||||||
|
//
|
||||||
|
// It uses \033[3xm for normal colours and \033[90Xm for bright colours.
|
||||||
|
//
|
||||||
|
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||||
|
var TTY16 = Register("terminal16", &indexedTTYFormatter{ttyTables[16]})
|
||||||
|
|
||||||
|
// TTY256 is a 256-colour terminal formatter.
|
||||||
|
//
|
||||||
|
// The Lab colour space is used to map RGB values to the most appropriate index colour.
|
||||||
|
var TTY256 = Register("terminal256", &indexedTTYFormatter{ttyTables[256]})
|
36
formatters/tty_indexed_test.go
Normal file
36
formatters/tty_indexed_test.go
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alecthomas/assert/v2"
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClosestColour(t *testing.T) {
|
||||||
|
actual := findClosest(ttyTables[256], chroma.MustParseColour("#e06c75"))
|
||||||
|
assert.Equal(t, chroma.MustParseColour("#d75f87"), actual)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoneColour(t *testing.T) {
|
||||||
|
formatter := TTY256
|
||||||
|
tokenType := chroma.None
|
||||||
|
|
||||||
|
style, err := chroma.NewStyle("test", "dark", chroma.StyleEntries{
|
||||||
|
chroma.Background: "#D0ab1e",
|
||||||
|
})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
stringBuilder := strings.Builder{}
|
||||||
|
err = formatter.Format(&stringBuilder, style, chroma.Literator(chroma.Token{
|
||||||
|
Type: tokenType,
|
||||||
|
Value: "WORD",
|
||||||
|
}))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// "178" = #d7af00 approximates #d0ab1e
|
||||||
|
//
|
||||||
|
// 178 color ref: https://jonasjacek.github.io/colors/
|
||||||
|
assert.Equal(t, "\033[38;5;178mWORD\033[0m", stringBuilder.String())
|
||||||
|
}
|
42
formatters/tty_truecolour.go
Normal file
42
formatters/tty_truecolour.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package formatters
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"toastielab.dev/toastie-stuff/chroma/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TTY16m is a true-colour terminal formatter.
|
||||||
|
var TTY16m = Register("terminal16m", chroma.FormatterFunc(trueColourFormatter))
|
||||||
|
|
||||||
|
func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) error {
|
||||||
|
style = clearBackground(style)
|
||||||
|
for token := it(); token != chroma.EOF; token = it() {
|
||||||
|
entry := style.Get(token.Type)
|
||||||
|
if !entry.IsZero() {
|
||||||
|
out := ""
|
||||||
|
if entry.Bold == chroma.Yes {
|
||||||
|
out += "\033[1m"
|
||||||
|
}
|
||||||
|
if entry.Underline == chroma.Yes {
|
||||||
|
out += "\033[4m"
|
||||||
|
}
|
||||||
|
if entry.Italic == chroma.Yes {
|
||||||
|
out += "\033[3m"
|
||||||
|
}
|
||||||
|
if entry.Colour.IsSet() {
|
||||||
|
out += fmt.Sprintf("\033[38;2;%d;%d;%dm", entry.Colour.Red(), entry.Colour.Green(), entry.Colour.Blue())
|
||||||
|
}
|
||||||
|
if entry.Background.IsSet() {
|
||||||
|
out += fmt.Sprintf("\033[48;2;%d;%d;%dm", entry.Background.Red(), entry.Background.Green(), entry.Background.Blue())
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, out)
|
||||||
|
}
|
||||||
|
fmt.Fprint(w, token.Value)
|
||||||
|
if !entry.IsZero() {
|
||||||
|
fmt.Fprint(w, "\033[0m")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue