mirror of
https://github.com/sbrow/thor.git
synced 2026-10-25 02:01:56 -04:00
feat: Added rel_url pipe for subsites.
This commit is contained in:
@@ -87,6 +87,7 @@
|
||||
- [ ] don't use bullshit "sub-tokens", add filters and pipes as proper tokens.
|
||||
- [ ] Create a Location struct that somewhat matches Odin's [Source_Code_Location](https://pkg.odin-lang.org/base/runtime/#Source_Code_Location)?
|
||||
- Note that odin's version doesn't contain the stack trace.
|
||||
- [ ] Link checker / linter that warns if links don't resolve. ("please use {{whatever | rel_url }}").
|
||||
|
||||
### Memory Management
|
||||
|
||||
|
||||
@@ -43,6 +43,9 @@ Remember to polish existing features before moving on to new ones.
|
||||
- do we want to do this or just use DIAGNOSTIC_TESTS.md as the tests?
|
||||
- [ ] also update to use file logger so they don't clog up the output
|
||||
- [ ] narrow highlight in diagnostic to relevent part of line.
|
||||
- [ ] page.url and page.permalink are confusing names. Switch to url + page_url
|
||||
- There is confusion over how to do this and whethor or not `| rel_url` should be needed to render these.
|
||||
Requires **careful thought**.
|
||||
|
||||
## Documentation
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{{&content}}
|
||||
</header>
|
||||
<ul>
|
||||
{{#pages}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date}}<time
|
||||
{{#pages}} <li><a href="{{permalink | rel_url}}">{{&title}}</a><span>{{#date}}<time
|
||||
datetime="{{date}}">{{date | format}}</time>{{/date}}</span>
|
||||
</li>
|
||||
{{/pages}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<header>
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/">{{> home-link}}</a></li>
|
||||
<li><a href="{{site.base_path}}/">{{> home-link}}</a></li>
|
||||
{{#menus.main}}
|
||||
<li><a href="{{url}}">{{name}}</a></li>
|
||||
<li><a href="{{url | rel_url}}">{{name}}</a></li>
|
||||
{{/menus.main}}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
@@ -220,4 +220,4 @@ label.margin-toggle:not(.sidenote-number) {
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{#params.stylesheets}}<link rel="stylesheet" href="{{.}}">{{/params.stylesheets}}
|
||||
{{#params.stylesheets}}<link rel="stylesheet" href="{{.|rel_url}}">{{/params.stylesheets}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<section>
|
||||
<h2>{{key}}</h2>
|
||||
<ul>
|
||||
{{#items}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date}}<time
|
||||
{{#items}} <li><a href="{{permalink | rel_url}}">{{&title}}</a><span>{{#date}}<time
|
||||
datetime="{{date}}">{{date | format}}</time>{{/date}}</span>
|
||||
</li>
|
||||
{{/items}}
|
||||
|
||||
@@ -4,3 +4,4 @@
|
||||
- [ ] Simplify `trim_standalone_whitespace` — four temp arrays (`li_buf`, `ri_buf`, `left_done`, `right_done`) could potentially be reduced to two by merging the done-tracking into the index buffers using sentinel values (e.g. `-2` = already trimmed).
|
||||
- [ ] Adjacent standalone tags don't capture indent for the second tag (e.g. ` {{>a}}\n {{>b}}` — `{{>b}}` gets `indent=""` because its left text was consumed by `{{>a}}`'s right-trim). Blocks recover via `deindent_blocks`, but partials/parents have no fallback.
|
||||
- [ ] lazy load partials ? (with mutex)
|
||||
- [ ] Support changing delimiters (Set Delimiter tag `{{=<% %>=}}`). Delimiters `{{`/`}}`/`{{{`/`}}}` are hardcoded in [tokenizer.odin](./tokenizer.odin); needs an `=` sigil case, mutable open/close delimiters threaded through `tokenize`, and wiring up `spec/specs/delimiters.json` in `spec_test.odin` (fixture present but not run).
|
||||
|
||||
+78
-19
@@ -24,6 +24,7 @@ Pipe_Op :: enum {
|
||||
Group_By,
|
||||
First,
|
||||
Last,
|
||||
Rel_Url,
|
||||
}
|
||||
|
||||
// Pipe op names are derived from the enum via reflection (lowercased).
|
||||
@@ -60,10 +61,10 @@ pipe_op_candidates :: proc(allocator := context.temp_allocator) -> []string {
|
||||
}
|
||||
|
||||
Pipe_Filter :: struct {
|
||||
op: string,
|
||||
args: [dynamic; MAX_PIPE_ARGS]string,
|
||||
op_pos: int,
|
||||
end_pos: int,
|
||||
op: string,
|
||||
args: [dynamic; MAX_PIPE_ARGS]string,
|
||||
op_pos: int,
|
||||
end_pos: int,
|
||||
}
|
||||
|
||||
Group :: struct {
|
||||
@@ -345,10 +346,56 @@ apply_filter :: proc(
|
||||
case .Last:
|
||||
span := filter.end_pos - filter.op_pos
|
||||
return take(value, filter.args[:], false, tmpl, filter.op_pos, span)
|
||||
case .Rel_Url:
|
||||
str, ok := reflect.as_string(value)
|
||||
if !ok {
|
||||
return value, Error_Body {
|
||||
msg = "rel_url may only be used on strings",
|
||||
pos = pos,
|
||||
kind = .Data,
|
||||
}
|
||||
}
|
||||
result := rel_url(str, ctx)
|
||||
return any{new_clone(result, context.temp_allocator), typeid_of(string)}, nil
|
||||
}
|
||||
return {}, nil
|
||||
}
|
||||
|
||||
// rel_url prefixes a root-relative path (e.g. "/faq/") with the site's
|
||||
// base_path so links resolve correctly under a subpath deployment. Values that
|
||||
// are already absolute — a scheme ("http://"), protocol-relative ("//"), an
|
||||
// in-page fragment ("#"), or a "mailto:" — are returned unchanged, which keeps
|
||||
// external menu and footer links intact. base_path is read from the context
|
||||
// stack the same way `format` reads date_format/timezone.
|
||||
rel_url :: proc(path: string, ctx: []any) -> string {
|
||||
if path == "" {
|
||||
return path
|
||||
}
|
||||
if strings.has_prefix(path, "http://") ||
|
||||
strings.has_prefix(path, "https://") ||
|
||||
strings.has_prefix(path, "//") ||
|
||||
strings.has_prefix(path, "#") ||
|
||||
strings.has_prefix(path, "mailto:") {
|
||||
return path
|
||||
}
|
||||
|
||||
base := ""
|
||||
if raw := resolve_name("base_path", ctx); raw != nil {
|
||||
if s, ok := reflect.as_string(raw); ok {
|
||||
base = s
|
||||
}
|
||||
}
|
||||
if base == "" {
|
||||
return path
|
||||
}
|
||||
|
||||
// Only prefix root-relative paths; leave document-relative ones alone.
|
||||
if path[0] == '/' {
|
||||
return fmt.tprintf("%s%s", base, path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
apply_format :: proc(
|
||||
iso: string,
|
||||
args: []string,
|
||||
@@ -386,7 +433,15 @@ apply_format :: proc(
|
||||
}
|
||||
|
||||
// Groups preserve first-appearance order from the input list.
|
||||
apply_group_by :: proc(value: any, args: []string, pos: int, span: int) -> (result: any, err: Error) {
|
||||
apply_group_by :: proc(
|
||||
value: any,
|
||||
args: []string,
|
||||
pos: int,
|
||||
span: int,
|
||||
) -> (
|
||||
result: any,
|
||||
err: Error,
|
||||
) {
|
||||
if len(args) != 1 {
|
||||
hint := ""
|
||||
if len(args) == 0 {
|
||||
@@ -406,7 +461,12 @@ apply_group_by :: proc(value: any, args: []string, pos: int, span: int) -> (resu
|
||||
|
||||
elem_info, count, data := list_info(value)
|
||||
if elem_info == nil {
|
||||
return nil, Error_Body{msg = "group_by expects a list", pos = pos, span = span, kind = .Data}
|
||||
return nil, Error_Body {
|
||||
msg = "group_by expects a list",
|
||||
pos = pos,
|
||||
span = span,
|
||||
kind = .Data,
|
||||
}
|
||||
}
|
||||
|
||||
groups := make([dynamic]Group, 0, 8, context.temp_allocator)
|
||||
@@ -486,13 +546,13 @@ take :: proc(
|
||||
n := 1
|
||||
if !is_list && len(args) == 0 {
|
||||
warning = Error_Body {
|
||||
msg = "defaulting to n=1 for string",
|
||||
pos = op_pos,
|
||||
span = span,
|
||||
kind = .Warning,
|
||||
msg = "defaulting to n=1 for string",
|
||||
pos = op_pos,
|
||||
span = span,
|
||||
kind = .Warning,
|
||||
source = tmpl.source,
|
||||
path = tmpl.path,
|
||||
hint = "consider specifying n explicitly",
|
||||
path = tmpl.path,
|
||||
hint = "consider specifying n explicitly",
|
||||
}
|
||||
}
|
||||
if len(args) > 0 {
|
||||
@@ -525,13 +585,13 @@ take :: proc(
|
||||
op_name := from_start ? "first" : "last"
|
||||
alt_name := from_start ? "last" : "first"
|
||||
warning = Error_Body {
|
||||
msg = fmt.tprintf("%s %d is equivalent to %s %d", op_name, n, alt_name, 0 - n),
|
||||
pos = op_pos,
|
||||
span = span,
|
||||
kind = .Warning,
|
||||
msg = fmt.tprintf("%s %d is equivalent to %s %d", op_name, n, alt_name, 0 - n),
|
||||
pos = op_pos,
|
||||
span = span,
|
||||
kind = .Warning,
|
||||
source = tmpl.source,
|
||||
path = tmpl.path,
|
||||
hint = "consider using the clearer form",
|
||||
path = tmpl.path,
|
||||
hint = "consider using the clearer form",
|
||||
}
|
||||
from_start = !from_start
|
||||
n = 0 - n
|
||||
@@ -557,4 +617,3 @@ take :: proc(
|
||||
return any{new_clone(str[start:end], context.temp_allocator), typeid_of(string)}, warning
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,6 +17,12 @@ Site_Context :: struct {
|
||||
title: string,
|
||||
description: string,
|
||||
base_url: string,
|
||||
// base_path is the path component of base_url, normalized to a leading slash
|
||||
// with no trailing slash ("" for root-hosted sites). Templates prefix internal
|
||||
// root-relative links with it (directly, or via the `rel_url` pipe) so that
|
||||
// subpath deployments — e.g. project pages at https://user.github.io/proj —
|
||||
// resolve correctly.
|
||||
base_path: string,
|
||||
params: json.Object,
|
||||
og: Open_Graph,
|
||||
menus: map[string][]Menu_Entry,
|
||||
@@ -127,6 +133,7 @@ init_site :: proc(site: ^Site, flags: Flags) {
|
||||
}
|
||||
|
||||
site_apply_cli_flags(site, flags)
|
||||
site.base_path = site_base_path(site.base_url, site_allocator(site))
|
||||
site.config_path = path
|
||||
|
||||
site.og = og_for_site(site)
|
||||
@@ -213,6 +220,48 @@ site_apply_config :: proc(site: ^Site, config: Config_File, config_dir: string)
|
||||
site.queries = expand_path(config.queries, site_allocator(site))
|
||||
}
|
||||
|
||||
// site_base_path extracts the path component of base_url, normalized to a
|
||||
// leading slash with no trailing slash. Returns "" for root-hosted sites.
|
||||
//
|
||||
// "https://ex.com/thor/" → "/thor"
|
||||
// "https://ex.com/a/b" → "/a/b"
|
||||
// "https://ex.com" → ""
|
||||
// "http://localhost:8080/" → ""
|
||||
site_base_path :: proc(base_url: string, allocator := context.allocator) -> string {
|
||||
url := base_url
|
||||
|
||||
// Strip scheme so the first '/' we find is the start of the path, not the
|
||||
// "//" separating scheme and host.
|
||||
if idx := strings.index(url, "://"); idx >= 0 {
|
||||
url = url[idx + 3:]
|
||||
}
|
||||
|
||||
// The path begins at the first '/' after the host.
|
||||
slash := strings.index_byte(url, '/')
|
||||
if slash < 0 {
|
||||
return ""
|
||||
}
|
||||
path := url[slash:]
|
||||
|
||||
// Drop any query or fragment.
|
||||
if idx := strings.index_byte(path, '?'); idx >= 0 {
|
||||
path = path[:idx]
|
||||
}
|
||||
if idx := strings.index_byte(path, '#'); idx >= 0 {
|
||||
path = path[:idx]
|
||||
}
|
||||
|
||||
// Trim trailing slashes; a bare "/" normalizes to "".
|
||||
for len(path) > 0 && path[len(path) - 1] == '/' {
|
||||
path = path[:len(path) - 1]
|
||||
}
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.clone(path, allocator)
|
||||
}
|
||||
|
||||
// expand_path replaces a leading ~/ with $HOME/.
|
||||
expand_path :: proc(path: string, allocator := context.allocator) -> string {
|
||||
if len(path) >= 2 && path[0] == '~' && path[1] == '/' {
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"date": {
|
||||
"timezone": "America/New_York"
|
||||
"timezone": "America/New_York",
|
||||
"format": "January 2, 2006"
|
||||
},
|
||||
"markdown_extensions": {
|
||||
"footnotes": true
|
||||
|
||||
Reference in New Issue
Block a user