1
0
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:
Alec Thomas 2017-09-23 21:55:56 +10:00
parent 9d7539a4cd
commit d5083b3f7c
12 changed files with 393 additions and 216 deletions

View File

@ -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)}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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, "; ")

View File

@ -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

View File

@ -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() {

View File

@ -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
View File

@ -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

View File

@ -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())
}

View File

@ -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",
}))

View File

@ -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",
}))

View File

@ -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",
}))