mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
perf: Improved performance of highlighter code.
This commit is contained in:
@@ -0,0 +1,88 @@
|
|||||||
|
# load_grammar Optimization Ideas
|
||||||
|
|
||||||
|
## Current bottleneck
|
||||||
|
|
||||||
|
`load_grammar` is called once per unique language on first use. Each call costs ~30-40ms:
|
||||||
|
- `dlopen` — loads `.so` from disk (dominant)
|
||||||
|
- `read_entire_file_from_path` — reads `.scm` from disk
|
||||||
|
- `query_new` — compiles query
|
||||||
|
- `query_cursor_new` — allocates cursor (cheap)
|
||||||
|
|
||||||
|
With ~7 languages, first-load cost is ~210-280ms. Subsequent calls hit cache (~0ms).
|
||||||
|
|
||||||
|
## Code-level optimizations
|
||||||
|
|
||||||
|
### Avoid double-cloning query source
|
||||||
|
|
||||||
|
Currently: `read_entire_file_from_path` → `[]byte` → `string(raw)` → `strings.clone_to_cstring(query_src)`. Two copies of the entire `.scm` file.
|
||||||
|
|
||||||
|
Fix: read into a buffer with a trailing null byte, use directly as cstring. One read, zero copies.
|
||||||
|
|
||||||
|
### Reuse the grammar path string
|
||||||
|
|
||||||
|
`fmt.tprintf("%s/%s.so", grammar_dir, lang)` is computed in `ensure_parser` AND again in error diagnostics (line 284). Compute once, store in `Grammar_Cache`.
|
||||||
|
|
||||||
|
### Avoid `fmt.tprintf` for symbol name
|
||||||
|
|
||||||
|
`"tree_sitter_" + lang` can be simple concatenation instead of format string parsing.
|
||||||
|
|
||||||
|
### Extract error diagnostics to a separate proc
|
||||||
|
|
||||||
|
The query error diagnostics (lines 250-300) make up more than half of `load_grammar`. Extract to `report_query_error(lang, query_src, err_offset, err_type, query_path)`. Makes `load_grammar` focused on the happy path.
|
||||||
|
|
||||||
|
## Architectural optimizations
|
||||||
|
|
||||||
|
### Pre-scan content for languages, then batch-load
|
||||||
|
|
||||||
|
Before rendering, scan all markdown for `language-X` code blocks. Collect the unique set. Load all grammars in one pass.
|
||||||
|
|
||||||
|
Benefits:
|
||||||
|
- Makes cost visible ("loading grammars for: bash, odin, nu...")
|
||||||
|
- Enables parallel loading
|
||||||
|
- Moves cost to a predictable point in the build
|
||||||
|
|
||||||
|
### Parallel grammar loading
|
||||||
|
|
||||||
|
`dlopen` is thread-safe (POSIX). Load N grammars on N threads simultaneously.
|
||||||
|
|
||||||
|
- Sequential: ~210ms (7 languages × ~30ms each)
|
||||||
|
- Parallel (4 cores): ~60ms
|
||||||
|
- Parallel (7 threads): ~30ms
|
||||||
|
|
||||||
|
Biggest single win for first-load time. Odin's `core:thread` or `core:sync` can manage the thread pool.
|
||||||
|
|
||||||
|
### Static-link more grammars
|
||||||
|
|
||||||
|
Extend the `mkGrammarStaticLib` pattern (already used for HTML/CSS in `flake.nix`) to bash, odin, nu, etc.
|
||||||
|
|
||||||
|
- Zero `dlopen` — grammars baked into binary
|
||||||
|
- Zero `read_entire_file_from_path` — queries embedded via `#load` or `#directory`
|
||||||
|
- Zero `query_new` compilation — could pre-compile or use compile-time embedding
|
||||||
|
- Downside: binary size grows (~500KB per language), adding languages requires recompiling
|
||||||
|
|
||||||
|
### Embed queries at compile time
|
||||||
|
|
||||||
|
Even without static-linking grammars, the `.scm` query files could be embedded via `#load` or `#directory`.
|
||||||
|
|
||||||
|
- Eliminates `read_entire_file_from_path` per language
|
||||||
|
- Smaller win than static linking but simpler
|
||||||
|
- Queries are small text files (~2-10KB each)
|
||||||
|
|
||||||
|
### Cache compiled queries across runs
|
||||||
|
|
||||||
|
`query_new` compiles the `.scm` source into an internal representation. If this could be serialized and cached to disk (like a `.thorcache`), subsequent builds could skip compilation.
|
||||||
|
|
||||||
|
- Tree-sitter's query format isn't documented as serializable — would need investigation
|
||||||
|
- Even if not serializable, caching the raw `.scm` source in memory avoids re-reading from disk in watch mode (already handled by `grammar_cache` persistence)
|
||||||
|
|
||||||
|
### Pre-link common grammars in the flake
|
||||||
|
|
||||||
|
The flake could compile grammar `.so` files as Nix build inputs and pass their paths to thor at runtime via the `grammars` config field. This doesn't eliminate `dlopen` but ensures the files are always available in the Nix store.
|
||||||
|
|
||||||
|
## Priority ranking
|
||||||
|
|
||||||
|
1. **Parallel grammar loading** — immediate ~4-7x speedup on first load, no binary changes
|
||||||
|
2. **Static-link common grammars** — eliminates the problem entirely, but requires flake work
|
||||||
|
3. **Pre-scan + batch-load** — enables parallel loading and better UX logging
|
||||||
|
4. **Avoid double-clone of query source** — quick code cleanup, small win
|
||||||
|
5. **Embed queries at compile time** — eliminates file reads, moderate effort
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
# Parallelization Plan
|
||||||
|
|
||||||
|
## Current bottleneck
|
||||||
|
|
||||||
|
`compile_query` (which contains `query_new`) is ~100% of the grammar loading cost. With ~7 languages, sequential loading costs ~200-280ms. The rest of the build is ~12ms.
|
||||||
|
|
||||||
|
## Thread safety concerns
|
||||||
|
|
||||||
|
Current code is single-threaded and assumes it:
|
||||||
|
- `grammar_cache` map — concurrent writes unsafe
|
||||||
|
- `context.temp_allocator` — shared, not thread-safe
|
||||||
|
- `Site.pages` dynamic array — concurrent appends unsafe
|
||||||
|
- Site arena — bump pointer, not atomic
|
||||||
|
|
||||||
|
Any parallelization must address these.
|
||||||
|
|
||||||
|
## Dependency graph
|
||||||
|
|
||||||
|
```
|
||||||
|
init_site → build_vfs → scan_content (discover files)
|
||||||
|
→ grammar loading (must finish before any highlight_code)
|
||||||
|
→ load_page × N (markdown pipeline + highlight_code)
|
||||||
|
→ render_site (template rendering × N + minify × N)
|
||||||
|
→ RSS + sitemap (needs all pages loaded)
|
||||||
|
→ asset copying (independent)
|
||||||
|
```
|
||||||
|
|
||||||
|
Key barriers:
|
||||||
|
- Grammar loading must finish before any highlighting
|
||||||
|
- Pages must be loaded before rendering
|
||||||
|
- All pages must be rendered before RSS/sitemap
|
||||||
|
- Minify + write are independent per page after render
|
||||||
|
|
||||||
|
## Three approaches
|
||||||
|
|
||||||
|
### Option 1: Per-subsystem parallelism
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 1: Parallel grammar loading (7 threads)
|
||||||
|
Phase 2: Parallel content loading (N pages on M threads)
|
||||||
|
Phase 3: Parallel template rendering (N pages on M threads)
|
||||||
|
Phase 4: Parallel minification (N pages on M threads)
|
||||||
|
Phase 5: Sequential: RSS, sitemap, assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Simple barriers — each phase completes before the next starts
|
||||||
|
- Easy to reason about
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Thread pool setup/teardown per phase (or reuse a pool with barriers)
|
||||||
|
- Memory pressure: all pages loaded before any rendered
|
||||||
|
- Load imbalance within phases (complex pages vs simple)
|
||||||
|
|
||||||
|
### Option 2: Per-page parallelism
|
||||||
|
|
||||||
|
```
|
||||||
|
Pre-load all grammars (sequential or parallel)
|
||||||
|
Then for each page (in parallel):
|
||||||
|
load_page → md.process → render → minify → write
|
||||||
|
Then sequential: RSS, sitemap, assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Natural work unit — each page flows through the full pipeline independently
|
||||||
|
- No barriers between phases
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Complex pages clog workers while simple pages finish fast
|
||||||
|
- Grammar loading must happen first (barrier)
|
||||||
|
- Needs thread-safe shared state (grammar cache, template cache, allocator)
|
||||||
|
|
||||||
|
### Option 3: Generic worker queue
|
||||||
|
|
||||||
|
```
|
||||||
|
Single thread pool with work stealing.
|
||||||
|
Jobs: grammar_load(lang), load_page(file), render_page(page), minify(html), write_output(path)
|
||||||
|
Dependencies tracked via futures or callbacks.
|
||||||
|
```
|
||||||
|
|
||||||
|
Pros:
|
||||||
|
- Most flexible — handles all work types
|
||||||
|
- Best load balancing (work stealing across types)
|
||||||
|
- No wasted thread setup between phases
|
||||||
|
|
||||||
|
Cons:
|
||||||
|
- Most complex to implement
|
||||||
|
- Need dependency tracking (can't render before load completes)
|
||||||
|
- Careful shared-state management needed
|
||||||
|
- Memory management with arena allocator (thread safety)
|
||||||
|
|
||||||
|
## Recommended starting point
|
||||||
|
|
||||||
|
**Pre-load grammars in parallel, keep everything else sequential.**
|
||||||
|
|
||||||
|
```
|
||||||
|
Phase 0: Pre-scan content for unique languages (fast, sequential)
|
||||||
|
Phase 1: Parallel grammar loading (one thread per language, ~30ms)
|
||||||
|
→ each thread writes to a pre-assigned slot (no map contention)
|
||||||
|
→ each thread uses its own temp allocator
|
||||||
|
→ main thread merges results into grammar_cache after join
|
||||||
|
Phase 2: Sequential build as today (~12ms)
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: ~42ms total instead of ~210ms. Minimal architecture change — no thread-safe allocators needed for the rest of the pipeline.
|
||||||
|
|
||||||
|
This can later evolve toward option 3 (generic queue) if page count grows or per-page processing becomes a bottleneck.
|
||||||
|
|
||||||
|
## Implementation notes
|
||||||
|
|
||||||
|
- Odin's `core:thread` or `core:sync` can manage the thread pool
|
||||||
|
- Each grammar-loading thread needs its own `context.temp_allocator` for path strings
|
||||||
|
- The `grammar_cache` map write happens on the main thread after all threads join
|
||||||
|
- `dlopen` is thread-safe (POSIX guarantee)
|
||||||
|
- `query_new` is likely thread-safe (independent computation per language, no shared state in tree-sitter)
|
||||||
@@ -17,9 +17,15 @@
|
|||||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
||||||
- [ ] Use spall to find ways to reduce run time.
|
- [ ] Use spall to find ways to reduce run time.
|
||||||
- [ ] Consider using `#soa` for Page lists.
|
- [ ] Consider using `#soa` for Page lists.
|
||||||
|
- [ ] too many `write_string` calls in `highlight_block`
|
||||||
|
- [ ] return `src: cstring` from `load_query`.
|
||||||
|
- [ ] Improve `unescape_html` with simd.
|
||||||
|
- [ ] generate summary before syntax highlighting.
|
||||||
|
|
||||||
|
|
||||||
## Memory Management
|
## Memory Management
|
||||||
|
|
||||||
|
- [ ] Leaks in highlighter code.
|
||||||
- [ ] Not sure whether to use temp allocator or site_allocator in opengraph.odin.
|
- [ ] Not sure whether to use temp allocator or site_allocator in opengraph.odin.
|
||||||
- [ ] Not sure whether to use temp allocator or site_allocator in `site_load_content`.
|
- [ ] Not sure whether to use temp allocator or site_allocator in `site_load_content`.
|
||||||
- [ ] Might not need to allocate in `strip_html_tags`
|
- [ ] Might not need to allocate in `strip_html_tags`
|
||||||
|
|||||||
@@ -133,7 +133,7 @@
|
|||||||
|
|
||||||
buildPhase = ''
|
buildPhase = ''
|
||||||
runHook preBuild
|
runHook preBuild
|
||||||
odin build . -o:speed -out:${pname}-keep
|
odin build . -o:speed -no-bounds-check -out:${pname}-keep
|
||||||
runHook postBuild
|
runHook postBuild
|
||||||
'';
|
'';
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ strip_html_tags :: proc(s: string, allocator := context.allocator) -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
unescape_html :: proc(s: string) -> string {
|
unescape_html :: proc(s: string) -> string {
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make_len(len(s))
|
||||||
defer strings.builder_destroy(&sb)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
start := 0
|
start := 0
|
||||||
@@ -44,12 +44,18 @@ unescape_html :: proc(s: string) -> string {
|
|||||||
entity := s[i:i + semi + 1]
|
entity := s[i:i + semi + 1]
|
||||||
replacement := ""
|
replacement := ""
|
||||||
switch entity {
|
switch entity {
|
||||||
case "&": replacement = "&"
|
case "&":
|
||||||
case "<": replacement = "<"
|
replacement = "&"
|
||||||
case ">": replacement = ">"
|
case "<":
|
||||||
case """: replacement = "\""
|
replacement = "<"
|
||||||
case "'", "'": replacement = "'"
|
case ">":
|
||||||
case: continue
|
replacement = ">"
|
||||||
|
case """:
|
||||||
|
replacement = "\""
|
||||||
|
case "'", "'":
|
||||||
|
replacement = "'"
|
||||||
|
case:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if i > start {
|
if i > start {
|
||||||
strings.write_string(&sb, s[start:i])
|
strings.write_string(&sb, s[start:i])
|
||||||
@@ -135,3 +141,4 @@ generate_summary :: proc(html: string, max_words: int = 70) -> string {
|
|||||||
|
|
||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+75
-49
@@ -28,55 +28,66 @@ find_first_error_line :: proc(root: ts.Node) -> int {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
capture_name_to_css :: proc(name: string) -> string {
|
write_span_open :: proc(b: ^strings.Builder, buf: ^[128]u8, name: string) {
|
||||||
sb := strings.builder_make()
|
pos := 0
|
||||||
seg := strings.builder_make()
|
|
||||||
|
prefix := "<span class=\""
|
||||||
|
for i in 0 ..< len(prefix) {
|
||||||
|
buf[pos] = prefix[i]
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
|
||||||
first := true
|
first := true
|
||||||
for i in 0 ..< len(name) {
|
for i in 0 ..< len(name) {
|
||||||
if name[i] == '.' {
|
if name[i] == '.' {
|
||||||
if !first do strings.write_byte(&sb, ' ')
|
if !first {buf[pos] = ' '; pos += 1}
|
||||||
first = false
|
first = false
|
||||||
strings.write_string(&sb, "hl-")
|
buf[pos] = 'h'; buf[pos + 1] = 'l'; buf[pos + 2] = '-'; pos += 3
|
||||||
strings.write_string(&sb, strings.to_string(seg))
|
for j in 0 ..< i {
|
||||||
strings.write_byte(&seg, '-')
|
buf[pos] = '-' if name[j] == '.' else name[j]
|
||||||
} else {
|
pos += 1
|
||||||
strings.write_byte(&seg, name[i])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !first do strings.write_byte(&sb, ' ')
|
}
|
||||||
strings.write_string(&sb, "hl-")
|
if !first {buf[pos] = ' '; pos += 1}
|
||||||
strings.write_string(&sb, strings.to_string(seg))
|
buf[pos] = 'h'; buf[pos + 1] = 'l'; buf[pos + 2] = '-'; pos += 3
|
||||||
return strings.to_string(sb)
|
for j in 0 ..< len(name) {
|
||||||
|
buf[pos] = '-' if name[j] == '.' else name[j]
|
||||||
|
pos += 1
|
||||||
|
}
|
||||||
|
buf[pos] = '"'; pos += 1
|
||||||
|
buf[pos] = '>'; pos += 1
|
||||||
|
|
||||||
|
strings.write_string(b, string(buf[:pos]))
|
||||||
}
|
}
|
||||||
|
|
||||||
escape_html :: proc(s: string) -> string {
|
write_escaped :: proc(b: ^strings.Builder, s: string) {
|
||||||
sb := strings.builder_make()
|
|
||||||
defer strings.builder_destroy(&sb)
|
|
||||||
|
|
||||||
start := 0
|
start := 0
|
||||||
for i in 0 ..< len(s) {
|
for i in 0 ..< len(s) {
|
||||||
switch s[i] {
|
switch s[i] {
|
||||||
case '&':
|
case '&':
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
if i > start do strings.write_string(b, s[start:i])
|
||||||
strings.write_string(&sb, "&")
|
strings.write_string(b, "&")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '<':
|
case '<':
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
if i > start do strings.write_string(b, s[start:i])
|
||||||
strings.write_string(&sb, "<")
|
strings.write_string(b, "<")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '>':
|
case '>':
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
if i > start do strings.write_string(b, s[start:i])
|
||||||
strings.write_string(&sb, ">")
|
strings.write_string(b, ">")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '"':
|
case '"':
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
if i > start do strings.write_string(b, s[start:i])
|
||||||
strings.write_string(&sb, """)
|
strings.write_string(b, """)
|
||||||
start = i + 1
|
start = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if start == 0 do return s
|
if start == 0 {
|
||||||
if start < len(s) do strings.write_string(&sb, s[start:])
|
strings.write_string(b, s)
|
||||||
return strings.to_string(sb)
|
} else if start < len(s) {
|
||||||
|
strings.write_string(b, s[start:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
unescape_html :: proc(s: string) -> string {
|
unescape_html :: proc(s: string) -> string {
|
||||||
@@ -91,12 +102,18 @@ unescape_html :: proc(s: string) -> string {
|
|||||||
entity := s[i:i + semi + 1]
|
entity := s[i:i + semi + 1]
|
||||||
replacement := ""
|
replacement := ""
|
||||||
switch entity {
|
switch entity {
|
||||||
case "&": replacement = "&"
|
case "&":
|
||||||
case "<": replacement = "<"
|
replacement = "&"
|
||||||
case ">": replacement = ">"
|
case "<":
|
||||||
case """: replacement = "\""
|
replacement = "<"
|
||||||
case "'", "'": replacement = "'"
|
case ">":
|
||||||
case: continue
|
replacement = ">"
|
||||||
|
case """:
|
||||||
|
replacement = "\""
|
||||||
|
case "'", "'":
|
||||||
|
replacement = "'"
|
||||||
|
case:
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
strings.write_string(&sb, replacement)
|
strings.write_string(&sb, replacement)
|
||||||
@@ -128,21 +145,25 @@ highlight_block :: proc(code: string, lang: string, file_path: string) -> string
|
|||||||
if ts.node_has_error(root) {
|
if ts.node_has_error(root) {
|
||||||
line := find_first_error_line(root)
|
line := find_first_error_line(root)
|
||||||
if line > 0 {
|
if line > 0 {
|
||||||
log.warnf("highlight: syntax errors in %s code block at line %d (%s)", lang, line, file_path)
|
log.warnf(
|
||||||
|
"highlight: syntax errors in %s code block at line %d (%s)",
|
||||||
|
lang,
|
||||||
|
line,
|
||||||
|
file_path,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path)
|
log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cursor := ts.query_cursor_new()
|
cursor := gc.cursor
|
||||||
if cursor == nil {
|
if cursor == nil {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
defer ts.query_cursor_delete(cursor)
|
|
||||||
|
|
||||||
ts.query_cursor_exec(cursor, gc.query, root)
|
ts.query_cursor_exec(cursor, gc.query, root)
|
||||||
|
|
||||||
captures: [dynamic]Capture
|
captures := make([dynamic]Capture, 0, 64, context.temp_allocator)
|
||||||
defer delete(captures)
|
defer delete(captures)
|
||||||
|
|
||||||
match: ts.Query_Match
|
match: ts.Query_Match
|
||||||
@@ -162,29 +183,34 @@ highlight_block :: proc(code: string, lang: string, file_path: string) -> string
|
|||||||
if len(name_full) > int(name_len) {
|
if len(name_full) > int(name_len) {
|
||||||
name = name_full[:int(name_len)]
|
name = name_full[:int(name_len)]
|
||||||
}
|
}
|
||||||
append(&captures, Capture{
|
append(
|
||||||
|
&captures,
|
||||||
|
Capture {
|
||||||
start = ts.node_start_byte(cap.node),
|
start = ts.node_start_byte(cap.node),
|
||||||
end = ts.node_end_byte(cap.node),
|
end = ts.node_end_byte(cap.node),
|
||||||
name = name,
|
name = name,
|
||||||
})
|
},
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(captures) == 0 {
|
if len(captures) == 0 {
|
||||||
return code
|
return code
|
||||||
}
|
}
|
||||||
|
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make_len(len(code) * 2)
|
||||||
|
|
||||||
last_pos: u32 = 0
|
last_pos: u32 = 0
|
||||||
stack: [dynamic]Capture
|
stack := make([dynamic]Capture, 0, 16, context.temp_allocator)
|
||||||
defer delete(stack)
|
defer delete(stack)
|
||||||
|
|
||||||
|
buf: [128]u8
|
||||||
|
|
||||||
for cap in captures {
|
for cap in captures {
|
||||||
for len(stack) > 0 {
|
for len(stack) > 0 {
|
||||||
top := stack[len(stack) - 1]
|
top := stack[len(stack) - 1]
|
||||||
if top.end <= cap.start {
|
if top.end <= cap.start {
|
||||||
if top.end > last_pos {
|
if top.end > last_pos {
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
write_escaped(&sb, raw_code[last_pos:top.end])
|
||||||
}
|
}
|
||||||
strings.write_string(&sb, "</span>")
|
strings.write_string(&sb, "</span>")
|
||||||
last_pos = top.end
|
last_pos = top.end
|
||||||
@@ -195,26 +221,25 @@ highlight_block :: proc(code: string, lang: string, file_path: string) -> string
|
|||||||
}
|
}
|
||||||
|
|
||||||
if cap.start > last_pos {
|
if cap.start > last_pos {
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:cap.start]))
|
write_escaped(&sb, raw_code[last_pos:cap.start])
|
||||||
last_pos = cap.start
|
last_pos = cap.start
|
||||||
}
|
}
|
||||||
|
|
||||||
css_class := capture_name_to_css(cap.name)
|
write_span_open(&sb, &buf, cap.name)
|
||||||
strings.write_string(&sb, fmt.tprintf("<span class=\"%s\">", css_class))
|
|
||||||
append(&stack, cap)
|
append(&stack, cap)
|
||||||
}
|
}
|
||||||
|
|
||||||
for len(stack) > 0 {
|
for len(stack) > 0 {
|
||||||
top := pop(&stack)
|
top := pop(&stack)
|
||||||
if top.end > last_pos {
|
if top.end > last_pos {
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
write_escaped(&sb, raw_code[last_pos:top.end])
|
||||||
}
|
}
|
||||||
strings.write_string(&sb, "</span>")
|
strings.write_string(&sb, "</span>")
|
||||||
last_pos = top.end
|
last_pos = top.end
|
||||||
}
|
}
|
||||||
|
|
||||||
if int(last_pos) < len(raw_code) {
|
if int(last_pos) < len(raw_code) {
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:]))
|
write_escaped(&sb, raw_code[last_pos:])
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
@@ -266,7 +291,7 @@ highlight_code :: proc(html: string, file_path: string) -> string {
|
|||||||
|
|
||||||
code := html[code_start:end_idx]
|
code := html[code_start:end_idx]
|
||||||
highlighted := highlight_block(code, lang, file_path)
|
highlighted := highlight_block(code, lang, file_path)
|
||||||
strings.write_string(&sb, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
fmt.sbprintf(&sb, `<pre><code class="language-%s">%s</code></pre>`, lang, highlighted)
|
||||||
|
|
||||||
pos = end_idx + len(CODE_END)
|
pos = end_idx + len(CODE_END)
|
||||||
}
|
}
|
||||||
@@ -280,3 +305,4 @@ highlight_code :: proc(html: string, file_path: string) -> string {
|
|||||||
}
|
}
|
||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -179,3 +179,24 @@ test_init_site_md_enable_disable :: proc(t: ^testing.T) {
|
|||||||
testing.expect(t, .Sidenotes in site.markdown_extensions)
|
testing.expect(t, .Sidenotes in site.markdown_extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_init_site_config_paths :: proc(t: ^testing.T) {
|
||||||
|
path := write_temp_config("paths", `{
|
||||||
|
"content_dir": "/custom/content",
|
||||||
|
"assets_dir": "/custom/assets",
|
||||||
|
"output_dir": "/custom/output",
|
||||||
|
"layouts_dir": "/custom/layouts"
|
||||||
|
}`)
|
||||||
|
defer os.remove(path)
|
||||||
|
|
||||||
|
site: Site
|
||||||
|
args := []string{"thor", fmt.tprintf("-config:%s", path)}
|
||||||
|
init_site(&site, args)
|
||||||
|
defer destroy_site(&site)
|
||||||
|
|
||||||
|
testing.expect_value(t, site.content_dir, "/custom/content")
|
||||||
|
testing.expect_value(t, site.assets_dir, "/custom/assets")
|
||||||
|
testing.expect_value(t, site.output_dir, "/custom/output")
|
||||||
|
testing.expect_value(t, site.layouts_dir, "/custom/layouts")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ Grammar_Cache :: struct {
|
|||||||
language: Language,
|
language: Language,
|
||||||
parser: Parser,
|
parser: Parser,
|
||||||
query: Query,
|
query: Query,
|
||||||
|
cursor: Query_Cursor,
|
||||||
query_failed: bool,
|
query_failed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,19 +185,15 @@ ensure_parser :: proc(lang: string) -> ^Grammar_Cache {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
so_path := fmt.tprintf("%s/%s.so", grammar_dir, lang)
|
so_path := fmt.caprintf("%s/%s.so", grammar_dir, lang, allocator = context.temp_allocator)
|
||||||
so_c := strings.clone_to_cstring(so_path)
|
handle := dlopen(so_path, RTLD_LAZY)
|
||||||
defer delete(so_c)
|
|
||||||
handle := dlopen(so_c, RTLD_LAZY)
|
|
||||||
if handle == nil {
|
if handle == nil {
|
||||||
log.warnf("treesitter: cannot load grammar %s (%s)", lang, so_path)
|
log.warnf("treesitter: cannot load grammar %s (%s)", lang, so_path)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sym_name := fmt.tprintf("tree_sitter_%s", lang)
|
sym_name := fmt.caprintf("tree_sitter_%s", lang, allocator = context.temp_allocator)
|
||||||
sym_c := strings.clone_to_cstring(sym_name)
|
sym := dlsym(handle, sym_name)
|
||||||
defer delete(sym_c)
|
|
||||||
sym := dlsym(handle, sym_c)
|
|
||||||
if sym == nil {
|
if sym == nil {
|
||||||
log.errorf("treesitter: cannot find symbol %s in %s", sym_name, so_path)
|
log.errorf("treesitter: cannot find symbol %s in %s", sym_name, so_path)
|
||||||
return nil
|
return nil
|
||||||
@@ -240,8 +237,7 @@ load_grammar :: proc(lang: string) -> ^Grammar_Cache {
|
|||||||
gc.query_failed = true
|
gc.query_failed = true
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
query_c := strings.clone_to_cstring(query_src)
|
query_c := strings.clone_to_cstring(query_src, context.temp_allocator)
|
||||||
defer delete(query_c)
|
|
||||||
|
|
||||||
err_offset: u32
|
err_offset: u32
|
||||||
err_type: Query_Error
|
err_type: Query_Error
|
||||||
@@ -299,6 +295,7 @@ load_grammar :: proc(lang: string) -> ^Grammar_Cache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gc.query = query
|
gc.query = query
|
||||||
|
gc.cursor = query_cursor_new()
|
||||||
return gc
|
return gc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user