mirror of
https://github.com/cheat/cheat.git
synced 2024-11-16 17:08:29 +01:00
1790aec85d
Bump `alecthomas/chroma` to `v2`: https://github.com/cheat/cheat/issues/735
481 lines
11 KiB
Go
481 lines
11 KiB
Go
package chroma
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// Trilean value for StyleEntry value inheritance.
|
|
type Trilean uint8
|
|
|
|
// Trilean states.
|
|
const (
|
|
Pass Trilean = iota
|
|
Yes
|
|
No
|
|
)
|
|
|
|
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
|
|
}
|