Skip to content

Commit

Permalink
feat: XML style definitions (#693)
Browse files Browse the repository at this point in the history
Fixes #635.
  • Loading branch information
alecthomas committed Nov 2, 2022
1 parent b264397 commit c263f6f
Show file tree
Hide file tree
Showing 132 changed files with 2,595 additions and 2,997 deletions.
19 changes: 12 additions & 7 deletions README.md
Expand Up @@ -230,17 +230,22 @@ formatter outputs raw tokens. The latter is useful for debugging lexers.
<a id="markdown-styles" name="styles"></a>
### Styles

Chroma styles use the [same syntax](http://pygments.org/docs/styles/) as Pygments.
Chroma styles are defined in XML. The style entries use the
[same syntax](http://pygments.org/docs/styles/) as Pygments.

All Pygments styles have been converted to Chroma using the `_tools/style.py` script.
All Pygments styles have been converted to Chroma using the `_tools/style.py`
script.

When you work with one of [Chroma's styles](https://github.com/alecthomas/chroma/tree/master/styles), know that the `chroma.Background` token type provides the default style for tokens. It does so by defining a foreground color and background color.
When you work with one of [Chroma's styles](https://github.com/alecthomas/chroma/tree/master/styles),
know that the `Background` token type provides the default style for tokens. It does so
by defining a foreground color and background color.

For example, this gives each token name not defined in the style a default color of `#f8f8f8` and uses `#000000` for the highlighted code block's background:
For example, this gives each token name not defined in the style a default color
of `#f8f8f8` and uses `#000000` for the highlighted code block's background:

~~~go
chroma.Background: "#f8f8f2 bg:#000000",
~~~
```xml
<entry type="Background" style="#f8f8f2 bg:#000000"/>
```

Also, token types in a style file are hierarchical. For instance, when `CommentSpecial` is not defined, Chroma uses the token style from `Comment`. So when several comment tokens use the same color, you'll only need to define `Comment` and override the one that has a different color.

Expand Down
File renamed without changes.
1 change: 1 addition & 0 deletions bin/enumer
1 change: 0 additions & 1 deletion bin/stringer

This file was deleted.

2 changes: 1 addition & 1 deletion formatters/html/html.go
Expand Up @@ -520,7 +520,7 @@ func (f *Formatter) styleToCSS(style *chroma.Style) map[chroma.TokenType]string
classes[chroma.LineNumbersTable] = lineNumbersStyle + classes[chroma.LineNumbersTable]
classes[chroma.LineTable] = "border-spacing: 0; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTable]
classes[chroma.LineTableTD] = "vertical-align: top; padding: 0; margin: 0; border: 0;" + classes[chroma.LineTableTD]
classes[chroma.LineLink] = "outline: none; text-decoration:none; color:inherit" + classes[chroma.LineLink]
classes[chroma.LineLink] = "outline: none; text-decoration: none; color: inherit" + classes[chroma.LineLink]
return classes
}

Expand Down
2 changes: 1 addition & 1 deletion formatters/html/html_test.go
Expand Up @@ -222,7 +222,7 @@ func TestTableLinkeableLineNumbers(t *testing.T) {

assert.Contains(t, buf.String(), `id="line1"><a class="lnlinks" href="#line1">1</a>`)
assert.Contains(t, buf.String(), `id="line5"><a class="lnlinks" href="#line5">5</a>`)
assert.Contains(t, buf.String(), `/* LineLinks */ .chroma .lnlinks { outline: none; text-decoration:none; color:inherit }`, buf.String())
assert.Contains(t, buf.String(), `/* LineLink */ .chroma .lnlinks { outline: none; text-decoration: none; color: inherit }`, buf.String())
}

func TestTableLineNumberSpacing(t *testing.T) {
Expand Down
11 changes: 5 additions & 6 deletions serialise.go
Expand Up @@ -372,13 +372,12 @@ func (t *TokenType) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
if err := d.DecodeElement(&el, &start); err != nil {
return err
}
for tt, text := range _TokenType_map {
if text == el.Type {
*t = tt
return nil
}
tt, err := TokenTypeString(el.Type)
if err != nil {
return err
}
return fmt.Errorf("unknown TokenType %q", el.Type)
*t = tt
return nil
}

func (t TokenType) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
Expand Down
106 changes: 106 additions & 0 deletions style.go
@@ -1,7 +1,10 @@
package chroma

import (
"encoding/xml"
"fmt"
"io"
"sort"
"strings"
)

Expand Down Expand Up @@ -49,6 +52,10 @@ type StyleEntry struct {
NoInherit bool
}

func (s StyleEntry) MarshalText() ([]byte, error) {
return []byte(s.String()), nil
}

func (s StyleEntry) String() string {
out := []string{}
if s.Bold != Pass {
Expand Down Expand Up @@ -216,6 +223,13 @@ func (s *StyleBuilder) Build() (*Style, error) {
// 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)
}

// NewStyle creates a new style definition.
func NewStyle(name string, entries StyleEntries) (*Style, error) {
return NewStyleBuilder(name).AddAll(entries).Build()
Expand All @@ -239,6 +253,89 @@ type Style struct {
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{}
Expand Down Expand Up @@ -319,6 +416,15 @@ 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{}
Expand Down
19 changes: 19 additions & 0 deletions style_test.go
@@ -1,6 +1,7 @@
package chroma

import (
"encoding/xml"
"testing"

assert "github.com/alecthomas/assert/v2"
Expand Down Expand Up @@ -101,3 +102,21 @@ func TestStyleBuilderTransform(t *testing.T) {
assert.Equal(t, "#ff0000", orig.Get(NameVariable).Colour.String())
assert.Equal(t, "#ff3300", deriv.Get(NameVariableGlobal).Colour.String())
}

func TestStyleMarshaller(t *testing.T) {
expected, err := NewStyle("test", StyleEntries{
Whitespace: "bg:#ffffff",
Text: "#000000 underline",
})
assert.NoError(t, err)
data, err := xml.MarshalIndent(expected, "", " ")
assert.NoError(t, err)
assert.Equal(t, `<style name="test">
<entry type="Text" style="underline #000000"></entry>
<entry type="TextWhitespace" style="bg:#ffffff"></entry>
</style>`, string(data))
actual := &Style{}
err = xml.Unmarshal(data, actual)
assert.NoError(t, err)
assert.Equal(t, expected, actual)
}
18 changes: 0 additions & 18 deletions styles/abap.go

This file was deleted.

11 changes: 11 additions & 0 deletions styles/abap.xml
@@ -0,0 +1,11 @@
<style name="abap">
<entry type="Error" style="#ff0000"/>
<entry type="Background" style="bg:#ffffff"/>
<entry type="Keyword" style="#0000ff"/>
<entry type="Name" style="#000000"/>
<entry type="LiteralString" style="#55aa22"/>
<entry type="LiteralNumber" style="#33aaff"/>
<entry type="OperatorWord" style="#0000ff"/>
<entry type="Comment" style="italic #888888"/>
<entry type="CommentSpecial" style="#888888"/>
</style>
25 changes: 0 additions & 25 deletions styles/algol.go

This file was deleted.

18 changes: 18 additions & 0 deletions styles/algol.xml
@@ -0,0 +1,18 @@
<style name="algol">
<entry type="Error" style="border:#ff0000"/>
<entry type="Background" style="bg:#ffffff"/>
<entry type="Keyword" style="bold underline"/>
<entry type="KeywordDeclaration" style="italic"/>
<entry type="NameBuiltin" style="bold italic"/>
<entry type="NameBuiltinPseudo" style="bold italic"/>
<entry type="NameClass" style="bold italic #666666"/>
<entry type="NameConstant" style="bold italic #666666"/>
<entry type="NameFunction" style="bold italic #666666"/>
<entry type="NameNamespace" style="bold italic #666666"/>
<entry type="NameVariable" style="bold italic #666666"/>
<entry type="LiteralString" style="italic #666666"/>
<entry type="OperatorWord" style="bold"/>
<entry type="Comment" style="italic #888888"/>
<entry type="CommentSpecial" style="bold noitalic #888888"/>
<entry type="CommentPreproc" style="bold noitalic #888888"/>
</style>
25 changes: 0 additions & 25 deletions styles/algol_nu.go

This file was deleted.

18 changes: 18 additions & 0 deletions styles/algol_nu.xml
@@ -0,0 +1,18 @@
<style name="algol_nu">
<entry type="Error" style="border:#ff0000"/>
<entry type="Background" style="bg:#ffffff"/>
<entry type="Keyword" style="bold"/>
<entry type="KeywordDeclaration" style="italic"/>
<entry type="NameBuiltin" style="bold italic"/>
<entry type="NameBuiltinPseudo" style="bold italic"/>
<entry type="NameClass" style="bold italic #666666"/>
<entry type="NameConstant" style="bold italic #666666"/>
<entry type="NameFunction" style="bold italic #666666"/>
<entry type="NameNamespace" style="bold italic #666666"/>
<entry type="NameVariable" style="bold italic #666666"/>
<entry type="LiteralString" style="italic #666666"/>
<entry type="OperatorWord" style="bold"/>
<entry type="Comment" style="italic #888888"/>
<entry type="CommentSpecial" style="bold noitalic #888888"/>
<entry type="CommentPreproc" style="bold noitalic #888888"/>
</style>

0 comments on commit c263f6f

Please sign in to comment.