mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
Compare commits
5 Commits
cfb41ef930
...
085811b312
| Author | SHA1 | Date | |
|---|---|---|---|
| 085811b312 | |||
| 618fabf0e4 | |||
| dfe863e293 | |||
| 528bef3941 | |||
| fdd9ef4d46 |
@@ -0,0 +1,40 @@
|
|||||||
|
# Thor
|
||||||
|
|
||||||
|
[TOC]
|
||||||
|
|
||||||
|
Thor is a simple Static Sire Generator designed for personal blogs and other small websites.
|
||||||
|
|
||||||
|
Its core principals are simplicity and minimal configuration, so you can get started as quickly as possible.
|
||||||
|
|
||||||
|
It is based on Hugo, and gingerbill's SSG. Templating is done with (extended?) Mustache templates.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
- Syntax Highlighting server-side (with Tree Sitter) or client-side with highlight.js
|
||||||
|
- Mustache Templating
|
||||||
|
- OpenGraph Tags
|
||||||
|
- Menus (WIP)
|
||||||
|
- Extended Markdown ([See below](#extended-markdown))
|
||||||
|
- Basic (whitespace) minification.
|
||||||
|
|
||||||
|
## What it doesn't do
|
||||||
|
- Internationalization
|
||||||
|
- Pagination (Yet)
|
||||||
|
- Themes
|
||||||
|
- Union File System (Yet)
|
||||||
|
- Image Manipulation
|
||||||
|
- TailwindCSS integration
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Run `thor`
|
||||||
|
Check out `public/index.html`
|
||||||
|
|
||||||
|
Then follow [The Guide]()
|
||||||
|
For a more complete setup, run `thor new site`.
|
||||||
|
|
||||||
|
## Extended Markdown
|
||||||
|
- Emoji expansion
|
||||||
|
- margin style footnotes
|
||||||
|
- Guthub style alerts
|
||||||
|
- [and more]
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
- README.md
|
- README.md
|
||||||
- [ ] "Zero is beautiful"
|
- [ ] "Zero is beautiful"
|
||||||
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
||||||
|
- [ ] Clean up the default layouts
|
||||||
- [ ] Performance
|
- [ ] Performance
|
||||||
- [ ] See if we can disable bounds checks in `write_indented` and elsewhere.
|
- [ ] See if we can disable bounds checks in `write_indented` and elsewhere.
|
||||||
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
||||||
`reload_site` proc, that just updates changed resources.
|
`reload_site` proc, that just updates changed resources.
|
||||||
|
- [ ] Only publish referenced assets.
|
||||||
|
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
||||||
- [ ] mustache data keys for opengraph, etc.
|
- [ ] mustache data keys for opengraph, etc.
|
||||||
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
||||||
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
||||||
@@ -34,7 +37,7 @@
|
|||||||
- [ ] event based
|
- [ ] event based
|
||||||
- [ ] Free cmark HTML output (`body_html`) — cmark allocates via C malloc, not the arena, so it leaks per iteration in watch mode
|
- [ ] Free cmark HTML output (`body_html`) — cmark allocates via C malloc, not the arena, so it leaks per iteration in watch mode
|
||||||
- [ ] Rename `{{#is_post}}` to `{{#is_article}}` in templates and data model — currently hardcoded to posts section, should use `is_article` (any page with a section) instead.
|
- [ ] Rename `{{#is_post}}` to `{{#is_article}}` in templates and data model — currently hardcoded to posts section, should use `is_article` (any page with a section) instead.
|
||||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
- [ ] Mount content in VFS
|
||||||
- [ ] commands
|
- [ ] commands
|
||||||
- [ ] `build` alias of default
|
- [ ] `build` alias of default
|
||||||
- [ ] `init` set up new project
|
- [ ] `init` set up new project
|
||||||
@@ -43,16 +46,21 @@
|
|||||||
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
||||||
- [ ] Review every file in thor
|
- [ ] Review every file in thor
|
||||||
- [ ] Review assets.odin
|
- [ ] Review assets.odin
|
||||||
- [ ] Review alerts.odin
|
|
||||||
- [ ] Review content.odin
|
- [ ] Review content.odin
|
||||||
- [ ] Review emoji.odin
|
- [ ] Review defaults.odin
|
||||||
- [ ] Review feed.odin
|
- [ ] Review feed.odin
|
||||||
- [ ] Review footnotes.odin
|
|
||||||
- [ ] Review frontmatter.odin
|
- [ ] Review frontmatter.odin
|
||||||
- [ ] Review highlight.odin
|
|
||||||
- [ ] Review main.odin
|
- [ ] Review main.odin
|
||||||
- [ ] Review minifiy.odin
|
- [ ] Review minify.odin
|
||||||
|
- [ ] Review `markdown/`
|
||||||
|
- [ ] Review alerts.odin
|
||||||
|
- [ ] Review emoji.odin
|
||||||
|
- [ ] Review footnotes.odin
|
||||||
|
- [ ] Review footnotes_test.odin
|
||||||
|
- [ ] Review highlight.odin
|
||||||
|
- [ ] Review markdown.odin
|
||||||
|
- [ ] Review sectionate.odin
|
||||||
- [ ] Review render.odin
|
- [ ] Review render.odin
|
||||||
- [ ] Review sectionate.odin
|
|
||||||
- [x] Review site.odin
|
- [x] Review site.odin
|
||||||
- [ ] Review tree_sitter.odin
|
- [ ] Review treesitter/treesitter.odin
|
||||||
|
- [ ] Review vfs.odin
|
||||||
|
|||||||
+25
-47
@@ -5,57 +5,35 @@ import "core:log"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
// copy_assets_dir recursively copies files from assets_dir to output_dir.
|
copy_assets_dir :: proc(vfs: ^VFS, output_dir: string, features: bit_set[Feature]) {
|
||||||
// .css files are minified when .Minify is enabled; all other files are copied verbatim.
|
for virtual_path, entry in vfs.files {
|
||||||
// Silently skips if assets_dir doesn't exist.
|
if !strings.has_prefix(virtual_path, "assets/") {
|
||||||
copy_assets_dir :: proc(assets_dir: string, output_dir: string, features: bit_set[Feature]) {
|
continue
|
||||||
if !os.exists(assets_dir) {
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
copy_assets_recursive(assets_dir, "", output_dir, features)
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_assets_recursive :: proc(
|
rel := virtual_path[len("assets/"):]
|
||||||
current: string,
|
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
||||||
rel_prefix: string,
|
|
||||||
output_dir: string,
|
|
||||||
features: bit_set[Feature],
|
|
||||||
) {
|
|
||||||
entries, err := os.read_all_directory_by_path(current, context.allocator)
|
|
||||||
if err != nil {
|
|
||||||
log.warnf("thor: cannot read %s: %v", current, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
|
||||||
|
|
||||||
for entry in entries {
|
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
||||||
rel := rel_prefix == "" ? entry.name : fmt.tprintf("%s/%s", rel_prefix, entry.name)
|
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
||||||
switch entry.type {
|
log.warnf("thor: cannot create %s: %v", dest[:idx], err)
|
||||||
case .Regular:
|
continue
|
||||||
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
|
||||||
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
|
||||||
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
|
||||||
log.warnf("thor: cannot create %s: %v", dest[:idx], err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if .Minify in features && strings.has_suffix(entry.name, ".css") {
|
}
|
||||||
data, read_err := os.read_entire_file_from_path(entry.fullpath, context.allocator)
|
|
||||||
if read_err != nil {
|
if .Minify in features && strings.has_suffix(rel, ".css") {
|
||||||
log.warnf("thor: cannot read %s: %v", entry.fullpath, read_err)
|
data, ok := vfs_get(vfs, virtual_path)
|
||||||
continue
|
if ok {
|
||||||
}
|
write_file(dest, minify_css(string(data)))
|
||||||
minified := minify_css(string(data))
|
}
|
||||||
write_file(dest, minified)
|
} else if entry.data != nil {
|
||||||
} else {
|
if err := os.write_entire_file(dest, entry.data); err != nil {
|
||||||
if err := os.copy_file(dest, entry.fullpath); err != nil {
|
log.warnf("thor: cannot write %s: %v", dest, err)
|
||||||
log.warnf("thor: cannot copy %s: %v", entry.fullpath, err)
|
}
|
||||||
}
|
} else {
|
||||||
|
if err := os.copy_file(dest, entry.fs_path); err != nil {
|
||||||
|
log.warnf("thor: cannot copy %s: %v", entry.fs_path, err)
|
||||||
}
|
}
|
||||||
case .Directory:
|
|
||||||
copy_assets_recursive(entry.fullpath, rel, output_dir, features)
|
|
||||||
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-25
@@ -1,6 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import cm "vendor:commonmark"
|
import md "markdown"
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
@@ -101,7 +101,7 @@ load_page :: proc(
|
|||||||
section: string,
|
section: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
is_index: bool,
|
is_index: bool,
|
||||||
ext: bit_set[Markdown_Extension],
|
ext: bit_set[md.Extension],
|
||||||
) -> (
|
) -> (
|
||||||
page: Page,
|
page: Page,
|
||||||
ok: bool,
|
ok: bool,
|
||||||
@@ -132,29 +132,7 @@ load_page :: proc(
|
|||||||
if strings.has_suffix(file_path, ".html") {
|
if strings.has_suffix(file_path, ".html") {
|
||||||
page.body_html = strings.clone(body)
|
page.body_html = strings.clone(body)
|
||||||
} else {
|
} else {
|
||||||
sn_defs := make(map[string]string)
|
page.body_html = md.process(body, ext, file_path)
|
||||||
mn_defs := make(map[string]string)
|
|
||||||
clean_body := body
|
|
||||||
if .Sidenotes in ext {
|
|
||||||
clean_body, sn_defs, mn_defs = strip_definitions(body)
|
|
||||||
}
|
|
||||||
html := cm.markdown_to_html_from_string(clean_body, {.Unsafe})
|
|
||||||
if .Emoji in ext {
|
|
||||||
html = expand_emoji(html)
|
|
||||||
}
|
|
||||||
if .Sidenotes in ext {
|
|
||||||
html = inject_notes(html, sn_defs, mn_defs)
|
|
||||||
}
|
|
||||||
if .Alerts in ext {
|
|
||||||
html = inject_alerts(html)
|
|
||||||
}
|
|
||||||
if .Highlight in ext {
|
|
||||||
html = highlight_code(html, file_path)
|
|
||||||
}
|
|
||||||
if .Sections in ext {
|
|
||||||
html = wrap_sections(html)
|
|
||||||
}
|
|
||||||
page.body_html = html
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if section == "" && is_index {
|
if section == "" && is_index {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:os"
|
||||||
|
|
||||||
|
DEFAULTS_PATH :: #directory + os.Path_Separator_String + "defaults"
|
||||||
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{title}}</title>
|
||||||
|
{{> head}}
|
||||||
|
<link rel="stylesheet" href="/css/main.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
{{> nav}}{{$content}}{{/content}}
|
||||||
|
{{> footer}}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{{<base}}
|
||||||
|
{{$content}}
|
||||||
|
<main>
|
||||||
|
<header>
|
||||||
|
{{&body}}
|
||||||
|
</header>
|
||||||
|
<ul>
|
||||||
|
{{#list_pages}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date_iso}}<time
|
||||||
|
datetime="{{date_iso}}">{{date_display}}</time>{{/date_iso}}</span>
|
||||||
|
</li>
|
||||||
|
{{/list_pages}}
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
{{/content}}
|
||||||
|
{{/base}}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{{<base}}
|
||||||
|
{{$content}}
|
||||||
|
<main>
|
||||||
|
<article>
|
||||||
|
<h1>{{page_title}}</h1>
|
||||||
|
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
|
||||||
|
{{/date_iso}} {{&body}}
|
||||||
|
</article>
|
||||||
|
</main>
|
||||||
|
{{/content}}
|
||||||
|
{{/base}}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<footer>
|
||||||
|
<p>© {{now.year}} {{author}}</p>
|
||||||
|
</footer>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<meta property="og:url" content="{{og_url}}">
|
||||||
|
<meta property="og:site_name" content="{{og_site_name}}">
|
||||||
|
<meta property="og:title" content="{{og_title}}">
|
||||||
|
<meta property="og:description" content="{{og_description}}">
|
||||||
|
<meta property="og:locale" content="en_us">
|
||||||
|
<meta property="og:type" content="{{og_type}}">
|
||||||
|
{{#is_article}}<meta property="article:section" content="{{og_section}}">
|
||||||
|
<meta property="article:published_time" content="{{og_published}}">
|
||||||
|
{{/is_article}}<meta property="og:image" content="{{og_image}}">
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li><a href="/">{{title}}</a></li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
{{<base}}
|
||||||
|
{{$content}}
|
||||||
|
<main>
|
||||||
|
<h1>{{page_title}}</h1>
|
||||||
|
{{&body}}
|
||||||
|
{{#year_sections}}
|
||||||
|
<section>
|
||||||
|
<h2>{{year}}</h2>
|
||||||
|
<ul>
|
||||||
|
{{#posts}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date_iso}}<time
|
||||||
|
datetime="{{date_iso}}">{{date_display}}</time>{{/date_iso}}</span>
|
||||||
|
</li>
|
||||||
|
{{/posts}}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{{/year_sections}}
|
||||||
|
</main>
|
||||||
|
{{/content}}
|
||||||
|
{{/base}}
|
||||||
-476
@@ -1,476 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:log"
|
|
||||||
import "core:os"
|
|
||||||
import "core:strings"
|
|
||||||
|
|
||||||
Grammar_Cache :: struct {
|
|
||||||
language: TSLanguage,
|
|
||||||
parser: TSParser,
|
|
||||||
query: TSQuery,
|
|
||||||
query_failed: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
Get_Language_Proc :: #type proc() -> TSLanguage
|
|
||||||
|
|
||||||
grammar_cache: map[string]^Grammar_Cache
|
|
||||||
|
|
||||||
builtin_language :: proc(lang: string) -> (language: TSLanguage, ok: bool) {
|
|
||||||
switch lang {
|
|
||||||
case "html":
|
|
||||||
language = tree_sitter_html()
|
|
||||||
ok = true
|
|
||||||
case "css":
|
|
||||||
language = tree_sitter_css()
|
|
||||||
ok = true
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_parser :: proc(lang: string) -> ^Grammar_Cache {
|
|
||||||
if grammar_cache == nil {
|
|
||||||
grammar_cache = make(map[string]^Grammar_Cache)
|
|
||||||
}
|
|
||||||
if cached, ok := grammar_cache[lang]; ok {
|
|
||||||
return cached
|
|
||||||
}
|
|
||||||
|
|
||||||
grammar_cache[lang] = nil
|
|
||||||
|
|
||||||
language: TSLanguage
|
|
||||||
|
|
||||||
if builtin, ok := builtin_language(lang); ok {
|
|
||||||
language = builtin
|
|
||||||
} else {
|
|
||||||
if GRAPHS_PATH == "" {
|
|
||||||
log.warnf("highlight: no grammars path set, skipping %s", lang)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
so_path := fmt.tprintf("%s/%s.so", GRAPHS_PATH, lang)
|
|
||||||
so_c := strings.clone_to_cstring(so_path)
|
|
||||||
defer delete(so_c)
|
|
||||||
handle := dlopen(so_c, RTLD_LAZY)
|
|
||||||
if handle == nil {
|
|
||||||
log.warnf("highlight: cannot load grammar %s (%s)", lang, so_path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
sym_name := fmt.tprintf("tree_sitter_%s", lang)
|
|
||||||
sym_c := strings.clone_to_cstring(sym_name)
|
|
||||||
defer delete(sym_c)
|
|
||||||
sym := dlsym(handle, sym_c)
|
|
||||||
if sym == nil {
|
|
||||||
log.errorf("highlight: cannot find symbol %s in %s", sym_name, so_path)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
get_language := transmute(Get_Language_Proc)(sym)
|
|
||||||
language = get_language()
|
|
||||||
}
|
|
||||||
|
|
||||||
parser := ts_parser_new()
|
|
||||||
if parser == nil {
|
|
||||||
log.errorf("highlight: cannot create parser for %s", lang)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !ts_parser_set_language(parser, language) {
|
|
||||||
log.errorf("highlight: ABI mismatch for %s grammar", lang)
|
|
||||||
ts_parser_delete(parser)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gc := new(Grammar_Cache)
|
|
||||||
gc.language = language
|
|
||||||
gc.parser = parser
|
|
||||||
grammar_cache[lang] = gc
|
|
||||||
return gc
|
|
||||||
}
|
|
||||||
|
|
||||||
load_grammar :: proc(lang: string) -> ^Grammar_Cache {
|
|
||||||
gc := ensure_parser(lang)
|
|
||||||
if gc == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if gc.query != nil {
|
|
||||||
return gc
|
|
||||||
}
|
|
||||||
if gc.query_failed {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if QUERIES_PATH == "" {
|
|
||||||
log.warnf("highlight: no queries path set, skipping %s", lang)
|
|
||||||
gc.query_failed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
query_path := fmt.tprintf("%s/%s/highlights.scm", QUERIES_PATH, lang)
|
|
||||||
query_src, err := os.read_entire_file_from_path(query_path, context.allocator)
|
|
||||||
if err != nil {
|
|
||||||
log.warnf("highlight: cannot load query %s", query_path)
|
|
||||||
gc.query_failed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
query_str := string(query_src)
|
|
||||||
query_c := strings.clone_to_cstring(query_str)
|
|
||||||
defer delete(query_c)
|
|
||||||
|
|
||||||
err_offset: u32
|
|
||||||
err_type: TSQueryError
|
|
||||||
query := ts_query_new(
|
|
||||||
gc.language,
|
|
||||||
query_c,
|
|
||||||
u32(len(query_src)),
|
|
||||||
&err_offset,
|
|
||||||
&err_type,
|
|
||||||
)
|
|
||||||
if query == nil {
|
|
||||||
tok := extract_query_token(query_src, err_offset)
|
|
||||||
cause := fmt.tprintf("query error at byte %d (type %v)", err_offset, err_type)
|
|
||||||
#partial switch err_type {
|
|
||||||
case .NodeType:
|
|
||||||
if tok != "" {
|
|
||||||
cause = fmt.tprintf("query references unknown node type '%s' (byte %d); the grammar (.so) and query (.scm) are likely from different tree-sitter-%s versions", tok, err_offset, lang)
|
|
||||||
} else {
|
|
||||||
cause = fmt.tprintf("query references an unknown node type at byte %d; the grammar (.so) and query (.scm) are likely from different tree-sitter-%s versions", err_offset, lang)
|
|
||||||
}
|
|
||||||
case .Field:
|
|
||||||
cause = fmt.tprintf("query references unknown field '%s' at byte %d", tok, err_offset)
|
|
||||||
case .Capture:
|
|
||||||
cause = fmt.tprintf("query uses an invalid capture '%s' at byte %d", tok, err_offset)
|
|
||||||
case .Syntax:
|
|
||||||
cause = fmt.tprintf("query has a syntax error at byte %d", err_offset)
|
|
||||||
case .Structure:
|
|
||||||
cause = fmt.tprintf("query has an illegal pattern structure at byte %d", err_offset)
|
|
||||||
case .Language:
|
|
||||||
cause = "grammar language is null (broken grammar .so)"
|
|
||||||
}
|
|
||||||
log.errorf("highlight: %s query failed: %s", lang, cause)
|
|
||||||
|
|
||||||
_, is_builtin := builtin_language(lang)
|
|
||||||
if !is_builtin {
|
|
||||||
so_path := fmt.tprintf("%s/%s.so", GRAPHS_PATH, lang)
|
|
||||||
gram_v := helix_version_from_path(so_path)
|
|
||||||
query_v := helix_version_from_path(query_path)
|
|
||||||
gram_note := "(version unknown)"
|
|
||||||
if gram_v != "" do gram_note = fmt.tprintf("helix %s", gram_v)
|
|
||||||
query_note := "(version unknown)"
|
|
||||||
if query_v != "" do query_note = fmt.tprintf("helix %s", query_v)
|
|
||||||
log.errorf(" grammar: %s [%s]", so_path, gram_note)
|
|
||||||
log.errorf(" query: %s [%s]", query_path, query_note)
|
|
||||||
if gram_v != "" && query_v != "" && gram_v != query_v {
|
|
||||||
log.errorf(" >> helix VERSION MISMATCH: grammar %s vs query %s", gram_v, query_v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.query_failed = true
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
gc.query = query
|
|
||||||
return gc
|
|
||||||
}
|
|
||||||
|
|
||||||
extract_query_token :: proc(src: []byte, offset: u32) -> string {
|
|
||||||
end := offset
|
|
||||||
for int(end) < len(src) {
|
|
||||||
c := src[end]
|
|
||||||
is_ident := (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
|
||||||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.'
|
|
||||||
if !is_ident do break
|
|
||||||
end += 1
|
|
||||||
}
|
|
||||||
if end <= offset do return ""
|
|
||||||
return string(src[offset:end])
|
|
||||||
}
|
|
||||||
|
|
||||||
helix_version_from_path :: proc(path: string) -> string {
|
|
||||||
tag := "-helix-"
|
|
||||||
idx := strings.index(path, tag)
|
|
||||||
if idx < 0 do return ""
|
|
||||||
start := idx + len(tag)
|
|
||||||
end := start
|
|
||||||
for end < len(path) {
|
|
||||||
c := path[end]
|
|
||||||
if !((c >= '0' && c <= '9') || c == '.') do break
|
|
||||||
end += 1
|
|
||||||
}
|
|
||||||
if end <= start do return ""
|
|
||||||
return path[start:end]
|
|
||||||
}
|
|
||||||
|
|
||||||
Capture :: struct {
|
|
||||||
start: u32,
|
|
||||||
end: u32,
|
|
||||||
name: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
find_first_error_line :: proc(root: TSNode) -> int {
|
|
||||||
if ts_node_is_error(root) {
|
|
||||||
return int(ts_node_start_point(root).row) + 1
|
|
||||||
}
|
|
||||||
for i in 0..<ts_node_child_count(root) {
|
|
||||||
child := ts_node_child(root, u32(i))
|
|
||||||
if ts_node_has_error(child) {
|
|
||||||
line := find_first_error_line(child)
|
|
||||||
if line > 0 {
|
|
||||||
return line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
capture_name_to_css :: proc(name: string) -> string {
|
|
||||||
sb := strings.builder_make()
|
|
||||||
seg := strings.builder_make()
|
|
||||||
first := true
|
|
||||||
for i in 0..<len(name) {
|
|
||||||
if name[i] == '.' {
|
|
||||||
if !first do strings.write_byte(&sb, ' ')
|
|
||||||
first = false
|
|
||||||
strings.write_string(&sb, "hl-")
|
|
||||||
strings.write_string(&sb, strings.to_string(seg))
|
|
||||||
strings.write_byte(&seg, '-')
|
|
||||||
} else {
|
|
||||||
strings.write_byte(&seg, name[i])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !first do strings.write_byte(&sb, ' ')
|
|
||||||
strings.write_string(&sb, "hl-")
|
|
||||||
strings.write_string(&sb, strings.to_string(seg))
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
escape_html :: proc(s: string) -> string {
|
|
||||||
sb := strings.builder_make()
|
|
||||||
defer strings.builder_destroy(&sb)
|
|
||||||
|
|
||||||
start := 0
|
|
||||||
for i in 0..<len(s) {
|
|
||||||
switch s[i] {
|
|
||||||
case '&':
|
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
|
||||||
strings.write_string(&sb, "&")
|
|
||||||
start = i + 1
|
|
||||||
case '<':
|
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
|
||||||
strings.write_string(&sb, "<")
|
|
||||||
start = i + 1
|
|
||||||
case '>':
|
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
|
||||||
strings.write_string(&sb, ">")
|
|
||||||
start = i + 1
|
|
||||||
case '"':
|
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
|
||||||
strings.write_string(&sb, """)
|
|
||||||
start = i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if start == 0 do return s
|
|
||||||
if start < len(s) do strings.write_string(&sb, s[start:])
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
unescape_html :: proc(s: string) -> string {
|
|
||||||
sb := strings.builder_make()
|
|
||||||
defer strings.builder_destroy(&sb)
|
|
||||||
|
|
||||||
start := 0
|
|
||||||
for i in 0..<len(s) {
|
|
||||||
if s[i] != '&' do continue
|
|
||||||
semi := strings.index(s[i:], ";")
|
|
||||||
if semi < 0 do break
|
|
||||||
entity := s[i : i + semi + 1]
|
|
||||||
replacement := ""
|
|
||||||
switch entity {
|
|
||||||
case "&": replacement = "&"
|
|
||||||
case "<": replacement = "<"
|
|
||||||
case ">": replacement = ">"
|
|
||||||
case """: replacement = "\""
|
|
||||||
case "'", "'": replacement = "'"
|
|
||||||
case: continue
|
|
||||||
}
|
|
||||||
if i > start do strings.write_string(&sb, s[start:i])
|
|
||||||
strings.write_string(&sb, replacement)
|
|
||||||
start = i + semi + 1
|
|
||||||
}
|
|
||||||
if start == 0 do return s
|
|
||||||
if start < len(s) do strings.write_string(&sb, s[start:])
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
|
|
||||||
gc := load_grammar(lang)
|
|
||||||
if gc == nil {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
raw_code := unescape_html(code)
|
|
||||||
raw_c := strings.clone_to_cstring(raw_code)
|
|
||||||
defer delete(raw_c)
|
|
||||||
|
|
||||||
tree := ts_parser_parse_string(gc.parser, nil, raw_c, u32(len(raw_code)))
|
|
||||||
if tree == nil {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
defer ts_tree_delete(tree)
|
|
||||||
|
|
||||||
root := ts_tree_root_node(tree)
|
|
||||||
|
|
||||||
if ts_node_has_error(root) {
|
|
||||||
line := find_first_error_line(root)
|
|
||||||
if line > 0 {
|
|
||||||
log.warnf("highlight: syntax errors in %s code block at line %d (%s)", lang, line, file_path)
|
|
||||||
} else {
|
|
||||||
log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor := ts_query_cursor_new()
|
|
||||||
if cursor == nil {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
defer ts_query_cursor_delete(cursor)
|
|
||||||
|
|
||||||
ts_query_cursor_exec(cursor, gc.query, root)
|
|
||||||
|
|
||||||
captures: [dynamic]Capture
|
|
||||||
defer delete(captures)
|
|
||||||
|
|
||||||
match: TSQueryMatch
|
|
||||||
capture_idx: u32
|
|
||||||
for ts_query_cursor_next_capture(cursor, &match, &capture_idx) {
|
|
||||||
if capture_idx >= u32(match.capture_count) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
cap := match.captures[capture_idx]
|
|
||||||
name_len: u32
|
|
||||||
name_c := ts_query_capture_name_for_id(gc.query, cap.index, &name_len)
|
|
||||||
if name_c == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name_full := string(name_c)
|
|
||||||
name := name_full
|
|
||||||
if len(name_full) > int(name_len) {
|
|
||||||
name = name_full[:int(name_len)]
|
|
||||||
}
|
|
||||||
append(&captures, Capture{
|
|
||||||
start = ts_node_start_byte(cap.node),
|
|
||||||
end = ts_node_end_byte(cap.node),
|
|
||||||
name = name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(captures) == 0 {
|
|
||||||
return code
|
|
||||||
}
|
|
||||||
|
|
||||||
sb := strings.builder_make()
|
|
||||||
|
|
||||||
last_pos: u32 = 0
|
|
||||||
stack: [dynamic]Capture
|
|
||||||
defer delete(stack)
|
|
||||||
|
|
||||||
for cap in captures {
|
|
||||||
for len(stack) > 0 {
|
|
||||||
top := stack[len(stack) - 1]
|
|
||||||
if top.end <= cap.start {
|
|
||||||
if top.end > last_pos {
|
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, "</span>")
|
|
||||||
last_pos = top.end
|
|
||||||
pop(&stack)
|
|
||||||
} else {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cap.start > last_pos {
|
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:cap.start]))
|
|
||||||
last_pos = cap.start
|
|
||||||
}
|
|
||||||
|
|
||||||
css_class := capture_name_to_css(cap.name)
|
|
||||||
strings.write_string(&sb, fmt.tprintf("<span class=\"%s\">", css_class))
|
|
||||||
append(&stack, cap)
|
|
||||||
}
|
|
||||||
|
|
||||||
for len(stack) > 0 {
|
|
||||||
top := pop(&stack)
|
|
||||||
if top.end > last_pos {
|
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
|
||||||
}
|
|
||||||
strings.write_string(&sb, "</span>")
|
|
||||||
last_pos = top.end
|
|
||||||
}
|
|
||||||
|
|
||||||
if int(last_pos) < len(raw_code) {
|
|
||||||
strings.write_string(&sb, escape_html(raw_code[last_pos:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
|
|
||||||
highlight_code :: proc(html: string, file_path: string) -> string {
|
|
||||||
PREFIX :: `<pre><code class="language-`
|
|
||||||
CODE_END :: `</code></pre>`
|
|
||||||
|
|
||||||
sb := strings.builder_make()
|
|
||||||
defer strings.builder_destroy(&sb)
|
|
||||||
|
|
||||||
pos := 0
|
|
||||||
found := false
|
|
||||||
|
|
||||||
for {
|
|
||||||
rel := strings.index(html[pos:], PREFIX)
|
|
||||||
if rel < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
found = true
|
|
||||||
idx := pos + rel
|
|
||||||
|
|
||||||
if idx > pos {
|
|
||||||
strings.write_string(&sb, html[pos:idx])
|
|
||||||
}
|
|
||||||
|
|
||||||
lang_start := idx + len(PREFIX)
|
|
||||||
lang_end_rel := strings.index(html[lang_start:], `"`)
|
|
||||||
if lang_end_rel < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
lang_end := lang_start + lang_end_rel
|
|
||||||
lang := html[lang_start:lang_end]
|
|
||||||
|
|
||||||
code_start := lang_end + 1
|
|
||||||
if code_start < len(html) && html[code_start] == '>' {
|
|
||||||
code_start += 1
|
|
||||||
} else {
|
|
||||||
pos = lang_end
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
end_rel := strings.index(html[code_start:], CODE_END)
|
|
||||||
if end_rel < 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
end_idx := code_start + end_rel
|
|
||||||
|
|
||||||
code := html[code_start:end_idx]
|
|
||||||
highlighted := highlight_block(code, lang, file_path)
|
|
||||||
strings.write_string(&sb, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
|
||||||
|
|
||||||
pos = end_idx + len(CODE_END)
|
|
||||||
}
|
|
||||||
|
|
||||||
if pos < len(html) && found {
|
|
||||||
strings.write_string(&sb, html[pos:])
|
|
||||||
}
|
|
||||||
|
|
||||||
if !found {
|
|
||||||
return html
|
|
||||||
}
|
|
||||||
return strings.to_string(sb)
|
|
||||||
}
|
|
||||||
@@ -44,13 +44,12 @@ main :: proc() {
|
|||||||
defer destroy_site(&site)
|
defer destroy_site(&site)
|
||||||
// TODO: Make it so this isn't necessary
|
// TODO: Make it so this isn't necessary
|
||||||
context.allocator = site_allocator(&site)
|
context.allocator = site_allocator(&site)
|
||||||
|
build_vfs(&site)
|
||||||
|
|
||||||
site_load_content(&site)
|
site_load_content(&site)
|
||||||
render_site(&site)
|
render_site(&site)
|
||||||
|
|
||||||
if !(.Watch in site.features) {
|
(.Watch in site.features) or_break
|
||||||
break
|
|
||||||
}
|
|
||||||
time.sleep(5 * time.Second)
|
time.sleep(5 * time.Second)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
#+feature dynamic-literals
|
#+feature dynamic-literals
|
||||||
package main
|
package markdown
|
||||||
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
#+feature dynamic-literals
|
#+feature dynamic-literals
|
||||||
package main
|
package markdown
|
||||||
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package markdown
|
||||||
|
|
||||||
import cm "vendor:commonmark"
|
import cm "vendor:commonmark"
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
#+feature dynamic-literals
|
#+feature dynamic-literals
|
||||||
#+test
|
#+test
|
||||||
package main
|
package markdown
|
||||||
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
@@ -0,0 +1,282 @@
|
|||||||
|
package markdown
|
||||||
|
|
||||||
|
import ts "../treesitter"
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
Capture :: struct {
|
||||||
|
start: u32,
|
||||||
|
end: u32,
|
||||||
|
name: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
find_first_error_line :: proc(root: ts.Node) -> int {
|
||||||
|
if ts.node_is_error(root) {
|
||||||
|
return int(ts.node_start_point(root).row) + 1
|
||||||
|
}
|
||||||
|
for i in 0..<ts.node_child_count(root) {
|
||||||
|
child := ts.node_child(root, u32(i))
|
||||||
|
if ts.node_has_error(child) {
|
||||||
|
line := find_first_error_line(child)
|
||||||
|
if line > 0 {
|
||||||
|
return line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
capture_name_to_css :: proc(name: string) -> string {
|
||||||
|
sb := strings.builder_make()
|
||||||
|
seg := strings.builder_make()
|
||||||
|
first := true
|
||||||
|
for i in 0..<len(name) {
|
||||||
|
if name[i] == '.' {
|
||||||
|
if !first do strings.write_byte(&sb, ' ')
|
||||||
|
first = false
|
||||||
|
strings.write_string(&sb, "hl-")
|
||||||
|
strings.write_string(&sb, strings.to_string(seg))
|
||||||
|
strings.write_byte(&seg, '-')
|
||||||
|
} else {
|
||||||
|
strings.write_byte(&seg, name[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !first do strings.write_byte(&sb, ' ')
|
||||||
|
strings.write_string(&sb, "hl-")
|
||||||
|
strings.write_string(&sb, strings.to_string(seg))
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
escape_html :: proc(s: string) -> string {
|
||||||
|
sb := strings.builder_make()
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
|
start := 0
|
||||||
|
for i in 0..<len(s) {
|
||||||
|
switch s[i] {
|
||||||
|
case '&':
|
||||||
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
|
strings.write_string(&sb, "&")
|
||||||
|
start = i + 1
|
||||||
|
case '<':
|
||||||
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
|
strings.write_string(&sb, "<")
|
||||||
|
start = i + 1
|
||||||
|
case '>':
|
||||||
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
|
strings.write_string(&sb, ">")
|
||||||
|
start = i + 1
|
||||||
|
case '"':
|
||||||
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
|
strings.write_string(&sb, """)
|
||||||
|
start = i + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if start == 0 do return s
|
||||||
|
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
unescape_html :: proc(s: string) -> string {
|
||||||
|
sb := strings.builder_make()
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
|
start := 0
|
||||||
|
for i in 0..<len(s) {
|
||||||
|
if s[i] != '&' do continue
|
||||||
|
semi := strings.index(s[i:], ";")
|
||||||
|
if semi < 0 do break
|
||||||
|
entity := s[i : i + semi + 1]
|
||||||
|
replacement := ""
|
||||||
|
switch entity {
|
||||||
|
case "&": replacement = "&"
|
||||||
|
case "<": replacement = "<"
|
||||||
|
case ">": replacement = ">"
|
||||||
|
case """: replacement = "\""
|
||||||
|
case "'", "'": replacement = "'"
|
||||||
|
case: continue
|
||||||
|
}
|
||||||
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
|
strings.write_string(&sb, replacement)
|
||||||
|
start = i + semi + 1
|
||||||
|
}
|
||||||
|
if start == 0 do return s
|
||||||
|
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
|
||||||
|
gc := ts.load_grammar(lang)
|
||||||
|
if gc == nil {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
raw_code := unescape_html(code)
|
||||||
|
raw_c := strings.clone_to_cstring(raw_code)
|
||||||
|
defer delete(raw_c)
|
||||||
|
|
||||||
|
tree := ts.parser_parse_string(gc.parser, nil, raw_c, u32(len(raw_code)))
|
||||||
|
if tree == nil {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
defer ts.tree_delete(tree)
|
||||||
|
|
||||||
|
root := ts.tree_root_node(tree)
|
||||||
|
|
||||||
|
if ts.node_has_error(root) {
|
||||||
|
line := find_first_error_line(root)
|
||||||
|
if line > 0 {
|
||||||
|
log.warnf("highlight: syntax errors in %s code block at line %d (%s)", lang, line, file_path)
|
||||||
|
} else {
|
||||||
|
log.warnf("highlight: syntax errors in %s code block (%s)", lang, file_path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor := ts.query_cursor_new()
|
||||||
|
if cursor == nil {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
defer ts.query_cursor_delete(cursor)
|
||||||
|
|
||||||
|
ts.query_cursor_exec(cursor, gc.query, root)
|
||||||
|
|
||||||
|
captures: [dynamic]Capture
|
||||||
|
defer delete(captures)
|
||||||
|
|
||||||
|
match: ts.Query_Match
|
||||||
|
capture_idx: u32
|
||||||
|
for ts.query_cursor_next_capture(cursor, &match, &capture_idx) {
|
||||||
|
if capture_idx >= u32(match.capture_count) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cap := match.captures[capture_idx]
|
||||||
|
name_len: u32
|
||||||
|
name_c := ts.query_capture_name_for_id(gc.query, cap.index, &name_len)
|
||||||
|
if name_c == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name_full := string(name_c)
|
||||||
|
name := name_full
|
||||||
|
if len(name_full) > int(name_len) {
|
||||||
|
name = name_full[:int(name_len)]
|
||||||
|
}
|
||||||
|
append(&captures, Capture{
|
||||||
|
start = ts.node_start_byte(cap.node),
|
||||||
|
end = ts.node_end_byte(cap.node),
|
||||||
|
name = name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(captures) == 0 {
|
||||||
|
return code
|
||||||
|
}
|
||||||
|
|
||||||
|
sb := strings.builder_make()
|
||||||
|
|
||||||
|
last_pos: u32 = 0
|
||||||
|
stack: [dynamic]Capture
|
||||||
|
defer delete(stack)
|
||||||
|
|
||||||
|
for cap in captures {
|
||||||
|
for len(stack) > 0 {
|
||||||
|
top := stack[len(stack) - 1]
|
||||||
|
if top.end <= cap.start {
|
||||||
|
if top.end > last_pos {
|
||||||
|
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, "</span>")
|
||||||
|
last_pos = top.end
|
||||||
|
pop(&stack)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cap.start > last_pos {
|
||||||
|
strings.write_string(&sb, escape_html(raw_code[last_pos:cap.start]))
|
||||||
|
last_pos = cap.start
|
||||||
|
}
|
||||||
|
|
||||||
|
css_class := capture_name_to_css(cap.name)
|
||||||
|
strings.write_string(&sb, fmt.tprintf("<span class=\"%s\">", css_class))
|
||||||
|
append(&stack, cap)
|
||||||
|
}
|
||||||
|
|
||||||
|
for len(stack) > 0 {
|
||||||
|
top := pop(&stack)
|
||||||
|
if top.end > last_pos {
|
||||||
|
strings.write_string(&sb, escape_html(raw_code[last_pos:top.end]))
|
||||||
|
}
|
||||||
|
strings.write_string(&sb, "</span>")
|
||||||
|
last_pos = top.end
|
||||||
|
}
|
||||||
|
|
||||||
|
if int(last_pos) < len(raw_code) {
|
||||||
|
strings.write_string(&sb, escape_html(raw_code[last_pos:]))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
|
|
||||||
|
highlight_code :: proc(html: string, file_path: string) -> string {
|
||||||
|
PREFIX :: `<pre><code class="language-`
|
||||||
|
CODE_END :: `</code></pre>`
|
||||||
|
|
||||||
|
sb := strings.builder_make()
|
||||||
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
|
pos := 0
|
||||||
|
found := false
|
||||||
|
|
||||||
|
for {
|
||||||
|
rel := strings.index(html[pos:], PREFIX)
|
||||||
|
if rel < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
found = true
|
||||||
|
idx := pos + rel
|
||||||
|
|
||||||
|
if idx > pos {
|
||||||
|
strings.write_string(&sb, html[pos:idx])
|
||||||
|
}
|
||||||
|
|
||||||
|
lang_start := idx + len(PREFIX)
|
||||||
|
lang_end_rel := strings.index(html[lang_start:], `"`)
|
||||||
|
if lang_end_rel < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
lang_end := lang_start + lang_end_rel
|
||||||
|
lang := html[lang_start:lang_end]
|
||||||
|
|
||||||
|
code_start := lang_end + 1
|
||||||
|
if code_start < len(html) && html[code_start] == '>' {
|
||||||
|
code_start += 1
|
||||||
|
} else {
|
||||||
|
pos = lang_end
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
end_rel := strings.index(html[code_start:], CODE_END)
|
||||||
|
if end_rel < 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
end_idx := code_start + end_rel
|
||||||
|
|
||||||
|
code := html[code_start:end_idx]
|
||||||
|
highlighted := highlight_block(code, lang, file_path)
|
||||||
|
strings.write_string(&sb, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
||||||
|
|
||||||
|
pos = end_idx + len(CODE_END)
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos < len(html) && found {
|
||||||
|
strings.write_string(&sb, html[pos:])
|
||||||
|
}
|
||||||
|
|
||||||
|
if !found {
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
return strings.to_string(sb)
|
||||||
|
}
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package markdown
|
||||||
|
|
||||||
|
import cm "vendor:commonmark"
|
||||||
|
|
||||||
|
import "core:encoding/json"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
Extension :: enum {
|
||||||
|
Emoji,
|
||||||
|
Sidenotes,
|
||||||
|
Alerts,
|
||||||
|
Highlight,
|
||||||
|
Sections,
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_EXTENSIONS :: bit_set[Extension]{.Emoji, .Sidenotes, .Alerts}
|
||||||
|
|
||||||
|
process :: proc(body: string, ext: bit_set[Extension], file_path: string) -> string {
|
||||||
|
side_notes := make(map[string]string)
|
||||||
|
margin_notes := make(map[string]string)
|
||||||
|
clean_body := body
|
||||||
|
if .Sidenotes in ext {
|
||||||
|
clean_body, side_notes, margin_notes = strip_definitions(body)
|
||||||
|
}
|
||||||
|
html := cm.markdown_to_html_from_string(clean_body, {.Unsafe})
|
||||||
|
if .Emoji in ext {
|
||||||
|
html = expand_emoji(html)
|
||||||
|
}
|
||||||
|
if .Sidenotes in ext {
|
||||||
|
html = inject_notes(html, side_notes, margin_notes)
|
||||||
|
}
|
||||||
|
if .Alerts in ext {
|
||||||
|
html = inject_alerts(html)
|
||||||
|
}
|
||||||
|
if .Highlight in ext {
|
||||||
|
html = highlight_code(html, file_path)
|
||||||
|
}
|
||||||
|
if .Sections in ext {
|
||||||
|
html = wrap_sections(html)
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a ',' separated list of case-insensitive extension names to a bit set.
|
||||||
|
parse_extension_list :: proc(s: string) -> (result: bit_set[Extension]) {
|
||||||
|
for part in strings.split(s, ",", allocator = context.temp_allocator) {
|
||||||
|
name := strings.to_lower(strings.trim_space(part), allocator = context.temp_allocator)
|
||||||
|
switch name {
|
||||||
|
case "emoji":
|
||||||
|
result += {.Emoji}
|
||||||
|
case "sidenotes":
|
||||||
|
result += {.Sidenotes}
|
||||||
|
case "alerts":
|
||||||
|
result += {.Alerts}
|
||||||
|
case "highlight":
|
||||||
|
result += {.Highlight}
|
||||||
|
case "sections":
|
||||||
|
result += {.Sections}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Given a map[Extension]bool, apply it to ext.
|
||||||
|
apply_extension_config :: proc(ext: ^bit_set[Extension], config: json.Object) {
|
||||||
|
for name, val in config {
|
||||||
|
// TODO: Silently discards invalid values.
|
||||||
|
enabled := val.(json.Boolean) or_continue
|
||||||
|
switch name {
|
||||||
|
case "emoji":
|
||||||
|
if enabled {ext^ += {.Emoji}} else {ext^ -= {.Emoji}}
|
||||||
|
case "sidenotes":
|
||||||
|
if enabled {ext^ += {.Sidenotes}} else {ext^ -= {.Sidenotes}}
|
||||||
|
case "alerts":
|
||||||
|
if enabled {ext^ += {.Alerts}} else {ext^ -= {.Alerts}}
|
||||||
|
case "highlight":
|
||||||
|
if enabled {ext^ += {.Highlight}} else {ext^ -= {.Highlight}}
|
||||||
|
case "sections":
|
||||||
|
if enabled {ext^ += {.Sections}} else {ext^ -= {.Sections}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package main
|
package markdown
|
||||||
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
+37
-35
@@ -1,5 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
import ts "treesitter"
|
||||||
|
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
@@ -11,7 +13,7 @@ Range :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minify_html :: proc(source: string) -> string {
|
minify_html :: proc(source: string) -> string {
|
||||||
gc := ensure_parser("html")
|
gc := ts.ensure_parser("html")
|
||||||
if gc == nil {
|
if gc == nil {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
@@ -19,15 +21,15 @@ minify_html :: proc(source: string) -> string {
|
|||||||
source_c := strings.clone_to_cstring(source)
|
source_c := strings.clone_to_cstring(source)
|
||||||
defer delete(source_c)
|
defer delete(source_c)
|
||||||
|
|
||||||
tree := ts_parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
tree := ts.parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
||||||
if tree == nil {
|
if tree == nil {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
defer ts_tree_delete(tree)
|
defer ts.tree_delete(tree)
|
||||||
|
|
||||||
root := ts_tree_root_node(tree)
|
root := ts.tree_root_node(tree)
|
||||||
|
|
||||||
if ts_node_has_error(root) {
|
if ts.node_has_error(root) {
|
||||||
log.warnf("minify: HTML parse errors, skipping minification")
|
log.warnf("minify: HTML parse errors, skipping minification")
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
@@ -95,32 +97,32 @@ minify_html :: proc(source: string) -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
collect_html_ranges :: proc(
|
collect_html_ranges :: proc(
|
||||||
node: TSNode,
|
node: ts.Node,
|
||||||
source: string,
|
source: string,
|
||||||
comments: ^[dynamic]Range,
|
comments: ^[dynamic]Range,
|
||||||
preserves: ^[dynamic]Range,
|
preserves: ^[dynamic]Range,
|
||||||
) {
|
) {
|
||||||
child_count := ts_node_named_child_count(node)
|
child_count := ts.node_named_child_count(node)
|
||||||
for i in 0..<child_count {
|
for i in 0..<child_count {
|
||||||
child := ts_node_named_child(node, u32(i))
|
child := ts.node_named_child(node, u32(i))
|
||||||
type_str := string(ts_node_type(child))
|
type_str := string(ts.node_type(child))
|
||||||
|
|
||||||
if type_str == "comment" {
|
if type_str == "comment" {
|
||||||
append(comments, Range{
|
append(comments, Range{
|
||||||
start = ts_node_start_byte(child),
|
start = ts.node_start_byte(child),
|
||||||
end = ts_node_end_byte(child),
|
end = ts.node_end_byte(child),
|
||||||
})
|
})
|
||||||
} else if type_str == "script_element" || type_str == "style_element" {
|
} else if type_str == "script_element" || type_str == "style_element" {
|
||||||
append(preserves, Range{
|
append(preserves, Range{
|
||||||
start = ts_node_start_byte(child),
|
start = ts.node_start_byte(child),
|
||||||
end = ts_node_end_byte(child),
|
end = ts.node_end_byte(child),
|
||||||
})
|
})
|
||||||
} else if type_str == "element" {
|
} else if type_str == "element" {
|
||||||
tag := html_tag_name(child, source)
|
tag := html_tag_name(child, source)
|
||||||
if is_preserve_tag(tag) {
|
if is_preserve_tag(tag) {
|
||||||
append(preserves, Range{
|
append(preserves, Range{
|
||||||
start = ts_node_start_byte(child),
|
start = ts.node_start_byte(child),
|
||||||
end = ts_node_end_byte(child),
|
end = ts.node_end_byte(child),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
collect_html_ranges(child, source, comments, preserves)
|
collect_html_ranges(child, source, comments, preserves)
|
||||||
@@ -131,17 +133,17 @@ collect_html_ranges :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html_tag_name :: proc(element: TSNode, source: string) -> string {
|
html_tag_name :: proc(element: ts.Node, source: string) -> string {
|
||||||
child_count := ts_node_named_child_count(element)
|
child_count := ts.node_named_child_count(element)
|
||||||
for i in 0..<child_count {
|
for i in 0..<child_count {
|
||||||
child := ts_node_named_child(element, u32(i))
|
child := ts.node_named_child(element, u32(i))
|
||||||
if string(ts_node_type(child)) == "start_tag" {
|
if string(ts.node_type(child)) == "start_tag" {
|
||||||
tag_child_count := ts_node_named_child_count(child)
|
tag_child_count := ts.node_named_child_count(child)
|
||||||
for j in 0..<tag_child_count {
|
for j in 0..<tag_child_count {
|
||||||
tag_child := ts_node_named_child(child, u32(j))
|
tag_child := ts.node_named_child(child, u32(j))
|
||||||
if string(ts_node_type(tag_child)) == "tag_name" {
|
if string(ts.node_type(tag_child)) == "tag_name" {
|
||||||
start := ts_node_start_byte(tag_child)
|
start := ts.node_start_byte(tag_child)
|
||||||
end := ts_node_end_byte(tag_child)
|
end := ts.node_end_byte(tag_child)
|
||||||
return source[start:end]
|
return source[start:end]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,7 +169,7 @@ is_css_delim :: proc(c: u8) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
minify_css :: proc(source: string) -> string {
|
minify_css :: proc(source: string) -> string {
|
||||||
gc := ensure_parser("css")
|
gc := ts.ensure_parser("css")
|
||||||
if gc == nil {
|
if gc == nil {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
@@ -175,15 +177,15 @@ minify_css :: proc(source: string) -> string {
|
|||||||
source_c := strings.clone_to_cstring(source)
|
source_c := strings.clone_to_cstring(source)
|
||||||
defer delete(source_c)
|
defer delete(source_c)
|
||||||
|
|
||||||
tree := ts_parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
tree := ts.parser_parse_string(gc.parser, nil, source_c, u32(len(source)))
|
||||||
if tree == nil {
|
if tree == nil {
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
defer ts_tree_delete(tree)
|
defer ts.tree_delete(tree)
|
||||||
|
|
||||||
root := ts_tree_root_node(tree)
|
root := ts.tree_root_node(tree)
|
||||||
|
|
||||||
if ts_node_has_error(root) {
|
if ts.node_has_error(root) {
|
||||||
log.warnf("minify: CSS parse errors, skipping minification")
|
log.warnf("minify: CSS parse errors, skipping minification")
|
||||||
return source
|
return source
|
||||||
}
|
}
|
||||||
@@ -237,14 +239,14 @@ minify_css :: proc(source: string) -> string {
|
|||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
collect_css_comments :: proc(node: TSNode, comments: ^[dynamic]Range) {
|
collect_css_comments :: proc(node: ts.Node, comments: ^[dynamic]Range) {
|
||||||
child_count := ts_node_named_child_count(node)
|
child_count := ts.node_named_child_count(node)
|
||||||
for i in 0..<child_count {
|
for i in 0..<child_count {
|
||||||
child := ts_node_named_child(node, u32(i))
|
child := ts.node_named_child(node, u32(i))
|
||||||
if string(ts_node_type(child)) == "comment" {
|
if string(ts.node_type(child)) == "comment" {
|
||||||
append(comments, Range{
|
append(comments, Range{
|
||||||
start = ts_node_start_byte(child),
|
start = ts.node_start_byte(child),
|
||||||
end = ts_node_end_byte(child),
|
end = ts.node_end_byte(child),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
collect_css_comments(child, comments)
|
collect_css_comments(child, comments)
|
||||||
|
|||||||
+42
-63
@@ -1,4 +1,3 @@
|
|||||||
#+feature dynamic-literals
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import "mustache"
|
import "mustache"
|
||||||
@@ -103,24 +102,24 @@ og_type :: proc(is_article: bool) -> string {
|
|||||||
return "website"
|
return "website"
|
||||||
}
|
}
|
||||||
|
|
||||||
load_template :: proc(layouts_dir: string, name: string) -> mustache.Template {
|
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
||||||
data, _ := os.read_entire_file_from_path(
|
data, ok := vfs_get(vfs, virtual_path)
|
||||||
fmt.tprintf("%s/%s", layouts_dir, name),
|
if !ok {
|
||||||
context.allocator,
|
log.warnf("thor: template %s not found", virtual_path)
|
||||||
)
|
return mustache.Template{}
|
||||||
|
}
|
||||||
tpl, err := mustache.parse(string(data))
|
tpl, err := mustache.parse(string(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.warnf("thor: failed to parse template %s: %v", name, err)
|
log.warnf("thor: failed to parse template %s: %v", virtual_path, err)
|
||||||
}
|
}
|
||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
|
|
||||||
get_template :: proc(
|
get_template :: proc(
|
||||||
layouts_dir: string,
|
vfs: ^VFS,
|
||||||
layout: string,
|
layout: string,
|
||||||
cache: ^map[string]mustache.Template,
|
cache: ^map[string]mustache.Template,
|
||||||
) -> mustache.Template {
|
) -> mustache.Template {
|
||||||
// Build fallback chain: <layout> → [section_index] → page → base
|
|
||||||
chain: [4]string
|
chain: [4]string
|
||||||
n := 0
|
n := 0
|
||||||
chain[n] = layout; n += 1
|
chain[n] = layout; n += 1
|
||||||
@@ -139,18 +138,18 @@ get_template :: proc(
|
|||||||
if cached, ok := cache[candidate]; ok {
|
if cached, ok := cache[candidate]; ok {
|
||||||
return cached
|
return cached
|
||||||
}
|
}
|
||||||
filename := fmt.tprintf("%s.html", candidate)
|
virtual := fmt.tprintf("layouts/%s.html", candidate)
|
||||||
if os.exists(fmt.tprintf("%s/%s", layouts_dir, filename)) {
|
if _, ok := vfs_get(vfs, virtual); ok {
|
||||||
tpl := load_template(layouts_dir, filename)
|
tpl := load_template(vfs, virtual)
|
||||||
cache[candidate] = tpl
|
cache[candidate] = tpl
|
||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
if candidate != chain[n - 1] {
|
if candidate != chain[n - 1] {
|
||||||
log.warnf("thor: template %s not found, falling back", filename)
|
log.warnf("thor: template %s not found, falling back", virtual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.errorf("thor: base.html not found in %s", layouts_dir)
|
log.errorf("thor: base.html not found in VFS")
|
||||||
return mustache.Template{}
|
return mustache.Template{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,8 +181,8 @@ render_site :: proc(site: ^Site) {
|
|||||||
sort_pages_by_date(pages)
|
sort_pages_by_date(pages)
|
||||||
|
|
||||||
// Load shared resources
|
// Load shared resources
|
||||||
partials := load_partials(site.layouts_dir)
|
partials := load_partials(&site.vfs)
|
||||||
partials["base"] = load_template(site.layouts_dir, "base.html")
|
partials["base"] = load_template(&site.vfs, "layouts/base.html")
|
||||||
|
|
||||||
template_cache: map[string]mustache.Template
|
template_cache: map[string]mustache.Template
|
||||||
defer delete(template_cache)
|
defer delete(template_cache)
|
||||||
@@ -226,7 +225,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
if page._is_index {
|
if page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
tpl := get_template(site.layouts_dir, page.layout, &template_cache)
|
tpl := get_template(&site.vfs, page.layout, &template_cache)
|
||||||
html := render_page_html(page, site, tpl, partials, base)
|
html := render_page_html(page, site, tpl, partials, base)
|
||||||
if .Minify in site.features {
|
if .Minify in site.features {
|
||||||
html = minify_html(html)
|
html = minify_html(html)
|
||||||
@@ -247,7 +246,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
layout := fmt.tprintf("%s_index", section)
|
layout := fmt.tprintf("%s_index", section)
|
||||||
section_tpl := get_template(site.layouts_dir, layout, &template_cache)
|
section_tpl := get_template(&site.vfs, layout, &template_cache)
|
||||||
html := render_section(
|
html := render_section(
|
||||||
site,
|
site,
|
||||||
section,
|
section,
|
||||||
@@ -265,7 +264,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
|
|
||||||
// Render home page
|
// Render home page
|
||||||
if has_home {
|
if has_home {
|
||||||
home_tpl := get_template(site.layouts_dir, "home", &template_cache)
|
home_tpl := get_template(&site.vfs, "home", &template_cache)
|
||||||
home_html := render_home_html(home, site, home_tpl, partials, base)
|
home_html := render_home_html(home, site, home_tpl, partials, base)
|
||||||
if .Minify in site.features {
|
if .Minify in site.features {
|
||||||
home_html = minify_html(home_html)
|
home_html = minify_html(home_html)
|
||||||
@@ -282,7 +281,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
write_file(fmt.tprintf("%s/sitemap.xml", site.output_dir), sitemap)
|
write_file(fmt.tprintf("%s/sitemap.xml", site.output_dir), sitemap)
|
||||||
|
|
||||||
// Copy and optionally minify assets directory
|
// Copy and optionally minify assets directory
|
||||||
copy_assets_dir(site.assets_dir, site.output_dir, site.features)
|
copy_assets_dir(&site.vfs, site.output_dir, site.features)
|
||||||
|
|
||||||
// Generate robots.txt
|
// Generate robots.txt
|
||||||
robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url)
|
robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url)
|
||||||
@@ -394,52 +393,32 @@ render_section :: proc(
|
|||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
}
|
}
|
||||||
|
|
||||||
load_partials :: proc(layouts_dir: string) -> map[string]mustache.Template {
|
load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
||||||
partials: map[string]mustache.Template
|
partials: map[string]mustache.Template
|
||||||
partials_dir := fmt.tprintf("%s/partials", layouts_dir)
|
prefix := "layouts/partials/"
|
||||||
load_partials_recursive(&partials, partials_dir, "")
|
for virtual_path in vfs.files {
|
||||||
return partials
|
if !strings.has_prefix(virtual_path, prefix) {
|
||||||
}
|
continue
|
||||||
|
|
||||||
load_partials_recursive :: proc(
|
|
||||||
partials: ^map[string]mustache.Template,
|
|
||||||
base_dir: string,
|
|
||||||
rel_prefix: string,
|
|
||||||
) {
|
|
||||||
entries, err := os.read_all_directory_by_path(base_dir, context.allocator)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
#partial switch entry.type {
|
|
||||||
case .Regular:
|
|
||||||
name := entry.name
|
|
||||||
if !strings.has_suffix(name, ".html") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stripped := name[:len(name) - len(".html")]
|
|
||||||
key := stripped
|
|
||||||
if rel_prefix != "" {
|
|
||||||
key = fmt.tprintf("%s/%s", rel_prefix, stripped)
|
|
||||||
}
|
|
||||||
data, ok := os.read_entire_file_from_path(entry.fullpath, context.allocator)
|
|
||||||
if ok == nil {
|
|
||||||
tpl, perr := mustache.parse(string(data))
|
|
||||||
if perr != nil {
|
|
||||||
log.warnf("thor: failed to parse partial %s: %v", key, perr)
|
|
||||||
}
|
|
||||||
partials[key] = tpl
|
|
||||||
}
|
|
||||||
case .Directory:
|
|
||||||
sub_prefix := entry.name
|
|
||||||
if rel_prefix != "" {
|
|
||||||
sub_prefix = fmt.tprintf("%s/%s", rel_prefix, entry.name)
|
|
||||||
}
|
|
||||||
load_partials_recursive(partials, entry.fullpath, sub_prefix)
|
|
||||||
}
|
}
|
||||||
|
if !strings.has_suffix(virtual_path, ".html") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
stripped := virtual_path[len(prefix):]
|
||||||
|
key := stripped[:len(stripped) - len(".html")]
|
||||||
|
|
||||||
|
data, ok := vfs_get(vfs, virtual_path)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tpl, err := mustache.parse(string(data))
|
||||||
|
if err != nil {
|
||||||
|
log.warnf("thor: failed to parse partial %s: %v", key, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
partials[key] = tpl
|
||||||
}
|
}
|
||||||
|
return partials
|
||||||
}
|
}
|
||||||
|
|
||||||
format_date :: proc(iso: string) -> string {
|
format_date :: proc(iso: string) -> string {
|
||||||
|
|||||||
@@ -8,11 +8,15 @@ import "core:mem"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
import md "markdown"
|
||||||
|
|
||||||
|
|
||||||
// Site is the primary workhorse.
|
// Site is the primary workhorse.
|
||||||
Site :: struct {
|
Site :: struct {
|
||||||
arena: mem.Dynamic_Arena,
|
arena: mem.Dynamic_Arena,
|
||||||
pages: [dynamic]Page,
|
pages: [dynamic]Page,
|
||||||
|
modules: [dynamic]string,
|
||||||
|
vfs: VFS,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
author: string,
|
author: string,
|
||||||
@@ -24,7 +28,7 @@ Site :: struct {
|
|||||||
layouts_dir: string,
|
layouts_dir: string,
|
||||||
params: json.Object,
|
params: json.Object,
|
||||||
features: bit_set[Feature],
|
features: bit_set[Feature],
|
||||||
markdown_extensions: bit_set[Markdown_Extension],
|
markdown_extensions: bit_set[md.Extension],
|
||||||
}
|
}
|
||||||
|
|
||||||
Feature :: enum {
|
Feature :: enum {
|
||||||
@@ -33,22 +37,6 @@ Feature :: enum {
|
|||||||
Watch,
|
Watch,
|
||||||
}
|
}
|
||||||
|
|
||||||
Markdown_Extension :: enum {
|
|
||||||
Emoji,
|
|
||||||
Sidenotes,
|
|
||||||
Alerts,
|
|
||||||
Highlight,
|
|
||||||
Sections,
|
|
||||||
}
|
|
||||||
|
|
||||||
DEFAULT_MARKDOWN_EXTENSIONS :: bit_set[Markdown_Extension] {
|
|
||||||
.Emoji,
|
|
||||||
.Sidenotes,
|
|
||||||
.Alerts,
|
|
||||||
// .Highlight,
|
|
||||||
// .Sections,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration loaded from `thor.json`. Gets folded in to Site before
|
// Configuration loaded from `thor.json`. Gets folded in to Site before
|
||||||
// Flags
|
// Flags
|
||||||
Config_File :: struct {
|
Config_File :: struct {
|
||||||
@@ -62,6 +50,7 @@ Config_File :: struct {
|
|||||||
layouts_dir: string,
|
layouts_dir: string,
|
||||||
markdown_extensions: json.Value,
|
markdown_extensions: json.Value,
|
||||||
params: json.Value,
|
params: json.Value,
|
||||||
|
modules: json.Value,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration loaded from command line arguments. Gets folded in to Site
|
// Configuration loaded from command line arguments. Gets folded in to Site
|
||||||
@@ -72,6 +61,7 @@ Flags :: struct {
|
|||||||
content_dir: string `args:"name=content" usage:"Path to content directory"`,
|
content_dir: string `args:"name=content" usage:"Path to content directory"`,
|
||||||
assets_dir: string `args:"name=assets" usage:"Path to assets directory (CSS, JS, fonts, images)"`,
|
assets_dir: string `args:"name=assets" usage:"Path to assets directory (CSS, JS, fonts, images)"`,
|
||||||
output_dir: string `args:"name=output" usage:"Path to output directory (default: public/)"`,
|
output_dir: string `args:"name=output" usage:"Path to output directory (default: public/)"`,
|
||||||
|
layouts_dir: string `args:"name=layouts" usage:"Path to layouts directory"`,
|
||||||
drafts: bool `args:"name=drafts" usage:"Include draft pages in the build"`,
|
drafts: bool `args:"name=drafts" usage:"Include draft pages in the build"`,
|
||||||
watch: bool `usage:"Rebuild on file changes (polls every 5 seconds)"`,
|
watch: bool `usage:"Rebuild on file changes (polls every 5 seconds)"`,
|
||||||
minify: bool `args:"name=minify" usage:"Minify HTML output and CSS assets"`,
|
minify: bool `args:"name=minify" usage:"Minify HTML output and CSS assets"`,
|
||||||
@@ -85,7 +75,7 @@ init_site :: proc(site: ^Site, args: []string) {
|
|||||||
|
|
||||||
// Set defaults
|
// Set defaults
|
||||||
site.base_url = "http://localhost:8080"
|
site.base_url = "http://localhost:8080"
|
||||||
site.markdown_extensions = DEFAULT_MARKDOWN_EXTENSIONS
|
site.markdown_extensions = md.DEFAULT_EXTENSIONS
|
||||||
|
|
||||||
_flags: Flags
|
_flags: Flags
|
||||||
flags.parse_or_exit(&_flags, args, .Odin, alloc)
|
flags.parse_or_exit(&_flags, args, .Odin, alloc)
|
||||||
@@ -159,7 +149,15 @@ site_apply_config :: proc(site: ^Site, config: Config_File, config_dir: string)
|
|||||||
|
|
||||||
// Apply markdown extensions from config
|
// Apply markdown extensions from config
|
||||||
if ext_obj, ok := config.markdown_extensions.(json.Object); ok {
|
if ext_obj, ok := config.markdown_extensions.(json.Object); ok {
|
||||||
apply_extension_config(&site.markdown_extensions, ext_obj)
|
md.apply_extension_config(&site.markdown_extensions, ext_obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
if modules_arr, ok := config.modules.(json.Array); ok {
|
||||||
|
for &item in modules_arr {
|
||||||
|
if s, ok2 := item.(json.String); ok2 {
|
||||||
|
append(&site.modules, fmt.tprintf("%s/%s", config_dir, s))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,53 +173,14 @@ site_apply_cli_flags :: proc(site: ^Site, flags: Flags) {
|
|||||||
if flags.content_dir != "" do site.content_dir = flags.content_dir
|
if flags.content_dir != "" do site.content_dir = flags.content_dir
|
||||||
if flags.assets_dir != "" do site.assets_dir = flags.assets_dir
|
if flags.assets_dir != "" do site.assets_dir = flags.assets_dir
|
||||||
if flags.output_dir != "" do site.output_dir = flags.output_dir
|
if flags.output_dir != "" do site.output_dir = flags.output_dir
|
||||||
|
if flags.layouts_dir != "" do site.layouts_dir = flags.layouts_dir
|
||||||
|
|
||||||
if flags.drafts {site.features += {.Drafts}}
|
if flags.drafts {site.features += {.Drafts}}
|
||||||
if flags.watch {site.features += {.Watch}}
|
if flags.watch {site.features += {.Watch}}
|
||||||
if flags.minify {site.features += {.Minify}}
|
if flags.minify {site.features += {.Minify}}
|
||||||
|
|
||||||
site.markdown_extensions += parse_extension_list(flags.md_enable)
|
site.markdown_extensions += md.parse_extension_list(flags.md_enable)
|
||||||
site.markdown_extensions -= parse_extension_list(flags.md_disable)
|
site.markdown_extensions -= md.parse_extension_list(flags.md_disable)
|
||||||
}
|
|
||||||
|
|
||||||
parse_extension_list :: proc(s: string) -> bit_set[Markdown_Extension] {
|
|
||||||
result: bit_set[Markdown_Extension]
|
|
||||||
if s == "" do return result
|
|
||||||
for part in strings.split(s, ",", allocator = context.temp_allocator) {
|
|
||||||
name := strings.to_lower(strings.trim_space(part), allocator = context.temp_allocator)
|
|
||||||
switch name {
|
|
||||||
case "emoji":
|
|
||||||
result += {.Emoji}
|
|
||||||
case "sidenotes":
|
|
||||||
result += {.Sidenotes}
|
|
||||||
case "alerts":
|
|
||||||
result += {.Alerts}
|
|
||||||
case "highlight":
|
|
||||||
result += {.Highlight}
|
|
||||||
case "sections":
|
|
||||||
result += {.Sections}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
apply_extension_config :: proc(ext: ^bit_set[Markdown_Extension], config: json.Object) {
|
|
||||||
for name, val in config {
|
|
||||||
enabled, is_bool := val.(json.Boolean)
|
|
||||||
if !is_bool do continue
|
|
||||||
switch name {
|
|
||||||
case "emoji":
|
|
||||||
if enabled {ext^ += {.Emoji}} else {ext^ -= {.Emoji}}
|
|
||||||
case "sidenotes":
|
|
||||||
if enabled {ext^ += {.Sidenotes}} else {ext^ -= {.Sidenotes}}
|
|
||||||
case "alerts":
|
|
||||||
if enabled {ext^ += {.Alerts}} else {ext^ -= {.Alerts}}
|
|
||||||
case "highlight":
|
|
||||||
if enabled {ext^ += {.Highlight}} else {ext^ -= {.Highlight}}
|
|
||||||
case "sections":
|
|
||||||
if enabled {ext^ += {.Sections}} else {ext^ -= {.Sections}}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
site_allocator :: proc(site: ^Site) -> mem.Allocator {
|
site_allocator :: proc(site: ^Site) -> mem.Allocator {
|
||||||
|
|||||||
@@ -1,129 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import "core:c"
|
|
||||||
|
|
||||||
GRAPHS_PATH: string = "/home/spencer/.config/helix/runtime/grammars"
|
|
||||||
QUERIES_PATH: string = "/nix/store/n9da8d007ygbgsx983jr3ar3wb1fsh6q-helix-25.07.1/lib/runtime/queries"
|
|
||||||
|
|
||||||
TSLanguage :: distinct rawptr
|
|
||||||
TSParser :: distinct rawptr
|
|
||||||
TSTree :: distinct rawptr
|
|
||||||
TSQuery :: distinct rawptr
|
|
||||||
TSQueryCursor :: distinct rawptr
|
|
||||||
|
|
||||||
TSPoint :: struct {
|
|
||||||
row: u32,
|
|
||||||
column: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
TSNode :: struct {
|
|
||||||
ctx: [4]u32,
|
|
||||||
id: rawptr,
|
|
||||||
tree: rawptr,
|
|
||||||
}
|
|
||||||
|
|
||||||
TSQueryCapture :: struct {
|
|
||||||
node: TSNode,
|
|
||||||
index: u32,
|
|
||||||
_: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
TSQueryMatch :: struct {
|
|
||||||
id: u32,
|
|
||||||
pattern_index: u16,
|
|
||||||
capture_count: u16,
|
|
||||||
captures: [^]TSQueryCapture,
|
|
||||||
}
|
|
||||||
|
|
||||||
TSQueryError :: enum c.int {
|
|
||||||
None = 0,
|
|
||||||
Syntax,
|
|
||||||
NodeType,
|
|
||||||
Field,
|
|
||||||
Capture,
|
|
||||||
Structure,
|
|
||||||
Language,
|
|
||||||
}
|
|
||||||
|
|
||||||
RTLD_LAZY :: c.int(1)
|
|
||||||
|
|
||||||
foreign import lib "system:tree-sitter"
|
|
||||||
foreign import libdl "system:dl"
|
|
||||||
foreign import html_grammar "system:tree-sitter-html"
|
|
||||||
foreign import css_grammar "system:tree-sitter-css"
|
|
||||||
|
|
||||||
foreign lib {
|
|
||||||
ts_parser_new :: proc() -> TSParser ---
|
|
||||||
ts_parser_delete :: proc(self: TSParser) ---
|
|
||||||
ts_parser_set_language :: proc(self: TSParser, language: TSLanguage) -> bool ---
|
|
||||||
ts_parser_parse_string :: proc(
|
|
||||||
self: TSParser,
|
|
||||||
old_tree: TSTree,
|
|
||||||
string: cstring,
|
|
||||||
length: u32,
|
|
||||||
) -> TSTree ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign lib {
|
|
||||||
ts_tree_root_node :: proc(self: TSTree) -> TSNode ---
|
|
||||||
ts_tree_delete :: proc(self: TSTree) ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign lib {
|
|
||||||
ts_node_start_byte :: proc(self: TSNode) -> u32 ---
|
|
||||||
ts_node_end_byte :: proc(self: TSNode) -> u32 ---
|
|
||||||
ts_node_has_error :: proc(self: TSNode) -> bool ---
|
|
||||||
ts_node_is_error :: proc(self: TSNode) -> bool ---
|
|
||||||
ts_node_child_count :: proc(self: TSNode) -> u32 ---
|
|
||||||
ts_node_child :: proc(self: TSNode, child_index: u32) -> TSNode ---
|
|
||||||
ts_node_named_child_count :: proc(self: TSNode) -> u32 ---
|
|
||||||
ts_node_named_child :: proc(self: TSNode, child_index: u32) -> TSNode ---
|
|
||||||
ts_node_start_point :: proc(self: TSNode) -> TSPoint ---
|
|
||||||
ts_node_type :: proc(self: TSNode) -> cstring ---
|
|
||||||
ts_node_parent :: proc(self: TSNode) -> TSNode ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign lib {
|
|
||||||
ts_query_new :: proc(
|
|
||||||
language: TSLanguage,
|
|
||||||
source: cstring,
|
|
||||||
source_len: u32,
|
|
||||||
error_offset: ^u32,
|
|
||||||
error_type: ^TSQueryError,
|
|
||||||
) -> TSQuery ---
|
|
||||||
ts_query_delete :: proc(self: TSQuery) ---
|
|
||||||
ts_query_capture_name_for_id :: proc(
|
|
||||||
self: TSQuery,
|
|
||||||
index: u32,
|
|
||||||
length: ^u32,
|
|
||||||
) -> cstring ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign lib {
|
|
||||||
ts_query_cursor_new :: proc() -> TSQueryCursor ---
|
|
||||||
ts_query_cursor_delete :: proc(self: TSQueryCursor) ---
|
|
||||||
ts_query_cursor_exec :: proc(
|
|
||||||
self: TSQueryCursor,
|
|
||||||
query: TSQuery,
|
|
||||||
node: TSNode,
|
|
||||||
) ---
|
|
||||||
ts_query_cursor_next_capture :: proc(
|
|
||||||
self: TSQueryCursor,
|
|
||||||
match: ^TSQueryMatch,
|
|
||||||
capture_index: ^u32,
|
|
||||||
) -> bool ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign libdl {
|
|
||||||
dlopen :: proc(filename: cstring, flags: c.int) -> rawptr ---
|
|
||||||
dlsym :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
|
|
||||||
dlclose :: proc(handle: rawptr) -> c.int ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign html_grammar {
|
|
||||||
tree_sitter_html :: proc() -> TSLanguage ---
|
|
||||||
}
|
|
||||||
|
|
||||||
foreign css_grammar {
|
|
||||||
tree_sitter_css :: proc() -> TSLanguage ---
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,333 @@
|
|||||||
|
package treesitter
|
||||||
|
|
||||||
|
import "core:c"
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
GRAPHS_PATH: string = "/home/spencer/.config/helix/runtime/grammars"
|
||||||
|
QUERIES_PATH: string = "/nix/store/n9da8d007ygbgsx983jr3ar3wb1fsh6q-helix-25.07.1/lib/runtime/queries"
|
||||||
|
|
||||||
|
Language :: distinct rawptr
|
||||||
|
Parser :: distinct rawptr
|
||||||
|
Tree :: distinct rawptr
|
||||||
|
Query :: distinct rawptr
|
||||||
|
Query_Cursor :: distinct rawptr
|
||||||
|
|
||||||
|
Point :: struct {
|
||||||
|
row: u32,
|
||||||
|
column: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Node :: struct {
|
||||||
|
ctx: [4]u32,
|
||||||
|
id: rawptr,
|
||||||
|
tree: rawptr,
|
||||||
|
}
|
||||||
|
|
||||||
|
Query_Capture :: struct {
|
||||||
|
node: Node,
|
||||||
|
index: u32,
|
||||||
|
_: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
Query_Match :: struct {
|
||||||
|
id: u32,
|
||||||
|
pattern_index: u16,
|
||||||
|
capture_count: u16,
|
||||||
|
captures: [^]Query_Capture,
|
||||||
|
}
|
||||||
|
|
||||||
|
Query_Error :: enum c.int {
|
||||||
|
None = 0,
|
||||||
|
Syntax,
|
||||||
|
NodeType,
|
||||||
|
Field,
|
||||||
|
Capture,
|
||||||
|
Structure,
|
||||||
|
Language,
|
||||||
|
}
|
||||||
|
|
||||||
|
RTLD_LAZY :: c.int(1)
|
||||||
|
|
||||||
|
foreign import lib "system:tree-sitter"
|
||||||
|
foreign import libdl "system:dl"
|
||||||
|
foreign import html_grammar "system:tree-sitter-html"
|
||||||
|
foreign import css_grammar "system:tree-sitter-css"
|
||||||
|
|
||||||
|
@(link_prefix="ts_")
|
||||||
|
foreign lib {
|
||||||
|
parser_new :: proc() -> Parser ---
|
||||||
|
parser_delete :: proc(self: Parser) ---
|
||||||
|
parser_set_language :: proc(self: Parser, language: Language) -> bool ---
|
||||||
|
parser_parse_string :: proc(
|
||||||
|
self: Parser,
|
||||||
|
old_tree: Tree,
|
||||||
|
string: cstring,
|
||||||
|
length: u32,
|
||||||
|
) -> Tree ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(link_prefix="ts_")
|
||||||
|
foreign lib {
|
||||||
|
tree_root_node :: proc(self: Tree) -> Node ---
|
||||||
|
tree_delete :: proc(self: Tree) ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(link_prefix="ts_")
|
||||||
|
foreign lib {
|
||||||
|
node_start_byte :: proc(self: Node) -> u32 ---
|
||||||
|
node_end_byte :: proc(self: Node) -> u32 ---
|
||||||
|
node_has_error :: proc(self: Node) -> bool ---
|
||||||
|
node_is_error :: proc(self: Node) -> bool ---
|
||||||
|
node_child_count :: proc(self: Node) -> u32 ---
|
||||||
|
node_child :: proc(self: Node, child_index: u32) -> Node ---
|
||||||
|
node_named_child_count :: proc(self: Node) -> u32 ---
|
||||||
|
node_named_child :: proc(self: Node, child_index: u32) -> Node ---
|
||||||
|
node_start_point :: proc(self: Node) -> Point ---
|
||||||
|
node_type :: proc(self: Node) -> cstring ---
|
||||||
|
node_parent :: proc(self: Node) -> Node ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(link_prefix="ts_")
|
||||||
|
foreign lib {
|
||||||
|
query_new :: proc(
|
||||||
|
language: Language,
|
||||||
|
source: cstring,
|
||||||
|
source_len: u32,
|
||||||
|
error_offset: ^u32,
|
||||||
|
error_type: ^Query_Error,
|
||||||
|
) -> Query ---
|
||||||
|
query_delete :: proc(self: Query) ---
|
||||||
|
query_capture_name_for_id :: proc(
|
||||||
|
self: Query,
|
||||||
|
index: u32,
|
||||||
|
length: ^u32,
|
||||||
|
) -> cstring ---
|
||||||
|
}
|
||||||
|
|
||||||
|
@(link_prefix="ts_")
|
||||||
|
foreign lib {
|
||||||
|
query_cursor_new :: proc() -> Query_Cursor ---
|
||||||
|
query_cursor_delete :: proc(self: Query_Cursor) ---
|
||||||
|
query_cursor_exec :: proc(
|
||||||
|
self: Query_Cursor,
|
||||||
|
query: Query,
|
||||||
|
node: Node,
|
||||||
|
) ---
|
||||||
|
query_cursor_next_capture :: proc(
|
||||||
|
self: Query_Cursor,
|
||||||
|
match: ^Query_Match,
|
||||||
|
capture_index: ^u32,
|
||||||
|
) -> bool ---
|
||||||
|
}
|
||||||
|
|
||||||
|
foreign libdl {
|
||||||
|
dlopen :: proc(filename: cstring, flags: c.int) -> rawptr ---
|
||||||
|
dlsym :: proc(handle: rawptr, symbol: cstring) -> rawptr ---
|
||||||
|
dlclose :: proc(handle: rawptr) -> c.int ---
|
||||||
|
}
|
||||||
|
|
||||||
|
foreign html_grammar {
|
||||||
|
tree_sitter_html :: proc() -> Language ---
|
||||||
|
}
|
||||||
|
|
||||||
|
foreign css_grammar {
|
||||||
|
tree_sitter_css :: proc() -> Language ---
|
||||||
|
}
|
||||||
|
|
||||||
|
Grammar_Cache :: struct {
|
||||||
|
language: Language,
|
||||||
|
parser: Parser,
|
||||||
|
query: Query,
|
||||||
|
query_failed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
Get_Language_Proc :: #type proc() -> Language
|
||||||
|
|
||||||
|
grammar_cache: map[string]^Grammar_Cache
|
||||||
|
|
||||||
|
builtin_language :: proc(lang: string) -> (language: Language, ok: bool) {
|
||||||
|
switch lang {
|
||||||
|
case "html":
|
||||||
|
language = tree_sitter_html()
|
||||||
|
ok = true
|
||||||
|
case "css":
|
||||||
|
language = tree_sitter_css()
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ensure_parser :: proc(lang: string) -> ^Grammar_Cache {
|
||||||
|
if grammar_cache == nil {
|
||||||
|
grammar_cache = make(map[string]^Grammar_Cache)
|
||||||
|
}
|
||||||
|
if cached, ok := grammar_cache[lang]; ok {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
|
grammar_cache[lang] = nil
|
||||||
|
|
||||||
|
language: Language
|
||||||
|
|
||||||
|
if builtin, ok := builtin_language(lang); ok {
|
||||||
|
language = builtin
|
||||||
|
} else {
|
||||||
|
if GRAPHS_PATH == "" {
|
||||||
|
log.warnf("treesitter: no grammars path set, skipping %s", lang)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
so_path := fmt.tprintf("%s/%s.so", GRAPHS_PATH, lang)
|
||||||
|
so_c := strings.clone_to_cstring(so_path)
|
||||||
|
defer delete(so_c)
|
||||||
|
handle := dlopen(so_c, RTLD_LAZY)
|
||||||
|
if handle == nil {
|
||||||
|
log.warnf("treesitter: cannot load grammar %s (%s)", lang, so_path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sym_name := fmt.tprintf("tree_sitter_%s", lang)
|
||||||
|
sym_c := strings.clone_to_cstring(sym_name)
|
||||||
|
defer delete(sym_c)
|
||||||
|
sym := dlsym(handle, sym_c)
|
||||||
|
if sym == nil {
|
||||||
|
log.errorf("treesitter: cannot find symbol %s in %s", sym_name, so_path)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
get_language := transmute(Get_Language_Proc)(sym)
|
||||||
|
language = get_language()
|
||||||
|
}
|
||||||
|
|
||||||
|
parser := parser_new()
|
||||||
|
if parser == nil {
|
||||||
|
log.errorf("treesitter: cannot create parser for %s", lang)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !parser_set_language(parser, language) {
|
||||||
|
log.errorf("treesitter: ABI mismatch for %s grammar", lang)
|
||||||
|
parser_delete(parser)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gc := new(Grammar_Cache)
|
||||||
|
gc.language = language
|
||||||
|
gc.parser = parser
|
||||||
|
grammar_cache[lang] = gc
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
|
||||||
|
load_grammar :: proc(lang: string) -> ^Grammar_Cache {
|
||||||
|
gc := ensure_parser(lang)
|
||||||
|
if gc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if gc.query != nil {
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
if gc.query_failed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if QUERIES_PATH == "" {
|
||||||
|
log.warnf("treesitter: no queries path set, skipping %s", lang)
|
||||||
|
gc.query_failed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query_path := fmt.tprintf("%s/%s/highlights.scm", QUERIES_PATH, lang)
|
||||||
|
query_src, err := os.read_entire_file_from_path(query_path, context.allocator)
|
||||||
|
if err != nil {
|
||||||
|
log.warnf("treesitter: cannot load query %s", query_path)
|
||||||
|
gc.query_failed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
query_str := string(query_src)
|
||||||
|
query_c := strings.clone_to_cstring(query_str)
|
||||||
|
defer delete(query_c)
|
||||||
|
|
||||||
|
err_offset: u32
|
||||||
|
err_type: Query_Error
|
||||||
|
query := query_new(
|
||||||
|
gc.language,
|
||||||
|
query_c,
|
||||||
|
u32(len(query_src)),
|
||||||
|
&err_offset,
|
||||||
|
&err_type,
|
||||||
|
)
|
||||||
|
if query == nil {
|
||||||
|
tok := extract_query_token(query_src, err_offset)
|
||||||
|
cause := fmt.tprintf("query error at byte %d (type %v)", err_offset, err_type)
|
||||||
|
#partial switch err_type {
|
||||||
|
case .NodeType:
|
||||||
|
if tok != "" {
|
||||||
|
cause = fmt.tprintf("query references unknown node type '%s' (byte %d); the grammar (.so) and query (.scm) are likely from different tree-sitter-%s versions", tok, err_offset, lang)
|
||||||
|
} else {
|
||||||
|
cause = fmt.tprintf("query references an unknown node type at byte %d; the grammar (.so) and query (.scm) are likely from different tree-sitter-%s versions", err_offset, lang)
|
||||||
|
}
|
||||||
|
case .Field:
|
||||||
|
cause = fmt.tprintf("query references unknown field '%s' at byte %d", tok, err_offset)
|
||||||
|
case .Capture:
|
||||||
|
cause = fmt.tprintf("query uses an invalid capture '%s' at byte %d", tok, err_offset)
|
||||||
|
case .Syntax:
|
||||||
|
cause = fmt.tprintf("query has a syntax error at byte %d", err_offset)
|
||||||
|
case .Structure:
|
||||||
|
cause = fmt.tprintf("query has an illegal pattern structure at byte %d", err_offset)
|
||||||
|
case .Language:
|
||||||
|
cause = "grammar language is null (broken grammar .so)"
|
||||||
|
}
|
||||||
|
log.errorf("treesitter: %s query failed: %s", lang, cause)
|
||||||
|
|
||||||
|
_, is_builtin := builtin_language(lang)
|
||||||
|
if !is_builtin {
|
||||||
|
so_path := fmt.tprintf("%s/%s.so", GRAPHS_PATH, lang)
|
||||||
|
gram_v := helix_version_from_path(so_path)
|
||||||
|
query_v := helix_version_from_path(query_path)
|
||||||
|
gram_note := "(version unknown)"
|
||||||
|
if gram_v != "" do gram_note = fmt.tprintf("helix %s", gram_v)
|
||||||
|
query_note := "(version unknown)"
|
||||||
|
if query_v != "" do query_note = fmt.tprintf("helix %s", query_v)
|
||||||
|
log.errorf(" grammar: %s [%s]", so_path, gram_note)
|
||||||
|
log.errorf(" query: %s [%s]", query_path, query_note)
|
||||||
|
if gram_v != "" && query_v != "" && gram_v != query_v {
|
||||||
|
log.errorf(" >> helix VERSION MISMATCH: grammar %s vs query %s", gram_v, query_v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.query_failed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
gc.query = query
|
||||||
|
return gc
|
||||||
|
}
|
||||||
|
|
||||||
|
extract_query_token :: proc(src: []byte, offset: u32) -> string {
|
||||||
|
end := offset
|
||||||
|
for int(end) < len(src) {
|
||||||
|
c := src[end]
|
||||||
|
is_ident := (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.'
|
||||||
|
if !is_ident do break
|
||||||
|
end += 1
|
||||||
|
}
|
||||||
|
if end <= offset do return ""
|
||||||
|
return string(src[offset:end])
|
||||||
|
}
|
||||||
|
|
||||||
|
helix_version_from_path :: proc(path: string) -> string {
|
||||||
|
tag := "-helix-"
|
||||||
|
idx := strings.index(path, tag)
|
||||||
|
if idx < 0 do return ""
|
||||||
|
start := idx + len(tag)
|
||||||
|
end := start
|
||||||
|
for end < len(path) {
|
||||||
|
c := path[end]
|
||||||
|
if !((c >= '0' && c <= '9') || c == '.') do break
|
||||||
|
end += 1
|
||||||
|
}
|
||||||
|
if end <= start do return ""
|
||||||
|
return path[start:end]
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
|
||||||
|
// Virtual File System Entry
|
||||||
|
VFS_Entry :: struct {
|
||||||
|
fs_path: string,
|
||||||
|
data: []byte,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual File System
|
||||||
|
VFS :: struct {
|
||||||
|
files: map[string]VFS_Entry,
|
||||||
|
}
|
||||||
|
|
||||||
|
build_vfs :: proc(site: ^Site) {
|
||||||
|
site.vfs.files = make(map[string]VFS_Entry, site_allocator(site))
|
||||||
|
|
||||||
|
mount_dir(&site.vfs, fmt.tprintf("%s/layouts", DEFAULTS_PATH), "layouts")
|
||||||
|
|
||||||
|
for i := len(site.modules) - 1; i >= 0; i -= 1 {
|
||||||
|
module := site.modules[i]
|
||||||
|
mount_subdir(&site.vfs, module, "layouts")
|
||||||
|
mount_subdir(&site.vfs, module, "assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
mount_dir(&site.vfs, site.layouts_dir, "layouts")
|
||||||
|
mount_dir(&site.vfs, site.assets_dir, "assets")
|
||||||
|
}
|
||||||
|
|
||||||
|
mount_subdir :: proc(vfs: ^VFS, module_dir: string, target: string) {
|
||||||
|
source := fmt.tprintf("%s/%s", module_dir, target)
|
||||||
|
mount_dir(vfs, source, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
mount_dir :: proc(vfs: ^VFS, source_dir: string, target: string) {
|
||||||
|
if !os.exists(source_dir) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
mount_recursive(vfs, source_dir, target)
|
||||||
|
}
|
||||||
|
|
||||||
|
mount_recursive :: proc(vfs: ^VFS, current_dir: string, target_prefix: string) {
|
||||||
|
entries, err := os.read_all_directory_by_path(current_dir, context.allocator)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.file_info_slice_delete(entries, context.allocator)
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
#partial switch entry.type {
|
||||||
|
case .Regular:
|
||||||
|
virtual := fmt.tprintf("%s/%s", target_prefix, entry.name)
|
||||||
|
vfs.files[virtual] = VFS_Entry {
|
||||||
|
fs_path = entry.fullpath,
|
||||||
|
}
|
||||||
|
case .Directory:
|
||||||
|
sub_prefix := fmt.tprintf("%s/%s", target_prefix, entry.name)
|
||||||
|
mount_recursive(vfs, entry.fullpath, sub_prefix)
|
||||||
|
case:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
vfs_get :: proc(vfs: ^VFS, virtual_path: string) -> ([]byte, bool) {
|
||||||
|
entry, ok := vfs.files[virtual_path]
|
||||||
|
if !ok {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if entry.data != nil {
|
||||||
|
return entry.data, true
|
||||||
|
}
|
||||||
|
data, err := os.read_entire_file_from_path(entry.fs_path, context.allocator)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user