mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
Compare commits
3 Commits
c7b42ce7aa
...
cfb41ef930
| Author | SHA1 | Date | |
|---|---|---|---|
| cfb41ef930 | |||
| fba62fe156 | |||
| 6694c0cf67 |
@@ -6,10 +6,9 @@
|
|||||||
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
||||||
`reload_site` proc, that just updates changed resources.
|
`reload_site` proc, that just updates changed resources.
|
||||||
- [ ] mustache data keys for opengraph, etc.
|
- [ ] mustache data keys for opengraph, etc.
|
||||||
- [ ] Infer page types / collections rather than having dedicated procs for
|
|
||||||
everything
|
|
||||||
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
||||||
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
- [ ] include-code shortcode (`{{< include-code ... >}}`) — i-ported-fd-to-odin
|
||||||
|
- [ ] follow symlinks in `scan_content`?
|
||||||
- [ ] ensure sidenote numbers render in display order and not in declaration order.
|
- [ ] ensure sidenote numbers render in display order and not in declaration order.
|
||||||
- [ ] OpenGraph meta tags — verify all fields match production site
|
- [ ] OpenGraph meta tags — verify all fields match production site
|
||||||
- [ ] Add Opt-in deflist support.
|
- [ ] Add Opt-in deflist support.
|
||||||
@@ -34,13 +33,16 @@
|
|||||||
- [ ] filesystem poll loop
|
- [ ] filesystem poll loop
|
||||||
- [ ] event based
|
- [ ] event based
|
||||||
- [ ] Free cmark HTML output (`body_html`) — cmark allocates via C malloc, not the arena, so it leaks per iteration in watch mode
|
- [ ] Free cmark HTML output (`body_html`) — cmark allocates via C malloc, not the arena, so it leaks per iteration in watch mode
|
||||||
|
- [ ] Rename `{{#is_post}}` to `{{#is_article}}` in templates and data model — currently hardcoded to posts section, should use `is_article` (any page with a section) instead.
|
||||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
||||||
- [ ] commands
|
- [ ] commands
|
||||||
- [ ] `build` alias of default
|
- [ ] `build` alias of default
|
||||||
- [ ] `init` set up new project
|
- [ ] `init` set up new project
|
||||||
- [ ] Use spall to find ways to reduce run time.
|
- [ ] Use spall to find ways to reduce run time.
|
||||||
- [ ] warn/error when unknown key used in mustache.
|
- [ ] warn/error when unknown key used in mustache.
|
||||||
|
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
||||||
- [ ] Review every file in thor
|
- [ ] Review every file in thor
|
||||||
|
- [ ] Review assets.odin
|
||||||
- [ ] Review alerts.odin
|
- [ ] Review alerts.odin
|
||||||
- [ ] Review content.odin
|
- [ ] Review content.odin
|
||||||
- [ ] Review emoji.odin
|
- [ ] Review emoji.odin
|
||||||
|
|||||||
+7
-10
@@ -21,37 +21,34 @@ ALERT_EMOJIS: map[string]string = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
inject_alerts :: proc(html: string) -> string {
|
inject_alerts :: proc(html: string) -> string {
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
remaining := html
|
remaining := html
|
||||||
|
|
||||||
for {
|
for {
|
||||||
bq_start := strings.index(remaining, "<blockquote>")
|
bq_start := strings.index(remaining, "<blockquote>")
|
||||||
if bq_start < 0 {
|
if bq_start < 0 {
|
||||||
append(&parts, remaining)
|
strings.write_string(&sb, remaining)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
bq_close := strings.index(remaining, "</blockquote>")
|
bq_close := strings.index(remaining, "</blockquote>")
|
||||||
if bq_close < 0 {
|
if bq_close < 0 {
|
||||||
append(&parts, remaining)
|
strings.write_string(&sb, remaining)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
bq_end := bq_close + len("</blockquote>")
|
bq_end := bq_close + len("</blockquote>")
|
||||||
bq := remaining[bq_start:bq_end]
|
bq := remaining[bq_start:bq_end]
|
||||||
|
|
||||||
// Append text before this blockquote
|
strings.write_string(&sb, remaining[:bq_start])
|
||||||
append(&parts, remaining[:bq_start])
|
strings.write_string(&sb, transform_alert(bq))
|
||||||
|
|
||||||
// Transform if it's an alert, otherwise keep as-is
|
|
||||||
append(&parts, transform_alert(bq))
|
|
||||||
|
|
||||||
remaining = remaining[bq_end:]
|
remaining = remaining[bq_end:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
transform_alert :: proc(bq: string) -> string {
|
transform_alert :: proc(bq: string) -> string {
|
||||||
|
|||||||
+61
@@ -0,0 +1,61 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "core:fmt"
|
||||||
|
import "core:log"
|
||||||
|
import "core:os"
|
||||||
|
import "core:strings"
|
||||||
|
|
||||||
|
// copy_assets_dir recursively copies files from assets_dir to output_dir.
|
||||||
|
// .css files are minified when .Minify is enabled; all other files are copied verbatim.
|
||||||
|
// Silently skips if assets_dir doesn't exist.
|
||||||
|
copy_assets_dir :: proc(assets_dir: string, output_dir: string, features: bit_set[Feature]) {
|
||||||
|
if !os.exists(assets_dir) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
copy_assets_recursive(assets_dir, "", output_dir, features)
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_assets_recursive :: proc(
|
||||||
|
current: string,
|
||||||
|
rel_prefix: string,
|
||||||
|
output_dir: string,
|
||||||
|
features: bit_set[Feature],
|
||||||
|
) {
|
||||||
|
entries, err := os.read_all_directory_by_path(current, context.allocator)
|
||||||
|
if err != nil {
|
||||||
|
log.warnf("thor: cannot read %s: %v", current, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.file_info_slice_delete(entries, context.allocator)
|
||||||
|
|
||||||
|
for entry in entries {
|
||||||
|
rel := rel_prefix == "" ? entry.name : fmt.tprintf("%s/%s", rel_prefix, entry.name)
|
||||||
|
switch entry.type {
|
||||||
|
case .Regular:
|
||||||
|
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
||||||
|
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
||||||
|
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
||||||
|
log.warnf("thor: cannot create %s: %v", dest[:idx], err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if .Minify in features && strings.has_suffix(entry.name, ".css") {
|
||||||
|
data, read_err := os.read_entire_file_from_path(entry.fullpath, context.allocator)
|
||||||
|
if read_err != nil {
|
||||||
|
log.warnf("thor: cannot read %s: %v", entry.fullpath, read_err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
minified := minify_css(string(data))
|
||||||
|
write_file(dest, minified)
|
||||||
|
} else {
|
||||||
|
if err := os.copy_file(dest, entry.fullpath); err != nil {
|
||||||
|
log.warnf("thor: cannot copy %s: %v", entry.fullpath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .Directory:
|
||||||
|
copy_assets_recursive(entry.fullpath, rel, output_dir, features)
|
||||||
|
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
+63
-151
@@ -7,100 +7,36 @@ import "core:log"
|
|||||||
import "core:os"
|
import "core:os"
|
||||||
import "core:strings"
|
import "core:strings"
|
||||||
|
|
||||||
Page_Type :: enum {
|
// Fields with underscores should never be set by the user.
|
||||||
Page,
|
|
||||||
Post,
|
|
||||||
Home,
|
|
||||||
}
|
|
||||||
|
|
||||||
Page :: struct {
|
Page :: struct {
|
||||||
type: Page_Type,
|
section: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
|
layout: string,
|
||||||
permalink: string,
|
permalink: string,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
date: string,
|
date: string,
|
||||||
draft: bool,
|
|
||||||
is_starred: bool,
|
|
||||||
menu: string,
|
menu: string,
|
||||||
body_html: string,
|
body_html: string,
|
||||||
bundle_dir: string,
|
draft: bool,
|
||||||
|
is_starred: bool,
|
||||||
|
_is_index: bool `private`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// site_load_content reads the content directory and populates site.pages.
|
// site_load_content reads the content directory and populates site.pages.
|
||||||
// Drafts are excluded unless .Drafts is enabled.
|
// Drafts are excluded unless .Drafts is enabled.
|
||||||
site_load_content :: proc(site: ^Site) {
|
site_load_content :: proc(site: ^Site) {
|
||||||
site.pages = make([dynamic]Page, 0, 8, site_allocator(site))
|
site.pages = make([dynamic]Page, 0, 8, site_allocator(site))
|
||||||
|
scan_content(site, site.content_dir, "")
|
||||||
load_homepage(site)
|
|
||||||
load_pages(site)
|
|
||||||
load_posts(site)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
load_homepage :: proc(site: ^Site) {
|
// scan_content walks the content directory. At the root level (section=""),
|
||||||
content_path := site.content_dir
|
// directories are treated as sections. Within a section, directories are
|
||||||
ext := site.markdown_extensions
|
// treated as leaf bundles (directory with an index file).
|
||||||
|
scan_content :: proc(site: ^Site, dir: string, section: string) {
|
||||||
html_path := fmt.tprintf("%s/index.html", content_path)
|
entries, err := os.read_all_directory_by_path(dir, context.allocator)
|
||||||
if os.exists(html_path) {
|
|
||||||
page, ok := load_page(html_path, .Home, "", ext)
|
|
||||||
if ok && (!page.draft || .Drafts in site.features) {
|
|
||||||
page.permalink = "/"
|
|
||||||
append(&site.pages, page)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
md_path := fmt.tprintf("%s/index.md", content_path)
|
|
||||||
if os.exists(md_path) {
|
|
||||||
page, ok := load_page(md_path, .Home, "", ext)
|
|
||||||
if ok && (!page.draft || .Drafts in site.features) {
|
|
||||||
page.permalink = "/"
|
|
||||||
append(&site.pages, page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load_pages :: proc(site: ^Site) {
|
|
||||||
content_path := site.content_dir
|
|
||||||
ext := site.markdown_extensions
|
|
||||||
|
|
||||||
entries, err := os.read_all_directory_by_path(content_path, context.allocator)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.warnf("thor: cannot read %s: %v", content_path, err)
|
log.warnf("thor: cannot read %s: %v", dir, err)
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
if !is_content_file(entry.name) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if entry.name == "index.md" || entry.name == "index.html" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if entry.type != .Regular {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
slug := strip_extension(entry.name)
|
|
||||||
page, ok := load_page(entry.fullpath, .Page, slug, ext)
|
|
||||||
if ok && (!page.draft || .Drafts in site.features) {
|
|
||||||
append(&site.pages, page)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
load_posts :: proc(site: ^Site) {
|
|
||||||
posts_path := fmt.tprintf("%s/posts", site.content_dir)
|
|
||||||
if !os.exists(posts_path) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ext := site.markdown_extensions
|
|
||||||
entries, err := os.read_all_directory_by_path(posts_path, context.allocator)
|
|
||||||
if err != nil {
|
|
||||||
log.warnf("thor: cannot read %s: %v", posts_path, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
defer os.file_info_slice_delete(entries, context.allocator)
|
||||||
@@ -111,33 +47,60 @@ load_posts :: proc(site: ^Site) {
|
|||||||
if !is_content_file(entry.name) {
|
if !is_content_file(entry.name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
slug := strip_extension(entry.name)
|
|
||||||
page, ok := load_page(entry.fullpath, .Post, slug, ext)
|
filename := strip_extension(entry.name)
|
||||||
|
is_idx := filename == "index"
|
||||||
|
slug := is_idx ? "" : filename
|
||||||
|
|
||||||
|
page, ok := load_page(entry.fullpath, section, slug, is_idx, site.markdown_extensions)
|
||||||
if ok && (!page.draft || .Drafts in site.features) {
|
if ok && (!page.draft || .Drafts in site.features) {
|
||||||
append(&site.pages, page)
|
append(&site.pages, page)
|
||||||
}
|
}
|
||||||
|
|
||||||
case .Directory:
|
case .Directory:
|
||||||
index_path := fmt.tprintf("%s/index.html", entry.fullpath)
|
if section == "" {
|
||||||
if !os.exists(index_path) {
|
scan_content(site, entry.fullpath, entry.name)
|
||||||
index_path = fmt.tprintf("%s/index.md", entry.fullpath)
|
} else {
|
||||||
}
|
index_path := fmt.tprintf("%s/index.html", entry.fullpath)
|
||||||
if !os.exists(index_path) {
|
if !os.exists(index_path) {
|
||||||
continue
|
index_path = fmt.tprintf("%s/index.md", entry.fullpath)
|
||||||
}
|
}
|
||||||
page, ok := load_page(index_path, .Post, entry.name, ext)
|
if os.exists(index_path) {
|
||||||
if ok && (!page.draft || .Drafts in site.features) {
|
page, ok := load_page(
|
||||||
page.bundle_dir = entry.fullpath
|
index_path,
|
||||||
append(&site.pages, page)
|
section,
|
||||||
|
entry.name,
|
||||||
|
false,
|
||||||
|
site.markdown_extensions,
|
||||||
|
)
|
||||||
|
if ok && (!page.draft || .Drafts in site.features) {
|
||||||
|
append(&site.pages, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
|
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infer_layout :: proc(section: string, is_index: bool) -> string {
|
||||||
|
if section == "" && is_index {
|
||||||
|
return "home"
|
||||||
|
}
|
||||||
|
if is_index {
|
||||||
|
return fmt.tprintf("%s_index", section)
|
||||||
|
}
|
||||||
|
if section != "" {
|
||||||
|
return section
|
||||||
|
}
|
||||||
|
return "page"
|
||||||
|
}
|
||||||
|
|
||||||
load_page :: proc(
|
load_page :: proc(
|
||||||
file_path: string,
|
file_path: string,
|
||||||
page_type: Page_Type,
|
section: string,
|
||||||
slug: string,
|
slug: string,
|
||||||
|
is_index: bool,
|
||||||
ext: bit_set[Markdown_Extension],
|
ext: bit_set[Markdown_Extension],
|
||||||
) -> (
|
) -> (
|
||||||
page: Page,
|
page: Page,
|
||||||
@@ -155,14 +118,16 @@ load_page :: proc(
|
|||||||
body = strings.trim_left(content, " \t\r\n")
|
body = strings.trim_left(content, " \t\r\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
page.type = page_type
|
page.section = section
|
||||||
page.slug = slug
|
page.slug = slug
|
||||||
|
page._is_index = is_index
|
||||||
page.title = fm.title
|
page.title = fm.title
|
||||||
page.description = fm.description
|
page.description = fm.description
|
||||||
page.date = fm.date
|
page.date = fm.date
|
||||||
page.draft = fm.draft
|
page.draft = fm.draft
|
||||||
page.is_starred = fm.isStarred
|
page.is_starred = fm.isStarred
|
||||||
page.menu = fm.menu
|
page.menu = fm.menu
|
||||||
|
page.layout = fm.layout if fm.layout != "" else infer_layout(section, is_index)
|
||||||
|
|
||||||
if strings.has_suffix(file_path, ".html") {
|
if strings.has_suffix(file_path, ".html") {
|
||||||
page.body_html = strings.clone(body)
|
page.body_html = strings.clone(body)
|
||||||
@@ -192,13 +157,14 @@ load_page :: proc(
|
|||||||
page.body_html = html
|
page.body_html = html
|
||||||
}
|
}
|
||||||
|
|
||||||
switch page_type {
|
if section == "" && is_index {
|
||||||
case .Home:
|
|
||||||
page.permalink = "/"
|
page.permalink = "/"
|
||||||
case .Post:
|
} else if is_index {
|
||||||
page.permalink = fmt.aprintf("/posts/%s/", slug)
|
page.permalink = fmt.aprintf("/%s/", section)
|
||||||
case .Page:
|
} else if section == "" {
|
||||||
page.permalink = fmt.aprintf("/%s/", slug)
|
page.permalink = fmt.aprintf("/%s/", slug)
|
||||||
|
} else {
|
||||||
|
page.permalink = fmt.aprintf("/%s/%s/", section, slug)
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
@@ -217,57 +183,3 @@ strip_extension :: proc(name: string) -> string {
|
|||||||
return name[:dot]
|
return name[:dot]
|
||||||
}
|
}
|
||||||
|
|
||||||
// copy_assets_dir recursively copies files from assets_dir to output_dir.
|
|
||||||
// .css files are minified when .Minify is enabled; all other files are copied verbatim.
|
|
||||||
// Silently skips if assets_dir doesn't exist.
|
|
||||||
copy_assets_dir :: proc(assets_dir: string, output_dir: string, features: bit_set[Feature]) {
|
|
||||||
if !os.exists(assets_dir) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
copy_assets_recursive(assets_dir, "", output_dir, features)
|
|
||||||
}
|
|
||||||
|
|
||||||
copy_assets_recursive :: proc(
|
|
||||||
current: string,
|
|
||||||
rel_prefix: string,
|
|
||||||
output_dir: string,
|
|
||||||
features: bit_set[Feature],
|
|
||||||
) {
|
|
||||||
entries, err := os.read_all_directory_by_path(current, context.allocator)
|
|
||||||
if err != nil {
|
|
||||||
log.warnf("thor: cannot read %s: %v", current, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
|
||||||
|
|
||||||
for entry in entries {
|
|
||||||
rel := rel_prefix == "" ? entry.name : fmt.tprintf("%s/%s", rel_prefix, entry.name)
|
|
||||||
switch entry.type {
|
|
||||||
case .Regular:
|
|
||||||
dest := fmt.tprintf("%s/%s", output_dir, rel)
|
|
||||||
if idx := strings.last_index(dest, "/"); idx >= 0 {
|
|
||||||
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
|
||||||
log.warnf("thor: cannot create %s: %v", dest[:idx], err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if .Minify in features && strings.has_suffix(entry.name, ".css") {
|
|
||||||
data, read_err := os.read_entire_file_from_path(entry.fullpath, context.allocator)
|
|
||||||
if read_err != nil {
|
|
||||||
log.warnf("thor: cannot read %s: %v", entry.fullpath, read_err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
minified := minify_css(string(data))
|
|
||||||
write_file(dest, minified)
|
|
||||||
} else {
|
|
||||||
if err := os.copy_file(dest, entry.fullpath); err != nil {
|
|
||||||
log.warnf("thor: cannot copy %s: %v", entry.fullpath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .Directory:
|
|
||||||
copy_assets_recursive(entry.fullpath, rel, output_dir, features)
|
|
||||||
case .Undetermined, .Symlink, .Named_Pipe, .Socket, .Block_Device, .Character_Device:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
+9
-13
@@ -384,30 +384,27 @@ expand_emoji :: proc(text: string) -> string {
|
|||||||
return text
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
remaining := text
|
remaining := text
|
||||||
|
|
||||||
for {
|
for {
|
||||||
colon := strings.index(remaining, ":")
|
colon := strings.index(remaining, ":")
|
||||||
if colon < 0 {
|
if colon < 0 {
|
||||||
append(&parts, remaining)
|
strings.write_string(&sb, remaining)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the closing colon
|
|
||||||
after := remaining[colon + 1:]
|
after := remaining[colon + 1:]
|
||||||
end := strings.index(after, ":")
|
end := strings.index(after, ":")
|
||||||
if end < 0 {
|
if end < 0 {
|
||||||
append(&parts, remaining)
|
strings.write_string(&sb, remaining)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcode := remaining[colon + 1 : colon + 1 + end]
|
shortcode := remaining[colon + 1 : colon + 1 + end]
|
||||||
|
|
||||||
// Validate: shortcode must be all lowercase letters, digits, or underscores
|
|
||||||
// and must not contain whitespace
|
|
||||||
valid := true
|
valid := true
|
||||||
for c in shortcode {
|
for c in shortcode {
|
||||||
if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' && c != '+' && c != '-' {
|
if !(c >= 'a' && c <= 'z') && !(c >= '0' && c <= '9') && c != '_' && c != '+' && c != '-' {
|
||||||
@@ -416,23 +413,22 @@ expand_emoji :: proc(text: string) -> string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !valid || len(shortcode) == 0 {
|
if !valid || len(shortcode) == 0 {
|
||||||
append(&parts, remaining[:colon + 1])
|
strings.write_string(&sb, remaining[:colon + 1])
|
||||||
remaining = remaining[colon + 1:]
|
remaining = remaining[colon + 1:]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
emoji, found := EMOJIS[shortcode]
|
emoji, found := EMOJIS[shortcode]
|
||||||
if !found {
|
if !found {
|
||||||
append(&parts, remaining[:colon + 1])
|
strings.write_string(&sb, remaining[:colon + 1])
|
||||||
remaining = remaining[colon + 1:]
|
remaining = remaining[colon + 1:]
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace :shortcode: with emoji
|
strings.write_string(&sb, remaining[:colon])
|
||||||
append(&parts, remaining[:colon])
|
strings.write_string(&sb, emoji)
|
||||||
append(&parts, emoji)
|
|
||||||
remaining = remaining[colon + 1 + end + 1:]
|
remaining = remaining[colon + 1 + end + 1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,10 @@ import "core:time"
|
|||||||
generate_rss :: proc(site: ^Site) -> string {
|
generate_rss :: proc(site: ^Site) -> string {
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make()
|
||||||
|
|
||||||
strings.write_string(&sb, fmt.aprintf(
|
strings.write_string(
|
||||||
`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
&sb,
|
||||||
|
fmt.aprintf(
|
||||||
|
`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
<channel>
|
<channel>
|
||||||
<title>%s</title>
|
<title>%s</title>
|
||||||
@@ -16,14 +18,15 @@ generate_rss :: proc(site: ^Site) -> string {
|
|||||||
<description>%s</description>
|
<description>%s</description>
|
||||||
<language>en-us</language>
|
<language>en-us</language>
|
||||||
<atom:link href="%s/index.xml" rel="self" type="application/rss+xml"/>`,
|
<atom:link href="%s/index.xml" rel="self" type="application/rss+xml"/>`,
|
||||||
xml_escape(site.title),
|
xml_escape(site.title),
|
||||||
site.base_url,
|
site.base_url,
|
||||||
xml_escape(site.description),
|
xml_escape(site.description),
|
||||||
site.base_url,
|
site.base_url,
|
||||||
))
|
),
|
||||||
|
)
|
||||||
|
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.type == .Home {
|
if page.section == "" && page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +35,10 @@ generate_rss :: proc(site: ^Site) -> string {
|
|||||||
pub_date = format_rfc822(page.date)
|
pub_date = format_rfc822(page.date)
|
||||||
}
|
}
|
||||||
|
|
||||||
strings.write_string(&sb, fmt.aprintf(
|
strings.write_string(
|
||||||
`<item>
|
&sb,
|
||||||
|
fmt.aprintf(
|
||||||
|
`<item>
|
||||||
<title>%s</title>
|
<title>%s</title>
|
||||||
<link>%s%s</link>
|
<link>%s%s</link>
|
||||||
<pubDate>%s</pubDate>
|
<pubDate>%s</pubDate>
|
||||||
@@ -41,14 +46,15 @@ generate_rss :: proc(site: ^Site) -> string {
|
|||||||
<description>%s</description>
|
<description>%s</description>
|
||||||
</item>
|
</item>
|
||||||
`,
|
`,
|
||||||
xml_escape(page.title),
|
xml_escape(page.title),
|
||||||
site.base_url,
|
site.base_url,
|
||||||
page.permalink,
|
page.permalink,
|
||||||
pub_date,
|
pub_date,
|
||||||
site.base_url,
|
site.base_url,
|
||||||
page.permalink,
|
page.permalink,
|
||||||
xml_escape(page.body_html),
|
xml_escape(page.body_html),
|
||||||
))
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
strings.write_string(&sb, "</channel>\n</rss>")
|
strings.write_string(&sb, "</channel>\n</rss>")
|
||||||
@@ -58,34 +64,59 @@ generate_rss :: proc(site: ^Site) -> string {
|
|||||||
generate_sitemap :: proc(site: ^Site) -> string {
|
generate_sitemap :: proc(site: ^Site) -> string {
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make()
|
||||||
|
|
||||||
strings.write_string(&sb, `<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
strings.write_string(
|
||||||
|
&sb,
|
||||||
|
`<?xml version="1.0" encoding="utf-8" standalone="yes"?>
|
||||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
|
||||||
`)
|
`,
|
||||||
|
)
|
||||||
|
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
lastmod := ""
|
lastmod := ""
|
||||||
if page.date != "" {
|
if page.date != "" {
|
||||||
lastmod = fmt.aprintf("<lastmod>%s</lastmod>", page.date)
|
lastmod = fmt.aprintf("<lastmod>%s</lastmod>", page.date)
|
||||||
}
|
}
|
||||||
strings.write_string(&sb, fmt.aprintf(
|
strings.write_string(
|
||||||
"<url><loc>%s%s</loc>%s</url>\n", site.base_url, page.permalink, lastmod,
|
&sb,
|
||||||
))
|
fmt.aprintf("<url><loc>%s%s</loc>%s</url>\n", site.base_url, page.permalink, lastmod),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Posts list page
|
// Section index pages (for sections without an index in content)
|
||||||
posts_lastmod := ""
|
sections := make(map[string]bool)
|
||||||
|
defer delete(sections)
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.type == .Post && page.date > posts_lastmod {
|
if page.section != "" && !page._is_index {
|
||||||
posts_lastmod = page.date
|
sections[page.section] = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
posts_lm := ""
|
for section in sections {
|
||||||
if posts_lastmod != "" {
|
has_index := false
|
||||||
posts_lm = fmt.aprintf("<lastmod>%s</lastmod>", posts_lastmod)
|
for page in site.pages {
|
||||||
|
if page.section == section && page._is_index {
|
||||||
|
has_index = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if has_index {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
section_lastmod := ""
|
||||||
|
for page in site.pages {
|
||||||
|
if page.section == section && !page._is_index && page.date > section_lastmod {
|
||||||
|
section_lastmod = page.date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lm := ""
|
||||||
|
if section_lastmod != "" {
|
||||||
|
lm = fmt.aprintf("<lastmod>%s</lastmod>", section_lastmod)
|
||||||
|
}
|
||||||
|
strings.write_string(
|
||||||
|
&sb,
|
||||||
|
fmt.aprintf("<url><loc>%s/%s/</loc>%s</url>\n", site.base_url, section, lm),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
strings.write_string(&sb, fmt.aprintf(
|
|
||||||
"<url><loc>%s/posts/</loc>%s</url>\n", site.base_url, posts_lm,
|
|
||||||
))
|
|
||||||
|
|
||||||
strings.write_string(&sb, "</urlset>")
|
strings.write_string(&sb, "</urlset>")
|
||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
|
|||||||
+18
-15
@@ -20,11 +20,11 @@ strip_definitions :: proc(
|
|||||||
clean_body: string,
|
clean_body: string,
|
||||||
sn_defs, mn_defs: map[string]string,
|
sn_defs, mn_defs: map[string]string,
|
||||||
) {
|
) {
|
||||||
// TODO: Should this be temp allocated?
|
|
||||||
lines := strings.split(body, "\n")
|
lines := strings.split(body, "\n")
|
||||||
defer delete(lines)
|
defer delete(lines)
|
||||||
output_lines: [dynamic]string
|
|
||||||
defer delete(output_lines)
|
out_sb := strings.builder_make()
|
||||||
|
defer strings.builder_destroy(&out_sb)
|
||||||
|
|
||||||
i := 0
|
i := 0
|
||||||
for i < len(lines) {
|
for i < len(lines) {
|
||||||
@@ -32,48 +32,51 @@ strip_definitions :: proc(
|
|||||||
|
|
||||||
id, def_text, kind, is_def := parse_def_line(line)
|
id, def_text, kind, is_def := parse_def_line(line)
|
||||||
if !is_def {
|
if !is_def {
|
||||||
append(&output_lines, line)
|
if strings.builder_len(out_sb) > 0 {
|
||||||
|
strings.write_string(&out_sb, "\n")
|
||||||
|
}
|
||||||
|
strings.write_string(&out_sb, line)
|
||||||
i += 1
|
i += 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collect definition text (initial line + multi-line continuations)
|
def_sb := strings.builder_make()
|
||||||
def_parts: [dynamic]string
|
|
||||||
if def_text != "" {
|
if def_text != "" {
|
||||||
append(&def_parts, def_text)
|
strings.write_string(&def_sb, def_text)
|
||||||
}
|
}
|
||||||
|
|
||||||
i += 1
|
i += 1
|
||||||
for i < len(lines) {
|
for i < len(lines) {
|
||||||
next := lines[i]
|
next := lines[i]
|
||||||
// Stop at blank lines
|
|
||||||
if len(next) == 0 {
|
if len(next) == 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Stop at new note definitions
|
|
||||||
_, _, _, is_new_def := parse_def_line(next)
|
_, _, _, is_new_def := parse_def_line(next)
|
||||||
if is_new_def {
|
if is_new_def {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
// Include as continuation (trim indented lines)
|
if strings.builder_len(def_sb) > 0 {
|
||||||
|
strings.write_string(&def_sb, "\n")
|
||||||
|
}
|
||||||
if is_indented(next) {
|
if is_indented(next) {
|
||||||
append(&def_parts, strings.trim_left(next, " \t"))
|
strings.write_string(&def_sb, strings.trim_left(next, " \t"))
|
||||||
} else {
|
} else {
|
||||||
append(&def_parts, next)
|
strings.write_string(&def_sb, next)
|
||||||
}
|
}
|
||||||
i += 1
|
i += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
joined := strings.join(def_parts[:], "\n")
|
joined := strings.clone(strings.to_string(def_sb))
|
||||||
|
strings.builder_destroy(&def_sb)
|
||||||
|
|
||||||
if kind == .Marginnote {
|
if kind == .Marginnote {
|
||||||
mn_defs[id] = joined
|
mn_defs[id] = joined
|
||||||
} else {
|
} else {
|
||||||
sn_defs[id] = joined
|
sn_defs[id] = joined
|
||||||
}
|
}
|
||||||
delete(def_parts)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
clean_body = strings.join(output_lines[:], "\n")
|
clean_body = strings.clone(strings.to_string(out_sb))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ Frontmatter :: struct {
|
|||||||
draft: bool,
|
draft: bool,
|
||||||
isStarred: bool,
|
isStarred: bool,
|
||||||
menu: string,
|
menu: string,
|
||||||
|
layout: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse_frontmatter splits raw file content into a Frontmatter struct and the
|
// parse_frontmatter splits raw file content into a Frontmatter struct and the
|
||||||
@@ -52,6 +53,7 @@ parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok
|
|||||||
fm.draft = json_get_bool(obj, "draft")
|
fm.draft = json_get_bool(obj, "draft")
|
||||||
fm.isStarred = json_get_bool(obj, "isStarred")
|
fm.isStarred = json_get_bool(obj, "isStarred")
|
||||||
fm.menu = json_get_string(obj, "menu")
|
fm.menu = json_get_string(obj, "menu")
|
||||||
|
fm.layout = json_get_string(obj, "layout")
|
||||||
|
|
||||||
ok = true
|
ok = true
|
||||||
return
|
return
|
||||||
|
|||||||
+33
-28
@@ -244,37 +244,39 @@ capture_name_to_css :: proc(name: string) -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
escape_html :: proc(s: string) -> string {
|
escape_html :: proc(s: string) -> string {
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
start := 0
|
start := 0
|
||||||
for i in 0..<len(s) {
|
for i in 0..<len(s) {
|
||||||
switch s[i] {
|
switch s[i] {
|
||||||
case '&':
|
case '&':
|
||||||
if i > start do append(&parts, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
append(&parts, "&")
|
strings.write_string(&sb, "&")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '<':
|
case '<':
|
||||||
if i > start do append(&parts, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
append(&parts, "<")
|
strings.write_string(&sb, "<")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '>':
|
case '>':
|
||||||
if i > start do append(&parts, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
append(&parts, ">")
|
strings.write_string(&sb, ">")
|
||||||
start = i + 1
|
start = i + 1
|
||||||
case '"':
|
case '"':
|
||||||
if i > start do append(&parts, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
append(&parts, """)
|
strings.write_string(&sb, """)
|
||||||
start = i + 1
|
start = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if start < len(s) do append(&parts, s[start:])
|
if start == 0 do return s
|
||||||
if len(parts) == 0 do return s
|
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
unescape_html :: proc(s: string) -> string {
|
unescape_html :: proc(s: string) -> string {
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
start := 0
|
start := 0
|
||||||
for i in 0..<len(s) {
|
for i in 0..<len(s) {
|
||||||
if s[i] != '&' do continue
|
if s[i] != '&' do continue
|
||||||
@@ -290,13 +292,13 @@ unescape_html :: proc(s: string) -> string {
|
|||||||
case "'", "'": replacement = "'"
|
case "'", "'": replacement = "'"
|
||||||
case: continue
|
case: continue
|
||||||
}
|
}
|
||||||
if i > start do append(&parts, s[start:i])
|
if i > start do strings.write_string(&sb, s[start:i])
|
||||||
append(&parts, replacement)
|
strings.write_string(&sb, replacement)
|
||||||
start = i + semi + 1
|
start = i + semi + 1
|
||||||
}
|
}
|
||||||
if start < len(s) do append(&parts, s[start:])
|
if start == 0 do return s
|
||||||
if len(parts) == 0 do return s
|
if start < len(s) do strings.write_string(&sb, s[start:])
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
|
highlight_block :: proc(code: string, lang: string, file_path: string) -> string {
|
||||||
@@ -416,19 +418,22 @@ highlight_code :: proc(html: string, file_path: string) -> string {
|
|||||||
PREFIX :: `<pre><code class="language-`
|
PREFIX :: `<pre><code class="language-`
|
||||||
CODE_END :: `</code></pre>`
|
CODE_END :: `</code></pre>`
|
||||||
|
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
|
found := false
|
||||||
|
|
||||||
for {
|
for {
|
||||||
rel := strings.index(html[pos:], PREFIX)
|
rel := strings.index(html[pos:], PREFIX)
|
||||||
if rel < 0 {
|
if rel < 0 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
found = true
|
||||||
idx := pos + rel
|
idx := pos + rel
|
||||||
|
|
||||||
if idx > pos {
|
if idx > pos {
|
||||||
append(&parts, html[pos:idx])
|
strings.write_string(&sb, html[pos:idx])
|
||||||
}
|
}
|
||||||
|
|
||||||
lang_start := idx + len(PREFIX)
|
lang_start := idx + len(PREFIX)
|
||||||
@@ -455,17 +460,17 @@ highlight_code :: proc(html: string, file_path: string) -> string {
|
|||||||
|
|
||||||
code := html[code_start:end_idx]
|
code := html[code_start:end_idx]
|
||||||
highlighted := highlight_block(code, lang, file_path)
|
highlighted := highlight_block(code, lang, file_path)
|
||||||
append(&parts, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
strings.write_string(&sb, fmt.tprintf(`<pre><code class="language-%s">%s</code></pre>`, lang, highlighted))
|
||||||
|
|
||||||
pos = end_idx + len(CODE_END)
|
pos = end_idx + len(CODE_END)
|
||||||
}
|
}
|
||||||
|
|
||||||
if pos < len(html) {
|
if pos < len(html) && found {
|
||||||
append(&parts, html[pos:])
|
strings.write_string(&sb, html[pos:])
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 0 {
|
if !found {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|||||||
+130
-35
@@ -54,12 +54,13 @@ Home_Data :: struct {
|
|||||||
list_pages: [dynamic]Page_Context,
|
list_pages: [dynamic]Page_Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
Posts_Data :: struct {
|
Section_Data :: struct {
|
||||||
using base: Base_Data,
|
using base: Base_Data,
|
||||||
|
page_title: string,
|
||||||
year_sections: [dynamic]Year_Section,
|
year_sections: [dynamic]Year_Section,
|
||||||
}
|
}
|
||||||
|
|
||||||
build_page_context :: proc(page: Page) -> Page_Context {
|
build_page_context :: proc(page: Page) -> Page_Context {
|
||||||
return Page_Context {
|
return Page_Context {
|
||||||
permalink = page.permalink,
|
permalink = page.permalink,
|
||||||
title = page.title,
|
title = page.title,
|
||||||
@@ -70,14 +71,15 @@ Posts_Data :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strip_html_tags :: proc(s: string) -> string {
|
strip_html_tags :: proc(s: string) -> string {
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
in_tag := false
|
in_tag := false
|
||||||
start := 0
|
start := 0
|
||||||
for i in 0 ..< len(s) {
|
for i in 0 ..< len(s) {
|
||||||
if s[i] == '<' && !in_tag {
|
if s[i] == '<' && !in_tag {
|
||||||
if i > start {
|
if i > start {
|
||||||
append(&parts, s[start:i])
|
strings.write_string(&sb, s[start:i])
|
||||||
}
|
}
|
||||||
in_tag = true
|
in_tag = true
|
||||||
} else if s[i] == '>' && in_tag {
|
} else if s[i] == '>' && in_tag {
|
||||||
@@ -85,13 +87,13 @@ strip_html_tags :: proc(s: string) -> string {
|
|||||||
start = i + 1
|
start = i + 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !in_tag && start < len(s) {
|
if start == 0 {
|
||||||
append(&parts, s[start:])
|
|
||||||
}
|
|
||||||
if len(parts) == 0 {
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
return strings.join(parts[:], "")
|
if !in_tag && start < len(s) {
|
||||||
|
strings.write_string(&sb, s[start:])
|
||||||
|
}
|
||||||
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
og_type :: proc(is_article: bool) -> string {
|
og_type :: proc(is_article: bool) -> string {
|
||||||
@@ -113,6 +115,55 @@ load_template :: proc(layouts_dir: string, name: string) -> mustache.Template {
|
|||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get_template :: proc(
|
||||||
|
layouts_dir: string,
|
||||||
|
layout: string,
|
||||||
|
cache: ^map[string]mustache.Template,
|
||||||
|
) -> mustache.Template {
|
||||||
|
// Build fallback chain: <layout> → [section_index] → page → base
|
||||||
|
chain: [4]string
|
||||||
|
n := 0
|
||||||
|
chain[n] = layout; n += 1
|
||||||
|
if strings.has_suffix(layout, "_index") && layout != "section_index" {
|
||||||
|
chain[n] = "section_index"; n += 1
|
||||||
|
}
|
||||||
|
if layout != "page" && layout != "base" {
|
||||||
|
chain[n] = "page"; n += 1
|
||||||
|
}
|
||||||
|
if layout != "base" {
|
||||||
|
chain[n] = "base"; n += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
for i in 0..<n {
|
||||||
|
candidate := chain[i]
|
||||||
|
if cached, ok := cache[candidate]; ok {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
filename := fmt.tprintf("%s.html", candidate)
|
||||||
|
if os.exists(fmt.tprintf("%s/%s", layouts_dir, filename)) {
|
||||||
|
tpl := load_template(layouts_dir, filename)
|
||||||
|
cache[candidate] = tpl
|
||||||
|
return tpl
|
||||||
|
}
|
||||||
|
if candidate != chain[n - 1] {
|
||||||
|
log.warnf("thor: template %s not found, falling back", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.errorf("thor: base.html not found in %s", layouts_dir)
|
||||||
|
return mustache.Template{}
|
||||||
|
}
|
||||||
|
|
||||||
|
capitalize :: proc(s: string) -> string {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if s[0] >= 'a' && s[0] <= 'z' {
|
||||||
|
return fmt.aprintf("%c%s", s[0] - 32, s[1:])
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
render_template :: proc(
|
render_template :: proc(
|
||||||
content_tpl: mustache.Template,
|
content_tpl: mustache.Template,
|
||||||
data: any,
|
data: any,
|
||||||
@@ -130,10 +181,12 @@ render_site :: proc(site: ^Site) {
|
|||||||
pages := site.pages[:]
|
pages := site.pages[:]
|
||||||
sort_pages_by_date(pages)
|
sort_pages_by_date(pages)
|
||||||
|
|
||||||
// Load shared resources once
|
// Load shared resources
|
||||||
partials := load_partials(site.layouts_dir)
|
partials := load_partials(site.layouts_dir)
|
||||||
partials["base"] = load_template(site.layouts_dir, "base.html")
|
partials["base"] = load_template(site.layouts_dir, "base.html")
|
||||||
post_tpl := load_template(site.layouts_dir, "post.html")
|
|
||||||
|
template_cache: map[string]mustache.Template
|
||||||
|
defer delete(template_cache)
|
||||||
|
|
||||||
now, ok := time.time_to_datetime(time.now())
|
now, ok := time.time_to_datetime(time.now())
|
||||||
assert(ok)
|
assert(ok)
|
||||||
@@ -152,28 +205,67 @@ render_site :: proc(site: ^Site) {
|
|||||||
home: Page
|
home: Page
|
||||||
has_home := false
|
has_home := false
|
||||||
for page in pages {
|
for page in pages {
|
||||||
if page.type == .Home {
|
if page.section == "" && page._is_index {
|
||||||
home = page
|
home = page
|
||||||
has_home = true
|
has_home = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render individual content pages (skip home)
|
// Collect sections
|
||||||
|
sections := make(map[string]bool)
|
||||||
|
defer delete(sections)
|
||||||
for page in pages {
|
for page in pages {
|
||||||
if page.type == .Home {
|
if page.section != "" {
|
||||||
|
sections[page.section] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render individual content pages (skip all index pages)
|
||||||
|
for page in pages {
|
||||||
|
if page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
html := render_page_html(page, site, post_tpl, partials, base)
|
tpl := get_template(site.layouts_dir, page.layout, &template_cache)
|
||||||
|
html := render_page_html(page, site, tpl, partials, base)
|
||||||
if .Minify in site.features {
|
if .Minify in site.features {
|
||||||
html = minify_html(html)
|
html = minify_html(html)
|
||||||
}
|
}
|
||||||
write_page(site.output_dir, page.permalink, html)
|
write_page(site.output_dir, page.permalink, html)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Render section index pages
|
||||||
|
for section in sections {
|
||||||
|
section_index: Page
|
||||||
|
has_section_index := false
|
||||||
|
for page in pages {
|
||||||
|
if page.section == section && page._is_index {
|
||||||
|
section_index = page
|
||||||
|
has_section_index = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layout := fmt.tprintf("%s_index", section)
|
||||||
|
section_tpl := get_template(site.layouts_dir, layout, &template_cache)
|
||||||
|
html := render_section(
|
||||||
|
site,
|
||||||
|
section,
|
||||||
|
section_index,
|
||||||
|
has_section_index,
|
||||||
|
section_tpl,
|
||||||
|
partials,
|
||||||
|
base,
|
||||||
|
)
|
||||||
|
if .Minify in site.features {
|
||||||
|
html = minify_html(html)
|
||||||
|
}
|
||||||
|
write_page(site.output_dir, fmt.aprintf("/%s/", section), html)
|
||||||
|
}
|
||||||
|
|
||||||
// Render home page
|
// Render home page
|
||||||
if has_home {
|
if has_home {
|
||||||
home_tpl := load_template(site.layouts_dir, "home.html")
|
home_tpl := get_template(site.layouts_dir, "home", &template_cache)
|
||||||
home_html := render_home_html(home, site, home_tpl, partials, base)
|
home_html := render_home_html(home, site, home_tpl, partials, base)
|
||||||
if .Minify in site.features {
|
if .Minify in site.features {
|
||||||
home_html = minify_html(home_html)
|
home_html = minify_html(home_html)
|
||||||
@@ -181,14 +273,6 @@ render_site :: proc(site: ^Site) {
|
|||||||
write_file(fmt.tprintf("%s/index.html", site.output_dir), home_html)
|
write_file(fmt.tprintf("%s/index.html", site.output_dir), home_html)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render posts list page
|
|
||||||
posts_tpl := load_template(site.layouts_dir, "posts_list.html")
|
|
||||||
posts_html := render_posts_html(site, posts_tpl, partials, base)
|
|
||||||
if .Minify in site.features {
|
|
||||||
posts_html = minify_html(posts_html)
|
|
||||||
}
|
|
||||||
write_page(site.output_dir, "/posts/", posts_html)
|
|
||||||
|
|
||||||
// Generate RSS feed
|
// Generate RSS feed
|
||||||
rss := generate_rss(site)
|
rss := generate_rss(site)
|
||||||
write_file(fmt.tprintf("%s/index.xml", site.output_dir), rss)
|
write_file(fmt.tprintf("%s/index.xml", site.output_dir), rss)
|
||||||
@@ -204,7 +288,7 @@ render_site :: proc(site: ^Site) {
|
|||||||
robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url)
|
robots := fmt.aprintf("User-agent: *\nAllow: /\nSitemap: %s/sitemap.xml\n", site.base_url)
|
||||||
write_file(fmt.tprintf("%s/robots.txt", site.output_dir), robots)
|
write_file(fmt.tprintf("%s/robots.txt", site.output_dir), robots)
|
||||||
|
|
||||||
total := len(pages) + 1
|
total := len(pages) + len(sections)
|
||||||
if !has_home {
|
if !has_home {
|
||||||
total += 1
|
total += 1
|
||||||
}
|
}
|
||||||
@@ -218,7 +302,7 @@ render_page_html :: proc(
|
|||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
base: Base_Data,
|
base: Base_Data,
|
||||||
) -> string {
|
) -> string {
|
||||||
is_article := page.type == .Post
|
is_article := page.section != ""
|
||||||
data := Page_Data {
|
data := Page_Data {
|
||||||
base = base,
|
base = base,
|
||||||
}
|
}
|
||||||
@@ -232,7 +316,7 @@ render_page_html :: proc(
|
|||||||
data.og_title = strip_html_tags(page.title)
|
data.og_title = strip_html_tags(page.title)
|
||||||
data.og_type = og_type(is_article)
|
data.og_type = og_type(is_article)
|
||||||
data.is_article = is_article
|
data.is_article = is_article
|
||||||
data.og_section = "posts"
|
data.og_section = page.section
|
||||||
data.og_published = page.date
|
data.og_published = page.date
|
||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
}
|
}
|
||||||
@@ -247,7 +331,7 @@ render_home_html :: proc(
|
|||||||
list_pages := make([dynamic]Page_Context)
|
list_pages := make([dynamic]Page_Context)
|
||||||
defer delete(list_pages)
|
defer delete(list_pages)
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.type == .Home {
|
if page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
append(&list_pages, build_page_context(page))
|
append(&list_pages, build_page_context(page))
|
||||||
@@ -266,8 +350,11 @@ render_home_html :: proc(
|
|||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
}
|
}
|
||||||
|
|
||||||
render_posts_html :: proc(
|
render_section :: proc(
|
||||||
site: ^Site,
|
site: ^Site,
|
||||||
|
section: string,
|
||||||
|
section_index: Page,
|
||||||
|
has_index: bool,
|
||||||
content_tpl: mustache.Template,
|
content_tpl: mustache.Template,
|
||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
base: Base_Data,
|
base: Base_Data,
|
||||||
@@ -276,7 +363,7 @@ render_posts_html :: proc(
|
|||||||
defer delete(year_sections)
|
defer delete(year_sections)
|
||||||
current_year := ""
|
current_year := ""
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.type != .Post {
|
if page.section != section || page._is_index {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
year := get_year(page.date)
|
year := get_year(page.date)
|
||||||
@@ -287,13 +374,21 @@ render_posts_html :: proc(
|
|||||||
append(&year_sections[len(year_sections) - 1].posts, build_page_context(page))
|
append(&year_sections[len(year_sections) - 1].posts, build_page_context(page))
|
||||||
}
|
}
|
||||||
|
|
||||||
data := Posts_Data {
|
data := Section_Data {
|
||||||
base = base,
|
base = base,
|
||||||
}
|
}
|
||||||
data.title = fmt.tprintf("Posts | %s", site.title)
|
if has_index {
|
||||||
|
data.body = section_index.body_html
|
||||||
|
data.page_title = section_index.title
|
||||||
|
data.title = fmt.tprintf("%s | %s", section_index.title, site.title)
|
||||||
|
data.og_title = section_index.title
|
||||||
|
} else {
|
||||||
|
data.page_title = capitalize(section)
|
||||||
|
data.title = fmt.tprintf("%s | %s", capitalize(section), site.title)
|
||||||
|
data.og_title = capitalize(section)
|
||||||
|
}
|
||||||
data.year_sections = year_sections
|
data.year_sections = year_sections
|
||||||
data.og_url = fmt.tprintf("%s/posts/", site.base_url)
|
data.og_url = fmt.tprintf("%s/%s/", site.base_url, section)
|
||||||
data.og_title = "Posts"
|
|
||||||
data.og_type = "website"
|
data.og_type = "website"
|
||||||
data.is_article = false
|
data.is_article = false
|
||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
|
|||||||
+14
-10
@@ -5,10 +5,12 @@ import "core:strings"
|
|||||||
wrap_sections :: proc(html: string) -> string {
|
wrap_sections :: proc(html: string) -> string {
|
||||||
H2 :: "<h2"
|
H2 :: "<h2"
|
||||||
|
|
||||||
parts: [dynamic]string
|
sb := strings.builder_make()
|
||||||
defer delete(parts)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
pos := 0
|
pos := 0
|
||||||
search_pos := 0
|
search_pos := 0
|
||||||
|
found := false
|
||||||
|
|
||||||
for {
|
for {
|
||||||
rel := strings.index(html[search_pos:], H2)
|
rel := strings.index(html[search_pos:], H2)
|
||||||
@@ -16,11 +18,12 @@ wrap_sections :: proc(html: string) -> string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
idx := search_pos + rel
|
idx := search_pos + rel
|
||||||
|
found = true
|
||||||
|
|
||||||
if idx > pos {
|
if idx > pos {
|
||||||
append(&parts, "<section>")
|
strings.write_string(&sb, "<section>")
|
||||||
append(&parts, html[pos:idx])
|
strings.write_string(&sb, html[pos:idx])
|
||||||
append(&parts, "</section>")
|
strings.write_string(&sb, "</section>")
|
||||||
}
|
}
|
||||||
|
|
||||||
pos = idx
|
pos = idx
|
||||||
@@ -28,13 +31,14 @@ wrap_sections :: proc(html: string) -> string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if pos < len(html) {
|
if pos < len(html) {
|
||||||
append(&parts, "<section>")
|
strings.write_string(&sb, "<section>")
|
||||||
append(&parts, html[pos:])
|
strings.write_string(&sb, html[pos:])
|
||||||
append(&parts, "</section>")
|
strings.write_string(&sb, "</section>")
|
||||||
|
found = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(parts) == 0 {
|
if !found {
|
||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
return strings.join(parts[:], "")
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user