1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
|
package main
import (
"bufio"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/signal"
"path"
"path/filepath"
"regexp"
"runtime"
"runtime/pprof"
"sort"
"strconv"
"strings"
"github.com/alecthomas/kong"
colorable "github.com/mattn/go-colorable"
isatty "github.com/mattn/go-isatty"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/lexers"
"github.com/alecthomas/chroma/v2/styles"
)
var (
// Populated by goreleaser.
version = "?"
commit = "?"
date = "?"
description = `
Chroma is a general purpose syntax highlighting library and corresponding
command, for Go.
`
cli struct {
Version kong.VersionFlag `help:"Show version."`
Profile string `hidden:"" help:"Enable profiling to file."`
List bool `help:"List lexers, styles and formatters."`
Unbuffered bool `help:"Do not buffer output."`
Trace bool `help:"Trace lexer states as they are traversed."`
Check bool `help:"Do not format, check for tokenisation errors instead."`
Filename string `help:"Filename to use for selecting a lexer when reading from stdin."`
Fail bool `help:"Exit silently with status 1 if no specific lexer was found."`
XML string `hidden:"" help:"Generate XML lexer definitions." type:"existingdir" placeholder:"DIR"`
Lexer string `group:"select" help:"Lexer to use when formatting or path to an XML file to load." default:"autodetect" short:"l"`
Style string `group:"select" help:"Style to use for formatting or path to an XML file to load." default:"swapoff" short:"s"`
Formatter string `group:"format" help:"Formatter to use." default:"terminal" short:"f" enum:"${formatters}"`
JSON bool `group:"format" help:"Convenience flag to use JSON formatter."`
HTML bool `group:"format" help:"Convenience flag to use HTML formatter."`
SVG bool `group:"format" help:"Convenience flag to use SVG formatter."`
HTMLPrefix string `group:"html" help:"HTML CSS class prefix." placeholder:"PREFIX"`
HTMLStyles bool `group:"html" help:"Output HTML CSS styles."`
HTMLAllStyles bool `group:"html" help:"Output all HTML CSS styles, including redundant ones."`
HTMLOnly bool `group:"html" help:"Output HTML fragment."`
HTMLInlineStyles bool `group:"html" help:"Output HTML with inline styles (no classes)."`
HTMLTabWidth int `group:"html" help:"Set the HTML tab width." default:"8"`
HTMLLines bool `group:"html" help:"Include line numbers in output."`
HTMLLinesTable bool `group:"html" help:"Split line numbers and code in a HTML table"`
HTMLLinesStyle string `group:"html" help:"Style for line numbers."`
HTMLHighlight string `group:"html" help:"Highlight these lines." placeholder:"N[:M][,...]"`
HTMLHighlightStyle string `group:"html" help:"Style used for highlighting lines."`
HTMLBaseLine int `group:"html" help:"Base line number." default:"1"`
HTMLPreventSurroundingPre bool `group:"html" help:"Prevent the surrounding pre tag."`
HTMLLinkableLines bool `group:"html" help:"Make the line numbers linkable and be a link to themselves."`
Files []string `arg:"" optional:"" help:"Files to highlight." type:"existingfile"`
}
)
type flushableWriter interface {
io.Writer
Flush() error
}
type nopFlushableWriter struct{ io.Writer }
func (n *nopFlushableWriter) Flush() error { return nil }
// prepareLenient prepares contents and lexer for input, using fallback lexer if no specific one is available for it.
func prepareLenient(r io.Reader, filename string) (string, chroma.Lexer, error) {
data, err := ioutil.ReadAll(r)
if err != nil {
return "", nil, err
}
contents := string(data)
lexer, err := selexer(filename, contents)
if err != nil {
return "", nil, err
}
if lexer == nil {
lexer = lexers.Fallback
}
return contents, lexer, nil
}
// prepareSpecific prepares contents and lexer for input, exiting if there is no specific lexer available for it.
// Input is consumed only up to peekSize for lexer selection. With fullSize -1, consume r until EOF.
func prepareSpecific(ctx *kong.Context, r io.Reader, filename string, peekSize, fullSize int) (string, chroma.Lexer) {
data := make([]byte, peekSize)
n, err := io.ReadFull(r, data)
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
ctx.FatalIfErrorf(err)
}
lexer, err := selexer(filename, string(data[:n]))
if err != nil {
ctx.FatalIfErrorf(err)
}
if lexer == nil {
ctx.Exit(1)
}
if n < peekSize {
return string(data[:n]), lexer
}
var ndata []byte
if fullSize == -1 {
rest, err := io.ReadAll(r)
ctx.FatalIfErrorf(err)
ndata = make([]byte, n+len(rest))
copy(ndata, data[:n])
copy(ndata[n:], rest)
} else {
ndata = make([]byte, fullSize)
copy(ndata, data[:n])
_, err = io.ReadFull(r, ndata[n:])
ctx.FatalIfErrorf(err)
}
return string(ndata), lexer
}
func main() {
ctx := kong.Parse(&cli, kong.Description(description), kong.Vars{
"version": fmt.Sprintf("%s-%s-%s", version, commit, date),
"lexers": "autodetect," + strings.Join(lexers.Names(true), ","),
"styles": strings.Join(styles.Names(), ","),
"formatters": strings.Join(formatters.Names(), ","),
}, kong.Groups{
"format": "Output format:",
"select": "Select lexer and style:",
"html": "HTML formatter options:",
})
if cli.XML != "" {
err := dumpXMLLexerDefinitions(cli.XML)
ctx.FatalIfErrorf(err)
return
}
if cli.List {
listAll()
return
}
if cli.Profile != "" {
f, err := os.Create(cli.Profile)
ctx.FatalIfErrorf(err)
err = pprof.StartCPUProfile(f)
ctx.FatalIfErrorf(err)
signals := make(chan os.Signal, 1)
signal.Notify(signals, os.Interrupt)
go func() {
<-signals
pprof.StopCPUProfile()
os.Exit(128 + 3)
}()
defer pprof.StopCPUProfile()
}
if path.Base(os.Args[0]) == ".lessfilter" {
// https://manpages.debian.org/lesspipe#USER_DEFINED_FILTERS
cli.Fail = true
}
var out io.Writer = os.Stdout
if runtime.GOOS == "windows" && isatty.IsTerminal(os.Stdout.Fd()) {
out = colorable.NewColorableStdout()
}
var w flushableWriter
if cli.Unbuffered {
w = &nopFlushableWriter{out}
} else {
w = bufio.NewWriterSize(out, 16384)
}
defer w.Flush() // nolint: errcheck
switch {
case cli.JSON:
cli.Formatter = "json"
case cli.HTML:
cli.Formatter = "html"
case cli.SVG:
cli.Formatter = "svg"
}
// Retrieve user-specified style, clone it, and add some overrides.
selectedStyle, err := selectStyle()
ctx.FatalIfErrorf(err)
builder := selectedStyle.Builder()
if cli.HTMLHighlightStyle != "" {
builder.Add(chroma.LineHighlight, cli.HTMLHighlightStyle)
}
if cli.HTMLLinesStyle != "" {
builder.Add(chroma.LineNumbers, cli.HTMLLinesStyle)
}
style, err := builder.Build()
ctx.FatalIfErrorf(err)
if cli.Formatter == "html" {
configureHTMLFormatter(ctx)
}
// Dump styles.
if cli.HTMLStyles {
formatter := formatters.Get("html").(*html.Formatter)
err = formatter.WriteCSS(w, style)
ctx.FatalIfErrorf(err)
return
}
if len(cli.Files) == 0 {
var contents string
var lexer chroma.Lexer
if cli.Fail {
contents, lexer = prepareSpecific(ctx, os.Stdin, cli.Filename, 1024, -1)
} else {
contents, lexer, err = prepareLenient(os.Stdin, cli.Filename)
ctx.FatalIfErrorf(err)
}
format(ctx, w, style, lex(ctx, lexer, contents))
} else {
for _, filename := range cli.Files {
file, err := os.Open(filename)
ctx.FatalIfErrorf(err)
if cli.Check {
contents, lexer, err := prepareLenient(file, filename)
ctx.FatalIfErrorf(err)
check(filename, lex(ctx, lexer, contents))
} else {
var contents string
var lexer chroma.Lexer
if cli.Fail {
fi, err := file.Stat()
ctx.FatalIfErrorf(err)
contents, lexer = prepareSpecific(ctx, file, filename, 1024, int(fi.Size()))
} else {
contents, lexer, err = prepareLenient(file, filename)
ctx.FatalIfErrorf(err)
}
format(ctx, w, style, lex(ctx, lexer, contents))
}
err = file.Close()
ctx.FatalIfErrorf(err)
}
}
}
func selectStyle() (*chroma.Style, error) {
style, ok := styles.Registry[cli.Style]
if ok {
return style, nil
}
r, err := os.Open(cli.Style)
if err != nil {
return nil, err
}
defer r.Close()
return chroma.NewXMLStyle(r)
}
func configureHTMLFormatter(ctx *kong.Context) {
options := []html.Option{
html.TabWidth(cli.HTMLTabWidth),
html.BaseLineNumber(cli.HTMLBaseLine),
html.ClassPrefix(cli.HTMLPrefix),
html.WithAllClasses(cli.HTMLAllStyles),
html.WithClasses(!cli.HTMLInlineStyles),
html.Standalone(!cli.HTMLOnly),
html.WithLineNumbers(cli.HTMLLines),
html.LineNumbersInTable(cli.HTMLLinesTable),
html.PreventSurroundingPre(cli.HTMLPreventSurroundingPre),
html.WithLinkableLineNumbers(cli.HTMLLinkableLines, "L"),
}
if len(cli.HTMLHighlight) > 0 {
ranges := [][2]int{}
for _, span := range strings.Split(cli.HTMLHighlight, ",") {
parts := strings.Split(span, ":")
if len(parts) > 2 {
ctx.Fatalf("range should be N[:M], not %q", span)
}
start, err := strconv.ParseInt(parts[0], 10, 64)
ctx.FatalIfErrorf(err, "min value of range should be integer not %q", parts[0])
end := start
if len(parts) == 2 {
end, err = strconv.ParseInt(parts[1], 10, 64)
ctx.FatalIfErrorf(err, "max value of range should be integer not %q", parts[1])
}
ranges = append(ranges, [2]int{int(start), int(end)})
}
options = append(options, html.HighlightLines(ranges))
}
formatters.Register("html", html.New(options...))
}
func listAll() {
fmt.Println("lexers:")
sort.Sort(lexers.GlobalLexerRegistry.Lexers)
for _, l := range lexers.GlobalLexerRegistry.Lexers {
config := l.Config()
fmt.Printf(" %s\n", config.Name)
filenames := []string{}
filenames = append(filenames, config.Filenames...)
filenames = append(filenames, config.AliasFilenames...)
if len(config.Aliases) > 0 {
fmt.Printf(" aliases: %s\n", strings.Join(config.Aliases, " "))
}
if len(filenames) > 0 {
fmt.Printf(" filenames: %s\n", strings.Join(filenames, " "))
}
if len(config.MimeTypes) > 0 {
fmt.Printf(" mimetypes: %s\n", strings.Join(config.MimeTypes, " "))
}
}
fmt.Println()
fmt.Printf("styles:")
for _, name := range styles.Names() {
fmt.Printf(" %s", name)
}
fmt.Println()
fmt.Printf("formatters:")
for _, name := range formatters.Names() {
fmt.Printf(" %s", name)
}
fmt.Println()
}
func lex(ctx *kong.Context, lexer chroma.Lexer, contents string) chroma.Iterator {
if rel, ok := lexer.(chroma.TracingLexer); ok {
rel.SetTracing(cli.Trace)
}
lexer = chroma.Coalesce(lexer)
it, err := lexer.Tokenise(nil, contents)
ctx.FatalIfErrorf(err)
return it
}
func selexer(path, contents string) (lexer chroma.Lexer, err error) {
if cli.Lexer == "autodetect" {
if path != "" {
lexer := lexers.Match(path)
if lexer != nil {
return lexer, nil
}
}
return lexers.Analyse(contents), nil
}
if _, err := os.Stat(cli.Lexer); err == nil {
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
absPath, err := filepath.Abs(cli.Lexer)
if err != nil {
return nil, err
}
path, err := filepath.Rel(cwd, absPath)
if err != nil {
return nil, err
}
return chroma.NewXMLLexer(os.DirFS("."), path)
}
return lexers.Get(cli.Lexer), nil
}
func format(ctx *kong.Context, w io.Writer, style *chroma.Style, it chroma.Iterator) {
formatter := formatters.Get(cli.Formatter)
err := formatter.Format(w, style, it)
ctx.FatalIfErrorf(err)
}
func check(filename string, it chroma.Iterator) {
line, col := 1, 0
for token := it(); token != chroma.EOF; token = it() {
if token.Type == chroma.Error {
fmt.Printf("%s:%d:%d %q\n", filename, line, col, token.String())
}
for _, c := range token.String() {
col++
if c == '\n' {
line, col = line+1, 0
}
}
}
}
var nameCleanRe = regexp.MustCompile(`[^A-Za-z0-9_#+-]`)
func dumpXMLLexerDefinitions(dir string) error {
for _, name := range lexers.Names(false) {
lex := lexers.Get(name)
if rlex, ok := lex.(*chroma.RegexLexer); ok {
data, err := chroma.Marshal(rlex)
if err != nil {
if errors.Is(err, chroma.ErrNotSerialisable) {
fmt.Fprintf(os.Stderr, "warning: %q: %s\n", name, err)
continue
}
return err
}
name := strings.ToLower(nameCleanRe.ReplaceAllString(lex.Config().Name, "_"))
filename := filepath.Join(dir, name) + ".xml"
// fmt.Println(name)
_, err = os.Stat(filename)
if err == nil {
fmt.Fprintf(os.Stderr, "warning: %s already exists\n", filename)
continue
}
err = os.WriteFile(filename, data, 0600)
if err != nil {
return err
}
}
}
return nil
}
|