style.go

  1package chroma
  2
  3import (
  4	"encoding/xml"
  5	"fmt"
  6	"io"
  7	"sort"
  8	"strings"
  9)
 10
 11// Trilean value for StyleEntry value inheritance.
 12type Trilean uint8
 13
 14// Trilean states.
 15const (
 16	Pass Trilean = iota
 17	Yes
 18	No
 19)
 20
 21func (t Trilean) String() string {
 22	switch t {
 23	case Yes:
 24		return "Yes"
 25	case No:
 26		return "No"
 27	default:
 28		return "Pass"
 29	}
 30}
 31
 32// Prefix returns s with "no" as a prefix if Trilean is no.
 33func (t Trilean) Prefix(s string) string {
 34	if t == Yes {
 35		return s
 36	} else if t == No {
 37		return "no" + s
 38	}
 39	return ""
 40}
 41
 42// A StyleEntry in the Style map.
 43type StyleEntry struct {
 44	// Hex colours.
 45	Colour     Colour
 46	Background Colour
 47	Border     Colour
 48
 49	Bold      Trilean
 50	Italic    Trilean
 51	Underline Trilean
 52	NoInherit bool
 53}
 54
 55func (s StyleEntry) MarshalText() ([]byte, error) {
 56	return []byte(s.String()), nil
 57}
 58
 59func (s StyleEntry) String() string {
 60	out := []string{}
 61	if s.Bold != Pass {
 62		out = append(out, s.Bold.Prefix("bold"))
 63	}
 64	if s.Italic != Pass {
 65		out = append(out, s.Italic.Prefix("italic"))
 66	}
 67	if s.Underline != Pass {
 68		out = append(out, s.Underline.Prefix("underline"))
 69	}
 70	if s.NoInherit {
 71		out = append(out, "noinherit")
 72	}
 73	if s.Colour.IsSet() {
 74		out = append(out, s.Colour.String())
 75	}
 76	if s.Background.IsSet() {
 77		out = append(out, "bg:"+s.Background.String())
 78	}
 79	if s.Border.IsSet() {
 80		out = append(out, "border:"+s.Border.String())
 81	}
 82	return strings.Join(out, " ")
 83}
 84
 85// Sub subtracts e from s where elements match.
 86func (s StyleEntry) Sub(e StyleEntry) StyleEntry {
 87	out := StyleEntry{}
 88	if e.Colour != s.Colour {
 89		out.Colour = s.Colour
 90	}
 91	if e.Background != s.Background {
 92		out.Background = s.Background
 93	}
 94	if e.Bold != s.Bold {
 95		out.Bold = s.Bold
 96	}
 97	if e.Italic != s.Italic {
 98		out.Italic = s.Italic
 99	}
100	if e.Underline != s.Underline {
101		out.Underline = s.Underline
102	}
103	if e.Border != s.Border {
104		out.Border = s.Border
105	}
106	return out
107}
108
109// Inherit styles from ancestors.
110//
111// Ancestors should be provided from oldest to newest.
112func (s StyleEntry) Inherit(ancestors ...StyleEntry) StyleEntry {
113	out := s
114	for i := len(ancestors) - 1; i >= 0; i-- {
115		if out.NoInherit {
116			return out
117		}
118		ancestor := ancestors[i]
119		if !out.Colour.IsSet() {
120			out.Colour = ancestor.Colour
121		}
122		if !out.Background.IsSet() {
123			out.Background = ancestor.Background
124		}
125		if !out.Border.IsSet() {
126			out.Border = ancestor.Border
127		}
128		if out.Bold == Pass {
129			out.Bold = ancestor.Bold
130		}
131		if out.Italic == Pass {
132			out.Italic = ancestor.Italic
133		}
134		if out.Underline == Pass {
135			out.Underline = ancestor.Underline
136		}
137	}
138	return out
139}
140
141func (s StyleEntry) IsZero() bool {
142	return s.Colour == 0 && s.Background == 0 && s.Border == 0 && s.Bold == Pass && s.Italic == Pass &&
143		s.Underline == Pass && !s.NoInherit
144}
145
146// A StyleBuilder is a mutable structure for building styles.
147//
148// Once built, a Style is immutable.
149type StyleBuilder struct {
150	entries map[TokenType]string
151	name    string
152	parent  *Style
153}
154
155func NewStyleBuilder(name string) *StyleBuilder {
156	return &StyleBuilder{name: name, entries: map[TokenType]string{}}
157}
158
159func (s *StyleBuilder) AddAll(entries StyleEntries) *StyleBuilder {
160	for ttype, entry := range entries {
161		s.entries[ttype] = entry
162	}
163	return s
164}
165
166func (s *StyleBuilder) Get(ttype TokenType) StyleEntry {
167	// This is less than ideal, but it's the price for not having to check errors on each Add().
168	entry, _ := ParseStyleEntry(s.entries[ttype])
169	if s.parent != nil {
170		entry = entry.Inherit(s.parent.Get(ttype))
171	}
172	return entry
173}
174
175// Add an entry to the Style map.
176//
177// See http://pygments.org/docs/styles/#style-rules for details.
178func (s *StyleBuilder) Add(ttype TokenType, entry string) *StyleBuilder { // nolint: gocyclo
179	s.entries[ttype] = entry
180	return s
181}
182
183func (s *StyleBuilder) AddEntry(ttype TokenType, entry StyleEntry) *StyleBuilder {
184	s.entries[ttype] = entry.String()
185	return s
186}
187
188// Transform passes each style entry currently defined in the builder to the supplied
189// function and saves the returned value. This can be used to adjust a style's colours;
190// see Colour's ClampBrightness function, for example.
191func (s *StyleBuilder) Transform(transform func(StyleEntry) StyleEntry) *StyleBuilder {
192	types := make(map[TokenType]struct{})
193	for tt := range s.entries {
194		types[tt] = struct{}{}
195	}
196	if s.parent != nil {
197		for _, tt := range s.parent.Types() {
198			types[tt] = struct{}{}
199		}
200	}
201	for tt := range types {
202		s.AddEntry(tt, transform(s.Get(tt)))
203	}
204	return s
205}
206
207func (s *StyleBuilder) Build() (*Style, error) {
208	style := &Style{
209		Name:    s.name,
210		entries: map[TokenType]StyleEntry{},
211		parent:  s.parent,
212	}
213	for ttype, descriptor := range s.entries {
214		entry, err := ParseStyleEntry(descriptor)
215		if err != nil {
216			return nil, fmt.Errorf("invalid entry for %s: %s", ttype, err)
217		}
218		style.entries[ttype] = entry
219	}
220	return style, nil
221}
222
223// StyleEntries mapping TokenType to colour definition.
224type StyleEntries map[TokenType]string
225
226// NewXMLStyle parses an XML style definition.
227func NewXMLStyle(r io.Reader) (*Style, error) {
228	dec := xml.NewDecoder(r)
229	style := &Style{}
230	return style, dec.Decode(style)
231}
232
233// MustNewXMLStyle is like NewXMLStyle but panics on error.
234func MustNewXMLStyle(r io.Reader) *Style {
235	style, err := NewXMLStyle(r)
236	if err != nil {
237		panic(err)
238	}
239	return style
240}
241
242// NewStyle creates a new style definition.
243func NewStyle(name string, entries StyleEntries) (*Style, error) {
244	return NewStyleBuilder(name).AddAll(entries).Build()
245}
246
247// MustNewStyle creates a new style or panics.
248func MustNewStyle(name string, entries StyleEntries) *Style {
249	style, err := NewStyle(name, entries)
250	if err != nil {
251		panic(err)
252	}
253	return style
254}
255
256// A Style definition.
257//
258// See http://pygments.org/docs/styles/ for details. Semantics are intended to be identical.
259type Style struct {
260	Name    string
261	entries map[TokenType]StyleEntry
262	parent  *Style
263}
264
265func (s *Style) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
266	if s.parent != nil {
267		return fmt.Errorf("cannot marshal style with parent")
268	}
269	start.Name = xml.Name{Local: "style"}
270	start.Attr = []xml.Attr{{Name: xml.Name{Local: "name"}, Value: s.Name}}
271	if err := e.EncodeToken(start); err != nil {
272		return err
273	}
274	sorted := make([]TokenType, 0, len(s.entries))
275	for ttype := range s.entries {
276		sorted = append(sorted, ttype)
277	}
278	sort.Slice(sorted, func(i, j int) bool { return sorted[i] < sorted[j] })
279	for _, ttype := range sorted {
280		entry := s.entries[ttype]
281		el := xml.StartElement{Name: xml.Name{Local: "entry"}}
282		el.Attr = []xml.Attr{
283			{Name: xml.Name{Local: "type"}, Value: ttype.String()},
284			{Name: xml.Name{Local: "style"}, Value: entry.String()},
285		}
286		if err := e.EncodeToken(el); err != nil {
287			return err
288		}
289		if err := e.EncodeToken(xml.EndElement{Name: el.Name}); err != nil {
290			return err
291		}
292	}
293	return e.EncodeToken(xml.EndElement{Name: start.Name})
294}
295
296func (s *Style) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
297	for _, attr := range start.Attr {
298		if attr.Name.Local == "name" {
299			s.Name = attr.Value
300		} else {
301			return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
302		}
303	}
304	if s.Name == "" {
305		return fmt.Errorf("missing style name attribute")
306	}
307	s.entries = map[TokenType]StyleEntry{}
308	for {
309		tok, err := d.Token()
310		if err != nil {
311			return err
312		}
313		switch el := tok.(type) {
314		case xml.StartElement:
315			if el.Name.Local != "entry" {
316				return fmt.Errorf("unexpected element %s", el.Name.Local)
317			}
318			var ttype TokenType
319			var entry StyleEntry
320			for _, attr := range el.Attr {
321				switch attr.Name.Local {
322				case "type":
323					ttype, err = TokenTypeString(attr.Value)
324					if err != nil {
325						return err
326					}
327
328				case "style":
329					entry, err = ParseStyleEntry(attr.Value)
330					if err != nil {
331						return err
332					}
333
334				default:
335					return fmt.Errorf("unexpected attribute %s", attr.Name.Local)
336				}
337			}
338			s.entries[ttype] = entry
339
340		case xml.EndElement:
341			if el.Name.Local == start.Name.Local {
342				return nil
343			}
344		}
345	}
346}
347
348// Types that are styled.
349func (s *Style) Types() []TokenType {
350	dedupe := map[TokenType]bool{}
351	for tt := range s.entries {
352		dedupe[tt] = true
353	}
354	if s.parent != nil {
355		for _, tt := range s.parent.Types() {
356			dedupe[tt] = true
357		}
358	}
359	out := make([]TokenType, 0, len(dedupe))
360	for tt := range dedupe {
361		out = append(out, tt)
362	}
363	return out
364}
365
366// Builder creates a mutable builder from this Style.
367//
368// The builder can then be safely modified. This is a cheap operation.
369func (s *Style) Builder() *StyleBuilder {
370	return &StyleBuilder{
371		name:    s.Name,
372		entries: map[TokenType]string{},
373		parent:  s,
374	}
375}
376
377// Has checks if an exact style entry match exists for a token type.
378//
379// This is distinct from Get() which will merge parent tokens.
380func (s *Style) Has(ttype TokenType) bool {
381	return !s.get(ttype).IsZero() || s.synthesisable(ttype)
382}
383
384// Get a style entry. Will try sub-category or category if an exact match is not found, and
385// finally return the Background.
386func (s *Style) Get(ttype TokenType) StyleEntry {
387	return s.get(ttype).Inherit(
388		s.get(Background),
389		s.get(Text),
390		s.get(ttype.Category()),
391		s.get(ttype.SubCategory()))
392}
393
394func (s *Style) get(ttype TokenType) StyleEntry {
395	out := s.entries[ttype]
396	if out.IsZero() && s.parent != nil {
397		return s.parent.get(ttype)
398	}
399	if out.IsZero() && s.synthesisable(ttype) {
400		out = s.synthesise(ttype)
401	}
402	return out
403}
404
405func (s *Style) synthesise(ttype TokenType) StyleEntry {
406	bg := s.get(Background)
407	text := StyleEntry{Colour: bg.Colour}
408	text.Colour = text.Colour.BrightenOrDarken(0.5)
409
410	switch ttype {
411	// If we don't have a line highlight colour, make one that is 10% brighter/darker than the background.
412	case LineHighlight:
413		return StyleEntry{Background: bg.Background.BrightenOrDarken(0.1)}
414
415	// If we don't have line numbers, use the text colour but 20% brighter/darker
416	case LineNumbers, LineNumbersTable:
417		return text
418
419	default:
420		return StyleEntry{}
421	}
422}
423
424func (s *Style) synthesisable(ttype TokenType) bool {
425	return ttype == LineHighlight || ttype == LineNumbers || ttype == LineNumbersTable
426}
427
428// MustParseStyleEntry parses a Pygments style entry or panics.
429func MustParseStyleEntry(entry string) StyleEntry {
430	out, err := ParseStyleEntry(entry)
431	if err != nil {
432		panic(err)
433	}
434	return out
435}
436
437// ParseStyleEntry parses a Pygments style entry.
438func ParseStyleEntry(entry string) (StyleEntry, error) { // nolint: gocyclo
439	out := StyleEntry{}
440	parts := strings.Fields(entry)
441	for _, part := range parts {
442		switch {
443		case part == "italic":
444			out.Italic = Yes
445		case part == "noitalic":
446			out.Italic = No
447		case part == "bold":
448			out.Bold = Yes
449		case part == "nobold":
450			out.Bold = No
451		case part == "underline":
452			out.Underline = Yes
453		case part == "nounderline":
454			out.Underline = No
455		case part == "inherit":
456			out.NoInherit = false
457		case part == "noinherit":
458			out.NoInherit = true
459		case part == "bg:":
460			out.Background = 0
461		case strings.HasPrefix(part, "bg:#"):
462			out.Background = ParseColour(part[3:])
463			if !out.Background.IsSet() {
464				return StyleEntry{}, fmt.Errorf("invalid background colour %q", part)
465			}
466		case strings.HasPrefix(part, "border:#"):
467			out.Border = ParseColour(part[7:])
468			if !out.Border.IsSet() {
469				return StyleEntry{}, fmt.Errorf("invalid border colour %q", part)
470			}
471		case strings.HasPrefix(part, "#"):
472			out.Colour = ParseColour(part)
473			if !out.Colour.IsSet() {
474				return StyleEntry{}, fmt.Errorf("invalid colour %q", part)
475			}
476		default:
477			return StyleEntry{}, fmt.Errorf("unknown style element %q", part)
478		}
479	}
480	return out, nil
481}