diff --git a/cmd/chroma/main.go b/cmd/chroma/main.go index 696bd03..614a41d 100644 --- a/cmd/chroma/main.go +++ b/cmd/chroma/main.go @@ -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)} diff --git a/colour.go b/colour.go index e48dd5f..edaa480 100644 --- a/colour.go +++ b/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, or #. // Will return an "unset" colour if invalid. func ParseColour(colour string) Colour { diff --git a/colour_test.go b/colour_test.go index c1b6189..9281399 100644 --- a/colour_test.go +++ b/colour_test.go @@ -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) } diff --git a/formatters/html/html.go b/formatters/html/html.go index f500b47..f02ea9e 100644 --- a/formatters/html/html.go +++ b/formatters/html/html.go @@ -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, "; ") diff --git a/formatters/tty_indexed.go b/formatters/tty_indexed.go index cb9cd81..d033c99 100644 --- a/formatters/tty_indexed.go +++ b/formatters/tty_indexed.go @@ -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 diff --git a/formatters/tty_truecolour.go b/formatters/tty_truecolour.go index 71f85e4..d332f07 100644 --- a/formatters/tty_truecolour.go +++ b/formatters/tty_truecolour.go @@ -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() { diff --git a/regexp.go b/regexp.go index 602e2e0..3953017 100644 --- a/regexp.go +++ b/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) { diff --git a/style.go b/style.go index f02014f..c768293 100644 --- a/style.go +++ b/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 diff --git a/style_test.go b/style_test.go index 1ad6282..fd8a983 100644 --- a/style_test.go +++ b/style_test.go @@ -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()) } diff --git a/styles/monokai.go b/styles/monokai.go index 9d887ec..b40c190 100644 --- a/styles/monokai.go +++ b/styles/monokai.go @@ -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", })) diff --git a/styles/paraiso-dark.go b/styles/paraiso-dark.go index f362bbd..bde61ae 100644 --- a/styles/paraiso-dark.go +++ b/styles/paraiso-dark.go @@ -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", })) diff --git a/styles/paraiso-light.go b/styles/paraiso-light.go index 63f5fa2..e2836ab 100644 --- a/styles/paraiso-light.go +++ b/styles/paraiso-light.go @@ -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", }))