1
0
mirror of https://github.com/alecthomas/chroma.git synced 2025-03-19 21:10:15 +02:00

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)
This commit is contained in:
rsteube 2019-09-28 23:27:59 +02:00 committed by Alec Thomas
parent 0ff62486a5
commit 22511fb8e4
3 changed files with 153 additions and 0 deletions

View File

@ -64,6 +64,8 @@ command, for Go.
HTMLBaseLine int `help:"Base line number." default:"1"` HTMLBaseLine int `help:"Base line number." default:"1"`
HTMLPreventSurroundingPre bool `help:"Prevent the surrounding pre tag."` HTMLPreventSurroundingPre bool `help:"Prevent the surrounding pre tag."`
SVG bool `help:"Output SVG representation of tokens."`
Files []string `arg:"" optional:"" help:"Files to highlight." type:"existingfile"` Files []string `arg:"" optional:"" help:"Files to highlight." type:"existingfile"`
} }
) )
@ -123,6 +125,10 @@ func main() {
cli.Formatter = "html" cli.Formatter = "html"
} }
if cli.SVG {
cli.Formatter = "svg"
}
// Retrieve user-specified style, clone it, and add some overrides. // Retrieve user-specified style, clone it, and add some overrides.
builder := styles.Get(cli.Style).Builder() builder := styles.Get(cli.Style).Builder()
if cli.HTMLHighlightStyle != "" { if cli.HTMLHighlightStyle != "" {

View File

@ -6,6 +6,7 @@ import (
"github.com/alecthomas/chroma" "github.com/alecthomas/chroma"
"github.com/alecthomas/chroma/formatters/html" "github.com/alecthomas/chroma/formatters/html"
"github.com/alecthomas/chroma/formatters/svg"
) )
var ( var (
@ -20,6 +21,7 @@ var (
})) }))
// Default HTML formatter outputs self-contained HTML. // Default HTML formatter outputs self-contained HTML.
htmlFull = Register("html", html.New(html.Standalone(), html.WithClasses())) // nolint htmlFull = Register("html", html.New(html.Standalone(), html.WithClasses())) // nolint
Svg = Register("svg", svg.New())
) )
// Fallback formatter. // Fallback formatter.

145
formatters/svg/svg.go Normal file
View File

@ -0,0 +1,145 @@
package svg
import (
"fmt"
"io"
"strings"
"github.com/alecthomas/chroma"
)
// Option sets an option of the SVG formatter.
type Option func(f *Formatter)
// New SVG formatter.
func New(options ...Option) *Formatter {
f := &Formatter{}
for _, option := range options {
option(f)
}
return f
}
// Formatter that generates SVG.
type Formatter struct {
}
func (f *Formatter) Format(w io.Writer, style *chroma.Style, iterator chroma.Iterator) (err error) {
defer func() {
if perr := recover(); perr != nil {
err = perr.(error)
}
}()
f.writeSVG(w, style, iterator.Tokens())
return err
}
var svgEscaper = strings.NewReplacer(
`&`, "&amp;",
`<`, "&lt;",
`>`, "&gt;",
`"`, "&quot;",
` `, "&#160;",
` `, "&#160;&#160;&#160;&#160;",
)
// EscapeString escapes special characters.
func escapeString(s string) string {
return svgEscaper.Replace(s)
}
func (f *Formatter) writeSVG(w io.Writer, style *chroma.Style, tokens []chroma.Token) { // nolint: gocyclo
svgStyles := f.styleToSVG(style)
lines := chroma.SplitTokensIntoLines(tokens)
fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
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")
fmt.Fprintf(w, "<svg height=\"%d\" xmlns=\"http://www.w3.org/2000/svg\">\n", len(lines)*22)
fmt.Fprintf(w, "<rect width=\"100%%\" height=\"100%%\" fill=\"%s\"/>\n", style.Get(chroma.Background).Background.String())
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())
f.writeTokenBackgrounds(w, lines, style)
for index, tokens := range lines {
fmt.Fprintf(w, "<text x=\"0\" y=\"%d\" xml:space=\"preserve\">", (14+7)*(index+1))
for _, token := range tokens {
text := escapeString(token.String())
attr := f.styleAttr(svgStyles, token.Type)
if attr != "" {
text = fmt.Sprintf("<tspan %s>%s</tspan>", attr, text)
}
fmt.Fprint(w, text)
}
fmt.Fprint(w, "</text>")
}
fmt.Fprint(w, "\n</g>\n")
fmt.Fprint(w, "</svg>\n")
}
// There is no background attribute for text in SVG so simply calculate the position and text
// of tokens with a background color that differs from the default and add a rectangle for each before
// adding the token.
func (f *Formatter) writeTokenBackgrounds(w io.Writer, lines [][]chroma.Token, style *chroma.Style) {
for index, tokens := range lines {
lineLength := 0
for _, token := range tokens {
length := len(strings.Replace(token.String(), ` `, " ", -1))
tokenBackground := style.Get(token.Type).Background
if tokenBackground.IsSet() && tokenBackground != style.Get(chroma.Background).Background {
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())
}
lineLength += length
}
}
}
func (f *Formatter) styleAttr(styles map[chroma.TokenType]string, tt chroma.TokenType) string {
if _, ok := styles[tt]; !ok {
tt = tt.SubCategory()
if _, ok := styles[tt]; !ok {
tt = tt.Category()
if _, ok := styles[tt]; !ok {
return ""
}
}
}
return styles[tt]
}
func (f *Formatter) styleToSVG(style *chroma.Style) map[chroma.TokenType]string {
converted := map[chroma.TokenType]string{}
bg := style.Get(chroma.Background)
// Convert the style.
for t := range chroma.StandardTypes {
entry := style.Get(t)
if t != chroma.Background {
entry = entry.Sub(bg)
}
if entry.IsZero() {
continue
}
converted[t] = StyleEntryToSVG(entry)
}
return converted
}
// StyleEntryToSVG converts a chroma.StyleEntry to SVG attributes.
func StyleEntryToSVG(e chroma.StyleEntry) string {
var styles []string
if e.Colour.IsSet() {
styles = append(styles, "fill=\""+e.Colour.String()+"\"")
}
if e.Bold == chroma.Yes {
styles = append(styles, "font-weight=\"bold\"")
}
if e.Italic == chroma.Yes {
styles = append(styles, "font-style=\"italic\"")
}
if e.Underline == chroma.Yes {
styles = append(styles, "text-decoration=\"underline\"")
}
return strings.Join(styles, " ")
}