diff options
| author | Gabriel Schneider <[email protected]> | 2026-07-30 17:05:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-07-30 17:56:23 -0300 |
| commit | 8ec4e135e318c7799b2576a8729493c063da4a97 (patch) | |
| tree | ab37cdbb7f478eb996264cf941891f127f303c85 /vrsite/render.go | |
| parent | cb05c363045bf0e7af5858f6d7bc26f026fa9d70 (diff) | |
| download | notevi-8ec4e135e318c7799b2576a8729493c063da4a97.tar.gz notevi-8ec4e135e318c7799b2576a8729493c063da4a97.zip | |
Diffstat (limited to 'vrsite/render.go')
| -rw-r--r-- | vrsite/render.go | 130 |
1 files changed, 95 insertions, 35 deletions
diff --git a/vrsite/render.go b/vrsite/render.go index de21fc5..f18c3a8 100644 --- a/vrsite/render.go +++ b/vrsite/render.go @@ -5,12 +5,22 @@ package main import ( "bytes" "html/template" + "path/filepath" "sort" "strconv" "strings" + "unsafe" sitter "github.com/tree-sitter/go-tree-sitter" + tsbash "github.com/tree-sitter/tree-sitter-bash/bindings/go" + tsc "github.com/tree-sitter/tree-sitter-c/bindings/go" + tscpp "github.com/tree-sitter/tree-sitter-cpp/bindings/go" + tsgo "github.com/tree-sitter/tree-sitter-go/bindings/go" + tsjs "github.com/tree-sitter/tree-sitter-javascript/bindings/go" + tsjson "github.com/tree-sitter/tree-sitter-json/bindings/go" + tspython "github.com/tree-sitter/tree-sitter-python/bindings/go" tsrust "github.com/tree-sitter/tree-sitter-rust/bindings/go" + tsts "github.com/tree-sitter/tree-sitter-typescript/bindings/go" ) // Lines per content-visibility chunk: the browser skips layout and paint of @@ -179,10 +189,41 @@ func escapeTo(b *strings.Builder, s []byte) { // ── highlighting ────────────────────────────────────────────────────────── -type highlighter struct { +// grammars lists every language the site can highlight: the extensions it +// claims, the vendored queries that paint it, and the cgo grammar. Query files +// concatenate the way upstream composes them — C's patterns underpin C++, +// JavaScript's underpin TypeScript — and the concatenation keeps the +// later-patterns-win order, so the supplement wins where both match. +var grammars = []struct { + exts []string + scms []string + lang func() unsafe.Pointer +}{ + {[]string{".rs"}, []string{"rust"}, tsrust.Language}, + {[]string{".go"}, []string{"go"}, tsgo.Language}, + {[]string{".py", ".pyi"}, []string{"python"}, tspython.Language}, + {[]string{".js", ".mjs", ".cjs", ".jsx"}, []string{"javascript", "javascript-params", "jsx"}, tsjs.Language}, + {[]string{".ts", ".mts", ".cts"}, []string{"javascript", "typescript"}, tsts.LanguageTypescript}, + {[]string{".tsx"}, []string{"javascript", "typescript", "jsx"}, tsts.LanguageTSX}, + {[]string{".c"}, []string{"c"}, tsc.Language}, + // .h is C++'s upstream claim, and the C++ grammar is a superset: C headers + // parse the same under it, C++ headers only parse under it + {[]string{".cc", ".cpp", ".cxx", ".h", ".hh", ".hpp", ".hxx"}, []string{"c", "cpp"}, tscpp.Language}, + {[]string{".json"}, []string{"json"}, tsjson.Language}, + {[]string{".sh", ".bash"}, []string{"bash"}, tsbash.Language}, +} + +// language is one grammar's share of the highlighter. Everything here is built +// at startup and never written again, which is what lets classify run from any +// number of goroutines at once. +type language struct { lang *sitter.Language query *sitter.Query captureCls []int16 // capture index -> class id +} + +type highlighter struct { + byExt map[string]*language classNames []string } @@ -213,72 +254,91 @@ func nearFg(t *Theme, name string) bool { } func newHighlighter(dark, light *Theme) *highlighter { - h := &highlighter{classNames: []string{""}} - h.lang = sitter.NewLanguage(tsrust.Language()) - scm, _ := tfs.ReadFile("assets/rust.scm") - q, qerr := sitter.NewQuery(h.lang, string(scm)) - if qerr != nil { - fatal("rust query: %v", qerr) - } - h.query = q - for _, name := range q.CaptureNames() { - // skip captures that both themes paint (nearly) as foreground — - // punctuation and friends are the bulk of all spans - if nearFg(dark, name) && nearFg(light, name) { - h.captureCls = append(h.captureCls, 0) - continue + h := &highlighter{byExt: map[string]*language{}, classNames: []string{""}} + for _, g := range grammars { + var scm strings.Builder + for _, name := range g.scms { + b, err := tfs.ReadFile("assets/" + name + ".scm") + if err != nil { + fatal("%v", err) + } + scm.Write(b) } - key := resolveKey(dark, name) - if key == "" { - key = resolveKey(light, name) + l := &language{lang: sitter.NewLanguage(g.lang())} + q, qerr := sitter.NewQuery(l.lang, scm.String()) + if qerr != nil { + fatal("%s query: %v", strings.Join(g.scms, "+"), qerr) } - cls := strings.ReplaceAll(key, ".", "-") - id := int16(0) - for i, n := range h.classNames { - if n == cls { - id = int16(i) - } + l.query = q + for _, name := range q.CaptureNames() { + l.captureCls = append(l.captureCls, h.classFor(dark, light, name)) } - if id == 0 { - h.classNames = append(h.classNames, cls) - id = int16(len(h.classNames) - 1) + for _, ext := range g.exts { + h.byExt[ext] = l } - h.captureCls = append(h.captureCls, id) } return h } +// classFor interns the class id of a capture name, which doubles as a zed +// theme syntax key. Captures both themes paint (nearly) as foreground get id 0 +// and no span at all — punctuation and friends are the bulk of all spans. +func (h *highlighter) classFor(dark, light *Theme, name string) int16 { + if nearFg(dark, name) && nearFg(light, name) { + return 0 + } + key := resolveKey(dark, name) + if key == "" { + key = resolveKey(light, name) + } + cls := strings.ReplaceAll(key, ".", "-") + for i, n := range h.classNames { + if n == cls { + return int16(i) + } + } + h.classNames = append(h.classNames, cls) + return int16(len(h.classNames) - 1) +} + type paint struct { start, end uint pattern uint cls int16 } -// classify is safe for concurrent use: the shared query is immutable, and -// parser and cursor are per-call. +// classify is safe for concurrent use: the shared per-language query is +// immutable, and parser and cursor are per-call. Extensions no grammar claims +// come back unclassified rather than guessed at. +// +// Matches arrive already filtered by their #eq?/#match?/#any-of? predicates — +// go-tree-sitter evaluates those in QueryMatches.Next — so every capture here +// is one the query really meant. func (h *highlighter) classify(path string, src []byte) []int16 { classes := make([]int16, len(src)) - if !strings.HasSuffix(path, ".rs") { + l := h.byExt[strings.ToLower(filepath.Ext(path))] + if l == nil { return classes } parser := sitter.NewParser() defer parser.Close() - parser.SetLanguage(h.lang) + parser.SetLanguage(l.lang) tree := parser.Parse(src, nil) defer tree.Close() qc := sitter.NewQueryCursor() defer qc.Close() - matches := qc.Matches(h.query, tree.RootNode(), src) + matches := qc.Matches(l.query, tree.RootNode(), src) var paints []paint for m := matches.Next(); m != nil; m = matches.Next() { for _, c := range m.Captures { - if cls := h.captureCls[c.Index]; cls != 0 { + if cls := l.captureCls[c.Index]; cls != 0 { paints = append(paints, paint{c.Node.StartByte(), c.Node.EndByte(), m.PatternIndex, cls}) } } } - // later patterns in the query paint over earlier ones + // later patterns in the query paint over earlier ones; two captures of one + // pattern must not overlap, since this sort leaves their order arbitrary sort.Slice(paints, func(i, j int) bool { return paints[i].pattern < paints[j].pattern }) for _, p := range paints { for i := p.start; i < p.end && int(i) < len(classes); i++ { |
