mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
Compare commits
6 Commits
main
...
0abac89d88
| Author | SHA1 | Date | |
|---|---|---|---|
| 0abac89d88 | |||
| 1824bae26c | |||
| 5627215d6f | |||
| 9d1954d70a | |||
| fcf349eb51 | |||
| d51172c836 |
@@ -220,7 +220,7 @@ Templates use Mustache with template inheritance (`{{<base}}` / `{{$block}}`):
|
|||||||
<!-- page.html (content layout) -->
|
<!-- page.html (content layout) -->
|
||||||
{{<base}}
|
{{<base}}
|
||||||
{{$main}}
|
{{$main}}
|
||||||
<main><article><h1>{{page_title}}</h1>{{&content}}</article></main>
|
<main><article><h1>{{page.title}}</h1>{{&content}}</article></main>
|
||||||
{{/main}}
|
{{/main}}
|
||||||
{{/base}}
|
{{/base}}
|
||||||
```
|
```
|
||||||
@@ -240,7 +240,7 @@ Base_Data :: struct {
|
|||||||
}
|
}
|
||||||
Page_Data :: struct {
|
Page_Data :: struct {
|
||||||
using base: Base_Data, // fields promoted via reflection fallback
|
using base: Base_Data, // fields promoted via reflection fallback
|
||||||
page_title: string,
|
page.title: string,
|
||||||
date: string, // raw ISO 8601; formatted via `| format` in templates
|
date: string, // raw ISO 8601; formatted via `| format` in templates
|
||||||
}
|
}
|
||||||
Home_Data :: struct {
|
Home_Data :: struct {
|
||||||
@@ -249,7 +249,7 @@ Home_Data :: struct {
|
|||||||
}
|
}
|
||||||
Section_Data :: struct {
|
Section_Data :: struct {
|
||||||
using base: Base_Data,
|
using base: Base_Data,
|
||||||
page_title: string,
|
page.title: string,
|
||||||
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
|
posts: [dynamic]Page_Context, // flat list; year grouping done in template via pipe
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
# Thor
|
# Thor
|
||||||
|
|
||||||
[TOC]
|
Thor is a simple Static Site Generator designed for personal blogs and other small websites.
|
||||||
|
|
||||||
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.
|
Its core principals are simplicity and minimal configuration, so you can get started as quickly as possible.
|
||||||
|
|
||||||
@@ -16,14 +14,14 @@ It is based on Hugo, and gingerbill's SSG. Templating is done with (extended?) M
|
|||||||
- Menus (WIP)
|
- Menus (WIP)
|
||||||
- Extended Markdown ([See below](#extended-markdown))
|
- Extended Markdown ([See below](#extended-markdown))
|
||||||
- Basic (whitespace) minification.
|
- Basic (whitespace) minification.
|
||||||
|
- Union File System (Modules)
|
||||||
|
|
||||||
## What it doesn't do
|
## What it doesn't do
|
||||||
- Internationalization
|
- Internationalization
|
||||||
- Pagination (Yet)
|
- Pagination (Yet)
|
||||||
- Themes
|
- Themes
|
||||||
- Union File System (Yet)
|
- Image Manipulation
|
||||||
- Image Manipulation
|
- TailwindCSS integration
|
||||||
- TailwindCSS integration
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -34,7 +32,8 @@ Then follow [The Guide]()
|
|||||||
For a more complete setup, run `thor new site`.
|
For a more complete setup, run `thor new site`.
|
||||||
|
|
||||||
## Extended Markdown
|
## Extended Markdown
|
||||||
|
|
||||||
- Emoji expansion
|
- Emoji expansion
|
||||||
- margin style footnotes
|
- margin style footnotes
|
||||||
- Guthub style alerts
|
- Github style alerts
|
||||||
- [and more]
|
- [and more]
|
||||||
|
|||||||
@@ -2,12 +2,25 @@
|
|||||||
|
|
||||||
- Polish existing features before moving on to new ones.
|
- Polish existing features before moving on to new ones.
|
||||||
- [ ] Improve diagnostics
|
- [ ] Improve diagnostics
|
||||||
- [ ] Simplify / unify template context stack. Come up with a name for it.
|
- [ ] show "stack traces" in template error diagnostics
|
||||||
- [ ] `render_template` should accept `Template_Context`, not `any`
|
- [ ] better diagnostics for syntax errors in treesitter.
|
||||||
|
- [ ] Ensure diagnostics for MAX_CONTEXT_DEPTH are good.
|
||||||
|
- [ ] show a proper diagnostic for timezones
|
||||||
|
- currently "unable to load timezone 'America/New_Yorkskie'"
|
||||||
|
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
|
||||||
|
- [x] Simplify / unify template context stack. Come up with a name for it.
|
||||||
|
- [x] `render_template` should accept `Template_Context`, not `any`
|
||||||
- [ ] Load grammars dynamically
|
- [ ] Load grammars dynamically
|
||||||
- [ ] consider adding a limit to the context stack in mustache.
|
- [x] consider adding a limit to the context stack in mustache.
|
||||||
- [ ] better diagnostics for syntax errors in treesitter.
|
|
||||||
- [x] Add heading ids as a default on extension.
|
- [x] Add heading ids as a default on extension.
|
||||||
|
- [ ] starred must be a param.
|
||||||
|
- [x] Add a `#config(MAX_CONTEXT_DEPTH, 16?)` to `mustache`.
|
||||||
|
- [ ] Documentation
|
||||||
|
- [ ] talk about the context stack (and its limit).
|
||||||
|
- [ ] menu system
|
||||||
|
- [ ] like Hugo's, but warn(/fail?) if menus are defined in the config *and* pages.
|
||||||
|
- i.e. force the user to choose one or the other.
|
||||||
|
- [ ] Don't show annoying log output in tests.
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
@@ -32,6 +45,11 @@
|
|||||||
- [ ] can markdown extensions run in parallel?
|
- [ ] can markdown extensions run in parallel?
|
||||||
- [ ] enforce MAX_SLUG_LENGTH
|
- [ ] enforce MAX_SLUG_LENGTH
|
||||||
|
|
||||||
|
## Remove Privileged content
|
||||||
|
|
||||||
|
- [ ] `group_by` currently requires a computed `year` field on the page.
|
||||||
|
- We should replace this with `{{ pages | group_by (date | "2006") }}` or similar
|
||||||
|
|
||||||
|
|
||||||
## Memory Management
|
## Memory Management
|
||||||
|
|
||||||
@@ -58,9 +76,6 @@
|
|||||||
- [ ] display an error when no part of the date appears in the output.
|
- [ ] display an error when no part of the date appears in the output.
|
||||||
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
- [ ] Handle 0 and whitespace padding i.e. "_2" -> " 2"
|
||||||
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
- [ ] Do we *need* mustache.Date_Components, or can we use core:time/datetime.DateTime?
|
||||||
- [ ] show a proper diagnostic for timezones
|
|
||||||
- currently "unable to load timezone 'America/New_Yorkskie'"
|
|
||||||
- want rust style diagnostic and better message, maybe "unknown timezone 'America/New_Yorkskie'"
|
|
||||||
|
|
||||||
## General
|
## General
|
||||||
- [ ] get rid of the global variables in the `treesitter` package.
|
- [ ] get rid of the global variables in the `treesitter` package.
|
||||||
@@ -145,6 +160,7 @@ main :: proc () {
|
|||||||
- [ ] `new site` set up new project
|
- [ ] `new site` set up new project
|
||||||
- [ ] warn/error when unknown key used in mustache.
|
- [ ] warn/error when unknown key used in mustache.
|
||||||
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
||||||
|
- [ ] opt-in "strict_keys" mode. in this mode, key lookups may not view parent objects.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
+13
-2
@@ -7,6 +7,7 @@ import "core:fmt"
|
|||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
import "core:time"
|
||||||
|
|
||||||
// Fields with underscores should never be set by the user.
|
// Fields with underscores should never be set by the user.
|
||||||
Page :: struct {
|
Page :: struct {
|
||||||
@@ -18,12 +19,13 @@ Page :: struct {
|
|||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
date: string,
|
date: string,
|
||||||
|
year: string,
|
||||||
lastmod: string,
|
lastmod: string,
|
||||||
menu: string,
|
menu: string,
|
||||||
content: string,
|
content: string,
|
||||||
og: Open_Graph,
|
og: Open_Graph,
|
||||||
draft: bool,
|
draft: bool,
|
||||||
is_starred: bool,
|
starred: bool,
|
||||||
_is_index: bool `private`,
|
_is_index: bool `private`,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,9 +233,18 @@ load_page :: proc(
|
|||||||
page.title = fm.title
|
page.title = fm.title
|
||||||
page.description = fm.description
|
page.description = fm.description
|
||||||
page.date = fm.date
|
page.date = fm.date
|
||||||
|
if page.date == "" {
|
||||||
|
info, stat_err := os.stat(file_path, context.allocator)
|
||||||
|
if stat_err == nil {
|
||||||
|
page.date, _ = time.time_to_rfc3339(info.modification_time, 0, false, context.allocator)
|
||||||
|
os.file_info_delete(info, context.allocator)
|
||||||
|
log.warnf("no date in frontmatter for %s, using file modification time: %s", file_path, page.date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
page.year = get_year(page.date)
|
||||||
page.lastmod = fm.lastmod
|
page.lastmod = fm.lastmod
|
||||||
page.draft = fm.draft
|
page.draft = fm.draft
|
||||||
page.is_starred = fm.isStarred
|
page.starred = fm.isStarred
|
||||||
page.menu = fm.menu
|
page.menu = fm.menu
|
||||||
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
|
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
|
||||||
page.og = fm.og
|
page.og = fm.og
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
{{$main}}
|
{{$main}}
|
||||||
<main>
|
<main>
|
||||||
<article>
|
<article>
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page.title}}</h1>
|
||||||
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
|
{{#date_iso}} <time class="subtitle" datetime="{{date_iso}}">{{date_display}}</time>
|
||||||
{{/date_iso}} {{&content}}
|
{{/date_iso}} {{&content}}
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{{<base}}
|
{{<base}}
|
||||||
{{$main}}
|
{{$main}}
|
||||||
<main>
|
<main>
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page.title}}</h1>
|
||||||
{{&content}}
|
{{&content}}
|
||||||
{{#posts | group_by year}}
|
{{#posts | group_by year}}
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
|
||||||
|
Ctx :: struct {
|
||||||
|
title: string,
|
||||||
|
using page: Page,
|
||||||
|
site: Site,
|
||||||
|
}
|
||||||
|
|
||||||
|
Page :: struct {
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
Site :: struct {
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: proc() {
|
||||||
|
site := Ctx {
|
||||||
|
site = Site{title = "foo"},
|
||||||
|
page = Page{title = "bar"},
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.printfln("%+v", site)
|
||||||
|
fmt.printf("%+v", site)
|
||||||
|
}
|
||||||
|
|
||||||
+198
-138
@@ -1,17 +1,28 @@
|
|||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
|
import "base:runtime"
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:reflect"
|
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
|
// A hypothetical maximum context depth. Trying to pass more than this many items
|
||||||
|
// to render(tpl, data), or nesting templates further than this depth would be
|
||||||
|
// an error.
|
||||||
|
// May be enforced in a later version (for performance)
|
||||||
|
MAX_CONTEXT_DEPTH :: #config(MAX_CONTEXT_DEPTH, 16)
|
||||||
|
|
||||||
|
// Context_Stack is the growable stack of data frames walked top-to-bottom by
|
||||||
|
// resolve_name. Frames are pushed on section descent and popped on exit; the
|
||||||
|
// root data (or each element of a root []any) forms the base frames.
|
||||||
|
Context_Stack :: [dynamic]any
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Error types
|
// Error types
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
Error_Kind :: enum {
|
Error_Kind :: enum {
|
||||||
Syntax, // parse-time: malformed template
|
Syntax, // parse-time: malformed template
|
||||||
Data, // render-time: template fine, data wrong (e.g. filter misuse)
|
Data, // render-time: template fine, data wrong (e.g. filter misuse)
|
||||||
}
|
}
|
||||||
|
|
||||||
Error_Body :: struct {
|
Error_Body :: struct {
|
||||||
@@ -21,14 +32,18 @@ Error_Body :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Error is nil when no error occurred.
|
// Error is nil when no error occurred.
|
||||||
Error :: union { Error_Body }
|
Error :: union {
|
||||||
|
Error_Body,
|
||||||
|
}
|
||||||
|
|
||||||
// body unwraps the Error_Body from a non-nil Error.
|
// body unwraps the Error_Body from a non-nil Error.
|
||||||
// Precondition: err != nil.
|
// Precondition: err != nil.
|
||||||
body :: proc(err: Error) -> Error_Body {
|
body :: proc(err: Error) -> Error_Body {
|
||||||
switch e in err {
|
switch e in err {
|
||||||
case Error_Body: return e
|
case Error_Body:
|
||||||
case: return {}
|
return e
|
||||||
|
case:
|
||||||
|
return {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,12 +157,24 @@ render :: proc(
|
|||||||
err: Error,
|
err: Error,
|
||||||
) {
|
) {
|
||||||
builder: strings.Builder
|
builder: strings.Builder
|
||||||
strings.builder_init(&builder, allocator)
|
strings.builder_init(&builder, context.temp_allocator)
|
||||||
defer strings.builder_destroy(&builder)
|
|
||||||
|
|
||||||
ctx := make([dynamic]any, 0, 4, allocator)
|
ctx := make(Context_Stack, 0, 4, context.temp_allocator)
|
||||||
defer delete(ctx)
|
|
||||||
append(&ctx, data)
|
// If data is a []any, expand into individual context frames.
|
||||||
|
// Otherwise, push as a single frame.
|
||||||
|
elem_info, count, slice_data := list_info(data)
|
||||||
|
if elem_info != nil {
|
||||||
|
if _, is_any := elem_info.variant.(runtime.Type_Info_Any); is_any {
|
||||||
|
for j in 0 ..< count {
|
||||||
|
append(&ctx, extract_list_element(elem_info, slice_data, j))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(&ctx, data)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
append(&ctx, data)
|
||||||
|
}
|
||||||
|
|
||||||
all_nodes := tmpl.nodes[:]
|
all_nodes := tmpl.nodes[:]
|
||||||
err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder)
|
err = render_nodes(tmpl, all_nodes, &ctx, partials, &builder)
|
||||||
@@ -272,14 +299,14 @@ parse_section :: proc(
|
|||||||
|
|
||||||
case .Section_Close:
|
case .Section_Close:
|
||||||
if strings.contains(tok.value, "|") {
|
if strings.contains(tok.value, "|") {
|
||||||
return Error_Body {
|
return Error_Body {
|
||||||
msg = fmt.tprintf(
|
msg = fmt.tprintf(
|
||||||
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
|
"pipe expression not allowed in close tag '{{{{/%s}}}}' — use the bare key",
|
||||||
tok.value,
|
tok.value,
|
||||||
),
|
),
|
||||||
pos = tok.pos,
|
pos = tok.pos,
|
||||||
kind = .Syntax,
|
kind = .Syntax,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if end_tag != "" && tok.value == end_tag {
|
if end_tag != "" && tok.value == end_tag {
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
@@ -316,12 +343,7 @@ parse_section :: proc(
|
|||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(
|
append(
|
||||||
nodes,
|
nodes,
|
||||||
Node {
|
Node{kind = .Parent, key = tok.value, indent = tok.indent, pos = tok.pos},
|
||||||
kind = .Parent,
|
|
||||||
key = tok.value,
|
|
||||||
indent = tok.indent,
|
|
||||||
pos = tok.pos,
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
||||||
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
||||||
@@ -329,15 +351,7 @@ parse_section :: proc(
|
|||||||
case .Block_Open:
|
case .Block_Open:
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
idx := len(nodes)
|
idx := len(nodes)
|
||||||
append(
|
append(nodes, Node{kind = .Block, key = tok.value, indent = tok.indent, pos = tok.pos})
|
||||||
nodes,
|
|
||||||
Node {
|
|
||||||
kind = .Block,
|
|
||||||
key = tok.value,
|
|
||||||
indent = tok.indent,
|
|
||||||
pos = tok.pos,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
parse_section(tokens, pos, nodes, tok.value, source, allocator, tok.pos) or_return
|
||||||
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
nodes[idx].children = nodes[idx + 1:len(nodes)]
|
||||||
}
|
}
|
||||||
@@ -484,21 +498,29 @@ remove_line_indent :: proc(s: string, indent: string, allocator := context.alloc
|
|||||||
|
|
||||||
render_template :: proc(
|
render_template :: proc(
|
||||||
pt: Template,
|
pt: Template,
|
||||||
ctx: ^[dynamic]any,
|
ctx: ^Context_Stack,
|
||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override,
|
blocks: map[string]Block_Override,
|
||||||
indent: string,
|
indent: string,
|
||||||
) -> Error {
|
) -> Error {
|
||||||
if len(indent) > 0 {
|
if len(indent) > 0 {
|
||||||
state := Indent_State{indent = indent, at_line_start = false}
|
state := Indent_State {
|
||||||
|
indent = indent,
|
||||||
|
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)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, &state)
|
||||||
}
|
}
|
||||||
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
|
return render_nodes(pt, pt.nodes[:], ctx, partials, b, blocks, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_line_start: ^bool) {
|
write_indented :: proc(
|
||||||
|
b: ^strings.Builder,
|
||||||
|
indent: string,
|
||||||
|
content: string,
|
||||||
|
at_line_start: ^bool,
|
||||||
|
) {
|
||||||
if len(indent) == 0 || len(content) == 0 {
|
if len(indent) == 0 || len(content) == 0 {
|
||||||
strings.write_string(b, content)
|
strings.write_string(b, content)
|
||||||
return
|
return
|
||||||
@@ -529,7 +551,7 @@ write_indented :: proc(b: ^strings.Builder, indent: string, content: string, at_
|
|||||||
render_nodes :: proc(
|
render_nodes :: proc(
|
||||||
current: Template,
|
current: Template,
|
||||||
nodes: []Node,
|
nodes: []Node,
|
||||||
ctx: ^[dynamic]any,
|
ctx: ^Context_Stack,
|
||||||
partials: map[string]Template,
|
partials: map[string]Template,
|
||||||
b: ^strings.Builder,
|
b: ^strings.Builder,
|
||||||
blocks: map[string]Block_Override = nil,
|
blocks: map[string]Block_Override = nil,
|
||||||
@@ -573,16 +595,16 @@ render_nodes :: proc(
|
|||||||
if perr == nil {
|
if perr == nil {
|
||||||
temp: strings.Builder
|
temp: strings.Builder
|
||||||
strings.builder_init(&temp, context.temp_allocator)
|
strings.builder_init(&temp, context.temp_allocator)
|
||||||
render_nodes(
|
render_nodes(
|
||||||
sub_tpl,
|
sub_tpl,
|
||||||
sub_tpl.nodes[:],
|
sub_tpl.nodes[:],
|
||||||
ctx,
|
ctx,
|
||||||
partials,
|
partials,
|
||||||
&temp,
|
&temp,
|
||||||
blocks,
|
blocks,
|
||||||
nil,
|
nil,
|
||||||
) or_return
|
) or_return
|
||||||
write_value(b, strings.to_string(temp), escape = true)
|
write_value(b, strings.to_string(temp), escape = true)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write_value(b, val, escape = true)
|
write_value(b, val, escape = true)
|
||||||
@@ -615,16 +637,16 @@ render_nodes :: proc(
|
|||||||
if perr == nil {
|
if perr == nil {
|
||||||
temp: strings.Builder
|
temp: strings.Builder
|
||||||
strings.builder_init(&temp, context.temp_allocator)
|
strings.builder_init(&temp, context.temp_allocator)
|
||||||
render_nodes(
|
render_nodes(
|
||||||
sub_tpl,
|
sub_tpl,
|
||||||
sub_tpl.nodes[:],
|
sub_tpl.nodes[:],
|
||||||
ctx,
|
ctx,
|
||||||
partials,
|
partials,
|
||||||
&temp,
|
&temp,
|
||||||
blocks,
|
blocks,
|
||||||
nil,
|
nil,
|
||||||
) or_return
|
) or_return
|
||||||
write_value(b, strings.to_string(temp), escape = false)
|
write_value(b, strings.to_string(temp), escape = false)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
write_value(b, val, escape = false)
|
write_value(b, val, escape = false)
|
||||||
@@ -651,23 +673,36 @@ render_nodes :: proc(
|
|||||||
context.temp_allocator,
|
context.temp_allocator,
|
||||||
)
|
)
|
||||||
if perr == nil {
|
if perr == nil {
|
||||||
render_nodes(
|
render_nodes(
|
||||||
sub_tpl,
|
sub_tpl,
|
||||||
sub_tpl.nodes[:],
|
sub_tpl.nodes[:],
|
||||||
ctx,
|
ctx,
|
||||||
partials,
|
partials,
|
||||||
b,
|
b,
|
||||||
blocks,
|
blocks,
|
||||||
nil,
|
nil,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
} else if is_truthy(val) {
|
} else if is_truthy(val) {
|
||||||
children := node.children
|
children := node.children
|
||||||
elem_info, count, data := list_info(val)
|
elem_info, count, data := list_info(val)
|
||||||
if elem_info != nil {
|
if elem_info != nil {
|
||||||
for j in 0 ..< count {
|
for j in 0 ..< count {
|
||||||
elem := extract_list_element(elem_info, data, j)
|
elem := extract_list_element(elem_info, data, j)
|
||||||
append(ctx, elem)
|
context_push(ctx, elem, current, node)
|
||||||
|
defer pop(ctx)
|
||||||
|
render_nodes(
|
||||||
|
current,
|
||||||
|
children,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
blocks,
|
||||||
|
indent_state,
|
||||||
|
) or_return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
context_push(ctx, val, current, node)
|
||||||
defer pop(ctx)
|
defer pop(ctx)
|
||||||
render_nodes(
|
render_nodes(
|
||||||
current,
|
current,
|
||||||
@@ -679,13 +714,8 @@ render_nodes :: proc(
|
|||||||
indent_state,
|
indent_state,
|
||||||
) or_return
|
) or_return
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
append(ctx, val)
|
|
||||||
defer pop(ctx)
|
|
||||||
render_nodes(current, children, ctx, partials, b, blocks, indent_state) or_return
|
|
||||||
}
|
}
|
||||||
}
|
i += 1 + len(node.children)
|
||||||
i += 1 + len(node.children)
|
|
||||||
|
|
||||||
case .Inverted:
|
case .Inverted:
|
||||||
val := resolve_name(node.key, ctx[:])
|
val := resolve_name(node.key, ctx[:])
|
||||||
@@ -699,10 +729,18 @@ render_nodes :: proc(
|
|||||||
}
|
}
|
||||||
val = transformed
|
val = transformed
|
||||||
}
|
}
|
||||||
if !is_truthy(val) {
|
if !is_truthy(val) {
|
||||||
render_nodes(current, node.children, ctx, partials, b, blocks, indent_state) or_return
|
render_nodes(
|
||||||
}
|
current,
|
||||||
i += 1 + len(node.children)
|
node.children,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
blocks,
|
||||||
|
indent_state,
|
||||||
|
) or_return
|
||||||
|
}
|
||||||
|
i += 1 + len(node.children)
|
||||||
|
|
||||||
case .Partial:
|
case .Partial:
|
||||||
name := node.key
|
name := node.key
|
||||||
@@ -721,64 +759,64 @@ render_nodes :: proc(
|
|||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
|
|
||||||
case .Block:
|
case .Block:
|
||||||
content_nodes: []Node
|
content_nodes: []Node
|
||||||
content_blocks := blocks
|
content_blocks := blocks
|
||||||
render_current := current
|
render_current := current
|
||||||
|
|
||||||
found_override := false
|
found_override := false
|
||||||
if blocks != nil {
|
if blocks != nil {
|
||||||
if o, ok := blocks[node.key]; ok {
|
if o, ok := blocks[node.key]; ok {
|
||||||
content_nodes = o.nodes
|
content_nodes = o.nodes
|
||||||
found_override = true
|
found_override = true
|
||||||
render_current = o.source
|
render_current = o.source
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
if !found_override {
|
||||||
if !found_override {
|
content_nodes = node.children
|
||||||
content_nodes = node.children
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(node.indent) > 0 {
|
|
||||||
temp: strings.Builder
|
|
||||||
strings.builder_init(&temp, context.temp_allocator)
|
|
||||||
render_nodes(
|
|
||||||
render_current,
|
|
||||||
content_nodes,
|
|
||||||
ctx,
|
|
||||||
partials,
|
|
||||||
&temp,
|
|
||||||
content_blocks,
|
|
||||||
nil,
|
|
||||||
) or_return
|
|
||||||
at_ls := true
|
|
||||||
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
|
|
||||||
} else {
|
|
||||||
render_nodes(
|
|
||||||
render_current,
|
|
||||||
content_nodes,
|
|
||||||
ctx,
|
|
||||||
partials,
|
|
||||||
b,
|
|
||||||
content_blocks,
|
|
||||||
indent_state,
|
|
||||||
) or_return
|
|
||||||
}
|
|
||||||
i += 1 + len(node.children)
|
|
||||||
|
|
||||||
case .Parent:
|
|
||||||
parent_children := node.children
|
|
||||||
merged := merge_block_overrides(parent_children, blocks, current)
|
|
||||||
pt, found := partials[node.key]
|
|
||||||
if !found {
|
|
||||||
warn_missing_partial(current, partials, node, node.key)
|
|
||||||
} else {
|
|
||||||
warn_unmatched_block_overrides(current, pt, parent_children)
|
|
||||||
render_template(pt, ctx, partials, b, merged, node.indent) or_return
|
|
||||||
if indent_state != nil {
|
|
||||||
indent_state.at_line_start = false
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
i += 1 + len(node.children)
|
if len(node.indent) > 0 {
|
||||||
|
temp: strings.Builder
|
||||||
|
strings.builder_init(&temp, context.temp_allocator)
|
||||||
|
render_nodes(
|
||||||
|
render_current,
|
||||||
|
content_nodes,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
&temp,
|
||||||
|
content_blocks,
|
||||||
|
nil,
|
||||||
|
) or_return
|
||||||
|
at_ls := true
|
||||||
|
write_indented(b, node.indent, strings.to_string(temp), &at_ls)
|
||||||
|
} else {
|
||||||
|
render_nodes(
|
||||||
|
render_current,
|
||||||
|
content_nodes,
|
||||||
|
ctx,
|
||||||
|
partials,
|
||||||
|
b,
|
||||||
|
content_blocks,
|
||||||
|
indent_state,
|
||||||
|
) or_return
|
||||||
|
}
|
||||||
|
i += 1 + len(node.children)
|
||||||
|
|
||||||
|
case .Parent:
|
||||||
|
parent_children := node.children
|
||||||
|
merged := merge_block_overrides(parent_children, blocks, current)
|
||||||
|
pt, found := partials[node.key]
|
||||||
|
if !found {
|
||||||
|
warn_missing_partial(current, partials, node, node.key)
|
||||||
|
} else {
|
||||||
|
warn_unmatched_block_overrides(current, pt, parent_children)
|
||||||
|
render_template(pt, ctx, partials, b, merged, node.indent) or_return
|
||||||
|
if indent_state != nil {
|
||||||
|
indent_state.at_line_start = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1 + len(node.children)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@@ -922,3 +960,25 @@ warn_unmatched_block_overrides :: proc(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
context_push :: proc(ctx: ^Context_Stack, val: any, current: Template, node: Node) {
|
||||||
|
append(ctx, val)
|
||||||
|
if len(ctx^) == MAX_CONTEXT_DEPTH + 1 {
|
||||||
|
warn_context_depth(current, node)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// warn_context_depth emits a diagnostic warning pointing at the section tag
|
||||||
|
// whose push carried the context stack past MAX_CONTEXT_DEPTH.
|
||||||
|
warn_context_depth :: proc(current: Template, node: Node) {
|
||||||
|
msg := fmt.tprintf(
|
||||||
|
"context stack depth exceeded %d (possible recursive section/partial)",
|
||||||
|
MAX_CONTEXT_DEPTH,
|
||||||
|
)
|
||||||
|
path := current.path
|
||||||
|
if path == "" {
|
||||||
|
path = "<input>"
|
||||||
|
}
|
||||||
|
diag := format_error(path, current.source, node.pos, msg, "", colorize = should_colorize())
|
||||||
|
log.warnf("%s", diag)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
#+test
|
#+test
|
||||||
package mustache
|
package mustache
|
||||||
|
|
||||||
|
import "core:log"
|
||||||
import "core:mem"
|
import "core:mem"
|
||||||
|
import "core:strings"
|
||||||
import "core:testing"
|
import "core:testing"
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -124,3 +126,39 @@ leak_repeated_render :: proc(t: ^testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A deeply nested map pushes the context stack past MAX_CONTEXT_DEPTH (16).
|
||||||
|
// The depth warning must be non-fatal: rendering still succeeds.
|
||||||
|
@(test)
|
||||||
|
test_context_depth_warns :: proc(t: ^testing.T) {
|
||||||
|
context.logger = log.nil_logger()
|
||||||
|
|
||||||
|
AMT :: MAX_CONTEXT_DEPTH + 2
|
||||||
|
|
||||||
|
// Build AMT nested {x: {...}} levels; the innermost holds `leaf`.
|
||||||
|
data := make(map[string]any, context.temp_allocator)
|
||||||
|
data["leaf"] = "found"
|
||||||
|
for _ in 0 ..< AMT {
|
||||||
|
outer := make(map[string]any, context.temp_allocator)
|
||||||
|
outer["x"] = data
|
||||||
|
data = outer
|
||||||
|
}
|
||||||
|
|
||||||
|
// Template: AMT nested {{#x}} sections around {{leaf}}.
|
||||||
|
src: strings.Builder
|
||||||
|
strings.builder_init(&src, context.temp_allocator)
|
||||||
|
for _ in 0 ..< AMT do strings.write_string(&src, "{{#x}}")
|
||||||
|
strings.write_string(&src, "{{leaf}}")
|
||||||
|
for _ in 0 ..< AMT do strings.write_string(&src, "{{/x}}")
|
||||||
|
template := strings.to_string(src)
|
||||||
|
|
||||||
|
tmpl, perr := parse(template, "<depth-test>", context.temp_allocator)
|
||||||
|
testing.expect(t, perr == nil, "should parse")
|
||||||
|
if perr != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
result, rerr := render(tmpl, data, allocator = context.temp_allocator)
|
||||||
|
testing.expect(t, rerr == nil, "depth warning must be non-fatal")
|
||||||
|
testing.expect_value(t, result, "found")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -153,10 +153,7 @@ suggest_correction :: proc(available: []string, missing: string) -> string {
|
|||||||
if len(available) == 0 || len(missing) == 0 {
|
if len(available) == 0 || len(missing) == 0 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
threshold := 2
|
threshold := max(2, len(missing) / 3)
|
||||||
if len(missing) > 8 {
|
|
||||||
threshold = len(missing) / 4
|
|
||||||
}
|
|
||||||
|
|
||||||
best: string
|
best: string
|
||||||
best_dist := threshold + 1
|
best_dist := threshold + 1
|
||||||
|
|||||||
@@ -10,11 +10,15 @@ Inner :: struct {
|
|||||||
bar: int,
|
bar: int,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Page :: struct {
|
||||||
|
title: string,
|
||||||
|
}
|
||||||
|
|
||||||
Outer :: struct {
|
Outer :: struct {
|
||||||
title: string,
|
title: string,
|
||||||
page_title: string,
|
page: Page,
|
||||||
inner: Inner,
|
inner: Inner,
|
||||||
numbers: [3]int,
|
numbers: [3]int,
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
@@ -150,8 +154,8 @@ test_validate_map_path_silent :: proc(t: ^testing.T) {
|
|||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
test_suggest_correction_exact :: proc(t: ^testing.T) {
|
test_suggest_correction_exact :: proc(t: ^testing.T) {
|
||||||
available := []string{"title", "page_title", "body"}
|
available := []string{"title", "page.title", "body"}
|
||||||
testing.expect_value(t, suggest_correction(available, "page_titel"), "page_title")
|
testing.expect_value(t, suggest_correction(available, "page_titel"), "page.title")
|
||||||
}
|
}
|
||||||
|
|
||||||
@(test)
|
@(test)
|
||||||
|
|||||||
+19
-47
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import "mustache"
|
import "mustache"
|
||||||
|
|
||||||
import "core:encoding/json"
|
|
||||||
import "core:fmt"
|
import "core:fmt"
|
||||||
import "core:log"
|
import "core:log"
|
||||||
import "core:os"
|
import "core:os"
|
||||||
@@ -11,43 +10,20 @@ import "core:time"
|
|||||||
import "core:time/datetime"
|
import "core:time/datetime"
|
||||||
|
|
||||||
Template_Context :: struct {
|
Template_Context :: struct {
|
||||||
params: json.Value,
|
|
||||||
now: string,
|
now: string,
|
||||||
content: string,
|
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
|
||||||
og: Open_Graph,
|
|
||||||
date_format: string,
|
date_format: string,
|
||||||
timezone: ^datetime.TZ_Region,
|
timezone: ^datetime.TZ_Region,
|
||||||
|
og: Open_Graph,
|
||||||
// Page Data
|
site: Site_Context,
|
||||||
page_title: string,
|
page: Page,
|
||||||
date: string,
|
|
||||||
|
|
||||||
// Home data
|
// Home data
|
||||||
pages: [dynamic]Page_Context,
|
pages: [dynamic]Page,
|
||||||
|
|
||||||
// Section Data
|
// Section Data
|
||||||
// TODO: Remove "posts" from the Odin code
|
// TODO: Remove "posts" from the Odin code
|
||||||
posts: [dynamic]Page_Context,
|
posts: [dynamic]Page,
|
||||||
}
|
|
||||||
|
|
||||||
Page_Context :: struct {
|
|
||||||
permalink: string,
|
|
||||||
title: string,
|
|
||||||
starred: bool,
|
|
||||||
date: string,
|
|
||||||
year: string,
|
|
||||||
}
|
|
||||||
|
|
||||||
build_page_context :: proc(page: Page) -> Page_Context {
|
|
||||||
return Page_Context {
|
|
||||||
permalink = page.permalink,
|
|
||||||
title = page.title,
|
|
||||||
starred = page.is_starred,
|
|
||||||
date = page.date,
|
|
||||||
year = get_year(page.date),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
||||||
@@ -125,10 +101,10 @@ capitalize :: proc(s: string) -> string {
|
|||||||
|
|
||||||
render_template :: proc(
|
render_template :: proc(
|
||||||
content_tpl: mustache.Template,
|
content_tpl: mustache.Template,
|
||||||
data: Template_Context,
|
ctx: Template_Context,
|
||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
) -> string {
|
) -> string {
|
||||||
result, err := mustache.render(content_tpl, data, partials)
|
result, err := mustache.render(content_tpl, []any{ctx.site, ctx.page, ctx}, partials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.errorf(
|
log.errorf(
|
||||||
"%s",
|
"%s",
|
||||||
@@ -157,9 +133,8 @@ render_site :: proc(site: ^Site) {
|
|||||||
assert(ok2)
|
assert(ok2)
|
||||||
|
|
||||||
ctx := Template_Context {
|
ctx := Template_Context {
|
||||||
|
site = site.site_context,
|
||||||
now = now,
|
now = now,
|
||||||
params = site.params,
|
|
||||||
description = site.description,
|
|
||||||
og = site.og,
|
og = site.og,
|
||||||
date_format = site.date.format,
|
date_format = site.date.format,
|
||||||
timezone = site.tz,
|
timezone = site.tz,
|
||||||
@@ -175,6 +150,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ctx.page = home
|
||||||
|
|
||||||
// Collect sections
|
// Collect sections
|
||||||
sections := make(map[string]bool)
|
sections := make(map[string]bool)
|
||||||
@@ -268,9 +244,7 @@ render_page_html :: proc(
|
|||||||
) -> string {
|
) -> string {
|
||||||
ctx := ctx
|
ctx := ctx
|
||||||
ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
|
ctx.title = fmt.tprintf("%s | %s", page.title, site.title)
|
||||||
ctx.page_title = page.title
|
ctx.page = page
|
||||||
ctx.content = page.content
|
|
||||||
ctx.date = page.date
|
|
||||||
ctx.og = og_for_page(site.og, page)
|
ctx.og = og_for_page(site.og, page)
|
||||||
return render_template(content_tpl, ctx, partials)
|
return render_template(content_tpl, ctx, partials)
|
||||||
}
|
}
|
||||||
@@ -282,18 +256,16 @@ render_home_html :: proc(
|
|||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
ctx: Template_Context,
|
ctx: Template_Context,
|
||||||
) -> string {
|
) -> string {
|
||||||
list_pages := make([dynamic]Page_Context)
|
list_pages := make([dynamic]Page, 0, 8, context.temp_allocator)
|
||||||
defer delete(list_pages)
|
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page._is_index {
|
if page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
append(&list_pages, build_page_context(page))
|
append(&list_pages, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := ctx
|
ctx := ctx
|
||||||
ctx.title = site.title
|
ctx.title = site.title
|
||||||
ctx.content = home.content
|
|
||||||
ctx.pages = list_pages
|
ctx.pages = list_pages
|
||||||
ctx.og = og_for_page(site.og, home)
|
ctx.og = og_for_page(site.og, home)
|
||||||
|
|
||||||
@@ -309,24 +281,24 @@ render_section :: proc(
|
|||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
ctx: Template_Context,
|
ctx: Template_Context,
|
||||||
) -> string {
|
) -> string {
|
||||||
posts := make([dynamic]Page_Context)
|
posts := make([dynamic]Page, 0, len(site.pages) / 2, context.temp_allocator)
|
||||||
defer delete(posts)
|
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.section != section || page._is_index {
|
if page.section != section || page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
append(&posts, build_page_context(page))
|
append(&posts, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := ctx
|
ctx := ctx
|
||||||
if has_index {
|
if has_index {
|
||||||
ctx.content = section_index.content
|
ctx.page = section_index
|
||||||
ctx.page_title = section_index.title
|
|
||||||
ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title)
|
ctx.title = fmt.tprintf("%s | %s", section_index.title, site.title)
|
||||||
ctx.og = og_for_page(site.og, section_index)
|
ctx.og = og_for_page(site.og, section_index)
|
||||||
} else {
|
} else {
|
||||||
ctx.page_title = capitalize(section)
|
ctx.page = Page {
|
||||||
ctx.title = fmt.tprintf("%s | %s", capitalize(section), site.title)
|
title = capitalize(section),
|
||||||
|
}
|
||||||
|
ctx.title = fmt.tprintf("%s | %s", ctx.page.title, site.title)
|
||||||
ctx.og.title = capitalize(section)
|
ctx.og.title = capitalize(section)
|
||||||
ctx.og.description = ""
|
ctx.og.description = ""
|
||||||
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section)
|
ctx.og.url = fmt.tprintf("%s/%s/", site.base_url, section)
|
||||||
|
|||||||
@@ -13,24 +13,31 @@ import "core:time/timezone"
|
|||||||
import md "markdown"
|
import md "markdown"
|
||||||
|
|
||||||
|
|
||||||
// Site is the primary workhorse.
|
// Site_Context holds the site date that is accessible in templates.
|
||||||
|
Site_Context :: struct {
|
||||||
|
title: string,
|
||||||
|
description: string,
|
||||||
|
base_url: string,
|
||||||
|
params: json.Object,
|
||||||
|
og: Open_Graph,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Site is the primary workhorse, containing everything needed to build the site,
|
||||||
|
// including an arena allocator, the pages, and all the various directories and
|
||||||
|
// enabled features.
|
||||||
Site :: struct {
|
Site :: struct {
|
||||||
|
using site_context: Site_Context,
|
||||||
arena: mem.Dynamic_Arena,
|
arena: mem.Dynamic_Arena,
|
||||||
pages: #soa[dynamic]Page,
|
pages: #soa[dynamic]Page,
|
||||||
modules: [dynamic]string,
|
modules: [dynamic]string,
|
||||||
vfs: VFS,
|
vfs: VFS,
|
||||||
title: string,
|
|
||||||
description: string,
|
|
||||||
base_url: string,
|
|
||||||
config_path: string,
|
config_path: string,
|
||||||
content_dir: string,
|
content_dir: string,
|
||||||
assets_dir: string,
|
assets_dir: string,
|
||||||
output_dir: string,
|
output_dir: string,
|
||||||
layouts_dir: string,
|
layouts_dir: string,
|
||||||
params: json.Object,
|
|
||||||
features: bit_set[Feature],
|
features: bit_set[Feature],
|
||||||
markdown_extensions: bit_set[md.Extension],
|
markdown_extensions: bit_set[md.Extension],
|
||||||
og: Open_Graph,
|
|
||||||
date: Date_Preferences,
|
date: Date_Preferences,
|
||||||
tz: ^datetime.TZ_Region,
|
tz: ^datetime.TZ_Region,
|
||||||
grammars: string,
|
grammars: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user