honk/vendor/humungus.tedunangst.com/r/webs/synlight/synlight.go
2022-11-13 16:19:53 +01:00

223 lines
5.6 KiB
Go

//
// Copyright (c) 2018,2019 Ted Unangst <tedu@tedunangst.com>
//
// Permission to use, copy, modify, and distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
// something like pygments in go
// written before I discovered https://github.com/alecthomas/chroma
package synlight
import (
"bufio"
"bytes"
"html/template"
"io"
"regexp"
"strconv"
"strings"
)
type token struct {
name string
re *regexp.Regexp
state int
nextstate int
}
// A syntax highlighter
type Lighter struct {
markers map[string]*marker
aliases map[string]string
lexers map[string][]*token
write func(io.Writer, []byte)
}
type marker struct {
before []byte
after []byte
}
// Options for creating a new highlighter.
// HTML or TTY output are supported.
type Options struct {
Format OutputFormat
}
type OutputFormat int
const (
None OutputFormat = iota
HTML
TTY
)
var htmlmarkers = make(map[string]*marker)
var ttymarkers = make(map[string]*marker)
func init() {
htmlmarkers["keyword"] = newmarker("<span class=kw>", "</span>")
htmlmarkers["builtin"] = newmarker("<span class=bi>", "</span>")
htmlmarkers["string"] = newmarker("<span class=st>", "</span>")
htmlmarkers["number"] = newmarker("<span class=nm>", "</span>")
htmlmarkers["type"] = newmarker("<span class=tp>", "</span>")
htmlmarkers["operator"] = newmarker("<span class=op>", "</span>")
htmlmarkers["comment"] = newmarker("<span class=cm>", "</span>")
htmlmarkers["addline"] = newmarker("<span class=al>", "</span>")
htmlmarkers["delline"] = newmarker("<span class=dl>", "</span>")
ttymarkers["keyword"] = newmarker("\x1b[33m", "\x1b[0m")
ttymarkers["builtin"] = newmarker("\x1b[32m", "\x1b[0m")
ttymarkers["string"] = newmarker("\x1b[31m", "\x1b[0m")
ttymarkers["number"] = newmarker("\x1b[31m", "\x1b[0m")
ttymarkers["type"] = newmarker("\x1b[32m", "\x1b[0m")
ttymarkers["comment"] = newmarker("\x1b[34m", "\x1b[0m")
ttymarkers["addline"] = newmarker("\x1b[32m", "\x1b[0m")
ttymarkers["delline"] = newmarker("\x1b[31m", "\x1b[0m")
}
func newmarker(before, after string) *marker {
return &marker{before: []byte(before), after: []byte(after)}
}
func plainwrite(w io.Writer, data []byte) {
w.Write(data)
}
func newtoken(name string, regex string) *token {
m := strings.Split(name, ":")
state := 0
nextstate := 0
if len(m) == 3 {
name = m[0]
state, _ = strconv.Atoi(m[1])
nextstate, _ = strconv.Atoi(m[2])
}
return &token{
name,
regexp.MustCompile("^" + regex),
state,
nextstate,
}
}
// Add a new lexer to this highlighter.
func (hl *Lighter) AddLexer(lang string, r io.Reader) {
var tokens []*token
scanner := bufio.NewScanner(r)
for scanner.Scan() {
line := scanner.Text()
m := strings.SplitN(line, " ", 2)
tokens = append(tokens, newtoken(m[0], m[1]))
}
tokens = append(tokens, newtoken("unknown", "(?s:.)"))
hl.lexers[lang] = tokens
}
// Create a new highlighter.
// It should be reused if possible.
func New(options Options) *Lighter {
hl := new(Lighter)
switch options.Format {
case HTML:
hl.markers = htmlmarkers
hl.write = template.HTMLEscape
case TTY:
hl.markers = ttymarkers
hl.write = plainwrite
default:
panic("invalid output format")
}
hl.lexers = make(map[string][]*token)
hl.AddLexer("c", strings.NewReader(lexer_c))
hl.AddLexer("diff", strings.NewReader(lexer_diff))
hl.AddLexer("go", strings.NewReader(lexer_go))
hl.AddLexer("html", strings.NewReader(lexer_html))
hl.AddLexer("js", strings.NewReader(lexer_js))
hl.AddLexer("lua", strings.NewReader(lexer_lua))
hl.AddLexer("py", strings.NewReader(lexer_py))
hl.AddLexer("rs", strings.NewReader(lexer_rs))
hl.AddLexer("sql", strings.NewReader(lexer_sql))
hl.aliases = make(map[string]string)
hl.aliases["h"] = "c"
hl.aliases["patch"] = "diff"
hl.aliases["python"] = "py"
hl.aliases["rust"] = "rs"
hl.aliases["xml"] = "html"
return hl
}
var pairnames = []string{"string", "keyword", "comment", "builtin"}
// Highlight code, writing it to w.
func (hl *Lighter) Highlight(data []byte, filename string, w io.Writer) {
dot := strings.LastIndex(filename, ".")
ext := filename[dot+1:]
alt, ok := hl.aliases[ext]
if ok {
ext = alt
}
markers := hl.markers
tokens := hl.lexers[ext]
if tokens == nil {
hl.write(w, data)
return
}
dataloc := 0
state := 0
pairidx := uint(0)
for dataloc < len(data) {
restart:
for _, tok := range tokens {
if tok.state != state {
continue
}
m := tok.re.Find(data[dataloc:])
if m != nil {
state = tok.nextstate
name := tok.name
if name == "pair" {
name = pairnames[pairidx%uint(len(pairnames))]
pairidx += 1
} else if name == "unpair" {
pairidx -= 1
name = pairnames[pairidx%uint(len(pairnames))]
}
mk := markers[name]
if mk != nil {
w.Write(mk.before)
}
hl.write(w, m)
if mk != nil {
w.Write(mk.after)
}
dataloc += len(m)
goto restart
}
}
state = 0
}
}
// Highlight code, returning a string
func (hl *Lighter) HighlightString(data string, filename string) string {
var buf bytes.Buffer
hl.Highlight([]byte(data), filename, &buf)
return buf.String()
}