summaryrefslogtreecommitdiff
path: root/vrsite/render.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-07-30 17:05:12 -0300
committerGabriel Schneider <[email protected]>2026-07-30 17:56:23 -0300
commit8ec4e135e318c7799b2576a8729493c063da4a97 (patch)
treeab37cdbb7f478eb996264cf941891f127f303c85 /vrsite/render.go
parentcb05c363045bf0e7af5858f6d7bc26f026fa9d70 (diff)
downloadnotevi-8ec4e135e318c7799b2576a8729493c063da4a97.tar.gz
notevi-8ec4e135e318c7799b2576a8729493c063da4a97.zip
Diffstat (limited to 'vrsite/render.go')
-rw-r--r--vrsite/render.go130
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++ {