mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 10:11:55 -04:00
Compare commits
3
Commits
90ad86c914
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4f5e06ec8 | ||
|
|
122627c51c | ||
|
|
2f997e89bb |
@@ -20,14 +20,30 @@
|
|||||||
- [ ] instead of `warnings: [dynamic]Error` we should use `warnings: [dynamic; 8]Error`
|
- [ ] instead of `warnings: [dynamic]Error` we should use `warnings: [dynamic; 8]Error`
|
||||||
- when limit reached, the template fails and stops rendering.
|
- when limit reached, the template fails and stops rendering.
|
||||||
- [ ] Brainstorm plugin/tool/(postccs/less/tailwind/sass) support.
|
- [ ] Brainstorm plugin/tool/(postccs/less/tailwind/sass) support.
|
||||||
|
- [ ] `fingerprint` pipe + contents-based composition. The `tool` pipe currently
|
||||||
|
carries a URL string and each stage publishes a file to the output dir, so
|
||||||
|
`| tool sass | fingerprint` would leave a dead unhashed intermediate. When
|
||||||
|
fingerprint lands, move the pipe value from a URL string to a contents/
|
||||||
|
Resource (bytes + logical name) where only the terminal stage publishes;
|
||||||
|
a Resource must stringify to its URL so existing bare-`tool` templates keep
|
||||||
|
working. stdin/stdout tools compose best there (bytes stay in memory), which
|
||||||
|
is why stdin/stdout is the documented preferred form over `{{in}}`/`{{out}}`.
|
||||||
- [ ] Reconsider `"menus": {}` opt-out: it currently discards all page frontmatter
|
- [ ] Reconsider `"menus": {}` opt-out: it currently discards all page frontmatter
|
||||||
menus (with only a warning). Decide whether empty config should instead just
|
menus (with only a warning). Decide whether empty config should instead just
|
||||||
suppress the auto-generated menu while still honoring frontmatter.
|
suppress the auto-generated menu while still honoring frontmatter.
|
||||||
- [ ] asset css files not automatically included in site.
|
- [ ] asset css files not automatically included in site.
|
||||||
- should we at least warn about them?
|
- should we at least warn about them?
|
||||||
|
- [ ] Review the whole `copy_assets_dir` pipeline. It copies every file under
|
||||||
|
`assets/` to the output dir blindly, whether or not anything references it,
|
||||||
|
and now also skips dests that already exist (a blunt collision guard so the
|
||||||
|
raw copy doesn't clobber `tool`-pipe output written earlier in the build).
|
||||||
|
Reconsider tracking which assets are actually used/published instead.
|
||||||
- [ ] warn if stylesheet url starts with /assets (and is therefore invalid).
|
- [ ] warn if stylesheet url starts with /assets (and is therefore invalid).
|
||||||
- [ ] warn when heading levels are skipped in a toc.
|
- [ ] warn when heading levels are skipped in a toc.
|
||||||
- [ ] extract `[TOC]` from `.md` body (or at least from the start.)
|
- [ ] extract `[TOC]` from `.md` body (or at least from the start.)
|
||||||
|
- [ ] add `--production` flag
|
||||||
|
- set `minify` to true
|
||||||
|
- warn when `base_url` unset
|
||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ copy_assets_dir :: proc(vfs: ^VFS, output_dir: string, features: bit_set[Feature
|
|||||||
rel := virtual_path[len("assets/"):]
|
rel := virtual_path[len("assets/"):]
|
||||||
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
||||||
|
|
||||||
|
// Don't clobber a file already written this build (e.g. the output of a
|
||||||
|
// `tool` pipe, which publishes to the same dest path as its source).
|
||||||
|
if os.exists(dest) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
||||||
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
||||||
log.warnf("cannot create %s: %v", dest[:idx], err)
|
log.warnf("cannot create %s: %v", dest[:idx], err)
|
||||||
|
|||||||
+33
-6
@@ -182,6 +182,7 @@ render :: proc(
|
|||||||
partials: map[string]Template = nil,
|
partials: map[string]Template = nil,
|
||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
warnings: ^[dynamic]Error = nil,
|
warnings: ^[dynamic]Error = nil,
|
||||||
|
tools: ^Tool_Registry = nil,
|
||||||
) -> (
|
) -> (
|
||||||
result: string,
|
result: string,
|
||||||
err: Error,
|
err: Error,
|
||||||
@@ -207,7 +208,15 @@ render :: proc(
|
|||||||
}
|
}
|
||||||
|
|
||||||
all_nodes := tmpl.nodes[:]
|
all_nodes := tmpl.nodes[:]
|
||||||
err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder, warnings = warnings)
|
err = render_nodes(
|
||||||
|
tmpl,
|
||||||
|
all_nodes,
|
||||||
|
&ctx,
|
||||||
|
partials,
|
||||||
|
&builder,
|
||||||
|
warnings = warnings,
|
||||||
|
tools = tools,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, err
|
return result, err
|
||||||
}
|
}
|
||||||
@@ -588,6 +597,7 @@ render_template :: proc(
|
|||||||
blocks: map[string]Block_Override,
|
blocks: map[string]Block_Override,
|
||||||
indent: string,
|
indent: string,
|
||||||
warnings: ^[dynamic]Error = nil,
|
warnings: ^[dynamic]Error = nil,
|
||||||
|
tools: ^Tool_Registry = nil,
|
||||||
) -> Error {
|
) -> Error {
|
||||||
if len(indent) > 0 {
|
if len(indent) > 0 {
|
||||||
state := Indent_State {
|
state := Indent_State {
|
||||||
@@ -595,9 +605,9 @@ render_template :: proc(
|
|||||||
at_line_start = false,
|
at_line_start = false,
|
||||||
}
|
}
|
||||||
strings.write_string(b, indent) // first line always gets indent
|
strings.write_string(b, indent) // first line always gets indent
|
||||||
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state, warnings)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state, warnings, tools)
|
||||||
}
|
}
|
||||||
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil, warnings)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil, warnings, tools)
|
||||||
}
|
}
|
||||||
|
|
||||||
write_indented :: proc(
|
write_indented :: proc(
|
||||||
@@ -642,6 +652,7 @@ render_nodes :: proc(
|
|||||||
blocks: map[string]Block_Override = nil,
|
blocks: map[string]Block_Override = nil,
|
||||||
indent_state: ^Indent_State = nil,
|
indent_state: ^Indent_State = nil,
|
||||||
warnings: ^[dynamic]Error = nil,
|
warnings: ^[dynamic]Error = nil,
|
||||||
|
tools: ^Tool_Registry = nil,
|
||||||
) -> Error {
|
) -> Error {
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(nodes) {
|
for i < len(nodes) {
|
||||||
@@ -672,6 +683,7 @@ render_nodes :: proc(
|
|||||||
ctx[:],
|
ctx[:],
|
||||||
current,
|
current,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
)
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
@@ -698,6 +710,7 @@ render_nodes :: proc(
|
|||||||
ctx[:],
|
ctx[:],
|
||||||
current,
|
current,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
)
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
@@ -720,6 +733,7 @@ render_nodes :: proc(
|
|||||||
ctx[:],
|
ctx[:],
|
||||||
current,
|
current,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
)
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
@@ -743,6 +757,7 @@ render_nodes :: proc(
|
|||||||
blocks,
|
blocks,
|
||||||
indent_state,
|
indent_state,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -775,6 +790,7 @@ render_nodes :: proc(
|
|||||||
ctx[:],
|
ctx[:],
|
||||||
current,
|
current,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
)
|
)
|
||||||
if perr != nil {
|
if perr != nil {
|
||||||
return tag_error(perr, current)
|
return tag_error(perr, current)
|
||||||
@@ -791,6 +807,7 @@ render_nodes :: proc(
|
|||||||
blocks,
|
blocks,
|
||||||
indent_state,
|
indent_state,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
i += 1 + len(node.children)
|
i += 1 + len(node.children)
|
||||||
@@ -805,7 +822,7 @@ render_nodes :: proc(
|
|||||||
if !found {
|
if !found {
|
||||||
warn_missing_partial(current, partials, node, name)
|
warn_missing_partial(current, partials, node, name)
|
||||||
} else {
|
} else {
|
||||||
render_template(pt, ctx, partials, b, nil, node.indent, warnings) or_return
|
render_template(pt, ctx, partials, b, nil, node.indent, warnings, tools) or_return
|
||||||
if indent_state != nil {
|
if indent_state != nil {
|
||||||
indent_state.at_line_start = false
|
indent_state.at_line_start = false
|
||||||
}
|
}
|
||||||
@@ -841,6 +858,7 @@ render_nodes :: proc(
|
|||||||
content_blocks,
|
content_blocks,
|
||||||
nil,
|
nil,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
) or_return
|
) or_return
|
||||||
at_ls := true
|
at_ls := true
|
||||||
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
|
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
|
||||||
@@ -854,6 +872,7 @@ render_nodes :: proc(
|
|||||||
content_blocks,
|
content_blocks,
|
||||||
indent_state,
|
indent_state,
|
||||||
warnings,
|
warnings,
|
||||||
|
tools,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
i += 1 + len(node.children)
|
i += 1 + len(node.children)
|
||||||
@@ -866,7 +885,16 @@ render_nodes :: proc(
|
|||||||
warn_missing_partial(current, partials, node, node.key)
|
warn_missing_partial(current, partials, node, node.key)
|
||||||
} else {
|
} else {
|
||||||
warn_unmatched_block_overrides(current, pt, parent_children)
|
warn_unmatched_block_overrides(current, pt, parent_children)
|
||||||
render_template(pt, ctx, partials, b, merged, node.indent, warnings) or_return
|
render_template(
|
||||||
|
pt,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
merged,
|
||||||
|
node.indent,
|
||||||
|
warnings,
|
||||||
|
tools,
|
||||||
|
) or_return
|
||||||
if indent_state != nil {
|
if indent_state != nil {
|
||||||
indent_state.at_line_start = false
|
indent_state.at_line_start = false
|
||||||
}
|
}
|
||||||
@@ -1057,4 +1085,3 @@ warn_context_depth :: proc(current: Template, node: Node) {
|
|||||||
)
|
)
|
||||||
log.warnf("%s", diag)
|
log.warnf("%s", diag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+30
-2
@@ -25,6 +25,7 @@ Pipe_Op :: enum {
|
|||||||
First,
|
First,
|
||||||
Last,
|
Last,
|
||||||
Rel_Url,
|
Rel_Url,
|
||||||
|
Tool,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pipe op names are derived from the enum via reflection (lowercased).
|
// Pipe op names are derived from the enum via reflection (lowercased).
|
||||||
@@ -225,13 +226,14 @@ apply_pipeline :: proc(
|
|||||||
ctx: []any,
|
ctx: []any,
|
||||||
tmpl: Template,
|
tmpl: Template,
|
||||||
warnings: ^[dynamic]Error = nil,
|
warnings: ^[dynamic]Error = nil,
|
||||||
|
tools: ^Tool_Registry = nil,
|
||||||
) -> (
|
) -> (
|
||||||
current: any,
|
current: any,
|
||||||
err: Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
current = value
|
current = value
|
||||||
for &filter in filters {
|
for &filter in filters {
|
||||||
result, ferr := apply_filter(current, &filter, pos, ctx, tmpl)
|
result, ferr := apply_filter(current, &filter, pos, ctx, tmpl, tools)
|
||||||
if ferr != nil {
|
if ferr != nil {
|
||||||
b := body(ferr)
|
b := body(ferr)
|
||||||
if b.kind != .Warning {
|
if b.kind != .Warning {
|
||||||
@@ -278,6 +280,7 @@ apply_filter :: proc(
|
|||||||
pos: int,
|
pos: int,
|
||||||
ctx: []any,
|
ctx: []any,
|
||||||
tmpl: Template,
|
tmpl: Template,
|
||||||
|
tools: ^Tool_Registry = nil,
|
||||||
) -> (
|
) -> (
|
||||||
any,
|
any,
|
||||||
Error,
|
Error,
|
||||||
@@ -358,6 +361,32 @@ apply_filter :: proc(
|
|||||||
}
|
}
|
||||||
result := rel_url(str, ctx)
|
result := rel_url(str, ctx)
|
||||||
return any{new_clone(result, context.temp_allocator), typeid_of(string)}, nil
|
return any{new_clone(result, context.temp_allocator), typeid_of(string)}, nil
|
||||||
|
case .Tool:
|
||||||
|
if len(filter.args) != 1 {
|
||||||
|
return value, Error_Body {
|
||||||
|
msg = fmt.tprintf(
|
||||||
|
"tool expects 1 argument (the tool name), got %d",
|
||||||
|
len(filter.args),
|
||||||
|
),
|
||||||
|
pos = filter.op_pos,
|
||||||
|
span = filter.end_pos - filter.op_pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bv, _ := base_value(value)
|
||||||
|
str, ok := reflect.as_string(bv)
|
||||||
|
if !ok {
|
||||||
|
return value, Error_Body {
|
||||||
|
msg = "tool may only be used on strings",
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result, terr := run_tool(tools, filter.args[0], str, filter.op_pos)
|
||||||
|
if terr != nil {
|
||||||
|
return value, terr
|
||||||
|
}
|
||||||
|
return any{new_clone(result, context.temp_allocator), typeid_of(string)}, nil
|
||||||
}
|
}
|
||||||
return {}, nil
|
return {}, nil
|
||||||
}
|
}
|
||||||
@@ -618,4 +647,3 @@ take :: proc(
|
|||||||
return any{new_clone(str[start:end], context.temp_allocator), typeid_of(string)}, warning
|
return any{new_clone(str[start:end], context.temp_allocator), typeid_of(string)}, warning
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,319 @@
|
|||||||
|
package mustache
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:sync"
|
||||||
|
|
||||||
|
// The `tool` pipe runs a user-configured external program over a site asset.
|
||||||
|
// A tool is a command string that is itself a mustache template; rendering it
|
||||||
|
// yields the command line, which is tokenized into argv and run via
|
||||||
|
// os.process_exec (no shell). The command template has three variables:
|
||||||
|
//
|
||||||
|
// {{in}} absolute path to the source file the tool should read
|
||||||
|
// {{out}} absolute path the tool should write its result to
|
||||||
|
// {{#minify}} section, non-empty when the site is building with minify on
|
||||||
|
//
|
||||||
|
// {{in}}/{{out}} are optional per tool. If a command omits {{in}}, thor feeds
|
||||||
|
// the source file on the tool's stdin; if it omits {{out}}, thor captures the
|
||||||
|
// tool's stdout. Stdin/stdout is the preferred form (it will compose best with
|
||||||
|
// the future `fingerprint` pipe); {{in}}/{{out}} is the fallback for tools that
|
||||||
|
// can't stream. The pipe takes an asset path (e.g. "/css/main.css"), runs the
|
||||||
|
// tool, and returns the root-relative URL of the written output.
|
||||||
|
|
||||||
|
// Tool_Registry is the host-supplied configuration for the `tool` pipe. The
|
||||||
|
// host (thor) owns one per build and threads a pointer to it through render, so
|
||||||
|
// it never enters the template context stack and can't be resolved as a key.
|
||||||
|
// The config fields are written once before rendering and only read thereafter;
|
||||||
|
// `manifest` is the sole mutable state and is guarded by `mu` — needed because
|
||||||
|
// render may go parallel and because os.process_start is not thread-safe.
|
||||||
|
Tool_Registry :: struct {
|
||||||
|
commands: map[string]string, // tool name -> command template
|
||||||
|
input_dir: string, // base dir for resolving {{in}} (the assets dir)
|
||||||
|
output_dir: string, // base dir for the written output (the output dir)
|
||||||
|
minify: bool,
|
||||||
|
manifest: map[string]string, // (name\x00input) -> output URL, per build
|
||||||
|
mu: sync.Mutex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// tool_registry_init prepares `reg` for one build: the input/output base dirs,
|
||||||
|
// the minify flag, and a fresh (empty) manifest. `commands` is configured
|
||||||
|
// separately (from thor.json) and left untouched. The manifest is temp-allocated
|
||||||
|
// — build-scoped, discarded when the build's temp allocator is reset. Nothing
|
||||||
|
// needs to be cleaned up between builds: the host recreates the whole registry.
|
||||||
|
tool_registry_init :: proc(reg: ^Tool_Registry, input_dir, output_dir: string, minify: bool) {
|
||||||
|
reg.input_dir = input_dir
|
||||||
|
reg.output_dir = output_dir
|
||||||
|
reg.minify = minify
|
||||||
|
reg.manifest = make(map[string]string, context.temp_allocator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// run_tool executes the named tool over `input` (an asset path) and returns the
|
||||||
|
// root-relative URL of the produced file. Any failure is a hard error (.Data).
|
||||||
|
run_tool :: proc(
|
||||||
|
reg: ^Tool_Registry,
|
||||||
|
name: string,
|
||||||
|
input: string,
|
||||||
|
pos: int,
|
||||||
|
) -> (
|
||||||
|
url: string,
|
||||||
|
err: Error,
|
||||||
|
) {
|
||||||
|
if reg == nil {
|
||||||
|
return "", Error_Body{msg = "tool pipe is not available here", pos = pos, kind = .Data}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URLs (CDN links, protocol-relative) have no local source; pass
|
||||||
|
// them through so a mixed stylesheet list stays safe.
|
||||||
|
if is_absolute_url(input) {
|
||||||
|
return input, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd_tmpl, ok := reg.commands[name]
|
||||||
|
if !ok {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("unknown tool '%s' (add it to thor.json \"tools\")", name),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logical path -> in/out/url. Strip a single leading '/'.
|
||||||
|
rel := input
|
||||||
|
if len(rel) > 0 && rel[0] == '/' {
|
||||||
|
rel = rel[1:]
|
||||||
|
}
|
||||||
|
if rel == "" {
|
||||||
|
return "", Error_Body{msg = "tool: empty asset path", pos = pos, kind = .Data}
|
||||||
|
}
|
||||||
|
in_path := fmt.tprintf("%s/%s", reg.input_dir, rel)
|
||||||
|
out_path := fmt.tprintf("%s/%s", reg.output_dir, rel)
|
||||||
|
out_url := fmt.tprintf("/%s", rel)
|
||||||
|
|
||||||
|
if !os.exists(in_path) {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': source file not found: %s", name, in_path),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render the command template. Done OUTSIDE the lock so a command template
|
||||||
|
// that (mistakenly) uses a `tool` pipe can't deadlock on reg.mu. The
|
||||||
|
// registry fields it reads are immutable during rendering.
|
||||||
|
cmd_str, rerr := render_command(cmd_tmpl, in_path, out_path, reg.minify, pos)
|
||||||
|
if rerr != nil {
|
||||||
|
return "", rerr
|
||||||
|
}
|
||||||
|
|
||||||
|
argv := tokenize_command(cmd_str, context.temp_allocator)
|
||||||
|
if len(argv) == 0 {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': command is empty", name),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !find_on_path(argv[0]) {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': program '%s' not found on PATH", name, argv[0]),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sync.lock(®.mu)
|
||||||
|
defer sync.unlock(®.mu)
|
||||||
|
|
||||||
|
key := fmt.tprintf("%s\x00%s", name, input)
|
||||||
|
if cached, hit := reg.manifest[key]; hit {
|
||||||
|
return cached, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// How the tool speaks I/O is inferred from whether the command references
|
||||||
|
// the paths thor substituted:
|
||||||
|
// {{in}} present -> the tool opens the source file itself
|
||||||
|
// {{in}} absent -> thor feeds the source file on stdin
|
||||||
|
// {{out}} present -> the tool writes the output file itself
|
||||||
|
// {{out}} absent -> thor captures stdout and writes it to out_path
|
||||||
|
// Either way the run ends with a real file at out_path.
|
||||||
|
uses_in := strings.contains(cmd_str, in_path)
|
||||||
|
uses_out := strings.contains(cmd_str, out_path)
|
||||||
|
|
||||||
|
// Ensure the output's parent dir exists (for the tool's {{out}} write or
|
||||||
|
// thor's stdout write).
|
||||||
|
if idx := strings.last_index(out_path, "/"); idx >= 0 {
|
||||||
|
if merr := os.make_directory_all(out_path[:idx]); merr != nil && merr != .Exist {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': cannot create %s: %v", name, out_path[:idx], merr),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
desc := os.Process_Desc {
|
||||||
|
command = argv,
|
||||||
|
}
|
||||||
|
// Feed the source on stdin unless the tool takes it as a {{in}} path.
|
||||||
|
stdin_file: ^os.File
|
||||||
|
if !uses_in {
|
||||||
|
f, oerr := os.open(in_path)
|
||||||
|
if oerr != nil {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': cannot open %s: %v", name, in_path, oerr),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
stdin_file = f
|
||||||
|
desc.stdin = f
|
||||||
|
}
|
||||||
|
defer if stdin_file != nil {
|
||||||
|
os.close(stdin_file)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, stdout, stderr, xerr := os.process_exec(desc, context.temp_allocator)
|
||||||
|
if xerr != nil {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': failed to run '%s': %v", name, argv[0], xerr),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state.exit_code != 0 {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf(
|
||||||
|
"tool '%s': '%s' exited %d\n%s",
|
||||||
|
name,
|
||||||
|
argv[0],
|
||||||
|
state.exit_code,
|
||||||
|
strings.trim_space(string(stderr)),
|
||||||
|
),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the tool streamed to stdout, persist it as the output file.
|
||||||
|
if !uses_out {
|
||||||
|
if werr := os.write_entire_file(out_path, stdout); werr != nil {
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("tool '%s': cannot write %s: %v", name, out_path, werr),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Memoize for the rest of this build. `key` and `out_url` are already on the
|
||||||
|
// temp allocator — the same build-scoped lifetime as the manifest — so they
|
||||||
|
// are stored directly, no clone needed.
|
||||||
|
if reg.manifest == nil {
|
||||||
|
reg.manifest = make(map[string]string, context.temp_allocator)
|
||||||
|
}
|
||||||
|
reg.manifest[key] = out_url
|
||||||
|
return out_url, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// render_command renders a tool's command template with the in/out/minify
|
||||||
|
// context. Paths render through mustache's default HTML escaping; the paths we
|
||||||
|
// generate are controlled and contain no HTML-special characters, so this is
|
||||||
|
// harmless in practice.
|
||||||
|
render_command :: proc(
|
||||||
|
tmpl_src, in_path, out_path: string,
|
||||||
|
minify: bool,
|
||||||
|
pos: int,
|
||||||
|
) -> (
|
||||||
|
string,
|
||||||
|
Error,
|
||||||
|
) {
|
||||||
|
tmpl, perr := parse(tmpl_src, "", context.temp_allocator, context.temp_allocator)
|
||||||
|
if perr != nil {
|
||||||
|
b := body(perr)
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("invalid tool command template: %s", b.msg),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// `in`/`out` are Odin keywords, so the context is a map rather than a struct.
|
||||||
|
data := make(map[string]any, 3, context.temp_allocator)
|
||||||
|
data["in"] = in_path
|
||||||
|
data["out"] = out_path
|
||||||
|
data["minify"] = minify
|
||||||
|
out, rerr := render(tmpl, data, allocator = context.temp_allocator)
|
||||||
|
if rerr != nil {
|
||||||
|
b := body(rerr)
|
||||||
|
return "", Error_Body {
|
||||||
|
msg = fmt.tprintf("failed to render tool command: %s", b.msg),
|
||||||
|
pos = pos,
|
||||||
|
kind = .Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// is_absolute_url reports whether a path points outside the local site.
|
||||||
|
is_absolute_url :: proc(s: string) -> bool {
|
||||||
|
return(
|
||||||
|
strings.has_prefix(s, "http://") ||
|
||||||
|
strings.has_prefix(s, "https://") ||
|
||||||
|
strings.has_prefix(s, "//") \
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// tokenize_command splits a command line into argv on whitespace, treating a
|
||||||
|
// double-quoted span as a single token with the quotes stripped. No escape
|
||||||
|
// sequences or single quotes (kept deliberately minimal — commands come from a
|
||||||
|
// trusted config, not untrusted input).
|
||||||
|
tokenize_command :: proc(cmd: string, allocator := context.temp_allocator) -> []string {
|
||||||
|
out := make([dynamic]string, 0, 8, allocator)
|
||||||
|
i := 0
|
||||||
|
for i < len(cmd) {
|
||||||
|
for i < len(cmd) && is_pipe_space(cmd[i]) {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
if i >= len(cmd) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if cmd[i] == '"' {
|
||||||
|
j := i + 1
|
||||||
|
for j < len(cmd) && cmd[j] != '"' {
|
||||||
|
j += 1
|
||||||
|
}
|
||||||
|
append(&out, strings.clone(cmd[i + 1:j], allocator))
|
||||||
|
i = j + 1 if j < len(cmd) else j
|
||||||
|
} else {
|
||||||
|
start := i
|
||||||
|
for i < len(cmd) && !is_pipe_space(cmd[i]) {
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
append(&out, strings.clone(cmd[start:i], allocator))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
// find_on_path reports whether `prog` is executable: an explicit path is checked
|
||||||
|
// directly, otherwise each PATH entry is probed.
|
||||||
|
find_on_path :: proc(prog: string) -> bool {
|
||||||
|
if strings.contains(prog, "/") {
|
||||||
|
return os.exists(prog)
|
||||||
|
}
|
||||||
|
path := os.get_env_alloc("PATH", context.temp_allocator)
|
||||||
|
if path == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
sep := ";" when ODIN_OS == .Windows else ":"
|
||||||
|
for dir in strings.split_iterator(&path, sep) {
|
||||||
|
if dir == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidate := fmt.tprintf("%s/%s", dir, prog)
|
||||||
|
if os.exists(candidate) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,208 @@
|
|||||||
|
#+test
|
||||||
|
package mustache
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
import "core:testing"
|
||||||
|
|
||||||
|
// tool_test_dirs makes a fresh in/out dir pair under the system temp dir and
|
||||||
|
// returns their paths (unique per name).
|
||||||
|
tool_test_dirs :: proc(name: string) -> (in_dir, out_dir: string) {
|
||||||
|
base, _ := os.temp_directory(context.temp_allocator)
|
||||||
|
in_dir = fmt.tprintf("%s/thor_tool_test/%s/in", base, name)
|
||||||
|
out_dir = fmt.tprintf("%s/thor_tool_test/%s/out", base, name)
|
||||||
|
os.make_directory_all(in_dir)
|
||||||
|
os.make_directory_all(out_dir)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tool_Test_Ctx carries the asset path as a context value (mustache resolves
|
||||||
|
// tag keys from the context, not from quoted literals).
|
||||||
|
Tool_Test_Ctx :: struct {
|
||||||
|
p: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_unknown_tool :: proc(t: ^testing.T) {
|
||||||
|
reg: Tool_Registry // no commands configured
|
||||||
|
tpl, _ := parse(`{{ p | tool nope }}`)
|
||||||
|
defer delete_template(&tpl)
|
||||||
|
_, err := render(tpl, Tool_Test_Ctx{p = "/a.css"}, tools = ®)
|
||||||
|
testing.expect(t, err != nil, "unknown tool should be a hard error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_unavailable_without_registry :: proc(t: ^testing.T) {
|
||||||
|
// No registry threaded through (tools == nil): the pipe must fail rather
|
||||||
|
// than silently do nothing.
|
||||||
|
tpl, _ := parse(`{{ p | tool anything }}`)
|
||||||
|
defer delete_template(&tpl)
|
||||||
|
_, err := render(tpl, Tool_Test_Ctx{p = "/a.css"})
|
||||||
|
testing.expect(t, err != nil, "tool pipe without a registry should error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_arg_count :: proc(t: ^testing.T) {
|
||||||
|
reg: Tool_Registry
|
||||||
|
|
||||||
|
// Zero args.
|
||||||
|
tpl0, _ := parse(`{{ p | tool }}`)
|
||||||
|
defer delete_template(&tpl0)
|
||||||
|
_, err0 := render(tpl0, Tool_Test_Ctx{p = "/a.css"}, tools = ®)
|
||||||
|
testing.expect(t, err0 != nil, "tool with no name should error")
|
||||||
|
|
||||||
|
// Two args.
|
||||||
|
tpl2, _ := parse(`{{ p | tool one two }}`)
|
||||||
|
defer delete_template(&tpl2)
|
||||||
|
_, err2 := render(tpl2, Tool_Test_Ctx{p = "/a.css"}, tools = ®)
|
||||||
|
testing.expect(t, err2 != nil, "tool with two args should error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_absolute_url_passthrough :: proc(t: ^testing.T) {
|
||||||
|
// A CDN URL has no local source; it should pass through untouched.
|
||||||
|
reg: Tool_Registry
|
||||||
|
url, err := run_tool(®, "anything", "https://cdn.example.com/x.css", 0)
|
||||||
|
testing.expect(t, err == nil, "absolute URL should not error")
|
||||||
|
testing.expect_value(t, url, "https://cdn.example.com/x.css")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_source_not_found :: proc(t: ^testing.T) {
|
||||||
|
in_dir, out_dir := tool_test_dirs("missing_src")
|
||||||
|
commands := make(map[string]string, context.temp_allocator)
|
||||||
|
commands["cp"] = "cp {{in}} {{out}}"
|
||||||
|
reg := Tool_Registry {
|
||||||
|
commands = commands,
|
||||||
|
input_dir = in_dir,
|
||||||
|
output_dir = out_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := run_tool(®, "cp", "/nope.css", 0)
|
||||||
|
testing.expect(t, err != nil, "missing source file should be a hard error")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_missing_binary :: proc(t: ^testing.T) {
|
||||||
|
in_dir, out_dir := tool_test_dirs("missing_bin")
|
||||||
|
_ = os.write_entire_file_from_string(fmt.tprintf("%s/a.css", in_dir), "body{}")
|
||||||
|
|
||||||
|
commands := make(map[string]string, context.temp_allocator)
|
||||||
|
commands["ghost"] = "thor-tool-does-not-exist-xyz {{in}} {{out}}"
|
||||||
|
reg := Tool_Registry {
|
||||||
|
commands = commands,
|
||||||
|
input_dir = in_dir,
|
||||||
|
output_dir = out_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := run_tool(®, "ghost", "/a.css", 0)
|
||||||
|
testing.expect(t, err != nil, "missing binary should be caught by PATH check")
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_runs :: proc(t: ^testing.T) {
|
||||||
|
when ODIN_OS != .Windows {
|
||||||
|
in_dir, out_dir := tool_test_dirs("runs")
|
||||||
|
os.make_directory_all(fmt.tprintf("%s/css", in_dir))
|
||||||
|
_ = os.write_entire_file_from_string(
|
||||||
|
fmt.tprintf("%s/css/main.css", in_dir),
|
||||||
|
"body { color: red }",
|
||||||
|
)
|
||||||
|
|
||||||
|
commands := make(map[string]string, context.temp_allocator)
|
||||||
|
commands["copyit"] = "cp {{in}} {{out}}"
|
||||||
|
reg := Tool_Registry {
|
||||||
|
commands = commands,
|
||||||
|
input_dir = in_dir,
|
||||||
|
output_dir = out_dir,
|
||||||
|
}
|
||||||
|
|
||||||
|
tpl, _ := parse(`{{ p | tool copyit }}`)
|
||||||
|
defer delete_template(&tpl)
|
||||||
|
result, err := render(tpl, Tool_Test_Ctx{p = "/css/main.css"}, tools = ®)
|
||||||
|
defer delete(result)
|
||||||
|
testing.expect(t, err == nil, "cp tool should succeed")
|
||||||
|
testing.expect_value(t, result, "/css/main.css")
|
||||||
|
|
||||||
|
// The tool wrote the output file.
|
||||||
|
written := fmt.tprintf("%s/css/main.css", out_dir)
|
||||||
|
data, rerr := os.read_entire_file_from_path(written, context.temp_allocator)
|
||||||
|
testing.expect(t, rerr == nil, "output file should exist")
|
||||||
|
testing.expect_value(t, string(data), "body { color: red }")
|
||||||
|
|
||||||
|
// The result was memoized; a second call for the same asset returns the
|
||||||
|
// same URL from the registry (mutated through the pointer).
|
||||||
|
url2, err2 := run_tool(®, "copyit", "/css/main.css", 0)
|
||||||
|
testing.expect(t, err2 == nil, "memoized call should not error")
|
||||||
|
testing.expect_value(t, url2, "/css/main.css")
|
||||||
|
testing.expect(t, len(reg.manifest) == 1, "result should be memoized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// tool_io_case runs `cmd` over a source file containing `content` and returns
|
||||||
|
// the resulting output-file contents. Exercises the in/out ↔ stdin/stdout 2×2.
|
||||||
|
tool_io_case :: proc(name, cmd, content: string) -> (out: string, ok: bool) {
|
||||||
|
in_dir, out_dir := tool_test_dirs(name)
|
||||||
|
_ = os.write_entire_file_from_string(fmt.tprintf("%s/a.css", in_dir), content)
|
||||||
|
commands := make(map[string]string, context.temp_allocator)
|
||||||
|
commands[name] = cmd
|
||||||
|
reg := Tool_Registry {
|
||||||
|
commands = commands,
|
||||||
|
input_dir = in_dir,
|
||||||
|
output_dir = out_dir,
|
||||||
|
}
|
||||||
|
_, err := run_tool(®, name, "/a.css", 0)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
data, rerr := os.read_entire_file_from_path(
|
||||||
|
fmt.tprintf("%s/a.css", out_dir),
|
||||||
|
context.temp_allocator,
|
||||||
|
)
|
||||||
|
return string(data), rerr == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_stdin_stdout :: proc(t: ^testing.T) {
|
||||||
|
when ODIN_OS != .Windows {
|
||||||
|
// `cat` with no args: reads stdin (thor feeds the source), writes stdout
|
||||||
|
// (thor captures and persists it).
|
||||||
|
out, ok := tool_io_case("stream", "cat", "body{a:1}")
|
||||||
|
testing.expect(t, ok, "cat (stdin→stdout) should produce output")
|
||||||
|
testing.expect_value(t, out, "body{a:1}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_in_path_stdout :: proc(t: ^testing.T) {
|
||||||
|
when ODIN_OS != .Windows {
|
||||||
|
// `cat {{in}}`: tool opens the file by path, streams to stdout.
|
||||||
|
out, ok := tool_io_case("in_stream", "cat {{in}}", "body{b:2}")
|
||||||
|
testing.expect(t, ok, "cat {{in}} (file→stdout) should produce output")
|
||||||
|
testing.expect_value(t, out, "body{b:2}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_stdin_out_path :: proc(t: ^testing.T) {
|
||||||
|
when ODIN_OS != .Windows {
|
||||||
|
// `tee {{out}}`: thor feeds stdin, tool writes the file itself.
|
||||||
|
out, ok := tool_io_case("stream_out", "tee {{out}}", "body{c:3}")
|
||||||
|
testing.expect(t, ok, "tee {{out}} (stdin→file) should produce output")
|
||||||
|
testing.expect_value(t, out, "body{c:3}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@(test)
|
||||||
|
test_tool_minify_section :: proc(t: ^testing.T) {
|
||||||
|
// The command template is itself mustache: {{#minify}} should expand only
|
||||||
|
// when the registry has minify on. Verify via the rendered command string.
|
||||||
|
cmd, err := render_command("prog {{#minify}}--min{{/minify}} {{in}}", "/a", "/b", true, 0)
|
||||||
|
testing.expect(t, err == nil, "render_command should succeed (minify on)")
|
||||||
|
testing.expect(t, strings.contains(cmd, "--min"), "minify section should render when on")
|
||||||
|
|
||||||
|
cmd2, err2 := render_command("prog {{#minify}}--min{{/minify}} {{in}}", "/a", "/b", false, 0)
|
||||||
|
testing.expect(t, err2 == nil, "render_command should succeed (minify off)")
|
||||||
|
testing.expect(t, !strings.contains(cmd2, "--min"), "minify section should be empty when off")
|
||||||
|
}
|
||||||
+20
-4
@@ -139,8 +139,14 @@ render_template :: proc(
|
|||||||
ctx: Template_Context,
|
ctx: Template_Context,
|
||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
reported_errors: ^map[string]bool,
|
reported_errors: ^map[string]bool,
|
||||||
|
tools: ^mustache.Tool_Registry,
|
||||||
) -> string {
|
) -> string {
|
||||||
result, err := mustache.render(content_tpl, []any{ctx.site, ctx.page, ctx}, partials)
|
result, err := mustache.render(
|
||||||
|
content_tpl,
|
||||||
|
[]any{ctx.site, ctx.page, ctx},
|
||||||
|
partials,
|
||||||
|
tools = tools,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
formatted := mustache.format_render_error(
|
formatted := mustache.format_render_error(
|
||||||
err,
|
err,
|
||||||
@@ -185,6 +191,16 @@ render_site :: proc(site: ^Site) {
|
|||||||
timezone = site.tz,
|
timezone = site.tz,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare the `tool` pipe registry for this build (commands were filled from
|
||||||
|
// thor.json at config time). A pointer to it is threaded into every
|
||||||
|
// mustache.render call.
|
||||||
|
mustache.tool_registry_init(
|
||||||
|
&site.tool_registry,
|
||||||
|
site.assets_dir,
|
||||||
|
site.output_dir,
|
||||||
|
.Minify in site.features,
|
||||||
|
)
|
||||||
|
|
||||||
// Find home page
|
// Find home page
|
||||||
home: Page
|
home: Page
|
||||||
has_home := false
|
has_home := false
|
||||||
@@ -317,7 +333,7 @@ render_page_html :: proc(
|
|||||||
ctx.page = page
|
ctx.page = page
|
||||||
ctx.og = og_for_page(site.og, page)
|
ctx.og = og_for_page(site.og, page)
|
||||||
ctx.params = merge_params(site.params, page.params)
|
ctx.params = merge_params(site.params, page.params)
|
||||||
return render_template(content_tpl, ctx, partials, seen)
|
return render_template(content_tpl, ctx, partials, seen, &site.tool_registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
render_home_html :: proc(
|
render_home_html :: proc(
|
||||||
@@ -341,7 +357,7 @@ render_home_html :: proc(
|
|||||||
ctx.og = og_for_page(site.og, home)
|
ctx.og = og_for_page(site.og, home)
|
||||||
ctx.params = merge_params(site.params, home.params)
|
ctx.params = merge_params(site.params, home.params)
|
||||||
|
|
||||||
return render_template(content_tpl, ctx, partials, seen)
|
return render_template(content_tpl, ctx, partials, seen, &site.tool_registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
render_section :: proc(
|
render_section :: proc(
|
||||||
@@ -380,7 +396,7 @@ render_section :: proc(
|
|||||||
}
|
}
|
||||||
ctx.posts = posts
|
ctx.posts = posts
|
||||||
ctx.params = merge_params(site.params, ctx.page.params)
|
ctx.params = merge_params(site.params, ctx.page.params)
|
||||||
return render_template(content_tpl, ctx, partials, seen)
|
return render_template(content_tpl, ctx, partials, seen, &site.tool_registry)
|
||||||
}
|
}
|
||||||
|
|
||||||
load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import "core:time/datetime"
|
|||||||
import "core:time/timezone"
|
import "core:time/timezone"
|
||||||
|
|
||||||
import md "markdown"
|
import md "markdown"
|
||||||
|
import "mustache"
|
||||||
|
|
||||||
|
|
||||||
// Site_Context holds the site date that is accessible in templates.
|
// Site_Context holds the site date that is accessible in templates.
|
||||||
@@ -48,6 +49,11 @@ Site :: struct {
|
|||||||
tz: ^datetime.TZ_Region,
|
tz: ^datetime.TZ_Region,
|
||||||
grammars: string,
|
grammars: string,
|
||||||
queries: string,
|
queries: string,
|
||||||
|
// tool_registry is the `tool` pipe's state: `commands` is filled from
|
||||||
|
// thor.json at config time; the per-build I/O dirs, minify flag, and memo
|
||||||
|
// are set by mustache.tool_registry_init in render_site. A pointer is
|
||||||
|
// threaded into mustache.render.
|
||||||
|
tool_registry: mustache.Tool_Registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
Date_Preferences :: struct {
|
Date_Preferences :: struct {
|
||||||
@@ -79,6 +85,7 @@ Config_File :: struct {
|
|||||||
date: Date_Preferences,
|
date: Date_Preferences,
|
||||||
grammars: string,
|
grammars: string,
|
||||||
queries: string,
|
queries: string,
|
||||||
|
tools: map[string]string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configuration loaded from command line arguments. Gets folded in to Site
|
// Configuration loaded from command line arguments. Gets folded in to Site
|
||||||
@@ -218,6 +225,10 @@ site_apply_config :: proc(site: ^Site, config: Config_File, config_dir: string)
|
|||||||
|
|
||||||
site.grammars = expand_path(config.grammars, site_allocator(site))
|
site.grammars = expand_path(config.grammars, site_allocator(site))
|
||||||
site.queries = expand_path(config.queries, site_allocator(site))
|
site.queries = expand_path(config.queries, site_allocator(site))
|
||||||
|
|
||||||
|
if config.tools != nil {
|
||||||
|
site.tool_registry.commands = config.tools
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// site_base_path extracts the path component of base_url, normalized to a
|
// site_base_path extracts the path component of base_url, normalized to a
|
||||||
|
|||||||
@@ -16,9 +16,15 @@ body>header {
|
|||||||
top: 116px;
|
top: 116px;
|
||||||
left: 0;
|
left: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
--pico-nav-element-spacing-vertical: 0.25rem;
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
flex-direction: column;
|
display: block;
|
||||||
|
|
||||||
|
&>li {
|
||||||
|
margin-left: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user