package chroma
import (
"encoding/xml"
"fmt"
"io"
"sort"
"strings"
)// Trilean value for StyleEntry value inheritance.type Trilean uint8
// Trilean states.const (
Pass Trilean = iota
YesNo)func (t Trilean) String() string {
switch t {
case Yes:
return "Yes"
case No:
return "No"
default:
return "Pass"
}}// Prefix returns s with "no" as a prefix if Trilean is no.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.
Colour Colour
Background Colour
Border Colour
Bold Trilean
Italic Trilean
Underline Trilean
NoInherit bool
}func (s StyleEntry) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}func (s StyleEntry) String() string {
out := []string{}
if s.Bold != Pass {
out = append(out, s.Bold.Prefix("bold"))
}if s.Italic != Pass {
out = append(out, s.Italic.Prefix("italic"))
}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())
}if s.Background.IsSet() {
out = append(out, "bg:"+s.Background.String())
}if s.Border.IsSet() {
out = append(out, "border:"+s.Border.String())
}return strings.Join(out, " ")
}// Sub subtracts e from s where elements match.func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
out := StyleEntry{}
if e.Colour != s.Colour {
out.Colour = s.Colour
}if e.Background != s.Background {
out.Background = s.Background
}if e.Bold != s.Bold {
out.Bold = s.Bold
}if e.Italic != s.Italic {
out.Italic = s.Italic
}if e.Underline != s.Underline {
out.Underline = s.Underline
}if e.Border != s.Border {
out.Border = s.Border
}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 not having to check errors on each Add().
entry, _ := ParseStyleEntry(s.entries[ttype])
if s.parent != nil {
entry = entry.Inherit(s.parent.Get(ttype))
}return entry
}// 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
}// Transform passes each style entry currently defined in the builder to the supplied// function and saves the returned value. This can be used to adjust a style's colours;// see Colour's ClampBrightness function, for example.func (s *StyleBuilder) Transform(transform func(StyleEntry) StyleEntry) *StyleBuilder {
types := make(map[TokenType]struct{})
for tt := range s.entries {
types[tt] = struct{}{}
}if s.parent != nil {
for _, tt := range s.parent.Types() {
types[tt] = struct{}{}
}}for tt := range types {
s.AddEntry(tt, transform(s.Get(tt)))
}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
// NewXMLStyle parses an XML style definition.func NewXMLStyle(r io.Reader) (*Style, error) {
dec := xml.NewDecoder(r)
style := &Style{}
return style, dec.Decode(style)
}// MustNewXMLStyle is like NewXMLStyle but panics on error.func MustNewXMLStyle(r io.Reader) *Style {
style, err := NewXMLStyle(r)
if err != nil {
panic(err)
}return style
}// NewStyle creates a new style definition.func NewStyle(name string, entries StyleEntries) (*Style, error) {
return NewStyleBuilder(name).AddAll(entries).Build()
}// MustNewStyle creates a new style or panics.func MustNewStyle(name string, entries StyleEntries) *Style {
style, err := NewStyle(name, entries)
if err != nil {
panic(err)
}return style
}// A Style definition.//// See http://pygments.org/docs/styles/ for details. Semantics are intended to be identical.type Style struct {
Name string
entries map[TokenType]StyleEntry
parent *Style
}func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
if s.parent != nil {
return fmt.Errorf("cannot marshal style with parent")
}start.Name = xml.Name{Local: "style"}
start.Attr = []xml.Attr{{Name: xml.Name{Local: "name"}, Value: s.Name}}
if err := e.EncodeToken(start); err != nil {
return err
}sorted := make([]TokenType, 0, len(s.entries))
for ttype := range s.entries {
sorted = append(sorted, ttype)
}sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
for _, ttype := range sorted {
entry := s.entries[ttype]
el := xml.StartElement{Name: xml.Name{Local: "entry"}}
el.Attr = []xml.Attr{
{Name: xml.Name{Local: "type"}, Value: ttype.String()},
{Name: xml.Name{Local: "style"}, Value: entry.String()},
}if err := e.EncodeToken(el); err != nil {
return err
}if err := e.EncodeToken(xml.EndElement{Name: el.Name}); err != nil {
return err
}}return e.EncodeToken(xml.EndElement{Name: start.Name})
}func (s *Style) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
for _, attr := range start.Attr {
if attr.Name.Local == "name" {
s.Name = attr.Value
} else {
return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
}}if s.Name == "" {
return fmt.Errorf("missing style name attribute")
}s.entries = map[TokenType]StyleEntry{}
for {
tok, err := d.Token()
if err != nil {
return err
}switch el := tok.(type) {
case xml.StartElement:
if el.Name.Local != "entry" {
return fmt.Errorf("unexpected element %s", el.Name.Local)
}var ttype TokenType
var entry StyleEntry
for _, attr := range el.Attr {
switch attr.Name.Local {
case "type":
ttype, err = TokenTypeString(attr.Value)
if err != nil {
return err
}case "style":
entry, err = ParseStyleEntry(attr.Value)
if err != nil {
return err
}default:
return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
}}s.entries[ttype] = entry
case xml.EndElement:
if el.Name.Local == start.Name.Local {
return nil
}}}}// Types that are styled.func (s *Style) Types() []TokenType {
dedupe := map[TokenType]bool{}
for tt := range s.entries {
dedupe[tt] = true
}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
}// 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,}}// Has checks if an exact style entry match exists for a token type.//// This is distinct from Get() which will merge parent tokens.func (s *Style) Has(ttype TokenType) bool {
return !s.get(ttype).IsZero() || s.synthesisable(ttype)
}// 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()))
}func (s *Style) get(ttype TokenType) StyleEntry {
out := s.entries[ttype]
if out.IsZero() && s.parent != nil {
return s.parent.get(ttype)
}if out.IsZero() && s.synthesisable(ttype) {
out = s.synthesise(ttype)
}return out
}func (s *Style) synthesise(ttype TokenType) StyleEntry {
bg := s.get(Background)
text := StyleEntry{Colour: bg.Colour}
text.Colour = text.Colour.BrightenOrDarken(0.5)
switch ttype {
// If we don't have a line highlight colour, make one that is 10% brighter/darker than the background.
case LineHighlight:
return StyleEntry{Background: bg.Background.BrightenOrDarken(0.1)}
// If we don't have line numbers, use the text colour but 20% brighter/darker
case LineNumbers, LineNumbersTable:
return text
default:
return StyleEntry{}
}}func (s *Style) synthesisable(ttype TokenType) bool {
return ttype == LineHighlight || ttype == LineNumbers || ttype == LineNumbersTable
}// MustParseStyleEntry parses a Pygments style entry or panics.func MustParseStyleEntry(entry string) StyleEntry {
out, err := ParseStyleEntry(entry)
if err != nil {
panic(err)
}return out
}// ParseStyleEntry parses a Pygments style entry.func ParseStyleEntry(entry string) (StyleEntry, error) { // nolint: gocyclo
out := StyleEntry{}
parts := strings.Fields(entry)
for _, part := range parts {
switch {
case part == "italic":
out.Italic = Yes
case part == "noitalic":
out.Italic = No
case part == "bold":
out.Bold = Yes
case part == "nobold":
out.Bold = No
case part == "underline":
out.Underline = Yes
case part == "nounderline":
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 StyleEntry{}, fmt.Errorf("invalid background colour %q", part)
}case strings.HasPrefix(part, "border:#"):
out.Border = ParseColour(part[7:])
if !out.Border.IsSet() {
return StyleEntry{}, fmt.Errorf("invalid border colour %q", part)
}case strings.HasPrefix(part, "#"):
out.Colour = ParseColour(part)
if !out.Colour.IsSet() {
return StyleEntry{}, fmt.Errorf("invalid colour %q", part)
}default:
return StyleEntry{}, fmt.Errorf("unknown style element %q", part)
}}return out, nil
}