mirror of
https://github.com/sbrow/thor.git
synced 2026-08-26 11:23:32 -04:00
Compare commits
15 Commits
085811b312
...
5c689638b1
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c689638b1 | |||
| 398f0eb1b4 | |||
| ac43ab1e5a | |||
| 4fc3eae64c | |||
| 90f6466f9c | |||
| ce0f662c92 | |||
| 6f6c0dddd1 | |||
| c0ff59513f | |||
| 6e66709f05 | |||
| a41a7803a4 | |||
| 66b5c5a934 | |||
| ebb4ef691a | |||
| 9d2b2c4395 | |||
| d05459af52 | |||
| c7a5ae2360 |
@@ -250,6 +250,7 @@ render(tmpl, data, partials) → render_nodes (walks flat node array against con
|
||||
|
||||
## Design decisions
|
||||
|
||||
You may never, *ever* remove `TODO:` or `FIXME:` comments. Those are for humans, not machines.
|
||||
See `HUGO.md` for analysis of why thor doesn't need Hugo's shortcode context isolation.
|
||||
See `mustache/PARTIAL_INDENT.md` for whitespace handling analysis.
|
||||
See `mustache/SPEC.md` for the original implementation specification.
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
# Plan: Computed Properties in Mustache
|
||||
|
||||
## Problem
|
||||
|
||||
`Year_Section` is a privileged, hardcoded struct in render.odin. Every section listing gets pages grouped by year — no choice. The grouping dimension (year) is baked into the code. Templates can't request different views of the same data.
|
||||
|
||||
## Solution: Computed Properties
|
||||
|
||||
Struct fields that are procs get called during mustache data resolution. When `lookup_in` resolves a field via reflection and finds a proc, it calls it and uses the return value. This is computed properties (Vue/Ember pattern), not spec lambdas (text transformers).
|
||||
|
||||
No mustache syntax changes. `{{#posts.by_year}}` is vanilla mustache — the magic is in data resolution, not template syntax.
|
||||
|
||||
## Template Usage
|
||||
|
||||
```handlebars
|
||||
{{#posts.by_year}}
|
||||
<h2>{{year}}</h2>
|
||||
{{#posts}}
|
||||
<li>{{title}}</li>
|
||||
{{/posts}}
|
||||
{{/posts.by_year}}
|
||||
|
||||
{{#posts.all}}
|
||||
<li>{{title}}</li>
|
||||
{{/posts.all}}
|
||||
```
|
||||
|
||||
Context resolution handles naming naturally — `posts` inside a Year_Group (the `.posts` field) shadows the outer `posts` view object.
|
||||
|
||||
## Data Model
|
||||
|
||||
```odin
|
||||
Year_Group :: struct {
|
||||
year: string,
|
||||
posts: [dynamic]Page_Context,
|
||||
}
|
||||
|
||||
Pages_View :: struct {
|
||||
all: [dynamic]Page_Context,
|
||||
by_year: proc() -> [dynamic]Year_Group,
|
||||
}
|
||||
|
||||
Section_Data :: struct {
|
||||
using base: Base_Data,
|
||||
page_title: string,
|
||||
posts: Pages_View,
|
||||
}
|
||||
```
|
||||
|
||||
`by_year` is a closure that captures `all` and groups lazily. Grouping only happens when the template requests it.
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
### 1. Mustache data layer (`mustache/data.odin`)
|
||||
|
||||
In `lookup_in` (or `resolve_name`), after resolving a field value via reflection:
|
||||
|
||||
```odin
|
||||
// If the resolved value is a proc, call it
|
||||
if value, ok := resolved.(proc() -> any); ok {
|
||||
return value()
|
||||
}
|
||||
```
|
||||
|
||||
Need to handle proc detection generically — Odin has many proc types (different signatures, calling conventions). The computed properties we use are all zero-argument procs returning `any`.
|
||||
|
||||
Alternative: check via `reflect.Type_Info` if the value is a proc type, then call it through `reflect.CallProcedure` or transmute.
|
||||
|
||||
### 2. Odin closure challenge
|
||||
|
||||
Contextless procs can't capture variables. Regular closure procs capture context by reference. We need the proc to access the page list when called later by mustache.
|
||||
|
||||
Options:
|
||||
- Store the data alongside the proc in the struct (e.g., `all` field on `Pages_View`). The proc accesses it via the struct instance.
|
||||
- Use `context.allocator` to store captured data that the closure references.
|
||||
- Pass the struct as an implicit `self` parameter (method-style).
|
||||
|
||||
The simplest approach: the closure captures a pointer to the data, which is arena-allocated and alive during rendering.
|
||||
|
||||
### 3. Build `Pages_View` in `render_section`
|
||||
|
||||
```odin
|
||||
all_pages := make([dynamic]Page_Context)
|
||||
for page in site.pages {
|
||||
if page.section != section || page._is_index { continue }
|
||||
append(&all_pages, build_page_context(page))
|
||||
}
|
||||
|
||||
view := Pages_View{
|
||||
all = all_pages,
|
||||
by_year = group_by_year_closure, // captures all_pages
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Remove `Year_Section` and `year_sections`
|
||||
|
||||
- Delete `Year_Section` struct
|
||||
- Remove `year_sections` from `Section_Data`
|
||||
- Remove year grouping logic from `render_section`
|
||||
- Delete `get_year` helper (or move it into the closure)
|
||||
|
||||
### 5. Update templates
|
||||
|
||||
`posts_index.html`:
|
||||
```handlebars
|
||||
{{#posts.by_year}}
|
||||
<section>
|
||||
<h2>{{year}}</h2>
|
||||
<ul class="post-list">
|
||||
{{#posts}} <li>...</li> {{/posts}}
|
||||
</ul>
|
||||
</section>
|
||||
{{/posts.by_year}}
|
||||
```
|
||||
|
||||
Home page could use the same pattern:
|
||||
```handlebars
|
||||
{{#pages.all}}
|
||||
<li>...</li>
|
||||
{{/pages.all}}
|
||||
```
|
||||
|
||||
### 6. Tests
|
||||
|
||||
- Verify `by_year` proc is called lazily (only when template requests it)
|
||||
- Verify year groups contain correct pages
|
||||
- Verify flat list (`all`) works independently
|
||||
|
||||
## Prerequisite
|
||||
|
||||
**Step 1 (mustache data layer) must be done first.** This is the foundation — without proc-calling in data resolution, none of the rest works.
|
||||
|
||||
## Future Extensions
|
||||
|
||||
Once computed properties work, they're available everywhere:
|
||||
- `posts.featured` — filter by starred
|
||||
- `posts.recent(n)` — most recent N posts (if we support params)
|
||||
- `site.tags` — all unique tags across posts
|
||||
- `pages.by_section("posts")` — filter by section
|
||||
|
||||
These are all zero-argument procs on view structs. No mustache changes needed beyond step 1.
|
||||
@@ -1,22 +1,45 @@
|
||||
- README.md
|
||||
- [ ] "Zero is beautiful"
|
||||
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
||||
- [ ] Clean up the default layouts
|
||||
- [ ] Performance
|
||||
- [ ] See if we can disable bounds checks in `write_indented` and elsewhere.
|
||||
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
||||
## Performance
|
||||
|
||||
- [ ] See if we can disable bounds checks in `write_indented` and elsewhere.
|
||||
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
||||
`reload_site` proc, that just updates changed resources.
|
||||
- [ ] Only publish referenced assets.
|
||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
||||
- [ ] mustache data keys for opengraph, etc.
|
||||
- [ ] Only publish referenced assets.
|
||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
||||
- [ ] Use spall to find ways to reduce run time.
|
||||
|
||||
## Memory Management
|
||||
|
||||
- [ ] Not sure whether to use temp allocator or site_allocator in opengraph.odin.
|
||||
- [ ] Not sure whether to use temp allocator or site_allocator in `site_load_content`.
|
||||
- [ ] Might not need to allocate in `strip_html_tags`
|
||||
|
||||
## Markdown
|
||||
- [ ] Add overloads for every extension - accept ^strings.Builder.
|
||||
- [ ] Add conventional (Hugo style) footnotes option.
|
||||
- [ ] Add heading ids as a default on extension.
|
||||
|
||||
## General
|
||||
- [ ] Integrity hash
|
||||
- Allows users to verify their output didn't change after upgrading to a new version
|
||||
- [ ] Content-hash fingerprinting for CSS and JS cache busting
|
||||
- [ ] Avoid `json.Value` / `json.Object` where possible.
|
||||
- [ ] running ./thor/thor still logs the debug message: using config /home/spencer/github.com/sbrow.github.io/thor.json
|
||||
- wrong cwd?
|
||||
- [ ] Clean up the default layouts
|
||||
- [ ] Add `-production` flag
|
||||
- sets `-minify`
|
||||
- [ ] Open Graph
|
||||
- [x] mustache data keys for opengraph, etc.
|
||||
- [ ] OpenGraph meta tags — verify all fields match production site
|
||||
- [ ] set opengraph tags / description automatically if unset. (Like hugo does)
|
||||
- [ ] can't set avatar.jpg directly in `og_init`.
|
||||
- [ ] Author should be a struct adhering to https://schema.org/author
|
||||
- [ ] Block attributes on code fences (`{ #ex-1 }`) — hello-world.md
|
||||
- [ ] 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.
|
||||
- [ ] OpenGraph meta tags — verify all fields match production site
|
||||
- [ ] Add Opt-in deflist support.
|
||||
- [ ] We need to be able to do `Year_Section` in a non-magical, unpriviledged way.
|
||||
- [ ] set opengraph tags / description automatically if unset. (Like hugo does)
|
||||
- [ ] We need to be able to do `Year_Section` in a non-magical, unprivileged way. See [PLAN.md](PLAN.md) for computed properties approach.
|
||||
- [ ] Table of contents support.
|
||||
- [ ] Nav items should be active when the current page is selected.
|
||||
- [ ] Theme selector for syntax highlighting.
|
||||
@@ -36,14 +59,29 @@
|
||||
- [ ] filesystem poll loop
|
||||
- [ ] event based
|
||||
- [ ] 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.
|
||||
- [ ] Mount content in VFS
|
||||
- [ ] commands
|
||||
- [ ] `build` alias of default
|
||||
- [ ] `init` set up new project
|
||||
- [ ] Use spall to find ways to reduce run time.
|
||||
- [ ] `new site` set up new project
|
||||
- [ ] warn/error when unknown key used in mustache.
|
||||
- [ ] Import/export packages. Hugo, jekyll, WordPress, etc.
|
||||
|
||||
## Notes
|
||||
|
||||
from the [Hugo docs](https://gohugo.io/quick-reference/glossary/#default-sort-order)
|
||||
|
||||
default sort order
|
||||
: The default sort order for page collections, used when no other criteria are set, follows this priority:
|
||||
1. weight (ascending)
|
||||
2. date (descending)
|
||||
3. linkTitle falling back to title (ascending)
|
||||
4. logical path (ascending)
|
||||
|
||||
## Code Review
|
||||
|
||||
A human should manually review every file in the project. AI cannot complete
|
||||
these tasks.
|
||||
|
||||
- [ ] Review every file in thor
|
||||
- [ ] Review assets.odin
|
||||
- [ ] Review content.odin
|
||||
@@ -53,14 +91,20 @@
|
||||
- [ ] Review main.odin
|
||||
- [ ] Review minify.odin
|
||||
- [ ] Review `markdown/`
|
||||
- [ ] Review alerts.odin
|
||||
- [ ] Review emoji.odin
|
||||
- [x] Review alerts.odin
|
||||
- [x] Review alerts_test.odin
|
||||
- [x] Review emoji.odin
|
||||
- [x] Review emoji_test.odin
|
||||
- [ ] Review footnotes.odin
|
||||
- [ ] Review footnotes_test.odin
|
||||
- [ ] Review highlight.odin
|
||||
- [ ] Review markdown.odin
|
||||
- [ ] Review sectionate.odin
|
||||
- [x] Review sectionate.odin
|
||||
- [x] Review sectionate_test.odin
|
||||
- [x] Review opengraph.odin
|
||||
- [ ] Review render.odin
|
||||
- [x] Review site.odin
|
||||
- [ ] Review treesitter/treesitter.odin
|
||||
- [ ] Review vfs.odin
|
||||
- [ ] Review procs
|
||||
- [ ] markdown.transform_alert
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ copy_assets_dir :: proc(vfs: ^VFS, output_dir: string, features: bit_set[Feature
|
||||
|
||||
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)
|
||||
log.warnf("cannot create %s: %v", dest[:idx], err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -28,11 +28,11 @@ copy_assets_dir :: proc(vfs: ^VFS, output_dir: string, features: bit_set[Feature
|
||||
}
|
||||
} else if entry.data != nil {
|
||||
if err := os.write_entire_file(dest, entry.data); err != nil {
|
||||
log.warnf("thor: cannot write %s: %v", dest, err)
|
||||
log.warnf("cannot write %s: %v", dest, err)
|
||||
}
|
||||
} else {
|
||||
if err := os.copy_file(dest, entry.fs_path); err != nil {
|
||||
log.warnf("thor: cannot copy %s: %v", entry.fs_path, err)
|
||||
log.warnf("cannot copy %s: %v", entry.fs_path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-2
@@ -13,6 +13,7 @@ Page :: struct {
|
||||
slug: string,
|
||||
layout: string,
|
||||
permalink: string,
|
||||
url: string,
|
||||
title: string,
|
||||
description: string,
|
||||
date: string,
|
||||
@@ -28,6 +29,10 @@ Page :: struct {
|
||||
site_load_content :: proc(site: ^Site) {
|
||||
site.pages = make([dynamic]Page, 0, 8, site_allocator(site))
|
||||
scan_content(site, site.content_dir, "")
|
||||
|
||||
for &page in site.pages {
|
||||
page.url = fmt.tprintf("%s%s", site.base_url, page.permalink)
|
||||
}
|
||||
}
|
||||
|
||||
// scan_content walks the content directory. At the root level (section=""),
|
||||
@@ -36,7 +41,7 @@ site_load_content :: proc(site: ^Site) {
|
||||
scan_content :: proc(site: ^Site, dir: string, section: string) {
|
||||
entries, err := os.read_all_directory_by_path(dir, context.allocator)
|
||||
if err != nil {
|
||||
log.warnf("thor: cannot read %s: %v", dir, err)
|
||||
log.warnf("cannot read %s: %v", dir, err)
|
||||
return
|
||||
}
|
||||
defer os.file_info_slice_delete(entries, context.allocator)
|
||||
@@ -91,6 +96,9 @@ infer_layout :: proc(section: string, is_index: bool) -> string {
|
||||
return fmt.tprintf("%s_index", section)
|
||||
}
|
||||
if section != "" {
|
||||
if len(section) > 1 && section[len(section) - 1] == 's' {
|
||||
return section[:len(section) - 1]
|
||||
}
|
||||
return section
|
||||
}
|
||||
return "page"
|
||||
@@ -108,7 +116,7 @@ load_page :: proc(
|
||||
) {
|
||||
data, err := os.read_entire_file_from_path(file_path, context.allocator)
|
||||
if err != nil {
|
||||
log.warnf("thor: cannot read %s: %v", file_path, err)
|
||||
log.warnf("cannot read %s: %v", file_path, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
{{&body}}
|
||||
</header>
|
||||
<ul>
|
||||
{{#list_pages}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date_iso}}<time
|
||||
{{#pages}} <li><a href="{{permalink}}">{{&title}}</a><span>{{#date_iso}}<time
|
||||
datetime="{{date_iso}}">{{date_display}}</time>{{/date_iso}}</span>
|
||||
</li>
|
||||
{{/list_pages}}
|
||||
{{/pages}}
|
||||
</ul>
|
||||
</main>
|
||||
{{/content}}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<meta property="og:url" content="{{og_url}}">
|
||||
<meta property="og:site_name" content="{{og_site_name}}">
|
||||
<meta property="og:title" content="{{og_title}}">
|
||||
<meta property="og:description" content="{{og_description}}">
|
||||
<meta property="og:locale" content="en_us">
|
||||
<meta property="og:type" content="{{og_type}}">
|
||||
{{#is_article}}<meta property="article:section" content="{{og_section}}">
|
||||
<meta property="article:published_time" content="{{og_published}}">
|
||||
{{/is_article}}<meta property="og:image" content="{{og_image}}">
|
||||
<meta property="og:url" content="{{og.url}}">
|
||||
<meta property="og:site_name" content="{{og.site_name}}">
|
||||
<meta property="og:title" content="{{og.title}}">
|
||||
<meta property="og:description" content="{{og.description}}">
|
||||
<meta property="og:locale" content="{{og.locale}}">
|
||||
<meta property="og:type" content="{{og.type}}">
|
||||
{{#og.is_article}}<meta property="article:section" content="{{og.section}}">
|
||||
<meta property="article:published_time" content="{{og.published_time}}">
|
||||
{{/og.is_article}}<meta property="og:image" content="{{og.image}}">
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<main>
|
||||
<h1>{{page_title}}</h1>
|
||||
{{&body}}
|
||||
{{#year_sections}}
|
||||
{{#by_year}}
|
||||
<section>
|
||||
<h2>{{year}}</h2>
|
||||
<ul>
|
||||
@@ -13,7 +13,7 @@
|
||||
{{/posts}}
|
||||
</ul>
|
||||
</section>
|
||||
{{/year_sections}}
|
||||
{{/by_year}}
|
||||
</main>
|
||||
{{/content}}
|
||||
{{/base}}
|
||||
|
||||
@@ -40,18 +40,16 @@ generate_rss :: proc(site: ^Site) -> string {
|
||||
fmt.aprintf(
|
||||
`<item>
|
||||
<title>%s</title>
|
||||
<link>%s%s</link>
|
||||
<link>%s</link>
|
||||
<pubDate>%s</pubDate>
|
||||
<guid>%s%s</guid>
|
||||
<guid>%s</guid>
|
||||
<description>%s</description>
|
||||
</item>
|
||||
`,
|
||||
xml_escape(page.title),
|
||||
site.base_url,
|
||||
page.permalink,
|
||||
page.url,
|
||||
pub_date,
|
||||
site.base_url,
|
||||
page.permalink,
|
||||
page.url,
|
||||
xml_escape(page.body_html),
|
||||
),
|
||||
)
|
||||
@@ -78,7 +76,7 @@ generate_sitemap :: proc(site: ^Site) -> string {
|
||||
}
|
||||
strings.write_string(
|
||||
&sb,
|
||||
fmt.aprintf("<url><loc>%s%s</loc>%s</url>\n", site.base_url, page.permalink, lastmod),
|
||||
fmt.aprintf("<url><loc>%s</loc>%s</url>\n", page.url, lastmod),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -36,7 +36,7 @@ parse_frontmatter :: proc(content: string) -> (fm: Frontmatter, body: string, ok
|
||||
|
||||
value, err := json.parse_string(json_str, spec = .JSON)
|
||||
if err != nil {
|
||||
fmt.eprintfln("thor: failed to parse frontmatter JSON: %v", err)
|
||||
fmt.eprintfln("failed to parse frontmatter JSON: %v", err)
|
||||
return
|
||||
}
|
||||
defer json.destroy_value(value)
|
||||
|
||||
+34
-41
@@ -1,23 +1,14 @@
|
||||
#+feature dynamic-literals
|
||||
package markdown
|
||||
|
||||
import "core:fmt"
|
||||
import "core:strings"
|
||||
|
||||
ALERT_STYLES: map[string]string = {
|
||||
"note" = "border-l-blue-500 bg-blue-950/30 text-slate-300",
|
||||
"tip" = "border-l-green-500 bg-green-950/30 text-slate-300",
|
||||
"important" = "border-l-purple-500 bg-purple-950/30 text-slate-300",
|
||||
"warning" = "border-l-yellow-500 bg-yellow-950/30 text-slate-300",
|
||||
"caution" = "border-l-red-500 bg-red-950/30 text-slate-300",
|
||||
}
|
||||
|
||||
ALERT_EMOJIS: map[string]string = {
|
||||
"note" = "\xe2\x84\xb9\xef\xb8\x8f",
|
||||
"tip" = "\xf0\x9f\x92\xa1",
|
||||
"important" = "\xe2\x9d\x97",
|
||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
||||
"caution" = "\xe2\x9d\x97",
|
||||
"note" = "ℹ️",
|
||||
"tip" = "💡",
|
||||
"important" = "❗",
|
||||
"warning" = "⚠️",
|
||||
"caution" = "❗",
|
||||
}
|
||||
|
||||
inject_alerts :: proc(html: string) -> string {
|
||||
@@ -43,7 +34,7 @@ inject_alerts :: proc(html: string) -> string {
|
||||
bq := remaining[bq_start:bq_end]
|
||||
|
||||
strings.write_string(&sb, remaining[:bq_start])
|
||||
strings.write_string(&sb, transform_alert(bq))
|
||||
transform_alert(&sb, bq)
|
||||
|
||||
remaining = remaining[bq_end:]
|
||||
}
|
||||
@@ -51,55 +42,57 @@ inject_alerts :: proc(html: string) -> string {
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
transform_alert :: proc(bq: string) -> string {
|
||||
// Skip <blockquote> tag and whitespace
|
||||
transform_alert :: proc(sb: ^strings.Builder, bq: string) {
|
||||
pos := len("<blockquote>")
|
||||
for pos < len(bq) && (bq[pos] == '\n' || bq[pos] == '\r' || bq[pos] == ' ' || bq[pos] == '\t') {
|
||||
for pos < len(bq) &&
|
||||
(bq[pos] == '\n' || bq[pos] == '\r' || bq[pos] == ' ' || bq[pos] == '\t') {
|
||||
pos += 1
|
||||
}
|
||||
|
||||
// Check for <p>[!
|
||||
if pos + 4 >= len(bq) || bq[pos] != '<' || bq[pos + 1] != 'p' || bq[pos + 2] != '>' ||
|
||||
bq[pos + 3] != '[' || bq[pos + 4] != '!' {
|
||||
return bq
|
||||
if pos + 4 >= len(bq) ||
|
||||
bq[pos] != '<' ||
|
||||
bq[pos + 1] != 'p' ||
|
||||
bq[pos + 2] != '>' ||
|
||||
bq[pos + 3] != '[' ||
|
||||
bq[pos + 4] != '!' {
|
||||
strings.write_string(sb, bq)
|
||||
return
|
||||
}
|
||||
|
||||
// Extract alert type until ]
|
||||
close := strings.index(bq[pos + 5:], "]")
|
||||
if close < 0 {
|
||||
return bq
|
||||
strings.write_string(sb, bq)
|
||||
return
|
||||
}
|
||||
|
||||
type_raw := bq[pos + 5 : pos + 5 + close]
|
||||
type_lower := strings.to_lower(type_raw)
|
||||
type_raw := bq[pos + 5:pos + 5 + close]
|
||||
type_lower := strings.to_lower(type_raw, context.temp_allocator)
|
||||
|
||||
style, has_style := ALERT_STYLES[type_lower]
|
||||
emoji, has_emoji := ALERT_EMOJIS[type_lower]
|
||||
if !has_style || !has_emoji {
|
||||
return bq
|
||||
emoji, found := ALERT_EMOJIS[type_lower]
|
||||
if !found {
|
||||
strings.write_string(sb, bq)
|
||||
return
|
||||
}
|
||||
|
||||
// Position after ]
|
||||
after_type := pos + 5 + close + 1
|
||||
|
||||
// Skip optional + or -
|
||||
content_start := after_type
|
||||
if content_start < len(bq) && (bq[content_start] == '+' || bq[content_start] == '-') {
|
||||
content_start += 1
|
||||
}
|
||||
// Skip space after marker
|
||||
if content_start < len(bq) && bq[content_start] == ' ' {
|
||||
content_start += 1
|
||||
}
|
||||
|
||||
// Rebuild: styled blockquote + bold title paragraph + rest
|
||||
rest := bq[content_start:]
|
||||
|
||||
return fmt.aprintf(
|
||||
`<blockquote class="alert %s rounded-r py-2">
|
||||
<p class="font-bold mb-1">%s %s`,
|
||||
style,
|
||||
emoji,
|
||||
rest,
|
||||
)
|
||||
strings.write_string(sb, `<blockquote class="alert alert-`)
|
||||
strings.write_string(sb, type_lower)
|
||||
strings.write_string(sb, `">`)
|
||||
strings.write_string(sb, "\n")
|
||||
strings.write_string(sb, `<p class="alert-title">`)
|
||||
strings.write_string(sb, emoji)
|
||||
strings.write_string(sb, " ")
|
||||
strings.write_string(sb, rest)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#+feature dynamic-literals
|
||||
#+test
|
||||
package markdown
|
||||
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_alerts_render_properly :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!NOTE] Hello.</p>\n</blockquote>"),
|
||||
`<blockquote class="alert alert-note">
|
||||
<p class="alert-title">ℹ️ Hello.</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!TIP] Be smart.</p>\n</blockquote>"),
|
||||
`<blockquote class="alert alert-tip">
|
||||
<p class="alert-title">💡 Be smart.</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!CAUTION] Danger!</p>\n</blockquote>"),
|
||||
`<blockquote class="alert alert-caution">
|
||||
<p class="alert-title">❗ Danger!</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_non_alerts_pass_through_inject_alerts :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>Just a quote.</p>\n</blockquote>"),
|
||||
"<blockquote>\n<p>Just a quote.</p>\n</blockquote>",
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!UNKNOWN] Nope.</p>\n</blockquote>"),
|
||||
"<blockquote>\n<p>[!UNKNOWN] Nope.</p>\n</blockquote>",
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<p>No blockquote here.</p>"),
|
||||
"<p>No blockquote here.</p>",
|
||||
)
|
||||
|
||||
testing.expect_value(t, inject_alerts(""), "")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_case_insensitive_alerts_render :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!Note] Mixed case.</p>\n</blockquote>"),
|
||||
`<blockquote class="alert alert-note">
|
||||
<p class="alert-title">ℹ️ Mixed case.</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts("<blockquote>\n<p>[!NOTE] Upper case.</p>\n</blockquote>"),
|
||||
`<blockquote class="alert alert-note">
|
||||
<p class="alert-title">ℹ️ Upper case.</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_multiple_alerts_render_together :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts(
|
||||
"<blockquote>\n<p>[!NOTE] First.</p>\n</blockquote>\n<blockquote>\n<p>[!TIP] Second.</p>\n</blockquote>",
|
||||
),
|
||||
`<blockquote class="alert alert-note">
|
||||
<p class="alert-title">ℹ️ First.</p>
|
||||
</blockquote>
|
||||
<blockquote class="alert alert-tip">
|
||||
<p class="alert-title">💡 Second.</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
inject_alerts(
|
||||
"<blockquote>\n<p>Regular.</p>\n</blockquote>\n<blockquote>\n<p>[!WARNING] Alert!</p>\n</blockquote>",
|
||||
),
|
||||
`<blockquote>
|
||||
<p>Regular.</p>
|
||||
</blockquote>
|
||||
<blockquote class="alert alert-warning">
|
||||
<p class="alert-title">⚠️ Alert!</p>
|
||||
</blockquote>`,
|
||||
)
|
||||
}
|
||||
|
||||
+367
-364
@@ -5,384 +5,382 @@ import "core:strings"
|
||||
|
||||
EMOJIS: map[string]string = {
|
||||
// People
|
||||
"smile" = "\xf0\x9f\x98\x84",
|
||||
"laughing" = "\xf0\x9f\x98\x86",
|
||||
"blush" = "\xf0\x9f\x98\x8a",
|
||||
"smiley" = "\xf0\x9f\x98\x83",
|
||||
"wink" = "\xf0\x9f\x98\x89",
|
||||
"joy" = "\xf0\x9f\xa4\xa3",
|
||||
"rofl" = "\xf0\x9f\xa4\xa3",
|
||||
"relaxed" = "\xe2\x98\xba\xef\xb8\x8f",
|
||||
"thinking" = "\xf0\x9f\xa4\x94",
|
||||
"neutral_face" = "\xf0\x9f\x98\x90",
|
||||
"expressionless" = "\xf0\x9f\x98\x91",
|
||||
"no_mouth" = "\xf0\x9f\x98\xb6",
|
||||
"rolling_eyes" = "\xf0\x9f\x99\x84",
|
||||
"smirk" = "\xf0\x9f\x98\x8f",
|
||||
"persevere" = "\xf0\x9f\x98\xa3",
|
||||
"disappointed_relieved" = "\xf0\x9f\x98\xa5",
|
||||
"open_mouth" = "\xf0\x9f\x98\xae",
|
||||
"zipper_mouth_face" = "\xf0\x9f\xa4\x90",
|
||||
"hushed" = "\xf0\x9f\x98\xaf",
|
||||
"sleepy" = "\xf0\x9f\x98\xaa",
|
||||
"tired_face" = "\xf0\x9f\x98\xab",
|
||||
"sleeping" = "\xf0\x9f\x98\xb4",
|
||||
"relieved" = "\xf0\x9f\x98\x8c",
|
||||
"stuck_out_tongue" = "\xf0\x9f\x98\x9b",
|
||||
"stuck_out_tongue_winking_eye" = "\xf0\x9f\x98\x9c",
|
||||
"stuck_out_tongue_closed_eyes" = "\xf0\x9f\x98\x9d",
|
||||
"drooling_face" = "\xf0\x9f\xa4\xa4",
|
||||
"unamused" = "\xf0\x9f\x98\x92",
|
||||
"sweat" = "\xf0\x9f\x98\x85",
|
||||
"pensive" = "\xf0\x9f\x98\x94",
|
||||
"confused" = "\xf0\x9f\x98\x95",
|
||||
"upside_down_face" = "\xf0\x9f\x99\x83",
|
||||
"money_mouth_face" = "\xf0\x9f\xa4\x91",
|
||||
"astonished" = "\xf0\x9f\x98\xb2",
|
||||
"white_frowning_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
||||
"slightly_sad_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
||||
"confounded" = "\xf0\x9f\x98\x96",
|
||||
"disappointed" = "\xf0\x9f\x98\x9e",
|
||||
"worried" = "\xf0\x9f\x98\x9f",
|
||||
"triumph" = "\xf0\x9f\x98\xa4",
|
||||
"cry" = "\xf0\x9f\x98\xa2",
|
||||
"sob" = "\xf0\x9f\x98\xad",
|
||||
"frowning" = "\xf0\x9f\x98\xa6",
|
||||
"frowning_face" = "\xf0\x9f\x99\x81",
|
||||
"anguished" = "\xf0\x9f\x98\xa7",
|
||||
"fearful" = "\xf0\x9f\x98\xa8",
|
||||
"weary" = "\xf0\x9f\x98\xa9",
|
||||
"grimacing" = "\xf0\x9f\x98\xac",
|
||||
"cold_sweat" = "\xf0\x9f\x98\xb0",
|
||||
"scream" = "\xf0\x9f\x98\xb1",
|
||||
"flushed" = "\xf0\x9f\x98\xb3",
|
||||
"dizzy_face" = "\xf0\x9f\x98\xb5",
|
||||
"rage" = "\xf0\x9f\x98\xa1",
|
||||
"angry" = "\xf0\x9f\x98\xa0",
|
||||
"innocent" = "\xf0\x9f\x98\x87",
|
||||
"cowboy_hat_face" = "\xf0\x9f\xa4\xa0",
|
||||
"clown_face" = "\xf0\x9f\xa4\xa1",
|
||||
"mask" = "\xf0\x9f\x98\xb7",
|
||||
"thermometer_face" = "\xf0\x9f\xa4\x92",
|
||||
"head_bandage" = "\xf0\x9f\xa4\x95",
|
||||
"nauseated_face" = "\xf0\x9f\xa4\xa2",
|
||||
"sneezing_face" = "\xf0\x9f\xa4\xa7",
|
||||
"smiling_imp" = "\xf0\x9f\x98\x88",
|
||||
"imp" = "\xf0\x9f\x91\xbf",
|
||||
"shrug" = "\xc2\xaf\\_(\xe3\x83\x84)_/\xc2\xaf",
|
||||
"facepalm" = "\xf0\x9f\xa4\xa6",
|
||||
"facepunch" = "\xf0\x9f\x91\x8a",
|
||||
"wave" = "\xf0\x9f\x91\x8b",
|
||||
"ok_hand" = "\xf0\x9f\x91\x8c",
|
||||
"thumbsup" = "\xf0\x9f\x91\x8d",
|
||||
"thumbsdown" = "\xf0\x9f\x91\x8e",
|
||||
"clap" = "\xf0\x9f\x91\x8f",
|
||||
"pray" = "\xf0\x9f\x99\x8f",
|
||||
"point_up" = "\xe2\x98\x9d\xef\xb8\x8f",
|
||||
"point_down" = "\xf0\x9f\x91\x87",
|
||||
"point_left" = "\xf0\x9f\x91\x88",
|
||||
"point_right" = "\xf0\x9f\x89\x89",
|
||||
"v" = "\xe2\x9c\x8c\xef\xb8\x8f",
|
||||
"raised_hands" = "\xf0\x9f\x99\x8c",
|
||||
"muscle" = "\xf0\x9f\x92\xaa",
|
||||
"fist" = "\xe2\x9c\x8a",
|
||||
"hand" = "\xe2\x9c\x8b",
|
||||
"smile" = "😄",
|
||||
"laughing" = "😆",
|
||||
"blush" = "😊",
|
||||
"smiley" = "😃",
|
||||
"wink" = "😉",
|
||||
"joy" = "😂",
|
||||
"rofl" = "🤣",
|
||||
"relaxed" = "☺️",
|
||||
"thinking" = "🤔",
|
||||
"neutral_face" = "😐",
|
||||
"expressionless" = "😑",
|
||||
"no_mouth" = "😶",
|
||||
"rolling_eyes" = "🙄",
|
||||
"smirk" = "😏",
|
||||
"persevere" = "😣",
|
||||
"disappointed_relieved" = "😥",
|
||||
"open_mouth" = "😮",
|
||||
"zipper_mouth_face" = "🤐",
|
||||
"hushed" = "😯",
|
||||
"sleepy" = "😪",
|
||||
"tired_face" = "😫",
|
||||
"sleeping" = "😴",
|
||||
"relieved" = "😌",
|
||||
"stuck_out_tongue" = "😛",
|
||||
"stuck_out_tongue_winking_eye" = "😜",
|
||||
"stuck_out_tongue_closed_eyes" = "😝",
|
||||
"drooling_face" = "🤤",
|
||||
"unamused" = "😒",
|
||||
"sweat" = "😅",
|
||||
"pensive" = "😔",
|
||||
"confused" = "😕",
|
||||
"upside_down_face" = "🙃",
|
||||
"money_mouth_face" = "🤑",
|
||||
"astonished" = "😲",
|
||||
"white_frowning_face" = "☹️",
|
||||
"slightly_sad_face" = "☹️",
|
||||
"confounded" = "😖",
|
||||
"disappointed" = "😞",
|
||||
"worried" = "😟",
|
||||
"triumph" = "😤",
|
||||
"cry" = "😢",
|
||||
"sob" = "😭",
|
||||
"frowning" = "😦",
|
||||
"frowning_face" = "🙁",
|
||||
"anguished" = "😧",
|
||||
"fearful" = "😨",
|
||||
"weary" = "😩",
|
||||
"grimacing" = "😬",
|
||||
"cold_sweat" = "😰",
|
||||
"scream" = "😱",
|
||||
"flushed" = "😳",
|
||||
"dizzy_face" = "😵",
|
||||
"rage" = "😡",
|
||||
"angry" = "😠",
|
||||
"innocent" = "😇",
|
||||
"cowboy_hat_face" = "🤠",
|
||||
"clown_face" = "🤡",
|
||||
"mask" = "😷",
|
||||
"thermometer_face" = "🤒",
|
||||
"head_bandage" = "🤕",
|
||||
"nauseated_face" = "🤢",
|
||||
"sneezing_face" = "🤧",
|
||||
"smiling_imp" = "😈",
|
||||
"imp" = "👿",
|
||||
"shrug" = "¯\\_(ツ)_/¯",
|
||||
"facepalm" = "🤦",
|
||||
"facepunch" = "👊",
|
||||
"wave" = "👋",
|
||||
"ok_hand" = "👌",
|
||||
"thumbsup" = "👍",
|
||||
"thumbsdown" = "👎",
|
||||
"clap" = "👏",
|
||||
"pray" = "🙏",
|
||||
"point_up" = "☝️",
|
||||
"point_down" = "👇",
|
||||
"point_left" = "👈",
|
||||
"point_right" = "👉",
|
||||
"v" = "✌️",
|
||||
"raised_hands" = "🙌",
|
||||
"muscle" = "💪",
|
||||
"fist" = "✊",
|
||||
"hand" = "✋",
|
||||
|
||||
// Hearts & symbols
|
||||
"heart" = "\xe2\x9d\xa4\xef\xb8\x8f",
|
||||
"orange_heart" = "\xf0\x9f\xa7\xa1",
|
||||
"yellow_heart" = "\xf0\x9f\x92\x9b",
|
||||
"green_heart" = "\xf0\x9f\x92\x9a",
|
||||
"blue_heart" = "\xf0\x9f\x92\x99",
|
||||
"purple_heart" = "\xf0\x9f\x92\x9c",
|
||||
"broken_heart" = "\xf0\x9f\x92\x94",
|
||||
"sparkling_heart" = "\xf0\x9f\x92\x96",
|
||||
"100" = "\xf0\x9f\x92\xaf",
|
||||
"anger" = "\xf0\x9f\x92\xa2",
|
||||
"checkered_flag" = "\xf0\x9f\x8f\x81",
|
||||
"crossed_flags" = "\xf0\x9f\x9a\xa9",
|
||||
"rocket" = "\xf0\x9f\x9a\x80",
|
||||
"star" = "\xe2\xad\x90",
|
||||
"star2" = "\xf0\x9f\x8c\x9f",
|
||||
"sparkles" = "\xe2\x9c\xa8",
|
||||
"boom" = "\xf0\x9f\x92\xa5",
|
||||
"exclamation" = "\xe2\x9d\x97",
|
||||
"question" = "\xe2\x9d\x93",
|
||||
"grey_exclamation" = "\xe2\x9d\x95",
|
||||
"grey_question" = "\xe2\x9d\x94",
|
||||
"zzz" = "\xf0\x9f\x92\xa4",
|
||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
||||
"no_entry_sign" = "\xf0\x9f\x9a\xab",
|
||||
"no_entry" = "\xe2\x9b\x94",
|
||||
"white_check_mark" = "\xe2\x9c\x85",
|
||||
"negative_squared_cross_mark" = "\xe2\x9d\x8e",
|
||||
"x" = "\xe2\x9d\x8c",
|
||||
"o" = "\xe2\xad\x95",
|
||||
"heavy_plus_sign" = "\xe2\x9e\x95",
|
||||
"heavy_minus_sign" = "\xe2\x9e\x96",
|
||||
"heavy_division_sign" = "\xe2\x9e\x97",
|
||||
"copyright" = "\xc2\xa9\xef\xb8\x8f",
|
||||
"registered" = "\xc2\xae\xef\xb8\x8f",
|
||||
"tm" = "\xe2\x84\xa2\xef\xb8\x8f",
|
||||
"heart" = "❤️",
|
||||
"orange_heart" = "🧡",
|
||||
"yellow_heart" = "💛",
|
||||
"green_heart" = "💚",
|
||||
"blue_heart" = "💙",
|
||||
"purple_heart" = "💜",
|
||||
"broken_heart" = "💔",
|
||||
"sparkling_heart" = "💖",
|
||||
"100" = "💯",
|
||||
"anger" = "💢",
|
||||
"checkered_flag" = "🏁",
|
||||
"crossed_flags" = "🚩",
|
||||
"rocket" = "🚀",
|
||||
"star" = "⭐",
|
||||
"star2" = "🌟",
|
||||
"sparkles" = "✨",
|
||||
"boom" = "💥",
|
||||
"exclamation" = "❗",
|
||||
"question" = "❓",
|
||||
"grey_exclamation" = "❕",
|
||||
"grey_question" = "❔",
|
||||
"zzz" = "💤",
|
||||
"warning" = "⚠️",
|
||||
"no_entry_sign" = "🚫",
|
||||
"no_entry" = "⛔",
|
||||
"white_check_mark" = "✅",
|
||||
"negative_squared_cross_mark" = "❎",
|
||||
"x" = "❌",
|
||||
"o" = "⭕",
|
||||
"heavy_plus_sign" = "➕",
|
||||
"heavy_minus_sign" = "➖",
|
||||
"heavy_division_sign" = "➗",
|
||||
"copyright" = "©️",
|
||||
"registered" = "®️",
|
||||
"tm" = "™️",
|
||||
|
||||
// Nature & weather
|
||||
"fire" = "\xf0\x9f\x94\xa5",
|
||||
"zap" = "\xe2\x9a\xa1",
|
||||
"sunny" = "\xe2\x98\x80\xef\xb8\x8f",
|
||||
"cloud" = "\xe2\x98\x81\xef\xb8\x8f",
|
||||
"rainbow" = "\xf0\x9f\x8c\x88",
|
||||
"snowflake" = "\xe2\x9d\x84\xef\xb8\x8f",
|
||||
"dash" = "\xf0\x9f\x92\xa8",
|
||||
"tornado" = "\xf0\x9f\x8c\xaa\xef\xb8\x8f",
|
||||
"deciduous_tree" = "\xf0\x9f\x8c\xb3",
|
||||
"evergreen_tree" = "\xf0\x9f\x8c\xb2",
|
||||
"palm_tree" = "\xf0\x9f\x8c\xb4",
|
||||
"cactus" = "\xf0\x9f\x8c\xb5",
|
||||
"tulip" = "\xf0\x9f\x8c\xb7",
|
||||
"rose" = "\xf0\x9f\x8c\xb9",
|
||||
"sunflower" = "\xf0\x9f\x8c\xbb",
|
||||
"hibiscus" = "\xf0\x9f\x8c\xba",
|
||||
"earth_africa" = "\xf0\x9f\x8c\x8d",
|
||||
"earth_americas" = "\xf0\x9f\x8c\x8e",
|
||||
"earth_asia" = "\xf0\x9f\x8c\x8f",
|
||||
"new_moon" = "\xf0\x9f\x8c\x9a",
|
||||
"full_moon" = "\xf0\x9f\x8c\x95",
|
||||
"fire" = "🔥",
|
||||
"zap" = "⚡",
|
||||
"sunny" = "☀️",
|
||||
"cloud" = "☁️",
|
||||
"rainbow" = "🌈",
|
||||
"snowflake" = "❄️",
|
||||
"dash" = "💨",
|
||||
"tornado" = "🌪️",
|
||||
"deciduous_tree" = "🌳",
|
||||
"evergreen_tree" = "🌲",
|
||||
"palm_tree" = "🌴",
|
||||
"cactus" = "🌵",
|
||||
"tulip" = "🌷",
|
||||
"rose" = "🌹",
|
||||
"sunflower" = "🌻",
|
||||
"hibiscus" = "🌺",
|
||||
"earth_africa" = "🌍",
|
||||
"earth_americas" = "🌎",
|
||||
"earth_asia" = "🌏",
|
||||
"new_moon" = "🌑",
|
||||
"full_moon" = "🌕",
|
||||
|
||||
// Food & drink
|
||||
"coffee" = "\xe2\x98\x95",
|
||||
"tea" = "\xf0\x9f\x8d\xb5",
|
||||
"beer" = "\xf0\x9f\x8d\xba",
|
||||
"beers" = "\xf0\x9f\x8d\xbb",
|
||||
"wine_glass" = "\xf0\x9f\x8d\xb7",
|
||||
"tropical_drink" = "\xf0\x9f\x8d\xb9",
|
||||
"apple" = "\xf0\x9f\x8d\x8e",
|
||||
"green_apple" = "\xf0\x9f\x8d\x8f",
|
||||
"orange" = "\xf0\x9f\x8d\x8a",
|
||||
"lemon" = "\xf0\x9f\x8d\x8b",
|
||||
"banana" = "\xf0\x9f\x8d\x8c",
|
||||
"watermelon" = "\xf0\x9f\x8d\x89",
|
||||
"grapes" = "\xf0\x9f\x8d\x87",
|
||||
"strawberry" = "\xf0\x9f\x8d\x93",
|
||||
"melon" = "\xf0\x9f\x8d\x88",
|
||||
"cherries" = "\xf0\x9f\x8d\x92",
|
||||
"peach" = "\xf0\x9f\x8d\x91",
|
||||
"pineapple" = "\xf0\x9f\x8d\x8d",
|
||||
"pizza" = "\xf0\x9f\x8d\x95",
|
||||
"hamburger" = "\xf0\x9f\x8d\x94",
|
||||
"hotdog" = "\xf0\x9f\x8c\xad",
|
||||
"taco" = "\xf0\x9f\x8c\xae",
|
||||
"burrito" = "\xf0\x9f\x8c\xaf",
|
||||
"cake" = "\xf0\x9f\x8d\xb0",
|
||||
"cookie" = "\xf0\x9f\x8d\xaa",
|
||||
"chocolate_bar" = "\xf0\x9f\x8d\xab",
|
||||
"candy" = "\xf0\x9f\x8d\xac",
|
||||
"popcorn" = "\xf0\x9f\x8d\xbf",
|
||||
"coffee" = "☕",
|
||||
"tea" = "🍵",
|
||||
"beer" = "🍺",
|
||||
"beers" = "🍻",
|
||||
"wine_glass" = "🍷",
|
||||
"tropical_drink" = "🍹",
|
||||
"apple" = "🍎",
|
||||
"green_apple" = "🍏",
|
||||
"orange" = "🍊",
|
||||
"lemon" = "🍋",
|
||||
"banana" = "🍌",
|
||||
"watermelon" = "🍉",
|
||||
"grapes" = "🍇",
|
||||
"strawberry" = "🍓",
|
||||
"melon" = "🍈",
|
||||
"cherries" = "🍒",
|
||||
"peach" = "🍑",
|
||||
"pineapple" = "🍍",
|
||||
"pizza" = "🍕",
|
||||
"hamburger" = "🍔",
|
||||
"hotdog" = "🌭",
|
||||
"taco" = "🌮",
|
||||
"burrito" = "🌯",
|
||||
"cake" = "🍰",
|
||||
"cookie" = "🍪",
|
||||
"chocolate_bar" = "🍫",
|
||||
"candy" = "🍬",
|
||||
"popcorn" = "🍿",
|
||||
|
||||
// Animals
|
||||
"dog" = "\xf0\x9f\x90\xb6",
|
||||
"cat" = "\xf0\x9f\x90\xb1",
|
||||
"mouse" = "\xf0\x9f\x90\xad",
|
||||
"hamster" = "\xf0\x9f\x90\xb9",
|
||||
"rabbit" = "\xf0\x9f\x90\xb0",
|
||||
"bear" = "\xf0\x9f\x90\xbb",
|
||||
"panda_face" = "\xf0\x9f\x90\xbc",
|
||||
"koala" = "\xf0\x9f\x90\xa8",
|
||||
"tiger" = "\xf0\x9f\x90\xaf",
|
||||
"lion_face" = "\xf0\x9f\xa6\x81",
|
||||
"cow" = "\xf0\x9f\x90\xae",
|
||||
"pig" = "\xf0\x9f\x90\xb7",
|
||||
"frog" = "\xf0\x9f\x90\xb8",
|
||||
"monkey_face" = "\xf0\x9f\x90\xb5",
|
||||
"see_no_evil" = "\xf0\x9f\x99\x88",
|
||||
"hear_no_evil" = "\xf0\x9f\x99\x89",
|
||||
"speak_no_evil" = "\xf0\x9f\x99\x8a",
|
||||
"owl" = "\xf0\x9f\xa6\x89",
|
||||
"bat" = "\xf0\x9f\xa6\x87",
|
||||
"wolf" = "\xf0\x9f\x90\xba",
|
||||
"boar" = "\xf0\x9f\x90\x97",
|
||||
"horse" = "\xf0\x9f\x90\x8e",
|
||||
"unicorn" = "\xf0\x9f\xa6\x84",
|
||||
"honeybee" = "\xf0\x9f\x90\x9d",
|
||||
"bug" = "\xf0\x9f\x90\x9b",
|
||||
"snail" = "\xf0\x9f\x90\x8c",
|
||||
"beetle" = "\xf0\x9f\x90\x9e",
|
||||
"ant" = "\xf0\x9f\x90\x9c",
|
||||
"spider" = "\xf0\x9f\x95\xb7",
|
||||
"scorpion" = "\xf0\x9f\xa6\x82",
|
||||
"turtle" = "\xf0\x9f\x90\xa2",
|
||||
"snake" = "\xf0\x9f\x90\x8d",
|
||||
"octopus" = "\xf0\x9f\x90\x99",
|
||||
"shell" = "\xf0\x9f\x90\x9a",
|
||||
"whale" = "\xf0\x9f\x90\xb3",
|
||||
"dolphin" = "\xf0\x9f\x90\xac",
|
||||
"fish" = "\xf0\x9f\x90\x9f",
|
||||
"tropical_fish" = "\xf0\x9f\x90\xa0",
|
||||
"blowfish" = "\xf0\x9f\x90\xa1",
|
||||
"penguin" = "\xf0\x9f\x90\xa7",
|
||||
"chicken" = "\xf0\x9f\x90\x94",
|
||||
"bird" = "\xf0\x9f\x90\xa6",
|
||||
"eagle" = "\xf0\x9f\xa6\x85",
|
||||
"dog" = "🐶",
|
||||
"cat" = "🐱",
|
||||
"mouse" = "🐭",
|
||||
"hamster" = "🐹",
|
||||
"rabbit" = "🐰",
|
||||
"bear" = "🐻",
|
||||
"panda_face" = "🐼",
|
||||
"koala" = "🐨",
|
||||
"tiger" = "🐯",
|
||||
"lion_face" = "🦁",
|
||||
"cow" = "🐮",
|
||||
"pig" = "🐷",
|
||||
"frog" = "🐸",
|
||||
"monkey_face" = "🐵",
|
||||
"see_no_evil" = "🙈",
|
||||
"hear_no_evil" = "🙉",
|
||||
"speak_no_evil" = "🙊",
|
||||
"owl" = "🦉",
|
||||
"bat" = "🦇",
|
||||
"wolf" = "🐺",
|
||||
"boar" = "🐗",
|
||||
"horse" = "🐴",
|
||||
"unicorn" = "🦄",
|
||||
"honeybee" = "🐝",
|
||||
"bug" = "🐛",
|
||||
"snail" = "🐌",
|
||||
"beetle" = "🐞",
|
||||
"ant" = "🐜",
|
||||
"spider" = "🕷",
|
||||
"scorpion" = "🦂",
|
||||
"turtle" = "🐢",
|
||||
"snake" = "🐍",
|
||||
"octopus" = "🐙",
|
||||
"shell" = "🐚",
|
||||
"whale" = "🐳",
|
||||
"dolphin" = "🐬",
|
||||
"fish" = "🐟",
|
||||
"tropical_fish" = "🐠",
|
||||
"blowfish" = "🐡",
|
||||
"penguin" = "🐧",
|
||||
"chicken" = "🐔",
|
||||
"bird" = "🐦",
|
||||
"eagle" = "🦅",
|
||||
|
||||
// Objects
|
||||
"computer" = "\xf0\x9f\x92\xbb",
|
||||
"iphone" = "\xf0\x9f\x93\xb1",
|
||||
"keyboard" = "\xe2\x8c\xa8\xef\xb8\x8f",
|
||||
"desktop_computer" = "\xf0\x9f\x96\xa5",
|
||||
"printer" = "\xf0\x9f\x96\xa8",
|
||||
"mouse_three_button" = "\xf0\x9f\x96\xb1",
|
||||
"joystick" = "\xf0\x9f\x95\xb9",
|
||||
"stash" = "\xf0\x9f\x92\xbe",
|
||||
"floppy_disk" = "\xf0\x9f\x92\xbe",
|
||||
"cd" = "\xf0\x9f\x92\xbf",
|
||||
"dvd" = "\xf0\x9f\x93\x80",
|
||||
"vhs" = "\xf0\x9f\x93\xbc",
|
||||
"camera" = "\xf0\x9f\x93\xb7",
|
||||
"video_camera" = "\xf0\x9f\x93\xb9",
|
||||
"tv" = "\xf0\x9f\x93\xba",
|
||||
"radio" = "\xf0\x9f\x93\xbb",
|
||||
"pager" = "\xf0\x9f\x93\x9f",
|
||||
"telephone" = "\xe2\x98\x8e\xef\xb8\x8f",
|
||||
"fax" = "\xf0\x9f\x93\xa0",
|
||||
"bulb" = "\xf0\x9f\x92\xa1",
|
||||
"candle" = "\xf0\x9f\x95\xaf",
|
||||
"bathtub" = "\xf0\x9f\x9b\x81",
|
||||
"shower" = "\xf0\x9f\x9a\xbf",
|
||||
"toilet" = "\xf0\x9f\x9a\xbd",
|
||||
"wrench" = "\xf0\x9f\x94\xa7",
|
||||
"hammer" = "\xf0\x9f\x94\xa8",
|
||||
"nut_and_bolt" = "\xf0\x9f\x94\xa9",
|
||||
"gear" = "\xe2\x9a\x99\xef\xb8\x8f",
|
||||
"link" = "\xf0\x9f\x94\x97",
|
||||
"lock" = "\xf0\x9f\x94\x92",
|
||||
"unlock" = "\xf0\x9f\x94\x93",
|
||||
"key" = "\xf0\x9f\x94\x91",
|
||||
"bell" = "\xf0\x9f\x94\x94",
|
||||
"bookmark" = "\xf0\x9f\x94\x96",
|
||||
"pushpin" = "\xf0\x9f\x93\x8c",
|
||||
"paperclip" = "\xf0\x9f\x93\x8e",
|
||||
"memo" = "\xf0\x9f\x93\x9d",
|
||||
"pencil2" = "\xe2\x9c\x8f\xef\xb8\x8f",
|
||||
"black_nib" = "\xe2\x9c\x92\xef\xb8\x8f",
|
||||
"pen" = "\xf0\x9f\x96\x8a",
|
||||
"paintbrush" = "\xf0\x9f\x96\x8c",
|
||||
"crayon" = "\xf0\x9f\x96\x8d",
|
||||
"book" = "\xf0\x9f\x93\x97",
|
||||
"books" = "\xf0\x9f\x93\x9a",
|
||||
"ledger" = "\xf0\x9f\x93\x92",
|
||||
"notebook" = "\xf0\x9f\x93\x93",
|
||||
"scroll" = "\xf0\x9f\x93\x9c",
|
||||
"page_with_curl" = "\xf0\x9f\x93\x84",
|
||||
"newspaper" = "\xf0\x9f\x93\xb0",
|
||||
"chart_with_upwards_trend" = "\xf0\x9f\x93\x88",
|
||||
"chart_with_downwards_trend" = "\xf0\x9f\x93\x89",
|
||||
"bar_chart" = "\xf0\x9f\x93\x8a",
|
||||
"calendar" = "\xf0\x9f\x93\x85",
|
||||
"date" = "\xf0\x9f\x93\x85",
|
||||
"hourglass" = "\xe2\x8f\xb3",
|
||||
"hourglass_flowing_sand" = "\xe2\x8f\xb3",
|
||||
"clock" = "\xf0\x9f\x95\x90",
|
||||
"alarm_clock" = "\xe2\x8f\xb0",
|
||||
"stopwatch" = "\xe2\x8f\xb1",
|
||||
"watch" = "\xe2\x8c\x9a",
|
||||
"moneybag" = "\xf0\x9f\x92\xb0",
|
||||
"yen" = "\xf0\x9f\x92\xb4",
|
||||
"dollar" = "\xf0\x9f\x92\xb5",
|
||||
"euro" = "\xf0\x9f\x92\xb6",
|
||||
"pound" = "\xf0\x9f\x92\xb7",
|
||||
"money_with_wings" = "\xf0\x9f\x92\xb8",
|
||||
"credit_card" = "\xf0\x9f\x92\xb3",
|
||||
"gem" = "\xf0\x9f\x92\x8e",
|
||||
"bomb" = "\xf0\x9f\x92\xa3",
|
||||
"gift" = "\xf0\x9f\x8e\x81",
|
||||
"balloon" = "\xf0\x9f\x8e\x88",
|
||||
"tada" = "\xf0\x9f\x8e\x89",
|
||||
"confetti_ball" = "\xf0\x9f\x8e\x8a",
|
||||
"package" = "\xf0\x9f\x93\xa6",
|
||||
"mailbox" = "\xf0\x9f\x93\xab",
|
||||
"inbox_tray" = "\xf0\x9f\x93\xa5",
|
||||
"outbox_tray" = "\xf0\x9f\x93\xa4",
|
||||
"email" = "\xe2\x9c\x89\xef\xb8\x8f",
|
||||
"envelope" = "\xe2\x9c\x89\xef\xb8\x8f",
|
||||
"incoming_envelope" = "\xf0\x9f\x93\xa8",
|
||||
"computer" = "💻",
|
||||
"iphone" = "📱",
|
||||
"keyboard" = "⌨️",
|
||||
"desktop_computer" = "🖥",
|
||||
"printer" = "🖨",
|
||||
"mouse_three_button" = "🖱",
|
||||
"joystick" = "🕹",
|
||||
"stash" = "💾",
|
||||
"floppy_disk" = "💾",
|
||||
"cd" = "💿",
|
||||
"dvd" = "📀",
|
||||
"vhs" = "📼",
|
||||
"camera" = "📷",
|
||||
"video_camera" = "📹",
|
||||
"tv" = "📺",
|
||||
"radio" = "📻",
|
||||
"pager" = "📟",
|
||||
"telephone" = "☎️",
|
||||
"fax" = "📠",
|
||||
"bulb" = "💡",
|
||||
"candle" = "🕯",
|
||||
"bathtub" = "🛁",
|
||||
"shower" = "🚿",
|
||||
"toilet" = "🚽",
|
||||
"wrench" = "🔧",
|
||||
"hammer" = "🔨",
|
||||
"nut_and_bolt" = "🔩",
|
||||
"gear" = "⚙️",
|
||||
"link" = "🔗",
|
||||
"lock" = "🔒",
|
||||
"unlock" = "🔓",
|
||||
"key" = "🔑",
|
||||
"bell" = "🔔",
|
||||
"bookmark" = "🔖",
|
||||
"pushpin" = "📌",
|
||||
"paperclip" = "📎",
|
||||
"memo" = "📝",
|
||||
"pencil2" = "✏️",
|
||||
"black_nib" = "✒️",
|
||||
"pen" = "🖊",
|
||||
"paintbrush" = "🖌",
|
||||
"crayon" = "🖍",
|
||||
"book" = "📖",
|
||||
"books" = "📚",
|
||||
"ledger" = "📒",
|
||||
"notebook" = "📓",
|
||||
"scroll" = "📜",
|
||||
"page_with_curl" = "📄",
|
||||
"newspaper" = "📰",
|
||||
"chart_with_upwards_trend" = "📈",
|
||||
"chart_with_downwards_trend" = "📉",
|
||||
"bar_chart" = "📊",
|
||||
"calendar" = "📅",
|
||||
"date" = "📅",
|
||||
"hourglass" = "⌛",
|
||||
"hourglass_flowing_sand" = "⏳",
|
||||
"clock" = "🕐",
|
||||
"alarm_clock" = "⏰",
|
||||
"stopwatch" = "⏱",
|
||||
"watch" = "⌚",
|
||||
"moneybag" = "💰",
|
||||
"yen" = "💴",
|
||||
"dollar" = "💵",
|
||||
"euro" = "💶",
|
||||
"pound" = "💷",
|
||||
"money_with_wings" = "💸",
|
||||
"credit_card" = "💳",
|
||||
"gem" = "💎",
|
||||
"bomb" = "💣",
|
||||
"gift" = "🎁",
|
||||
"balloon" = "🎈",
|
||||
"tada" = "🎉",
|
||||
"confetti_ball" = "🎊",
|
||||
"package" = "📦",
|
||||
"mailbox" = "📫",
|
||||
"inbox_tray" = "📥",
|
||||
"outbox_tray" = "📤",
|
||||
"email" = "✉️",
|
||||
"envelope" = "✉️",
|
||||
"incoming_envelope" = "📨",
|
||||
|
||||
// Activities
|
||||
"soccer" = "\xe2\x9a\xbd",
|
||||
"basketball" = "\xf0\x9f\x8f\x80",
|
||||
"football" = "\xf0\x9f\x8f\x88",
|
||||
"baseball" = "\xe2\x9a\xbe",
|
||||
"tennis" = "\xf0\x9f\x8e\xbe",
|
||||
"8ball" = "\xf0\x9f\x8e\xb1",
|
||||
"bowling" = "\xf0\x9f\x8e\xb3",
|
||||
"video_game" = "\xf0\x9f\x8e\xae",
|
||||
"dart" = "\xf0\x9f\x8e\xaf",
|
||||
"game_die" = "\xf0\x9f\x8e\xb2",
|
||||
"slot_machine" = "\xf0\x9f\x8e\xb0",
|
||||
"cards" = "\xf0\x9f\x83\x8f",
|
||||
"black_joker" = "\xf0\x9f\x83\x9f",
|
||||
"mahjong" = "\xf0\x9f\x80\x84",
|
||||
"musical_note" = "\xf0\x9f\x8e\xb5",
|
||||
"notes" = "\xf0\x9f\x8e\xb6",
|
||||
"saxophone" = "\xf0\x9f\x8e\xb7",
|
||||
"guitar" = "\xf0\x9f\x8e\xb8",
|
||||
"musical_keyboard" = "\xf0\x9f\x8e\xb9",
|
||||
"trumpet" = "\xf0\x9f\x8e\xba",
|
||||
"violin" = "\xf0\x9f\x8e\xbb",
|
||||
"drum" = "\xf0\x9f\xa5\x81",
|
||||
"headphones" = "\xf0\x9f\x8e\xa7",
|
||||
"microphone" = "\xf0\x9f\x8e\xa4",
|
||||
"level_slider" = "\xf0\x9f\x8e\x9a",
|
||||
"control_knobs" = "\xf0\x9f\x8e\x9b",
|
||||
"ticket" = "\xf0\x9f\x8e\xab",
|
||||
"art" = "\xf0\x9f\x8e\xa8",
|
||||
"circus_tent" = "\xf0\x9f\x8e\xaa",
|
||||
"theater_masks" = "\xf0\x9f\x8e\xad",
|
||||
"clapper" = "\xf0\x9f\x8e\xac",
|
||||
"soccer" = "⚽",
|
||||
"basketball" = "🏀",
|
||||
"football" = "🏈",
|
||||
"baseball" = "⚾",
|
||||
"tennis" = "🎾",
|
||||
"8ball" = "🎱",
|
||||
"bowling" = "🎳",
|
||||
"video_game" = "🎮",
|
||||
"dart" = "🎯",
|
||||
"game_die" = "🎲",
|
||||
"slot_machine" = "🎰",
|
||||
"cards" = "🃏",
|
||||
"black_joker" = "🃟",
|
||||
"mahjong" = "🀄",
|
||||
"musical_note" = "🎵",
|
||||
"notes" = "🎶",
|
||||
"saxophone" = "🎷",
|
||||
"guitar" = "🎸",
|
||||
"musical_keyboard" = "🎹",
|
||||
"trumpet" = "🎺",
|
||||
"violin" = "🎻",
|
||||
"drum" = "🥁",
|
||||
"headphones" = "🎧",
|
||||
"microphone" = "🎤",
|
||||
"level_slider" = "🎚",
|
||||
"control_knobs" = "🎛",
|
||||
"ticket" = "🎫",
|
||||
"art" = "🎨",
|
||||
"circus_tent" = "🎪",
|
||||
"theater_masks" = "🎭",
|
||||
"clapper" = "🎬",
|
||||
|
||||
// Travel
|
||||
"car" = "\xf0\x9f\x9a\x97",
|
||||
"taxi" = "\xf0\x9f\x9a\x95",
|
||||
"bus" = "\xf0\x9f\x9a\x8d",
|
||||
"train" = "\xf0\x9f\x9a\x86",
|
||||
"metro" = "\xf0\x9f\x9a\x87",
|
||||
"light_rail" = "\xf0\x9f\x9a\x88",
|
||||
"tram" = "\xf0\x9f\x9a\x8a",
|
||||
"bike" = "\xf0\x9f\x9a\xb2",
|
||||
"motorcycle" = "\xf0\x9f\x8f\x8d",
|
||||
"airplane" = "\xe2\x9c\x88\xef\xb8\x8f",
|
||||
"helicopter" = "\xf0\x9f\x9a\x81",
|
||||
"boat" = "\xe2\x9b\xb5",
|
||||
"sailboat" = "\xe2\x9b\xb5",
|
||||
"ship" = "\xf0\x9f\x9a\xa2",
|
||||
"fuelpump" = "\xe2\x9b\xbd",
|
||||
"construction" = "\xf0\x9f\x9a\xa7",
|
||||
"house" = "\xf0\x9f\x8f\xa0",
|
||||
"house_with_garden" = "\xf0\x9f\x8f\xa1",
|
||||
"office" = "\xf0\x9f\x8f\xa2",
|
||||
"post_office" = "\xf0\x9f\x8f\xa3",
|
||||
"hospital" = "\xf0\x9f\x8f\xa5",
|
||||
"bank" = "\xf0\x9f\x8f\xa6",
|
||||
"hotel" = "\xf0\x9f\x8f\xa8",
|
||||
"school" = "\xf0\x9f\x8f\xab",
|
||||
"department_store" = "\xf0\x9f\x8f\xac",
|
||||
"church" = "\xe2\x9b\xaa",
|
||||
"castle" = "\xf0\x9f\x8f\xb0",
|
||||
"factory" = "\xf0\x9f\x8f\xad",
|
||||
"tokyo_tower" = "\xf0\x9f\x97\xbc",
|
||||
"statue_of_liberty" = "\xf0\x9f\x97\xbd",
|
||||
"fountain" = "\xe2\x9b\xb2",
|
||||
"tent" = "\xe2\x9b\xba",
|
||||
"mountain" = "\xf0\x9f\x8f\x94",
|
||||
"snow_capped_mountain" = "\xf0\x9f\x8f\x94",
|
||||
"beach" = "\xf0\x9f\x8f\x96",
|
||||
"camping" = "\xf0\x9f\x8f\x95",
|
||||
"world_map" = "\xf0\x9f\x97\xba",
|
||||
"japan" = "\xf0\x9f\x97\xbe",
|
||||
};
|
||||
"car" = "🚗",
|
||||
"taxi" = "🚕",
|
||||
"bus" = "🚍",
|
||||
"train" = "🚆",
|
||||
"metro" = "🚇",
|
||||
"light_rail" = "🚈",
|
||||
"tram" = "🚊",
|
||||
"bike" = "🚲",
|
||||
"motorcycle" = "🏍",
|
||||
"airplane" = "✈️",
|
||||
"helicopter" = "🚁",
|
||||
"boat" = "⛵",
|
||||
"sailboat" = "⛵",
|
||||
"ship" = "🚢",
|
||||
"fuelpump" = "⛽",
|
||||
"construction" = "🚧",
|
||||
"house" = "🏠",
|
||||
"house_with_garden" = "🏡",
|
||||
"office" = "🏢",
|
||||
"post_office" = "🏣",
|
||||
"hospital" = "🏥",
|
||||
"bank" = "🏦",
|
||||
"hotel" = "🏨",
|
||||
"school" = "🏫",
|
||||
"department_store" = "🏬",
|
||||
"church" = "⛪",
|
||||
"castle" = "🏰",
|
||||
"factory" = "🏭",
|
||||
"tokyo_tower" = "🗼",
|
||||
"statue_of_liberty" = "🗽",
|
||||
"fountain" = "⛲",
|
||||
"tent" = "⛺",
|
||||
"mountain" = "⛰️",
|
||||
"snow_capped_mountain" = "🏔",
|
||||
"beach" = "🏖",
|
||||
"camping" = "🏕",
|
||||
"world_map" = "🗺",
|
||||
"japan" = "🗾",
|
||||
}
|
||||
|
||||
expand_emoji :: proc(text: string) -> string {
|
||||
if len(EMOJIS) == 0 {
|
||||
return text
|
||||
}
|
||||
assert(len(EMOJIS) > 0)
|
||||
|
||||
sb := strings.builder_make()
|
||||
defer strings.builder_destroy(&sb)
|
||||
@@ -403,11 +401,15 @@ expand_emoji :: proc(text: string) -> string {
|
||||
break
|
||||
}
|
||||
|
||||
shortcode := remaining[colon + 1 : colon + 1 + end]
|
||||
shortcode := remaining[colon + 1:colon + 1 + end]
|
||||
|
||||
valid := true
|
||||
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 != '-' {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
@@ -432,3 +434,4 @@ expand_emoji :: proc(text: string) -> string {
|
||||
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#+test
|
||||
package markdown
|
||||
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_basic_emoji_expansion_works :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, expand_emoji(":smile:"), "😄")
|
||||
testing.expect_value(t, expand_emoji(":smile: :heart:"), "😄 ❤️")
|
||||
testing.expect_value(t, expand_emoji(":smile::heart:"), "😄❤️")
|
||||
testing.expect_value(t, expand_emoji("Hello :wave:!"), "Hello 👋!")
|
||||
testing.expect_value(t, expand_emoji(":smile: rest"), "😄 rest")
|
||||
testing.expect_value(t, expand_emoji("rest :smile:"), "rest 😄")
|
||||
testing.expect_value(t, expand_emoji(":100:"), "💯")
|
||||
testing.expect_value(t, expand_emoji(":stuck_out_tongue:"), "😛")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_emoji_skips_non_matches :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, expand_emoji("Hello world"), "Hello world")
|
||||
testing.expect_value(t, expand_emoji("Hello:"), "Hello:")
|
||||
testing.expect_value(t, expand_emoji(":notreal:"), ":notreal:")
|
||||
testing.expect_value(t, expand_emoji("http://example.com"), "http://example.com")
|
||||
testing.expect_value(t, expand_emoji(""), "")
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_emoji_skips_invalid_shortcodes :: proc(t: ^testing.T) {
|
||||
testing.expect_value(t, expand_emoji("::"), "::")
|
||||
testing.expect_value(t, expand_emoji(":Smile:"), ":Smile:")
|
||||
testing.expect_value(t, expand_emoji(": not real :"), ": not real :")
|
||||
}
|
||||
|
||||
@@ -37,8 +37,10 @@ wrap_sections :: proc(html: string) -> string {
|
||||
found = true
|
||||
}
|
||||
|
||||
if !found {
|
||||
if found {
|
||||
return strings.to_string(sb)
|
||||
} else {
|
||||
return html
|
||||
}
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
#+test
|
||||
package markdown
|
||||
|
||||
import "core:testing"
|
||||
|
||||
@(test)
|
||||
test_wrap_sections_works :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<p>intro</p><h2>Title</h2><p>body</p>"),
|
||||
"<section><p>intro</p></section><section><h2>Title</h2><p>body</p></section>",
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<p>a</p><h2>A</h2><p>b</p><h2>B</h2><p>c</p>"),
|
||||
"<section><p>a</p></section><section><h2>A</h2><p>b</p></section><section><h2>B</h2><p>c</p></section>",
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<h2>Title</h2><p>body</p>"),
|
||||
"<section><h2>Title</h2><p>body</p></section>",
|
||||
)
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<p>intro</p><h2>Title</h2>"),
|
||||
"<section><p>intro</p></section><section><h2>Title</h2></section>",
|
||||
)
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_wrap_sections_doesnt_split_content :: proc(t: ^testing.T) {
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<p>just text</p>"),
|
||||
"<section><p>just text</p></section>",
|
||||
)
|
||||
|
||||
testing.expect_value(t, wrap_sections(""), "")
|
||||
|
||||
testing.expect_value(
|
||||
t,
|
||||
wrap_sections("<h1>Big</h1><h3>Small</h3>"),
|
||||
"<section><h1>Big</h1><h3>Small</h3></section>",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -155,6 +155,32 @@ is_truthy :: proc(a: any) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
call_interp_lambda :: proc(val: any) -> (result: string, ok: bool) {
|
||||
switch v in val {
|
||||
case proc() -> string:
|
||||
return v(), true
|
||||
case proc() -> int:
|
||||
return fmt.tprintf("%d", v()), true
|
||||
case proc() -> bool:
|
||||
return "true" if v() else "false", true
|
||||
case:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
call_section_lambda :: proc(val: any, text: string) -> (result: string, ok: bool) {
|
||||
switch v in val {
|
||||
case proc(string) -> string:
|
||||
return v(text), true
|
||||
case proc(string) -> int:
|
||||
return fmt.tprintf("%d", v(text)), true
|
||||
case proc(string) -> bool:
|
||||
return "true" if v(text) else "false", true
|
||||
case:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
// list_info returns element type info, count, and data pointer for a list value.
|
||||
// Returns elem_info=nil if the value is not a list.
|
||||
list_info :: proc(a: any) -> (elem_info: ^runtime.Type_Info, count: int, data: rawptr) {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
#+test
|
||||
package mustache
|
||||
|
||||
import "core:fmt"
|
||||
import "core:testing"
|
||||
|
||||
// --- Spec test 1: Interpolation ---
|
||||
// A lambda's return value should be interpolated.
|
||||
|
||||
Interp_Data :: struct {
|
||||
lambda: proc() -> string,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
Interp_Data_Int :: struct {
|
||||
lambda: proc() -> int,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return "world"},
|
||||
}
|
||||
tpl, _ := parse("Hello, {{lambda}}!", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "Hello, world!")
|
||||
}
|
||||
|
||||
// --- Spec test 2: Interpolation - Expansion ---
|
||||
// A lambda's return value should be parsed.
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation_expansion :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return "{{planet}}"},
|
||||
planet = "world",
|
||||
}
|
||||
tpl, _ := parse("Hello, {{lambda}}!", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "Hello, world!")
|
||||
}
|
||||
|
||||
// --- Spec test 4: Interpolation - Multiple Calls ---
|
||||
// Interpolated lambdas should not be cached.
|
||||
|
||||
counter_lambda :: proc() -> int {
|
||||
@(static) call_count := 0
|
||||
call_count += 1
|
||||
return call_count
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_interpolation_multiple_calls :: proc(t: ^testing.T) {
|
||||
data := Interp_Data_Int {
|
||||
lambda = counter_lambda,
|
||||
}
|
||||
tpl, _ := parse("{{lambda}} == {{{lambda}}} == {{lambda}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "1 == 2 == 3")
|
||||
}
|
||||
|
||||
// --- Spec test 5: Escaping ---
|
||||
// Lambda results should be appropriately escaped.
|
||||
|
||||
@(test)
|
||||
test_lambda_escaping :: proc(t: ^testing.T) {
|
||||
data := Interp_Data {
|
||||
lambda = proc() -> string {return ">"},
|
||||
}
|
||||
tpl, _ := parse("<{{lambda}}{{{lambda}}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<>>")
|
||||
}
|
||||
|
||||
// --- Spec test 6: Section ---
|
||||
// Lambdas used for sections should receive the raw section string.
|
||||
|
||||
Section_Data :: struct {
|
||||
lambda: proc(_: string) -> string,
|
||||
x: string,
|
||||
planet: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_section :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
if text == "{{x}}" {return "yes"} else {return "no"}
|
||||
},
|
||||
x = "Error!",
|
||||
}
|
||||
tpl, _ := parse("<{{#lambda}}{{x}}{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<yes>")
|
||||
}
|
||||
|
||||
// --- Spec test 7: Section - Expansion ---
|
||||
// Lambdas used for sections should have their results parsed.
|
||||
|
||||
@(test)
|
||||
test_lambda_section_expansion :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
return fmt.tprintf("%s{{{{planet}}}}%s", text, text)
|
||||
},
|
||||
planet = "Earth",
|
||||
}
|
||||
tpl, _ := parse("<{{#lambda}}-{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<-Earth->")
|
||||
}
|
||||
|
||||
// --- Spec test 9: Section - Multiple Calls ---
|
||||
// Lambdas used for sections should not be cached.
|
||||
|
||||
@(test)
|
||||
test_lambda_section_multiple_calls :: proc(t: ^testing.T) {
|
||||
data := Section_Data {
|
||||
lambda = proc(text: string) -> string {
|
||||
return fmt.tprintf("__%s__", text)
|
||||
},
|
||||
}
|
||||
tpl, _ := parse("{{#lambda}}FILE{{/lambda}} != {{#lambda}}LINE{{/lambda}}", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "__FILE__ != __LINE__")
|
||||
}
|
||||
|
||||
// --- Spec test 10: Inverted Section ---
|
||||
// Lambdas used for inverted sections should be considered truthy.
|
||||
|
||||
Inverted_Data :: struct {
|
||||
lambda: proc(_: string) -> bool,
|
||||
static: string,
|
||||
}
|
||||
|
||||
@(test)
|
||||
test_lambda_inverted_section :: proc(t: ^testing.T) {
|
||||
data := Inverted_Data {
|
||||
lambda = proc(text: string) -> bool {return false},
|
||||
static = "static",
|
||||
}
|
||||
tpl, _ := parse("<{{^lambda}}{{static}}{{/lambda}}>", context.temp_allocator)
|
||||
result, _ := render(tpl, data, allocator = context.temp_allocator)
|
||||
testing.expect_value(t, result, "<>")
|
||||
}
|
||||
|
||||
+45
-7
@@ -48,6 +48,7 @@ Node :: struct {
|
||||
indent: string,
|
||||
first_child: int,
|
||||
child_count: int,
|
||||
content: string,
|
||||
}
|
||||
|
||||
// node_span returns the number of flat-array entries a node occupies:
|
||||
@@ -103,7 +104,7 @@ parse :: proc(
|
||||
return {}, terr
|
||||
}
|
||||
|
||||
tmpl.nodes, err = parse_tokens(tokens[:], allocator)
|
||||
tmpl.nodes, err = parse_tokens(tokens[:], source, allocator)
|
||||
if err != nil {
|
||||
delete(tmpl.nodes)
|
||||
return {}, err
|
||||
@@ -148,6 +149,7 @@ render :: proc(
|
||||
|
||||
parse_tokens :: proc(
|
||||
tokens: []Token,
|
||||
source: string,
|
||||
allocator := context.allocator,
|
||||
) -> (
|
||||
nodes: [dynamic]Node,
|
||||
@@ -155,7 +157,7 @@ parse_tokens :: proc(
|
||||
) {
|
||||
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
||||
pos := 0
|
||||
err = parse_section(tokens, &pos, &nodes, "")
|
||||
err = parse_section(tokens, &pos, &nodes, "", source)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -164,6 +166,7 @@ parse_section :: proc(
|
||||
pos: ^int,
|
||||
nodes: ^[dynamic]Node,
|
||||
end_tag: string,
|
||||
source: string,
|
||||
) -> Render_Error {
|
||||
for pos^ < len(tokens) {
|
||||
tok := tokens[pos^]
|
||||
@@ -187,18 +190,28 @@ parse_section :: proc(
|
||||
case .Section_Open:
|
||||
pos^ += 1
|
||||
idx := len(nodes)
|
||||
content_start := 0
|
||||
if pos^ < len(tokens) { content_start = tokens[pos^].pos }
|
||||
append(nodes, Node{kind = .Section, key = tok.value, first_child = -1})
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
close_pos := 0
|
||||
if pos^ - 1 >= 0 && pos^ - 1 < len(tokens) { close_pos = tokens[pos^ - 1].pos }
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
nodes[idx].content = source[content_start:close_pos]
|
||||
|
||||
case .Inverted_Open:
|
||||
pos^ += 1
|
||||
idx := len(nodes)
|
||||
content_start := 0
|
||||
if pos^ < len(tokens) { content_start = tokens[pos^].pos }
|
||||
append(nodes, Node{kind = .Inverted, key = tok.value, first_child = -1})
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
close_pos := 0
|
||||
if pos^ - 1 >= 0 && pos^ - 1 < len(tokens) { close_pos = tokens[pos^ - 1].pos }
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
nodes[idx].content = source[content_start:close_pos]
|
||||
|
||||
case .Section_Close:
|
||||
if end_tag != "" && tok.value == end_tag {
|
||||
@@ -236,7 +249,7 @@ parse_section :: proc(
|
||||
nodes,
|
||||
Node{kind = .Parent, key = tok.value, indent = tok.indent, first_child = -1},
|
||||
)
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
|
||||
@@ -247,7 +260,7 @@ parse_section :: proc(
|
||||
nodes,
|
||||
Node{kind = .Block, key = tok.value, indent = tok.indent, first_child = -1},
|
||||
)
|
||||
parse_section(tokens, pos, nodes, tok.value) or_return
|
||||
parse_section(tokens, pos, nodes, tok.value, source) or_return
|
||||
nodes[idx].first_child = idx + 1
|
||||
nodes[idx].child_count = len(nodes) - idx - 1
|
||||
}
|
||||
@@ -447,17 +460,42 @@ render_nodes :: proc(
|
||||
|
||||
case .Variable:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if result_str, ok := call_interp_lambda(val); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
temp: strings.Builder
|
||||
strings.builder_init(&temp, context.temp_allocator)
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, &temp, blocks) or_return
|
||||
write_value(b, strings.to_string(temp), escape = true)
|
||||
}
|
||||
} else {
|
||||
write_value(b, val, escape = true)
|
||||
}
|
||||
i += 1
|
||||
|
||||
case .Unescaped:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if result_str, ok := call_interp_lambda(val); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
temp: strings.Builder
|
||||
strings.builder_init(&temp, context.temp_allocator)
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, &temp, blocks) or_return
|
||||
write_value(b, strings.to_string(temp), escape = false)
|
||||
}
|
||||
} else {
|
||||
write_value(b, val, escape = false)
|
||||
}
|
||||
i += 1
|
||||
|
||||
case .Section:
|
||||
val := resolve_name(node.key, ctx[:])
|
||||
if is_truthy(val) {
|
||||
if result_str, ok := call_section_lambda(val, node.content); ok {
|
||||
sub_tpl, perr := parse(result_str, context.temp_allocator, context.temp_allocator)
|
||||
if perr == nil {
|
||||
render_nodes(sub_tpl.nodes[:], sub_tpl.nodes[:], ctx, partials, b, blocks) or_return
|
||||
}
|
||||
} else if is_truthy(val) {
|
||||
children := all_nodes[node.first_child:node.first_child + node.child_count]
|
||||
elem_info, count, data := list_info(val)
|
||||
if elem_info != nil {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import "core:fmt"
|
||||
|
||||
Open_Graph :: struct {
|
||||
title: string,
|
||||
type: string,
|
||||
image: string,
|
||||
url: string,
|
||||
description: string,
|
||||
locale: string,
|
||||
site_name: string,
|
||||
is_article: bool,
|
||||
published_time: string,
|
||||
modified_time: string,
|
||||
section: string,
|
||||
}
|
||||
|
||||
og_init :: proc(site: Site) -> Open_Graph {
|
||||
return {
|
||||
site_name = site.title,
|
||||
description = site.description,
|
||||
image = fmt.tprintf("%s/avatar.jpg", site.base_url),
|
||||
locale = "en_US",
|
||||
}
|
||||
}
|
||||
|
||||
og_for_page :: proc(site: Site, page: Page, base: Open_Graph) -> Open_Graph {
|
||||
og := base
|
||||
is_article := page.section != ""
|
||||
og.url = page.url
|
||||
og.title = strip_html_tags(page.title, context.temp_allocator)
|
||||
og.type = "article" if is_article else "website"
|
||||
og.is_article = is_article
|
||||
og.section = page.section
|
||||
og.published_time = page.date
|
||||
|
||||
return og
|
||||
}
|
||||
|
||||
+31
-55
@@ -29,13 +29,7 @@ Base_Data :: struct {
|
||||
params: json.Value,
|
||||
body: string,
|
||||
title: string,
|
||||
og_site_name: string,
|
||||
og_description: string,
|
||||
og_image: string,
|
||||
og_url: string,
|
||||
og_title: string,
|
||||
og_type: string,
|
||||
is_article: bool,
|
||||
og: Open_Graph,
|
||||
}
|
||||
|
||||
Page_Data :: struct {
|
||||
@@ -43,20 +37,17 @@ Page_Data :: struct {
|
||||
page_title: string,
|
||||
date_iso: string,
|
||||
date_display: string,
|
||||
is_post: bool,
|
||||
og_section: string,
|
||||
og_published: string,
|
||||
}
|
||||
|
||||
Home_Data :: struct {
|
||||
using base: Base_Data,
|
||||
list_pages: [dynamic]Page_Context,
|
||||
pages: [dynamic]Page_Context,
|
||||
}
|
||||
|
||||
Section_Data :: struct {
|
||||
using base: Base_Data,
|
||||
page_title: string,
|
||||
year_sections: [dynamic]Year_Section,
|
||||
by_year: [dynamic]Year_Section,
|
||||
}
|
||||
|
||||
build_page_context :: proc(page: Page) -> Page_Context {
|
||||
@@ -69,8 +60,8 @@ build_page_context :: proc(page: Page) -> Page_Context {
|
||||
}
|
||||
}
|
||||
|
||||
strip_html_tags :: proc(s: string) -> string {
|
||||
sb := strings.builder_make()
|
||||
strip_html_tags :: proc(s: string, allocator := context.allocator) -> string {
|
||||
sb := strings.builder_make(allocator)
|
||||
defer strings.builder_destroy(&sb)
|
||||
|
||||
in_tag := false
|
||||
@@ -95,22 +86,15 @@ strip_html_tags :: proc(s: string) -> string {
|
||||
return strings.to_string(sb)
|
||||
}
|
||||
|
||||
og_type :: proc(is_article: bool) -> string {
|
||||
if is_article {
|
||||
return "article"
|
||||
}
|
||||
return "website"
|
||||
}
|
||||
|
||||
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
||||
data, ok := vfs_get(vfs, virtual_path)
|
||||
if !ok {
|
||||
log.warnf("thor: template %s not found", virtual_path)
|
||||
log.warnf("template %s not found", virtual_path)
|
||||
return mustache.Template{}
|
||||
}
|
||||
tpl, err := mustache.parse(string(data))
|
||||
if err != nil {
|
||||
log.warnf("thor: failed to parse template %s: %v", virtual_path, err)
|
||||
log.warnf("failed to parse template %s: %v", virtual_path, err)
|
||||
}
|
||||
return tpl
|
||||
}
|
||||
@@ -133,7 +117,7 @@ get_template :: proc(
|
||||
chain[n] = "base"; n += 1
|
||||
}
|
||||
|
||||
for i in 0..<n {
|
||||
for i in 0 ..< n {
|
||||
candidate := chain[i]
|
||||
if cached, ok := cache[candidate]; ok {
|
||||
return cached
|
||||
@@ -145,11 +129,11 @@ get_template :: proc(
|
||||
return tpl
|
||||
}
|
||||
if candidate != chain[n - 1] {
|
||||
log.warnf("thor: template %s not found, falling back", virtual)
|
||||
log.debugf("template %s not found, falling back", virtual)
|
||||
}
|
||||
}
|
||||
|
||||
log.errorf("thor: base.html not found in VFS")
|
||||
log.errorf("base.html not found in VFS")
|
||||
return mustache.Template{}
|
||||
}
|
||||
|
||||
@@ -170,7 +154,7 @@ render_template :: proc(
|
||||
) -> string {
|
||||
result, err := mustache.render(content_tpl, data, partials)
|
||||
if err != nil {
|
||||
fmt.eprintfln("thor: mustache error: %v", err)
|
||||
fmt.eprintfln("mustache error: %v", err)
|
||||
return ""
|
||||
}
|
||||
return result
|
||||
@@ -195,9 +179,7 @@ render_site :: proc(site: ^Site) {
|
||||
now = now,
|
||||
author = site.author,
|
||||
params = site.params,
|
||||
og_site_name = site.title,
|
||||
og_description = site.description,
|
||||
og_image = fmt.tprintf("%s/avatar.jpg", site.base_url),
|
||||
og = og_init(site^),
|
||||
}
|
||||
|
||||
// Find home page
|
||||
@@ -310,13 +292,7 @@ render_page_html :: proc(
|
||||
data.body = page.body_html
|
||||
data.date_iso = page.date
|
||||
data.date_display = format_date(page.date)
|
||||
data.is_post = is_article
|
||||
data.og_url = fmt.tprintf("%s%s", site.base_url, page.permalink)
|
||||
data.og_title = strip_html_tags(page.title)
|
||||
data.og_type = og_type(is_article)
|
||||
data.is_article = is_article
|
||||
data.og_section = page.section
|
||||
data.og_published = page.date
|
||||
data.og = og_for_page(site^, page, base.og)
|
||||
return render_template(content_tpl, data, partials)
|
||||
}
|
||||
|
||||
@@ -341,11 +317,11 @@ render_home_html :: proc(
|
||||
}
|
||||
data.title = site.title
|
||||
data.body = home.body_html
|
||||
data.list_pages = list_pages
|
||||
data.og_url = fmt.tprintf("%s/", site.base_url)
|
||||
data.og_title = site.title
|
||||
data.og_type = "website"
|
||||
data.is_article = false
|
||||
data.pages = list_pages
|
||||
data.og.url = fmt.tprintf("%s/", site.base_url)
|
||||
data.og.title = site.title
|
||||
data.og.type = "website"
|
||||
data.og.is_article = false
|
||||
return render_template(content_tpl, data, partials)
|
||||
}
|
||||
|
||||
@@ -358,8 +334,8 @@ render_section :: proc(
|
||||
partials: map[string]mustache.Template,
|
||||
base: Base_Data,
|
||||
) -> string {
|
||||
year_sections := make([dynamic]Year_Section)
|
||||
defer delete(year_sections)
|
||||
by_year := make([dynamic]Year_Section)
|
||||
defer delete(by_year)
|
||||
current_year := ""
|
||||
for page in site.pages {
|
||||
if page.section != section || page._is_index {
|
||||
@@ -367,10 +343,10 @@ render_section :: proc(
|
||||
}
|
||||
year := get_year(page.date)
|
||||
if year != current_year {
|
||||
append(&year_sections, Year_Section{year = year})
|
||||
append(&by_year, Year_Section{year = year})
|
||||
current_year = year
|
||||
}
|
||||
append(&year_sections[len(year_sections) - 1].posts, build_page_context(page))
|
||||
append(&by_year[len(by_year) - 1].posts, build_page_context(page))
|
||||
}
|
||||
|
||||
data := Section_Data {
|
||||
@@ -380,16 +356,16 @@ render_section :: proc(
|
||||
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
|
||||
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.og.title = capitalize(section)
|
||||
}
|
||||
data.year_sections = year_sections
|
||||
data.og_url = fmt.tprintf("%s/%s/", site.base_url, section)
|
||||
data.og_type = "website"
|
||||
data.is_article = false
|
||||
data.by_year = by_year
|
||||
data.og.url = fmt.tprintf("%s/%s/", site.base_url, section)
|
||||
data.og.type = "website"
|
||||
data.og.is_article = false
|
||||
return render_template(content_tpl, data, partials)
|
||||
}
|
||||
|
||||
@@ -413,7 +389,7 @@ load_partials :: proc(vfs: ^VFS) -> map[string]mustache.Template {
|
||||
}
|
||||
tpl, err := mustache.parse(string(data))
|
||||
if err != nil {
|
||||
log.warnf("thor: failed to parse partial %s: %v", key, err)
|
||||
log.warnf("failed to parse partial %s: %v", key, err)
|
||||
continue
|
||||
}
|
||||
partials[key] = tpl
|
||||
@@ -456,7 +432,7 @@ write_page :: proc(output_dir: string, permalink: string, html: string) {
|
||||
|
||||
dir := fmt.tprintf("%s/%s", output_dir, rel)
|
||||
if err := os.make_directory_all(dir); err != nil && err != .Exist {
|
||||
fmt.eprintfln("thor: cannot create %s: %v", dir, err)
|
||||
fmt.eprintfln("cannot create %s: %v", dir, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -466,7 +442,7 @@ write_page :: proc(output_dir: string, permalink: string, html: string) {
|
||||
|
||||
write_file :: proc(path: string, html: string) {
|
||||
if err := os.write_entire_file_from_string(path, html); err != nil {
|
||||
fmt.eprintfln("thor: cannot write %s: %v", path, err)
|
||||
fmt.eprintfln("cannot write %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -85,7 +85,7 @@ init_site :: proc(site: ^Site, args: []string) {
|
||||
found, ok := find_config("thor.json")
|
||||
if ok {
|
||||
path = found
|
||||
log.debugf("thor: using config %s", path)
|
||||
log.debugf("using config %s", path)
|
||||
} else {
|
||||
path = "./thor.json"
|
||||
}
|
||||
@@ -121,7 +121,7 @@ load_config_file :: proc(
|
||||
|
||||
unmarshal_err := json.unmarshal_string(string(data), config, allocator = allocator)
|
||||
if unmarshal_err != nil {
|
||||
log.warnf("thor: failed to parse %s: %v", path, unmarshal_err)
|
||||
log.warnf("failed to parse %s: %v", path, unmarshal_err)
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user