Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
feat: XML style definitions
Fixes #635.
  • Loading branch information
alecthomas committed Nov 1, 2022
commit a1656154cc837a89748fa5fd471d9d489101f012
19 changes: 12 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>
Loading