Skip to content

Commit 22511fb

Browse files
rsteubealecthomas
authored andcommitted
added svg formatter
basic version without any options. colors and font-styles seem to be ok. rough support for text background in styles like `murphy` using predrawn rectangles (svg has no text background attribute). things to improve: - svg width attribute (`<svg width=""`) - linenumbers - highlighting - embedded font - tabwidth option - margins? - better position/width calculation (rectangles not correctly drawn on resize)
1 parent 0ff6248 commit 22511fb

3 files changed

Lines changed: 153 additions & 0 deletions

File tree

cmd/chroma/main.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ command, for Go.
6464
HTMLBaseLine int `help:"Base line number." default:"1"`
6565
HTMLPreventSurroundingPre bool `help:"Prevent the surrounding pre tag."`
6666

67+
SVG bool `help:"Output SVG representation of tokens."`
68+
6769
Files []string `arg:"" optional:"" help:"Files to highlight." type:"existingfile"`
6870
}
6971
)
@@ -123,6 +125,10 @@ func main() {
123125
cli.Formatter = "html"
124126
}
125127

128+
if cli.SVG {
129+
cli.Formatter = "svg"
130+
}
131+
126132
// Retrieve user-specified style, clone it, and add some overrides.
127133
builder := styles.Get(cli.Style).Builder()
128134
if cli.HTMLHighlightStyle != "" {

formatters/api.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
"github.com/alecthomas/chroma"
88
"github.com/alecthomas/chroma/formatters/html"
9+
"github.com/alecthomas/chroma/formatters/svg"
910
)
1011

1112
var (
@@ -20,6 +21,7 @@ var (
2021
}))
2122
// Default HTML formatter outputs self-contained HTML.
2223
htmlFull = Register("html", html.New(html.Standalone(), html.WithClasses())) // nolint
24+
Svg = Register("svg", svg.New())
2325
)
2426

2527
// Fallback formatter.

formatters/svg/svg.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package svg
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"strings"
7+
8+
"github.com/alecthomas/chroma"
9+
)
10+
11+
// Option sets an option of the SVG formatter.
12+
type Option func(f *Formatter)
13+
14+
// New SVG formatter.
15+
func New(options ...Option) *Formatter {
16+
f := &Formatter{}
17+
for _, option := range options {
18+
option(f)
19+
}
20+
return f
21+
}
22+
23+
// Formatter that generates SVG.
24+
type Formatter struct {
25+
}
26+
27+
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
28+
defer func() {
29+
if perr := recover(); perr != nil {
30+
err = perr.(error)
31+
}
32+
}()
33+
f.writeSVG(w, style, iterator.Tokens())
34+
return err
35+
}
36+
37+
var svgEscaper = strings.NewReplacer(
38+
`&`, "&amp;",
39+
`<`, "&lt;",
40+
`>`, "&gt;",
41+
`"`, "&quot;",
42+
` `, "&#160;",
43+
` `, "&#160;&#160;&#160;&#160;",
44+
)
45+
46+
// EscapeString escapes special characters.
47+
func escapeString(s string) string {
48+
return svgEscaper.Replace(s)
49+
}
50+
51+
func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo
52+
svgStyles := f.styleToSVG(style)
53+
lines := chroma.SplitTokensIntoLines(tokens)
54+
55+
fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
56+
fmt.Fprint(w, "<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.0//EN\" \"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd\">\n")
57+
fmt.Fprintf(w, "<svg height=\"%d\" xmlns=\"http://www.w3.org/2000/svg\">\n", len(lines)*22)
58+
fmt.Fprintf(w, "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n", style.Get(chroma.Background).Background.String())
59+
fmt.Fprintf(w, "<g font-family=\"Consolas, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace\" font-size=\"14px\" fill=\"%s\">\n", style.Get(chroma.Text).Colour.String())
60+
61+
f.writeTokenBackgrounds(w, lines, style)
62+
63+
for index, tokens := range lines {
64+
fmt.Fprintf(w, "<text x=\"0\" y=\"%d\" xml:space=\"preserve\">", (14+7)*(index+1))
65+
66+
for _, token := range tokens {
67+
text := escapeString(token.String())
68+
attr := f.styleAttr(svgStyles, token.Type)
69+
if attr != "" {
70+
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
71+
}
72+
fmt.Fprint(w, text)
73+
}
74+
fmt.Fprint(w, "</text>")
75+
}
76+
77+
fmt.Fprint(w, "\n</g>\n")
78+
fmt.Fprint(w, "</svg>\n")
79+
}
80+
81+
// There is no background attribute for text in SVG so simply calculate the position and text
82+
// of tokens with a background color that differs from the default and add a rectangle for each before
83+
// adding the token.
84+
func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) {
85+
for index, tokens := range lines {
86+
lineLength := 0
87+
for _, token := range tokens {
88+
length := len(strings.Replace(token.String(), ` `, " ", -1))
89+
tokenBackground := style.Get(token.Type).Background
90+
if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
91+
fmt.Fprintf(w, "<rect id=\"%s\" x=\"%d\" y=\"%d\" width=\"%d\" height=\"%d\" fill=\"%s\" />\n", escapeString(token.String()), lineLength*8, (14+7)*index+7, length*8, 14, style.Get(token.Type).Background.String())
92+
}
93+
lineLength += length
94+
}
95+
}
96+
}
97+
98+
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
99+
if _, ok := styles[tt]; !ok {
100+
tt = tt.SubCategory()
101+
if _, ok := styles[tt]; !ok {
102+
tt = tt.Category()
103+
if _, ok := styles[tt]; !ok {
104+
return ""
105+
}
106+
}
107+
}
108+
return styles[tt]
109+
}
110+
111+
func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string {
112+
converted := map[chroma.TokenType]string{}
113+
bg := style.Get(chroma.Background)
114+
// Convert the style.
115+
for t := range chroma.StandardTypes {
116+
entry := style.Get(t)
117+
if t != chroma.Background {
118+
entry = entry.Sub(bg)
119+
}
120+
if entry.IsZero() {
121+
continue
122+
}
123+
converted[t] = StyleEntryToSVG(entry)
124+
}
125+
return converted
126+
}
127+
128+
// StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes.
129+
func StyleEntryToSVG(e chroma.StyleEntry) string {
130+
var styles []string
131+
132+
if e.Colour.IsSet() {
133+
styles = append(styles, "fill=\""+e.Colour.String()+"\"")
134+
}
135+
if e.Bold == chroma.Yes {
136+
styles = append(styles, "font-weight=\"bold\"")
137+
}
138+
if e.Italic == chroma.Yes {
139+
styles = append(styles, "font-style=\"italic\"")
140+
}
141+
if e.Underline == chroma.Yes {
142+
styles = append(styles, "text-decoration=\"underline\"")
143+
}
144+
return strings.Join(styles, " ")
145+
}

0 commit comments

Comments
 (0)