mirror of
https://github.com/alecthomas/chroma.git
synced 2025-01-12 01:22:30 +02:00
Big changes to the style and colour APIs.
- Styles now use a builder system, to enforce immutability of styles. - Corrected and cleaned up how style inheritance works. - Added a brightening function to colours - HTML formatter will now automatically pick line and highlight colours if they are not provided in the style. This is done by slightly darkening or lightening. Fixes #21.
This commit is contained in:
parent
9d7539a4cd
commit
d5083b3f7c
@ -41,9 +41,9 @@ var (
|
||||
htmlInlineStyleFlag = kingpin.Flag("html-inline-styles", "Output HTML with inline styles (no classes).").Bool()
|
||||
htmlTabWidthFlag = kingpin.Flag("html-tab-width", "Set the HTML tab width.").Default("8").Int()
|
||||
htmlLinesFlag = kingpin.Flag("html-line-numbers", "Include line numbers in output.").Bool()
|
||||
htmlLinesStyleFlag = kingpin.Flag("html-line-numbers-style", "Style for line numbers.").Default("#888").String()
|
||||
htmlLinesStyleFlag = kingpin.Flag("html-line-numbers-style", "Style for line numbers.").String()
|
||||
htmlHighlightFlag = kingpin.Flag("html-highlight", "Highlight these lines.").PlaceHolder("N[:M][,...]").String()
|
||||
htmlHighlightStyleFlag = kingpin.Flag("html-highlight-style", "Style used for highlighting lines.").Default("bg:#282828").String()
|
||||
htmlHighlightStyleFlag = kingpin.Flag("html-highlight-style", "Style used for highlighting lines.").String()
|
||||
|
||||
filesArgs = kingpin.Arg("files", "Files to highlight.").ExistingFiles()
|
||||
)
|
||||
@ -98,15 +98,15 @@ command, for Go.
|
||||
}
|
||||
|
||||
// Retrieve user-specified style, clone it, and add some overrides.
|
||||
style := styles.Get(*styleFlag).Clone()
|
||||
builder := styles.Get(*styleFlag).Builder()
|
||||
if *htmlHighlightStyleFlag != "" {
|
||||
err := style.Add(chroma.LineHighlight, *htmlHighlightStyleFlag)
|
||||
kingpin.FatalIfError(err, "invalid line highlight style")
|
||||
builder.Add(chroma.LineHighlight, *htmlHighlightStyleFlag)
|
||||
}
|
||||
if *htmlLinesStyleFlag != "" {
|
||||
err := style.Add(chroma.LineNumbers, *htmlLinesStyleFlag)
|
||||
kingpin.FatalIfError(err, "invalid line style")
|
||||
builder.Add(chroma.LineNumbers, *htmlLinesStyleFlag)
|
||||
}
|
||||
style, err := builder.Build()
|
||||
kingpin.FatalIfError(err, "")
|
||||
|
||||
if *formatterFlag == "html" {
|
||||
options := []html.Option{html.TabWidth(*htmlTabWidthFlag)}
|
||||
|
52
colour.go
52
colour.go
@ -50,21 +50,51 @@ var ANSI2RGB = map[string]string{
|
||||
// Colour represents an RGB colour.
|
||||
type Colour int32
|
||||
|
||||
// func (c1 Colour) Distance(c2 Colour) float64 {
|
||||
// rd := float64(c2.Red() - c1.Red())
|
||||
// gd := float64(c2.Green() - c1.Green())
|
||||
// bd := float64(c2.Blue() - c1.Blue())
|
||||
// return math.Sqrt(2*rd*rd + 4*gd*gd + 3*bd*bd)
|
||||
// }
|
||||
// NewColour creates a Colour directly from RGB values.
|
||||
func NewColour(r, g, b uint8) Colour {
|
||||
return ParseColour(fmt.Sprintf("%02x%02x%02x", r, g, b))
|
||||
}
|
||||
|
||||
func (e1 Colour) Distance(e2 Colour) float64 {
|
||||
rmean := int(e1.Red()+e2.Red()) / 2
|
||||
r := int(e1.Red() - e2.Red())
|
||||
g := int(e1.Green() - e2.Green())
|
||||
b := int(e1.Blue() - e2.Blue())
|
||||
// Distance between this colour and another.
|
||||
//
|
||||
// This uses the approach described here (https://www.compuphase.com/cmetric.htm).
|
||||
// This is not as accurate as LAB, et. al. but is *vastly* simpler and sufficient for our needs.
|
||||
func (c Colour) Distance(e2 Colour) float64 {
|
||||
rmean := int(c.Red()+e2.Red()) / 2
|
||||
r := int(c.Red() - e2.Red())
|
||||
g := int(c.Green() - e2.Green())
|
||||
b := int(c.Blue() - e2.Blue())
|
||||
return math.Sqrt(float64((((512 + rmean) * r * r) >> 8) + 4*g*g + (((767 - rmean) * b * b) >> 8)))
|
||||
}
|
||||
|
||||
// Brighten returns a copy of this colour with its brightness adjusted.
|
||||
//
|
||||
// If factor is negative, the colour is darkened.
|
||||
//
|
||||
// Uses approach described here (http://www.pvladov.com/2012/09/make-color-lighter-or-darker.html).
|
||||
func (c Colour) Brighten(factor float64) Colour {
|
||||
r := float64(c.Red())
|
||||
g := float64(c.Green())
|
||||
b := float64(c.Blue())
|
||||
|
||||
if factor < 0 {
|
||||
factor++
|
||||
r *= factor
|
||||
g *= factor
|
||||
b *= factor
|
||||
} else {
|
||||
r = (255-r)*factor + r
|
||||
g = (255-g)*factor + g
|
||||
b = (255-b)*factor + b
|
||||
}
|
||||
return NewColour(uint8(r), uint8(g), uint8(b))
|
||||
}
|
||||
|
||||
// Brightness of the colour (roughly) in the range 0.0 to 1.0
|
||||
func (c Colour) Brightness() float64 {
|
||||
return (float64(c.Red()) + float64(c.Green()) + float64(c.Blue())) / 255.0 / 3.0
|
||||
}
|
||||
|
||||
// ParseColour in the forms #rgb, #rrggbb, #ansi<colour>, or #<colour>.
|
||||
// Will return an "unset" colour if invalid.
|
||||
func ParseColour(colour string) Colour {
|
||||
|
@ -3,16 +3,40 @@ package chroma
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestColourRGB(t *testing.T) {
|
||||
colour := ParseColour("#8913af")
|
||||
require.Equal(t, uint8(0x89), colour.Red())
|
||||
require.Equal(t, uint8(0x13), colour.Green())
|
||||
require.Equal(t, uint8(0xaf), colour.Blue())
|
||||
assert.Equal(t, uint8(0x89), colour.Red())
|
||||
assert.Equal(t, uint8(0x13), colour.Green())
|
||||
assert.Equal(t, uint8(0xaf), colour.Blue())
|
||||
}
|
||||
|
||||
func TestColourString(t *testing.T) {
|
||||
require.Equal(t, "#8913af", ParseColour("#8913af").String())
|
||||
assert.Equal(t, "#8913af", ParseColour("#8913af").String())
|
||||
}
|
||||
|
||||
func distance(a, b uint8) uint8 {
|
||||
if a < b {
|
||||
return b - a
|
||||
}
|
||||
return a - b
|
||||
}
|
||||
|
||||
func TestColourBrighten(t *testing.T) {
|
||||
actual := NewColour(128, 128, 128).Brighten(0.5)
|
||||
// Closeish to what we expect is fine.
|
||||
assert.True(t, distance(192, actual.Red()) <= 2)
|
||||
assert.True(t, distance(192, actual.Blue()) <= 2)
|
||||
assert.True(t, distance(192, actual.Green()) <= 2)
|
||||
actual = NewColour(128, 128, 128).Brighten(-0.5)
|
||||
assert.True(t, distance(65, actual.Red()) <= 2)
|
||||
assert.True(t, distance(65, actual.Blue()) <= 2)
|
||||
assert.True(t, distance(65, actual.Green()) <= 2)
|
||||
}
|
||||
|
||||
func TestColourBrightess(t *testing.T) {
|
||||
actual := NewColour(128, 128, 128).Brightness()
|
||||
assert.True(t, distance(128, uint8(actual*255.0)) <= 2)
|
||||
}
|
||||
|
@ -76,10 +76,40 @@ func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Ite
|
||||
return f.writeHTML(w, style, iterator.Tokens())
|
||||
}
|
||||
|
||||
func (f *Formatter) writeHTML(w io.Writer, style *chroma.Style, tokens []*chroma.Token) error { // nolint: gocyclo
|
||||
// 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 brightenOrDarken(colour chroma.Colour, factor float64) chroma.Colour {
|
||||
if colour.Brightness() < 0.5 {
|
||||
return colour.Brighten(factor)
|
||||
}
|
||||
return colour.Brighten(-factor)
|
||||
}
|
||||
|
||||
// Ensure that style entries exist for highlighting, etc.
|
||||
func (f *Formatter) restyle(style *chroma.Style) (*chroma.Style, error) {
|
||||
builder := style.Builder()
|
||||
bg := builder.Get(chroma.Background)
|
||||
// If we don't have a line highlight colour, make one that is 10% brighter/darker than the background.
|
||||
if !style.Has(chroma.LineHighlight) {
|
||||
highlight := chroma.StyleEntry{Background: bg.Background}
|
||||
highlight.Background = brightenOrDarken(highlight.Background, 0.1)
|
||||
builder.AddEntry(chroma.LineHighlight, highlight)
|
||||
}
|
||||
// If we don't have line numbers, use the text colour but 20% brighter/darker
|
||||
if !style.Has(chroma.LineNumbers) {
|
||||
text := chroma.StyleEntry{Colour: bg.Colour}
|
||||
text.Colour = brightenOrDarken(text.Colour, 0.5)
|
||||
builder.AddEntry(chroma.LineNumbers, text)
|
||||
}
|
||||
return builder.Build()
|
||||
}
|
||||
|
||||
// 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
|
||||
style, err = f.restyle(style)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
css := f.styleToCSS(style)
|
||||
if !f.classes {
|
||||
for t, style := range css {
|
||||
@ -205,14 +235,15 @@ func (f *Formatter) WriteCSS(w io.Writer, style *chroma.Style) error {
|
||||
}
|
||||
|
||||
func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string {
|
||||
bg := style.Get(chroma.Background)
|
||||
classes := map[chroma.TokenType]string{}
|
||||
bg := style.Get(chroma.Background)
|
||||
// Convert the style.
|
||||
for t, e := range style.Entries {
|
||||
for _, t := range style.Types() {
|
||||
entry := style.Get(t)
|
||||
if t != chroma.Background {
|
||||
e = e.Sub(bg)
|
||||
entry = entry.Sub(bg)
|
||||
}
|
||||
classes[t] = StyleEntryToCSS(e)
|
||||
classes[t] = StyleEntryToCSS(entry)
|
||||
}
|
||||
classes[chroma.Background] += f.tabWidthStyle()
|
||||
classes[chroma.LineNumbers] += "; margin-right: 0.5em"
|
||||
@ -221,7 +252,7 @@ func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string
|
||||
}
|
||||
|
||||
// StyleEntryToCSS converts a chroma.StyleEntry to CSS attributes.
|
||||
func StyleEntryToCSS(e *chroma.StyleEntry) string {
|
||||
func StyleEntryToCSS(e chroma.StyleEntry) string {
|
||||
styles := []string{}
|
||||
if e.Colour.IsSet() {
|
||||
styles = append(styles, "color: "+e.Colour.String())
|
||||
@ -229,10 +260,10 @@ func StyleEntryToCSS(e *chroma.StyleEntry) string {
|
||||
if e.Background.IsSet() {
|
||||
styles = append(styles, "background-color: "+e.Background.String())
|
||||
}
|
||||
if e.Bold {
|
||||
if e.Bold == chroma.Yes {
|
||||
styles = append(styles, "font-weight: bold")
|
||||
}
|
||||
if e.Italic {
|
||||
if e.Italic == chroma.Yes {
|
||||
styles = append(styles, "font-style: italic")
|
||||
}
|
||||
return strings.Join(styles, "; ")
|
||||
|
@ -166,12 +166,12 @@ var ttyTables = map[int]*ttyTable{
|
||||
},
|
||||
}
|
||||
|
||||
func entryToEscapeSequence(table *ttyTable, entry *chroma.StyleEntry) string {
|
||||
func entryToEscapeSequence(table *ttyTable, entry chroma.StyleEntry) string {
|
||||
out := ""
|
||||
if entry.Bold {
|
||||
if entry.Bold == chroma.Yes {
|
||||
out += "\033[1m"
|
||||
}
|
||||
if entry.Underline {
|
||||
if entry.Underline == chroma.Yes {
|
||||
out += "\033[4m"
|
||||
}
|
||||
if entry.Colour.IsSet() {
|
||||
@ -198,7 +198,8 @@ func findClosest(table *ttyTable, seeking chroma.Colour) chroma.Colour {
|
||||
|
||||
func styleToEscapeSequence(table *ttyTable, style *chroma.Style) map[chroma.TokenType]string {
|
||||
out := map[chroma.TokenType]string{}
|
||||
for ttype, entry := range style.Entries {
|
||||
for _, ttype := range style.Types() {
|
||||
entry := style.Get(ttype)
|
||||
out[ttype] = entryToEscapeSequence(table, entry)
|
||||
}
|
||||
return out
|
||||
|
@ -15,10 +15,10 @@ func trueColourFormatter(w io.Writer, style *chroma.Style, it chroma.Iterator) e
|
||||
entry := style.Get(token.Type)
|
||||
if !entry.IsZero() {
|
||||
out := ""
|
||||
if entry.Bold {
|
||||
if entry.Bold == chroma.Yes {
|
||||
out += "\033[1m"
|
||||
}
|
||||
if entry.Underline {
|
||||
if entry.Underline == chroma.Yes {
|
||||
out += "\033[4m"
|
||||
}
|
||||
if entry.Colour.IsSet() {
|
||||
|
98
regexp.go
98
regexp.go
@ -158,6 +158,7 @@ type LexerState struct {
|
||||
Groups []string
|
||||
// Custum context for mutators.
|
||||
MutatorContext map[interface{}]interface{}
|
||||
iteratorStack []Iterator
|
||||
}
|
||||
|
||||
func (l *LexerState) Set(key interface{}, value interface{}) {
|
||||
@ -168,63 +169,60 @@ func (l *LexerState) Get(key interface{}) interface{} {
|
||||
return l.MutatorContext[key]
|
||||
}
|
||||
|
||||
func (l *LexerState) Iterator() Iterator {
|
||||
iteratorStack := []Iterator{}
|
||||
return func() *Token {
|
||||
for l.Pos < len(l.Text) && len(l.Stack) > 0 {
|
||||
// Exhaust the iterator stack, if any.
|
||||
for len(iteratorStack) > 0 {
|
||||
n := len(iteratorStack) - 1
|
||||
t := iteratorStack[n]()
|
||||
if t == nil {
|
||||
iteratorStack = iteratorStack[:n]
|
||||
continue
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
l.State = l.Stack[len(l.Stack)-1]
|
||||
if l.Lexer.trace {
|
||||
fmt.Fprintf(os.Stderr, "%s: pos=%d, text=%q\n", l.State, l.Pos, string(l.Text[l.Pos:]))
|
||||
}
|
||||
ruleIndex, rule, groups := matchRules(l.Text[l.Pos:], l.Rules[l.State])
|
||||
// No match.
|
||||
if groups == nil {
|
||||
l.Pos++
|
||||
return &Token{Error, string(l.Text[l.Pos-1 : l.Pos])}
|
||||
}
|
||||
l.Rule = ruleIndex
|
||||
l.Groups = groups
|
||||
l.Pos += utf8.RuneCountInString(groups[0])
|
||||
if rule.Mutator != nil {
|
||||
if err := rule.Mutator.Mutate(l); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if rule.Type != nil {
|
||||
iteratorStack = append(iteratorStack, rule.Type.Emit(l.Groups, l.Lexer))
|
||||
}
|
||||
}
|
||||
// Exhaust the IteratorStack, if any.
|
||||
// Duplicate code, but eh.
|
||||
for len(iteratorStack) > 0 {
|
||||
n := len(iteratorStack) - 1
|
||||
t := iteratorStack[n]()
|
||||
func (l *LexerState) Iterator() *Token {
|
||||
for l.Pos < len(l.Text) && len(l.Stack) > 0 {
|
||||
// Exhaust the iterator stack, if any.
|
||||
for len(l.iteratorStack) > 0 {
|
||||
n := len(l.iteratorStack) - 1
|
||||
t := l.iteratorStack[n]()
|
||||
if t == nil {
|
||||
iteratorStack = iteratorStack[:n]
|
||||
l.iteratorStack = l.iteratorStack[:n]
|
||||
continue
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// If we get to here and we still have text, return it as an error.
|
||||
if l.Pos != len(l.Text) && len(l.Stack) == 0 {
|
||||
value := string(l.Text[l.Pos:])
|
||||
l.Pos = len(l.Text)
|
||||
return &Token{Type: Error, Value: value}
|
||||
l.State = l.Stack[len(l.Stack)-1]
|
||||
if l.Lexer.trace {
|
||||
fmt.Fprintf(os.Stderr, "%s: pos=%d, text=%q\n", l.State, l.Pos, string(l.Text[l.Pos:]))
|
||||
}
|
||||
ruleIndex, rule, groups := matchRules(l.Text[l.Pos:], l.Rules[l.State])
|
||||
// No match.
|
||||
if groups == nil {
|
||||
l.Pos++
|
||||
return &Token{Error, string(l.Text[l.Pos-1 : l.Pos])}
|
||||
}
|
||||
l.Rule = ruleIndex
|
||||
l.Groups = groups
|
||||
l.Pos += utf8.RuneCountInString(groups[0])
|
||||
if rule.Mutator != nil {
|
||||
if err := rule.Mutator.Mutate(l); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
if rule.Type != nil {
|
||||
l.iteratorStack = append(l.iteratorStack, rule.Type.Emit(l.Groups, l.Lexer))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Exhaust the IteratorStack, if any.
|
||||
// Duplicate code, but eh.
|
||||
for len(l.iteratorStack) > 0 {
|
||||
n := len(l.iteratorStack) - 1
|
||||
t := l.iteratorStack[n]()
|
||||
if t == nil {
|
||||
l.iteratorStack = l.iteratorStack[:n]
|
||||
continue
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
// If we get to here and we still have text, return it as an error.
|
||||
if l.Pos != len(l.Text) && len(l.Stack) == 0 {
|
||||
value := string(l.Text[l.Pos:])
|
||||
l.Pos = len(l.Text)
|
||||
return &Token{Type: Error, Value: value}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type RegexLexer struct {
|
||||
@ -309,7 +307,7 @@ func (r *RegexLexer) Tokenise(options *TokeniseOptions, text string) (Iterator,
|
||||
Rules: r.rules,
|
||||
MutatorContext: map[interface{}]interface{}{},
|
||||
}
|
||||
return state.Iterator(), nil
|
||||
return state.Iterator, nil
|
||||
}
|
||||
|
||||
func matchRules(text []rune, rules []*CompiledRule) (int, *CompiledRule, []string) {
|
||||
|
314
style.go
314
style.go
@ -2,10 +2,38 @@ package chroma
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Trilean value for StyleEntry value inheritance.
|
||||
type Trilean uint8
|
||||
|
||||
const (
|
||||
Pass Trilean = iota
|
||||
Yes
|
||||
No
|
||||
)
|
||||
|
||||
func (t Trilean) String() string {
|
||||
switch t {
|
||||
case Yes:
|
||||
return "Yes"
|
||||
case No:
|
||||
return "No"
|
||||
default:
|
||||
return "Pass"
|
||||
}
|
||||
}
|
||||
|
||||
func (t Trilean) Prefix(s string) string {
|
||||
if t == Yes {
|
||||
return s
|
||||
} else if t == No {
|
||||
return "no" + s
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// A StyleEntry in the Style map.
|
||||
type StyleEntry struct {
|
||||
// Hex colours.
|
||||
@ -13,28 +41,25 @@ type StyleEntry struct {
|
||||
Background Colour
|
||||
Border Colour
|
||||
|
||||
Bold bool
|
||||
Italic bool
|
||||
Underline bool
|
||||
Bold Trilean
|
||||
Italic Trilean
|
||||
Underline Trilean
|
||||
NoInherit bool
|
||||
}
|
||||
|
||||
// Clone this StyleEntry.
|
||||
func (s *StyleEntry) Clone() *StyleEntry {
|
||||
clone := &StyleEntry{}
|
||||
*clone = *s
|
||||
return clone
|
||||
}
|
||||
|
||||
func (s *StyleEntry) String() string {
|
||||
func (s StyleEntry) String() string {
|
||||
out := []string{}
|
||||
if s.Bold {
|
||||
out = append(out, "bold")
|
||||
if s.Bold != Pass {
|
||||
out = append(out, s.Bold.Prefix("bold"))
|
||||
}
|
||||
if s.Italic {
|
||||
out = append(out, "italic")
|
||||
if s.Italic != Pass {
|
||||
out = append(out, s.Italic.Prefix("italic"))
|
||||
}
|
||||
if s.Underline {
|
||||
out = append(out, "underline")
|
||||
if s.Underline != Pass {
|
||||
out = append(out, s.Underline.Prefix("underline"))
|
||||
}
|
||||
if s.NoInherit {
|
||||
out = append(out, "noinherit")
|
||||
}
|
||||
if s.Colour.IsSet() {
|
||||
out = append(out, s.Colour.String())
|
||||
@ -48,12 +73,8 @@ func (s *StyleEntry) String() string {
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
func (s *StyleEntry) IsZero() bool {
|
||||
return s.Colour == 0 && s.Background == 0 && s.Border == 0 && !s.Bold && !s.Italic && !s.Underline
|
||||
}
|
||||
|
||||
func (s *StyleEntry) Sub(e *StyleEntry) *StyleEntry {
|
||||
out := &StyleEntry{}
|
||||
func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
|
||||
out := StyleEntry{}
|
||||
if e.Colour != s.Colour {
|
||||
out.Colour = s.Colour
|
||||
}
|
||||
@ -75,22 +96,104 @@ func (s *StyleEntry) Sub(e *StyleEntry) *StyleEntry {
|
||||
return out
|
||||
}
|
||||
|
||||
// Inherit styles from ancestors.
|
||||
//
|
||||
// Ancestors should be provided from oldest to newest.
|
||||
func (s StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry {
|
||||
out := s
|
||||
for i := len(ancestors) - 1; i >= 0; i-- {
|
||||
if out.NoInherit {
|
||||
return out
|
||||
}
|
||||
ancestor := ancestors[i]
|
||||
if !out.Colour.IsSet() {
|
||||
out.Colour = ancestor.Colour
|
||||
}
|
||||
if !out.Background.IsSet() {
|
||||
out.Background = ancestor.Background
|
||||
}
|
||||
if !out.Border.IsSet() {
|
||||
out.Border = ancestor.Border
|
||||
}
|
||||
if out.Bold == Pass {
|
||||
out.Bold = ancestor.Bold
|
||||
}
|
||||
if out.Italic == Pass {
|
||||
out.Italic = ancestor.Italic
|
||||
}
|
||||
if out.Underline == Pass {
|
||||
out.Underline = ancestor.Underline
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s StyleEntry) IsZero() bool {
|
||||
return s.Colour == 0 && s.Background == 0 && s.Border == 0 && s.Bold == Pass && s.Italic == Pass &&
|
||||
s.Underline == Pass && !s.NoInherit
|
||||
}
|
||||
|
||||
// A StyleBuilder is a mutable structure for building styles.
|
||||
//
|
||||
// Once built, a Style is immutable.
|
||||
type StyleBuilder struct {
|
||||
entries map[TokenType]string
|
||||
name string
|
||||
parent *Style
|
||||
}
|
||||
|
||||
func NewStyleBuilder(name string) *StyleBuilder {
|
||||
return &StyleBuilder{name: name, entries: map[TokenType]string{}}
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) AddAll(entries StyleEntries) *StyleBuilder {
|
||||
for ttype, entry := range entries {
|
||||
s.entries[ttype] = entry
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) Get(ttype TokenType) StyleEntry {
|
||||
// This is less than ideal, but it's the price for having to check errors on each Add().
|
||||
entry, _ := ParseStyleEntry(s.entries[ttype])
|
||||
return entry.Inherit(s.parent.Get(ttype))
|
||||
}
|
||||
|
||||
// Add an entry to the Style map.
|
||||
//
|
||||
// See http://pygments.org/docs/styles/#style-rules for details.
|
||||
func (s *StyleBuilder) Add(ttype TokenType, entry string) *StyleBuilder { // nolint: gocyclo
|
||||
s.entries[ttype] = entry
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) AddEntry(ttype TokenType, entry StyleEntry) *StyleBuilder {
|
||||
s.entries[ttype] = entry.String()
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *StyleBuilder) Build() (*Style, error) {
|
||||
style := &Style{
|
||||
Name: s.name,
|
||||
entries: map[TokenType]StyleEntry{},
|
||||
parent: s.parent,
|
||||
}
|
||||
for ttype, descriptor := range s.entries {
|
||||
entry, err := ParseStyleEntry(descriptor)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid entry for %s: %s", ttype, err)
|
||||
}
|
||||
style.entries[ttype] = entry
|
||||
}
|
||||
return style, nil
|
||||
}
|
||||
|
||||
// StyleEntries mapping TokenType to colour definition.
|
||||
type StyleEntries map[TokenType]string
|
||||
|
||||
// NewStyle creates a new style definition.
|
||||
func NewStyle(name string, entries StyleEntries) (*Style, error) {
|
||||
s := &Style{
|
||||
Name: name,
|
||||
Entries: map[TokenType]*StyleEntry{},
|
||||
}
|
||||
if err := s.Add(Background, ""); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.AddAll(entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
return NewStyleBuilder(name).AddAll(entries).Build()
|
||||
}
|
||||
|
||||
// MustNewStyle creates a new style or panics.
|
||||
@ -107,128 +210,105 @@ func MustNewStyle(name string, entries StyleEntries) *Style {
|
||||
// See http://pygments.org/docs/styles/ for details. Semantics are intended to be identical.
|
||||
type Style struct {
|
||||
Name string
|
||||
Entries map[TokenType]*StyleEntry
|
||||
entries map[TokenType]StyleEntry
|
||||
parent *Style
|
||||
}
|
||||
|
||||
// Clone this style. The clone can then be safely modified.
|
||||
func (s *Style) Clone() *Style {
|
||||
clone := &Style{
|
||||
Name: s.Name,
|
||||
Entries: map[TokenType]*StyleEntry{},
|
||||
// Types that are styled.
|
||||
func (s *Style) Types() []TokenType {
|
||||
dedupe := map[TokenType]bool{}
|
||||
for tt := range s.entries {
|
||||
dedupe[tt] = true
|
||||
}
|
||||
for tt, e := range s.Entries {
|
||||
clone.Entries[tt] = e.Clone()
|
||||
}
|
||||
return clone
|
||||
}
|
||||
|
||||
// Get a style entry. Will try sub-category or category if an exact match is not found, and
|
||||
// finally return the entry mapped to `InheritStyle`.
|
||||
func (s *Style) Get(ttype TokenType) *StyleEntry {
|
||||
out := s.Entries[ttype]
|
||||
if out == nil {
|
||||
out = s.Entries[ttype.SubCategory()]
|
||||
if out == nil {
|
||||
out = s.Entries[ttype.Category()]
|
||||
if out == nil {
|
||||
out = s.Entries[Background]
|
||||
}
|
||||
if s.parent != nil {
|
||||
for _, tt := range s.parent.Types() {
|
||||
dedupe[tt] = true
|
||||
}
|
||||
}
|
||||
out := make([]TokenType, 0, len(dedupe))
|
||||
for tt := range dedupe {
|
||||
out = append(out, tt)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Style) AddAll(entries StyleEntries) error {
|
||||
tis := []int{}
|
||||
for tt := range entries {
|
||||
tis = append(tis, int(tt))
|
||||
// Builder creates a mutable builder from this Style.
|
||||
//
|
||||
// The builder can then be safely modified. This is a cheap operation.
|
||||
func (s *Style) Builder() *StyleBuilder {
|
||||
return &StyleBuilder{
|
||||
name: s.Name,
|
||||
entries: map[TokenType]string{},
|
||||
parent: s,
|
||||
}
|
||||
sort.Ints(tis)
|
||||
for _, ti := range tis {
|
||||
tt := TokenType(ti)
|
||||
entry := entries[tt]
|
||||
if err := s.Add(tt, entry); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add a StyleEntry to the Style map.
|
||||
// Has checks if an exact style entry match exists for a token type.
|
||||
//
|
||||
// See http://pygments.org/docs/styles/#style-rules for details.
|
||||
func (s *Style) Add(ttype TokenType, entry string) error { // nolint: gocyclo
|
||||
dupl := s.Entries[ttype.SubCategory()]
|
||||
if dupl == nil {
|
||||
dupl = s.Entries[ttype.Category()]
|
||||
if dupl == nil {
|
||||
dupl = s.Entries[Background]
|
||||
if dupl == nil {
|
||||
dupl = &StyleEntry{}
|
||||
}
|
||||
}
|
||||
// This is distinct from Get() which will merge parent tokens.
|
||||
func (s *Style) Has(ttype TokenType) bool {
|
||||
return !s.get(ttype).IsZero()
|
||||
}
|
||||
|
||||
func (s *Style) get(ttype TokenType) StyleEntry {
|
||||
out := s.entries[ttype]
|
||||
if out.IsZero() && s.parent != nil {
|
||||
return s.parent.get(ttype)
|
||||
}
|
||||
parent := &StyleEntry{}
|
||||
// Duplicate ancestor node.
|
||||
*parent = *dupl
|
||||
se, err := ParseStyleEntry(parent, entry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.Entries[ttype] = se
|
||||
return nil
|
||||
return out
|
||||
}
|
||||
|
||||
// Get a style entry. Will try sub-category or category if an exact match is not found, and
|
||||
// finally return the Background.
|
||||
func (s *Style) Get(ttype TokenType) StyleEntry {
|
||||
return s.get(ttype).Inherit(
|
||||
s.get(Background),
|
||||
s.get(Text),
|
||||
s.get(ttype.Category()),
|
||||
s.get(ttype.SubCategory()))
|
||||
}
|
||||
|
||||
// ParseStyleEntry parses a Pygments style entry.
|
||||
func ParseStyleEntry(parent *StyleEntry, entry string) (*StyleEntry, error) { // nolint: gocyclo
|
||||
out := &StyleEntry{}
|
||||
func ParseStyleEntry(entry string) (StyleEntry, error) { // nolint: gocyclo
|
||||
out := StyleEntry{}
|
||||
parts := strings.Fields(entry)
|
||||
// Check if parent style should be inherited...
|
||||
if parent != nil {
|
||||
inherit := true
|
||||
for _, part := range parts {
|
||||
if part == "noinherit" {
|
||||
inherit = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if inherit {
|
||||
*out = *parent
|
||||
}
|
||||
}
|
||||
for _, part := range parts {
|
||||
switch {
|
||||
case part == "italic":
|
||||
out.Italic = true
|
||||
out.Italic = Yes
|
||||
case part == "noitalic":
|
||||
out.Italic = false
|
||||
out.Italic = No
|
||||
case part == "bold":
|
||||
out.Bold = true
|
||||
out.Bold = Yes
|
||||
case part == "nobold":
|
||||
out.Bold = false
|
||||
out.Bold = No
|
||||
case part == "underline":
|
||||
out.Underline = true
|
||||
out.Underline = Yes
|
||||
case part == "nounderline":
|
||||
out.Underline = false
|
||||
out.Underline = No
|
||||
case part == "inherit":
|
||||
out.NoInherit = false
|
||||
case part == "noinherit":
|
||||
out.NoInherit = true
|
||||
case part == "bg:":
|
||||
out.Background = 0
|
||||
case strings.HasPrefix(part, "bg:#"):
|
||||
out.Background = ParseColour(part[3:])
|
||||
if !out.Background.IsSet() {
|
||||
return nil, fmt.Errorf("invalid background colour %q", part)
|
||||
return StyleEntry{}, fmt.Errorf("invalid background colour %q", part)
|
||||
}
|
||||
case strings.HasPrefix(part, "border:#"):
|
||||
out.Border = ParseColour(part[7:])
|
||||
if !out.Border.IsSet() {
|
||||
return nil, fmt.Errorf("invalid border colour %q", part)
|
||||
return StyleEntry{}, fmt.Errorf("invalid border colour %q", part)
|
||||
}
|
||||
case strings.HasPrefix(part, "#"):
|
||||
out.Colour = ParseColour(part)
|
||||
if !out.Colour.IsSet() {
|
||||
return nil, fmt.Errorf("invalid colour %q", part)
|
||||
return StyleEntry{}, fmt.Errorf("invalid colour %q", part)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown style element %q", part)
|
||||
return StyleEntry{}, fmt.Errorf("unknown style element %q", part)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
|
@ -3,7 +3,7 @@ package chroma
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStyleInherit(t *testing.T) {
|
||||
@ -11,14 +11,27 @@ func TestStyleInherit(t *testing.T) {
|
||||
Name: "bold #f00",
|
||||
NameVariable: "#fff",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &StyleEntry{Colour: 0x1000000, Bold: true}, s.Get(NameVariable))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, StyleEntry{Colour: 0x1000000, Bold: Yes}, s.Get(NameVariable))
|
||||
}
|
||||
|
||||
func TestColours(t *testing.T) {
|
||||
func TestStyleColours(t *testing.T) {
|
||||
s, err := NewStyle("test", StyleEntries{
|
||||
Name: "#f00 bg:#001 border:#ansiblue",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, &StyleEntry{Colour: 0xff0001, Background: 0x000012, Border: 0x000100}, s.Get(Name))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, StyleEntry{Colour: 0xff0001, Background: 0x000012, Border: 0x000100}, s.Get(Name))
|
||||
}
|
||||
|
||||
func TestStyleClone(t *testing.T) {
|
||||
parent, err := NewStyle("test", StyleEntries{
|
||||
Background: "bg:#ffffff",
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
clone, err := parent.Builder().Add(Comment, "#0f0").Build()
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "bg:#ffffff", clone.Get(Background).String())
|
||||
assert.Equal(t, "#00ff00 bg:#ffffff", clone.Get(Comment).String())
|
||||
assert.Equal(t, "bg:#ffffff", parent.Get(Comment).String())
|
||||
}
|
||||
|
@ -32,5 +32,5 @@ var Monokai = Register(chroma.MustNewStyle("monokai", chroma.StyleEntries{
|
||||
chroma.GenericInserted: "#a6e22e",
|
||||
chroma.GenericStrong: "bold",
|
||||
chroma.GenericSubheading: "#75715e",
|
||||
chroma.Background: " bg:#272822",
|
||||
chroma.Background: "bg:#272822",
|
||||
}))
|
||||
|
@ -40,5 +40,5 @@ var ParaisoDark = Register(chroma.MustNewStyle("paraiso-dark", chroma.StyleEntri
|
||||
chroma.GenericPrompt: "bold #776e71",
|
||||
chroma.GenericStrong: "bold",
|
||||
chroma.GenericSubheading: "bold #5bc4bf",
|
||||
chroma.Background: " bg:#2f1e2e",
|
||||
chroma.Background: "bg:#2f1e2e",
|
||||
}))
|
||||
|
@ -40,5 +40,5 @@ var ParaisoLight = Register(chroma.MustNewStyle("paraiso-light", chroma.StyleEnt
|
||||
chroma.GenericPrompt: "bold #8d8687",
|
||||
chroma.GenericStrong: "bold",
|
||||
chroma.GenericSubheading: "bold #5bc4bf",
|
||||
chroma.Background: " bg:#e7e9db",
|
||||
chroma.Background: "bg:#e7e9db",
|
||||
}))
|
||||
|
Loading…
Reference in New Issue
Block a user