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
|
## 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 `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/PARTIAL_INDENT.md` for whitespace handling analysis.
|
||||||
See `mustache/SPEC.md` for the original implementation specification.
|
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
|
## Performance
|
||||||
- [ ] "Zero is beautiful"
|
|
||||||
|
- [ ] 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
|
||||||
|
- [ ] 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
|
- [ ] 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
|
- [ ] Clean up the default layouts
|
||||||
- [ ] Performance
|
- [ ] Add `-production` flag
|
||||||
- [ ] See if we can disable bounds checks in `write_indented` and elsewhere.
|
- sets `-minify`
|
||||||
- [ ] Instead of loading the site fresh each time in watch mode, create a
|
- [ ] Open Graph
|
||||||
`reload_site` proc, that just updates changed resources.
|
- [x] mustache data keys for opengraph, etc.
|
||||||
- [ ] Only publish referenced assets.
|
- [ ] OpenGraph meta tags — verify all fields match production site
|
||||||
- [ ] Split `load_page` into frontmatter-parse + body-process phases so draft pages can skip the markdown pipeline entirely
|
- [ ] set opengraph tags / description automatically if unset. (Like hugo does)
|
||||||
- [ ] mustache data keys for opengraph, etc.
|
- [ ] 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
|
- [ ] 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`?
|
- [ ] 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
|
|
||||||
- [ ] Add Opt-in deflist support.
|
- [ ] Add Opt-in deflist support.
|
||||||
- [ ] We need to be able to do `Year_Section` in a non-magical, unpriviledged way.
|
- [ ] We need to be able to do `Year_Section` in a non-magical, unprivileged way. See [PLAN.md](PLAN.md) for computed properties approach.
|
||||||
- [ ] set opengraph tags / description automatically if unset. (Like hugo does)
|
|
||||||
- [ ] Table of contents support.
|
- [ ] Table of contents support.
|
||||||
- [ ] Nav items should be active when the current page is selected.
|
- [ ] Nav items should be active when the current page is selected.
|
||||||
- [ ] Theme selector for syntax highlighting.
|
- [ ] Theme selector for syntax highlighting.
|
||||||
@@ -36,14 +59,29 @@
|
|||||||
- [ ] 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.
|
|
||||||
- [ ] Mount content in VFS
|
- [ ] Mount content in VFS
|
||||||
- [ ] commands
|
- [ ] commands
|
||||||
- [ ] `build` alias of default
|
- [ ] `build` alias of default
|
||||||
- [ ] `init` set up new project
|
- [ ] `new site` set up new project
|
||||||
- [ ] 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.
|
- [ ] 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 every file in thor
|
||||||
- [ ] Review assets.odin
|
- [ ] Review assets.odin
|
||||||
- [ ] Review content.odin
|
- [ ] Review content.odin
|
||||||
@@ -53,14 +91,20 @@
|
|||||||
- [ ] Review main.odin
|
- [ ] Review main.odin
|
||||||
- [ ] Review minify.odin
|
- [ ] Review minify.odin
|
||||||
- [ ] Review `markdown/`
|
- [ ] Review `markdown/`
|
||||||
- [ ] Review alerts.odin
|
- [x] Review alerts.odin
|
||||||
- [ ] Review emoji.odin
|
- [x] Review alerts_test.odin
|
||||||
|
- [x] Review emoji.odin
|
||||||
|
- [x] Review emoji_test.odin
|
||||||
- [ ] Review footnotes.odin
|
- [ ] Review footnotes.odin
|
||||||
- [ ] Review footnotes_test.odin
|
- [ ] Review footnotes_test.odin
|
||||||
- [ ] Review highlight.odin
|
- [ ] Review highlight.odin
|
||||||
- [ ] Review markdown.odin
|
- [ ] Review markdown.odin
|
||||||
- [ ] Review sectionate.odin
|
- [x] Review sectionate.odin
|
||||||
|
- [x] Review sectionate_test.odin
|
||||||
|
- [x] Review opengraph.odin
|
||||||
- [ ] Review render.odin
|
- [ ] Review render.odin
|
||||||
- [x] Review site.odin
|
- [x] Review site.odin
|
||||||
- [ ] Review treesitter/treesitter.odin
|
- [ ] Review treesitter/treesitter.odin
|
||||||
- [ ] Review vfs.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 idx := strings.last_index(dest, "/"); idx >= 0 {
|
||||||
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
if err := os.make_directory_all(dest[:idx]); err != nil && err != .Exist {
|
||||||
log.warnf("thor: cannot create %s: %v", dest[:idx], err)
|
log.warnf("cannot create %s: %v", dest[:idx], err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -28,11 +28,11 @@ copy_assets_dir :: proc(vfs: ^VFS, output_dir: string, features: bit_set[Feature
|
|||||||
}
|
}
|
||||||
} else if entry.data != nil {
|
} else if entry.data != nil {
|
||||||
if err := os.write_entire_file(dest, entry.data); err != 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 {
|
} else {
|
||||||
if err := os.copy_file(dest, entry.fs_path); err != nil {
|
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,
|
slug: string,
|
||||||
layout: string,
|
layout: string,
|
||||||
permalink: string,
|
permalink: string,
|
||||||
|
url: string,
|
||||||
title: string,
|
title: string,
|
||||||
description: string,
|
description: string,
|
||||||
date: string,
|
date: string,
|
||||||
@@ -28,6 +29,10 @@ Page :: struct {
|
|||||||
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, "")
|
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=""),
|
// 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) {
|
scan_content :: proc(site: ^Site, dir: string, section: string) {
|
||||||
entries, err := os.read_all_directory_by_path(dir, context.allocator)
|
entries, err := os.read_all_directory_by_path(dir, context.allocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.warnf("thor: cannot read %s: %v", dir, err)
|
log.warnf("cannot read %s: %v", dir, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer os.file_info_slice_delete(entries, context.allocator)
|
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)
|
return fmt.tprintf("%s_index", section)
|
||||||
}
|
}
|
||||||
if section != "" {
|
if section != "" {
|
||||||
|
if len(section) > 1 && section[len(section) - 1] == 's' {
|
||||||
|
return section[:len(section) - 1]
|
||||||
|
}
|
||||||
return section
|
return section
|
||||||
}
|
}
|
||||||
return "page"
|
return "page"
|
||||||
@@ -108,7 +116,7 @@ load_page :: proc(
|
|||||||
) {
|
) {
|
||||||
data, err := os.read_entire_file_from_path(file_path, context.allocator)
|
data, err := os.read_entire_file_from_path(file_path, context.allocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.warnf("thor: cannot read %s: %v", file_path, err)
|
log.warnf("cannot read %s: %v", file_path, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
{{&body}}
|
{{&body}}
|
||||||
</header>
|
</header>
|
||||||
<ul>
|
<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>
|
datetime="{{date_iso}}">{{date_display}}</time>{{/date_iso}}</span>
|
||||||
</li>
|
</li>
|
||||||
{{/list_pages}}
|
{{/pages}}
|
||||||
</ul>
|
</ul>
|
||||||
</main>
|
</main>
|
||||||
{{/content}}
|
{{/content}}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
<meta property="og:url" content="{{og_url}}">
|
<meta property="og:url" content="{{og.url}}">
|
||||||
<meta property="og:site_name" content="{{og_site_name}}">
|
<meta property="og:site_name" content="{{og.site_name}}">
|
||||||
<meta property="og:title" content="{{og_title}}">
|
<meta property="og:title" content="{{og.title}}">
|
||||||
<meta property="og:description" content="{{og_description}}">
|
<meta property="og:description" content="{{og.description}}">
|
||||||
<meta property="og:locale" content="en_us">
|
<meta property="og:locale" content="{{og.locale}}">
|
||||||
<meta property="og:type" content="{{og_type}}">
|
<meta property="og:type" content="{{og.type}}">
|
||||||
{{#is_article}}<meta property="article:section" content="{{og_section}}">
|
{{#og.is_article}}<meta property="article:section" content="{{og.section}}">
|
||||||
<meta property="article:published_time" content="{{og_published}}">
|
<meta property="article:published_time" content="{{og.published_time}}">
|
||||||
{{/is_article}}<meta property="og:image" content="{{og_image}}">
|
{{/og.is_article}}<meta property="og:image" content="{{og.image}}">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<main>
|
<main>
|
||||||
<h1>{{page_title}}</h1>
|
<h1>{{page_title}}</h1>
|
||||||
{{&body}}
|
{{&body}}
|
||||||
{{#year_sections}}
|
{{#by_year}}
|
||||||
<section>
|
<section>
|
||||||
<h2>{{year}}</h2>
|
<h2>{{year}}</h2>
|
||||||
<ul>
|
<ul>
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
{{/posts}}
|
{{/posts}}
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
{{/year_sections}}
|
{{/by_year}}
|
||||||
</main>
|
</main>
|
||||||
{{/content}}
|
{{/content}}
|
||||||
{{/base}}
|
{{/base}}
|
||||||
|
|||||||
@@ -40,18 +40,16 @@ generate_rss :: proc(site: ^Site) -> string {
|
|||||||
fmt.aprintf(
|
fmt.aprintf(
|
||||||
`<item>
|
`<item>
|
||||||
<title>%s</title>
|
<title>%s</title>
|
||||||
<link>%s%s</link>
|
<link>%s</link>
|
||||||
<pubDate>%s</pubDate>
|
<pubDate>%s</pubDate>
|
||||||
<guid>%s%s</guid>
|
<guid>%s</guid>
|
||||||
<description>%s</description>
|
<description>%s</description>
|
||||||
</item>
|
</item>
|
||||||
`,
|
`,
|
||||||
xml_escape(page.title),
|
xml_escape(page.title),
|
||||||
site.base_url,
|
page.url,
|
||||||
page.permalink,
|
|
||||||
pub_date,
|
pub_date,
|
||||||
site.base_url,
|
page.url,
|
||||||
page.permalink,
|
|
||||||
xml_escape(page.body_html),
|
xml_escape(page.body_html),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -78,7 +76,7 @@ generate_sitemap :: proc(site: ^Site) -> string {
|
|||||||
}
|
}
|
||||||
strings.write_string(
|
strings.write_string(
|
||||||
&sb,
|
&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)
|
value, err := json.parse_string(json_str, spec = .JSON)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.eprintfln("thor: failed to parse frontmatter JSON: %v", err)
|
fmt.eprintfln("failed to parse frontmatter JSON: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer json.destroy_value(value)
|
defer json.destroy_value(value)
|
||||||
|
|||||||
+34
-41
@@ -1,23 +1,14 @@
|
|||||||
#+feature dynamic-literals
|
#+feature dynamic-literals
|
||||||
package markdown
|
package markdown
|
||||||
|
|
||||||
import "core:fmt"
|
|
||||||
import "core:strings"
|
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 = {
|
ALERT_EMOJIS: map[string]string = {
|
||||||
"note" = "\xe2\x84\xb9\xef\xb8\x8f",
|
"note" = "ℹ️",
|
||||||
"tip" = "\xf0\x9f\x92\xa1",
|
"tip" = "💡",
|
||||||
"important" = "\xe2\x9d\x97",
|
"important" = "❗",
|
||||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
"warning" = "⚠️",
|
||||||
"caution" = "\xe2\x9d\x97",
|
"caution" = "❗",
|
||||||
}
|
}
|
||||||
|
|
||||||
inject_alerts :: proc(html: string) -> string {
|
inject_alerts :: proc(html: string) -> string {
|
||||||
@@ -43,7 +34,7 @@ inject_alerts :: proc(html: string) -> string {
|
|||||||
bq := remaining[bq_start:bq_end]
|
bq := remaining[bq_start:bq_end]
|
||||||
|
|
||||||
strings.write_string(&sb, remaining[:bq_start])
|
strings.write_string(&sb, remaining[:bq_start])
|
||||||
strings.write_string(&sb, transform_alert(bq))
|
transform_alert(&sb, bq)
|
||||||
|
|
||||||
remaining = remaining[bq_end:]
|
remaining = remaining[bq_end:]
|
||||||
}
|
}
|
||||||
@@ -51,55 +42,57 @@ inject_alerts :: proc(html: string) -> string {
|
|||||||
return strings.to_string(sb)
|
return strings.to_string(sb)
|
||||||
}
|
}
|
||||||
|
|
||||||
transform_alert :: proc(bq: string) -> string {
|
transform_alert :: proc(sb: ^strings.Builder, bq: string) {
|
||||||
// Skip <blockquote> tag and whitespace
|
|
||||||
pos := len("<blockquote>")
|
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
|
pos += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for <p>[!
|
if pos + 4 >= len(bq) ||
|
||||||
if pos + 4 >= len(bq) || bq[pos] != '<' || bq[pos + 1] != 'p' || bq[pos + 2] != '>' ||
|
bq[pos] != '<' ||
|
||||||
bq[pos + 3] != '[' || bq[pos + 4] != '!' {
|
bq[pos + 1] != 'p' ||
|
||||||
return bq
|
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:], "]")
|
close := strings.index(bq[pos + 5:], "]")
|
||||||
if close < 0 {
|
if close < 0 {
|
||||||
return bq
|
strings.write_string(sb, bq)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type_raw := bq[pos + 5 : pos + 5 + close]
|
type_raw := bq[pos + 5:pos + 5 + close]
|
||||||
type_lower := strings.to_lower(type_raw)
|
type_lower := strings.to_lower(type_raw, context.temp_allocator)
|
||||||
|
|
||||||
style, has_style := ALERT_STYLES[type_lower]
|
emoji, found := ALERT_EMOJIS[type_lower]
|
||||||
emoji, has_emoji := ALERT_EMOJIS[type_lower]
|
if !found {
|
||||||
if !has_style || !has_emoji {
|
strings.write_string(sb, bq)
|
||||||
return bq
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Position after ]
|
|
||||||
after_type := pos + 5 + close + 1
|
after_type := pos + 5 + close + 1
|
||||||
|
|
||||||
// Skip optional + or -
|
|
||||||
content_start := after_type
|
content_start := after_type
|
||||||
if content_start < len(bq) && (bq[content_start] == '+' || bq[content_start] == '-') {
|
if content_start < len(bq) && (bq[content_start] == '+' || bq[content_start] == '-') {
|
||||||
content_start += 1
|
content_start += 1
|
||||||
}
|
}
|
||||||
// Skip space after marker
|
|
||||||
if content_start < len(bq) && bq[content_start] == ' ' {
|
if content_start < len(bq) && bq[content_start] == ' ' {
|
||||||
content_start += 1
|
content_start += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rebuild: styled blockquote + bold title paragraph + rest
|
|
||||||
rest := bq[content_start:]
|
rest := bq[content_start:]
|
||||||
|
|
||||||
return fmt.aprintf(
|
strings.write_string(sb, `<blockquote class="alert alert-`)
|
||||||
`<blockquote class="alert %s rounded-r py-2">
|
strings.write_string(sb, type_lower)
|
||||||
<p class="font-bold mb-1">%s %s`,
|
strings.write_string(sb, `">`)
|
||||||
style,
|
strings.write_string(sb, "\n")
|
||||||
emoji,
|
strings.write_string(sb, `<p class="alert-title">`)
|
||||||
rest,
|
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 = {
|
EMOJIS: map[string]string = {
|
||||||
// People
|
// People
|
||||||
"smile" = "\xf0\x9f\x98\x84",
|
"smile" = "😄",
|
||||||
"laughing" = "\xf0\x9f\x98\x86",
|
"laughing" = "😆",
|
||||||
"blush" = "\xf0\x9f\x98\x8a",
|
"blush" = "😊",
|
||||||
"smiley" = "\xf0\x9f\x98\x83",
|
"smiley" = "😃",
|
||||||
"wink" = "\xf0\x9f\x98\x89",
|
"wink" = "😉",
|
||||||
"joy" = "\xf0\x9f\xa4\xa3",
|
"joy" = "😂",
|
||||||
"rofl" = "\xf0\x9f\xa4\xa3",
|
"rofl" = "🤣",
|
||||||
"relaxed" = "\xe2\x98\xba\xef\xb8\x8f",
|
"relaxed" = "☺️",
|
||||||
"thinking" = "\xf0\x9f\xa4\x94",
|
"thinking" = "🤔",
|
||||||
"neutral_face" = "\xf0\x9f\x98\x90",
|
"neutral_face" = "😐",
|
||||||
"expressionless" = "\xf0\x9f\x98\x91",
|
"expressionless" = "😑",
|
||||||
"no_mouth" = "\xf0\x9f\x98\xb6",
|
"no_mouth" = "😶",
|
||||||
"rolling_eyes" = "\xf0\x9f\x99\x84",
|
"rolling_eyes" = "🙄",
|
||||||
"smirk" = "\xf0\x9f\x98\x8f",
|
"smirk" = "😏",
|
||||||
"persevere" = "\xf0\x9f\x98\xa3",
|
"persevere" = "😣",
|
||||||
"disappointed_relieved" = "\xf0\x9f\x98\xa5",
|
"disappointed_relieved" = "😥",
|
||||||
"open_mouth" = "\xf0\x9f\x98\xae",
|
"open_mouth" = "😮",
|
||||||
"zipper_mouth_face" = "\xf0\x9f\xa4\x90",
|
"zipper_mouth_face" = "🤐",
|
||||||
"hushed" = "\xf0\x9f\x98\xaf",
|
"hushed" = "😯",
|
||||||
"sleepy" = "\xf0\x9f\x98\xaa",
|
"sleepy" = "😪",
|
||||||
"tired_face" = "\xf0\x9f\x98\xab",
|
"tired_face" = "😫",
|
||||||
"sleeping" = "\xf0\x9f\x98\xb4",
|
"sleeping" = "😴",
|
||||||
"relieved" = "\xf0\x9f\x98\x8c",
|
"relieved" = "😌",
|
||||||
"stuck_out_tongue" = "\xf0\x9f\x98\x9b",
|
"stuck_out_tongue" = "😛",
|
||||||
"stuck_out_tongue_winking_eye" = "\xf0\x9f\x98\x9c",
|
"stuck_out_tongue_winking_eye" = "😜",
|
||||||
"stuck_out_tongue_closed_eyes" = "\xf0\x9f\x98\x9d",
|
"stuck_out_tongue_closed_eyes" = "😝",
|
||||||
"drooling_face" = "\xf0\x9f\xa4\xa4",
|
"drooling_face" = "🤤",
|
||||||
"unamused" = "\xf0\x9f\x98\x92",
|
"unamused" = "😒",
|
||||||
"sweat" = "\xf0\x9f\x98\x85",
|
"sweat" = "😅",
|
||||||
"pensive" = "\xf0\x9f\x98\x94",
|
"pensive" = "😔",
|
||||||
"confused" = "\xf0\x9f\x98\x95",
|
"confused" = "😕",
|
||||||
"upside_down_face" = "\xf0\x9f\x99\x83",
|
"upside_down_face" = "🙃",
|
||||||
"money_mouth_face" = "\xf0\x9f\xa4\x91",
|
"money_mouth_face" = "🤑",
|
||||||
"astonished" = "\xf0\x9f\x98\xb2",
|
"astonished" = "😲",
|
||||||
"white_frowning_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
"white_frowning_face" = "☹️",
|
||||||
"slightly_sad_face" = "\xe2\x98\xb9\xef\xb8\x8f",
|
"slightly_sad_face" = "☹️",
|
||||||
"confounded" = "\xf0\x9f\x98\x96",
|
"confounded" = "😖",
|
||||||
"disappointed" = "\xf0\x9f\x98\x9e",
|
"disappointed" = "😞",
|
||||||
"worried" = "\xf0\x9f\x98\x9f",
|
"worried" = "😟",
|
||||||
"triumph" = "\xf0\x9f\x98\xa4",
|
"triumph" = "😤",
|
||||||
"cry" = "\xf0\x9f\x98\xa2",
|
"cry" = "😢",
|
||||||
"sob" = "\xf0\x9f\x98\xad",
|
"sob" = "😭",
|
||||||
"frowning" = "\xf0\x9f\x98\xa6",
|
"frowning" = "😦",
|
||||||
"frowning_face" = "\xf0\x9f\x99\x81",
|
"frowning_face" = "🙁",
|
||||||
"anguished" = "\xf0\x9f\x98\xa7",
|
"anguished" = "😧",
|
||||||
"fearful" = "\xf0\x9f\x98\xa8",
|
"fearful" = "😨",
|
||||||
"weary" = "\xf0\x9f\x98\xa9",
|
"weary" = "😩",
|
||||||
"grimacing" = "\xf0\x9f\x98\xac",
|
"grimacing" = "😬",
|
||||||
"cold_sweat" = "\xf0\x9f\x98\xb0",
|
"cold_sweat" = "😰",
|
||||||
"scream" = "\xf0\x9f\x98\xb1",
|
"scream" = "😱",
|
||||||
"flushed" = "\xf0\x9f\x98\xb3",
|
"flushed" = "😳",
|
||||||
"dizzy_face" = "\xf0\x9f\x98\xb5",
|
"dizzy_face" = "😵",
|
||||||
"rage" = "\xf0\x9f\x98\xa1",
|
"rage" = "😡",
|
||||||
"angry" = "\xf0\x9f\x98\xa0",
|
"angry" = "😠",
|
||||||
"innocent" = "\xf0\x9f\x98\x87",
|
"innocent" = "😇",
|
||||||
"cowboy_hat_face" = "\xf0\x9f\xa4\xa0",
|
"cowboy_hat_face" = "🤠",
|
||||||
"clown_face" = "\xf0\x9f\xa4\xa1",
|
"clown_face" = "🤡",
|
||||||
"mask" = "\xf0\x9f\x98\xb7",
|
"mask" = "😷",
|
||||||
"thermometer_face" = "\xf0\x9f\xa4\x92",
|
"thermometer_face" = "🤒",
|
||||||
"head_bandage" = "\xf0\x9f\xa4\x95",
|
"head_bandage" = "🤕",
|
||||||
"nauseated_face" = "\xf0\x9f\xa4\xa2",
|
"nauseated_face" = "🤢",
|
||||||
"sneezing_face" = "\xf0\x9f\xa4\xa7",
|
"sneezing_face" = "🤧",
|
||||||
"smiling_imp" = "\xf0\x9f\x98\x88",
|
"smiling_imp" = "😈",
|
||||||
"imp" = "\xf0\x9f\x91\xbf",
|
"imp" = "👿",
|
||||||
"shrug" = "\xc2\xaf\\_(\xe3\x83\x84)_/\xc2\xaf",
|
"shrug" = "¯\\_(ツ)_/¯",
|
||||||
"facepalm" = "\xf0\x9f\xa4\xa6",
|
"facepalm" = "🤦",
|
||||||
"facepunch" = "\xf0\x9f\x91\x8a",
|
"facepunch" = "👊",
|
||||||
"wave" = "\xf0\x9f\x91\x8b",
|
"wave" = "👋",
|
||||||
"ok_hand" = "\xf0\x9f\x91\x8c",
|
"ok_hand" = "👌",
|
||||||
"thumbsup" = "\xf0\x9f\x91\x8d",
|
"thumbsup" = "👍",
|
||||||
"thumbsdown" = "\xf0\x9f\x91\x8e",
|
"thumbsdown" = "👎",
|
||||||
"clap" = "\xf0\x9f\x91\x8f",
|
"clap" = "👏",
|
||||||
"pray" = "\xf0\x9f\x99\x8f",
|
"pray" = "🙏",
|
||||||
"point_up" = "\xe2\x98\x9d\xef\xb8\x8f",
|
"point_up" = "☝️",
|
||||||
"point_down" = "\xf0\x9f\x91\x87",
|
"point_down" = "👇",
|
||||||
"point_left" = "\xf0\x9f\x91\x88",
|
"point_left" = "👈",
|
||||||
"point_right" = "\xf0\x9f\x89\x89",
|
"point_right" = "👉",
|
||||||
"v" = "\xe2\x9c\x8c\xef\xb8\x8f",
|
"v" = "✌️",
|
||||||
"raised_hands" = "\xf0\x9f\x99\x8c",
|
"raised_hands" = "🙌",
|
||||||
"muscle" = "\xf0\x9f\x92\xaa",
|
"muscle" = "💪",
|
||||||
"fist" = "\xe2\x9c\x8a",
|
"fist" = "✊",
|
||||||
"hand" = "\xe2\x9c\x8b",
|
"hand" = "✋",
|
||||||
|
|
||||||
// Hearts & symbols
|
// Hearts & symbols
|
||||||
"heart" = "\xe2\x9d\xa4\xef\xb8\x8f",
|
"heart" = "❤️",
|
||||||
"orange_heart" = "\xf0\x9f\xa7\xa1",
|
"orange_heart" = "🧡",
|
||||||
"yellow_heart" = "\xf0\x9f\x92\x9b",
|
"yellow_heart" = "💛",
|
||||||
"green_heart" = "\xf0\x9f\x92\x9a",
|
"green_heart" = "💚",
|
||||||
"blue_heart" = "\xf0\x9f\x92\x99",
|
"blue_heart" = "💙",
|
||||||
"purple_heart" = "\xf0\x9f\x92\x9c",
|
"purple_heart" = "💜",
|
||||||
"broken_heart" = "\xf0\x9f\x92\x94",
|
"broken_heart" = "💔",
|
||||||
"sparkling_heart" = "\xf0\x9f\x92\x96",
|
"sparkling_heart" = "💖",
|
||||||
"100" = "\xf0\x9f\x92\xaf",
|
"100" = "💯",
|
||||||
"anger" = "\xf0\x9f\x92\xa2",
|
"anger" = "💢",
|
||||||
"checkered_flag" = "\xf0\x9f\x8f\x81",
|
"checkered_flag" = "🏁",
|
||||||
"crossed_flags" = "\xf0\x9f\x9a\xa9",
|
"crossed_flags" = "🚩",
|
||||||
"rocket" = "\xf0\x9f\x9a\x80",
|
"rocket" = "🚀",
|
||||||
"star" = "\xe2\xad\x90",
|
"star" = "⭐",
|
||||||
"star2" = "\xf0\x9f\x8c\x9f",
|
"star2" = "🌟",
|
||||||
"sparkles" = "\xe2\x9c\xa8",
|
"sparkles" = "✨",
|
||||||
"boom" = "\xf0\x9f\x92\xa5",
|
"boom" = "💥",
|
||||||
"exclamation" = "\xe2\x9d\x97",
|
"exclamation" = "❗",
|
||||||
"question" = "\xe2\x9d\x93",
|
"question" = "❓",
|
||||||
"grey_exclamation" = "\xe2\x9d\x95",
|
"grey_exclamation" = "❕",
|
||||||
"grey_question" = "\xe2\x9d\x94",
|
"grey_question" = "❔",
|
||||||
"zzz" = "\xf0\x9f\x92\xa4",
|
"zzz" = "💤",
|
||||||
"warning" = "\xe2\x9a\xa0\xef\xb8\x8f",
|
"warning" = "⚠️",
|
||||||
"no_entry_sign" = "\xf0\x9f\x9a\xab",
|
"no_entry_sign" = "🚫",
|
||||||
"no_entry" = "\xe2\x9b\x94",
|
"no_entry" = "⛔",
|
||||||
"white_check_mark" = "\xe2\x9c\x85",
|
"white_check_mark" = "✅",
|
||||||
"negative_squared_cross_mark" = "\xe2\x9d\x8e",
|
"negative_squared_cross_mark" = "❎",
|
||||||
"x" = "\xe2\x9d\x8c",
|
"x" = "❌",
|
||||||
"o" = "\xe2\xad\x95",
|
"o" = "⭕",
|
||||||
"heavy_plus_sign" = "\xe2\x9e\x95",
|
"heavy_plus_sign" = "➕",
|
||||||
"heavy_minus_sign" = "\xe2\x9e\x96",
|
"heavy_minus_sign" = "➖",
|
||||||
"heavy_division_sign" = "\xe2\x9e\x97",
|
"heavy_division_sign" = "➗",
|
||||||
"copyright" = "\xc2\xa9\xef\xb8\x8f",
|
"copyright" = "©️",
|
||||||
"registered" = "\xc2\xae\xef\xb8\x8f",
|
"registered" = "®️",
|
||||||
"tm" = "\xe2\x84\xa2\xef\xb8\x8f",
|
"tm" = "™️",
|
||||||
|
|
||||||
// Nature & weather
|
// Nature & weather
|
||||||
"fire" = "\xf0\x9f\x94\xa5",
|
"fire" = "🔥",
|
||||||
"zap" = "\xe2\x9a\xa1",
|
"zap" = "⚡",
|
||||||
"sunny" = "\xe2\x98\x80\xef\xb8\x8f",
|
"sunny" = "☀️",
|
||||||
"cloud" = "\xe2\x98\x81\xef\xb8\x8f",
|
"cloud" = "☁️",
|
||||||
"rainbow" = "\xf0\x9f\x8c\x88",
|
"rainbow" = "🌈",
|
||||||
"snowflake" = "\xe2\x9d\x84\xef\xb8\x8f",
|
"snowflake" = "❄️",
|
||||||
"dash" = "\xf0\x9f\x92\xa8",
|
"dash" = "💨",
|
||||||
"tornado" = "\xf0\x9f\x8c\xaa\xef\xb8\x8f",
|
"tornado" = "🌪️",
|
||||||
"deciduous_tree" = "\xf0\x9f\x8c\xb3",
|
"deciduous_tree" = "🌳",
|
||||||
"evergreen_tree" = "\xf0\x9f\x8c\xb2",
|
"evergreen_tree" = "🌲",
|
||||||
"palm_tree" = "\xf0\x9f\x8c\xb4",
|
"palm_tree" = "🌴",
|
||||||
"cactus" = "\xf0\x9f\x8c\xb5",
|
"cactus" = "🌵",
|
||||||
"tulip" = "\xf0\x9f\x8c\xb7",
|
"tulip" = "🌷",
|
||||||
"rose" = "\xf0\x9f\x8c\xb9",
|
"rose" = "🌹",
|
||||||
"sunflower" = "\xf0\x9f\x8c\xbb",
|
"sunflower" = "🌻",
|
||||||
"hibiscus" = "\xf0\x9f\x8c\xba",
|
"hibiscus" = "🌺",
|
||||||
"earth_africa" = "\xf0\x9f\x8c\x8d",
|
"earth_africa" = "🌍",
|
||||||
"earth_americas" = "\xf0\x9f\x8c\x8e",
|
"earth_americas" = "🌎",
|
||||||
"earth_asia" = "\xf0\x9f\x8c\x8f",
|
"earth_asia" = "🌏",
|
||||||
"new_moon" = "\xf0\x9f\x8c\x9a",
|
"new_moon" = "🌑",
|
||||||
"full_moon" = "\xf0\x9f\x8c\x95",
|
"full_moon" = "🌕",
|
||||||
|
|
||||||
// Food & drink
|
// Food & drink
|
||||||
"coffee" = "\xe2\x98\x95",
|
"coffee" = "☕",
|
||||||
"tea" = "\xf0\x9f\x8d\xb5",
|
"tea" = "🍵",
|
||||||
"beer" = "\xf0\x9f\x8d\xba",
|
"beer" = "🍺",
|
||||||
"beers" = "\xf0\x9f\x8d\xbb",
|
"beers" = "🍻",
|
||||||
"wine_glass" = "\xf0\x9f\x8d\xb7",
|
"wine_glass" = "🍷",
|
||||||
"tropical_drink" = "\xf0\x9f\x8d\xb9",
|
"tropical_drink" = "🍹",
|
||||||
"apple" = "\xf0\x9f\x8d\x8e",
|
"apple" = "🍎",
|
||||||
"green_apple" = "\xf0\x9f\x8d\x8f",
|
"green_apple" = "🍏",
|
||||||
"orange" = "\xf0\x9f\x8d\x8a",
|
"orange" = "🍊",
|
||||||
"lemon" = "\xf0\x9f\x8d\x8b",
|
"lemon" = "🍋",
|
||||||
"banana" = "\xf0\x9f\x8d\x8c",
|
"banana" = "🍌",
|
||||||
"watermelon" = "\xf0\x9f\x8d\x89",
|
"watermelon" = "🍉",
|
||||||
"grapes" = "\xf0\x9f\x8d\x87",
|
"grapes" = "🍇",
|
||||||
"strawberry" = "\xf0\x9f\x8d\x93",
|
"strawberry" = "🍓",
|
||||||
"melon" = "\xf0\x9f\x8d\x88",
|
"melon" = "🍈",
|
||||||
"cherries" = "\xf0\x9f\x8d\x92",
|
"cherries" = "🍒",
|
||||||
"peach" = "\xf0\x9f\x8d\x91",
|
"peach" = "🍑",
|
||||||
"pineapple" = "\xf0\x9f\x8d\x8d",
|
"pineapple" = "🍍",
|
||||||
"pizza" = "\xf0\x9f\x8d\x95",
|
"pizza" = "🍕",
|
||||||
"hamburger" = "\xf0\x9f\x8d\x94",
|
"hamburger" = "🍔",
|
||||||
"hotdog" = "\xf0\x9f\x8c\xad",
|
"hotdog" = "🌭",
|
||||||
"taco" = "\xf0\x9f\x8c\xae",
|
"taco" = "🌮",
|
||||||
"burrito" = "\xf0\x9f\x8c\xaf",
|
"burrito" = "🌯",
|
||||||
"cake" = "\xf0\x9f\x8d\xb0",
|
"cake" = "🍰",
|
||||||
"cookie" = "\xf0\x9f\x8d\xaa",
|
"cookie" = "🍪",
|
||||||
"chocolate_bar" = "\xf0\x9f\x8d\xab",
|
"chocolate_bar" = "🍫",
|
||||||
"candy" = "\xf0\x9f\x8d\xac",
|
"candy" = "🍬",
|
||||||
"popcorn" = "\xf0\x9f\x8d\xbf",
|
"popcorn" = "🍿",
|
||||||
|
|
||||||
// Animals
|
// Animals
|
||||||
"dog" = "\xf0\x9f\x90\xb6",
|
"dog" = "🐶",
|
||||||
"cat" = "\xf0\x9f\x90\xb1",
|
"cat" = "🐱",
|
||||||
"mouse" = "\xf0\x9f\x90\xad",
|
"mouse" = "🐭",
|
||||||
"hamster" = "\xf0\x9f\x90\xb9",
|
"hamster" = "🐹",
|
||||||
"rabbit" = "\xf0\x9f\x90\xb0",
|
"rabbit" = "🐰",
|
||||||
"bear" = "\xf0\x9f\x90\xbb",
|
"bear" = "🐻",
|
||||||
"panda_face" = "\xf0\x9f\x90\xbc",
|
"panda_face" = "🐼",
|
||||||
"koala" = "\xf0\x9f\x90\xa8",
|
"koala" = "🐨",
|
||||||
"tiger" = "\xf0\x9f\x90\xaf",
|
"tiger" = "🐯",
|
||||||
"lion_face" = "\xf0\x9f\xa6\x81",
|
"lion_face" = "🦁",
|
||||||
"cow" = "\xf0\x9f\x90\xae",
|
"cow" = "🐮",
|
||||||
"pig" = "\xf0\x9f\x90\xb7",
|
"pig" = "🐷",
|
||||||
"frog" = "\xf0\x9f\x90\xb8",
|
"frog" = "🐸",
|
||||||
"monkey_face" = "\xf0\x9f\x90\xb5",
|
"monkey_face" = "🐵",
|
||||||
"see_no_evil" = "\xf0\x9f\x99\x88",
|
"see_no_evil" = "🙈",
|
||||||
"hear_no_evil" = "\xf0\x9f\x99\x89",
|
"hear_no_evil" = "🙉",
|
||||||
"speak_no_evil" = "\xf0\x9f\x99\x8a",
|
"speak_no_evil" = "🙊",
|
||||||
"owl" = "\xf0\x9f\xa6\x89",
|
"owl" = "🦉",
|
||||||
"bat" = "\xf0\x9f\xa6\x87",
|
"bat" = "🦇",
|
||||||
"wolf" = "\xf0\x9f\x90\xba",
|
"wolf" = "🐺",
|
||||||
"boar" = "\xf0\x9f\x90\x97",
|
"boar" = "🐗",
|
||||||
"horse" = "\xf0\x9f\x90\x8e",
|
"horse" = "🐴",
|
||||||
"unicorn" = "\xf0\x9f\xa6\x84",
|
"unicorn" = "🦄",
|
||||||
"honeybee" = "\xf0\x9f\x90\x9d",
|
"honeybee" = "🐝",
|
||||||
"bug" = "\xf0\x9f\x90\x9b",
|
"bug" = "🐛",
|
||||||
"snail" = "\xf0\x9f\x90\x8c",
|
"snail" = "🐌",
|
||||||
"beetle" = "\xf0\x9f\x90\x9e",
|
"beetle" = "🐞",
|
||||||
"ant" = "\xf0\x9f\x90\x9c",
|
"ant" = "🐜",
|
||||||
"spider" = "\xf0\x9f\x95\xb7",
|
"spider" = "🕷",
|
||||||
"scorpion" = "\xf0\x9f\xa6\x82",
|
"scorpion" = "🦂",
|
||||||
"turtle" = "\xf0\x9f\x90\xa2",
|
"turtle" = "🐢",
|
||||||
"snake" = "\xf0\x9f\x90\x8d",
|
"snake" = "🐍",
|
||||||
"octopus" = "\xf0\x9f\x90\x99",
|
"octopus" = "🐙",
|
||||||
"shell" = "\xf0\x9f\x90\x9a",
|
"shell" = "🐚",
|
||||||
"whale" = "\xf0\x9f\x90\xb3",
|
"whale" = "🐳",
|
||||||
"dolphin" = "\xf0\x9f\x90\xac",
|
"dolphin" = "🐬",
|
||||||
"fish" = "\xf0\x9f\x90\x9f",
|
"fish" = "🐟",
|
||||||
"tropical_fish" = "\xf0\x9f\x90\xa0",
|
"tropical_fish" = "🐠",
|
||||||
"blowfish" = "\xf0\x9f\x90\xa1",
|
"blowfish" = "🐡",
|
||||||
"penguin" = "\xf0\x9f\x90\xa7",
|
"penguin" = "🐧",
|
||||||
"chicken" = "\xf0\x9f\x90\x94",
|
"chicken" = "🐔",
|
||||||
"bird" = "\xf0\x9f\x90\xa6",
|
"bird" = "🐦",
|
||||||
"eagle" = "\xf0\x9f\xa6\x85",
|
"eagle" = "🦅",
|
||||||
|
|
||||||
// Objects
|
// Objects
|
||||||
"computer" = "\xf0\x9f\x92\xbb",
|
"computer" = "💻",
|
||||||
"iphone" = "\xf0\x9f\x93\xb1",
|
"iphone" = "📱",
|
||||||
"keyboard" = "\xe2\x8c\xa8\xef\xb8\x8f",
|
"keyboard" = "⌨️",
|
||||||
"desktop_computer" = "\xf0\x9f\x96\xa5",
|
"desktop_computer" = "🖥",
|
||||||
"printer" = "\xf0\x9f\x96\xa8",
|
"printer" = "🖨",
|
||||||
"mouse_three_button" = "\xf0\x9f\x96\xb1",
|
"mouse_three_button" = "🖱",
|
||||||
"joystick" = "\xf0\x9f\x95\xb9",
|
"joystick" = "🕹",
|
||||||
"stash" = "\xf0\x9f\x92\xbe",
|
"stash" = "💾",
|
||||||
"floppy_disk" = "\xf0\x9f\x92\xbe",
|
"floppy_disk" = "💾",
|
||||||
"cd" = "\xf0\x9f\x92\xbf",
|
"cd" = "💿",
|
||||||
"dvd" = "\xf0\x9f\x93\x80",
|
"dvd" = "📀",
|
||||||
"vhs" = "\xf0\x9f\x93\xbc",
|
"vhs" = "📼",
|
||||||
"camera" = "\xf0\x9f\x93\xb7",
|
"camera" = "📷",
|
||||||
"video_camera" = "\xf0\x9f\x93\xb9",
|
"video_camera" = "📹",
|
||||||
"tv" = "\xf0\x9f\x93\xba",
|
"tv" = "📺",
|
||||||
"radio" = "\xf0\x9f\x93\xbb",
|
"radio" = "📻",
|
||||||
"pager" = "\xf0\x9f\x93\x9f",
|
"pager" = "📟",
|
||||||
"telephone" = "\xe2\x98\x8e\xef\xb8\x8f",
|
"telephone" = "☎️",
|
||||||
"fax" = "\xf0\x9f\x93\xa0",
|
"fax" = "📠",
|
||||||
"bulb" = "\xf0\x9f\x92\xa1",
|
"bulb" = "💡",
|
||||||
"candle" = "\xf0\x9f\x95\xaf",
|
"candle" = "🕯",
|
||||||
"bathtub" = "\xf0\x9f\x9b\x81",
|
"bathtub" = "🛁",
|
||||||
"shower" = "\xf0\x9f\x9a\xbf",
|
"shower" = "🚿",
|
||||||
"toilet" = "\xf0\x9f\x9a\xbd",
|
"toilet" = "🚽",
|
||||||
"wrench" = "\xf0\x9f\x94\xa7",
|
"wrench" = "🔧",
|
||||||
"hammer" = "\xf0\x9f\x94\xa8",
|
"hammer" = "🔨",
|
||||||
"nut_and_bolt" = "\xf0\x9f\x94\xa9",
|
"nut_and_bolt" = "🔩",
|
||||||
"gear" = "\xe2\x9a\x99\xef\xb8\x8f",
|
"gear" = "⚙️",
|
||||||
"link" = "\xf0\x9f\x94\x97",
|
"link" = "🔗",
|
||||||
"lock" = "\xf0\x9f\x94\x92",
|
"lock" = "🔒",
|
||||||
"unlock" = "\xf0\x9f\x94\x93",
|
"unlock" = "🔓",
|
||||||
"key" = "\xf0\x9f\x94\x91",
|
"key" = "🔑",
|
||||||
"bell" = "\xf0\x9f\x94\x94",
|
"bell" = "🔔",
|
||||||
"bookmark" = "\xf0\x9f\x94\x96",
|
"bookmark" = "🔖",
|
||||||
"pushpin" = "\xf0\x9f\x93\x8c",
|
"pushpin" = "📌",
|
||||||
"paperclip" = "\xf0\x9f\x93\x8e",
|
"paperclip" = "📎",
|
||||||
"memo" = "\xf0\x9f\x93\x9d",
|
"memo" = "📝",
|
||||||
"pencil2" = "\xe2\x9c\x8f\xef\xb8\x8f",
|
"pencil2" = "✏️",
|
||||||
"black_nib" = "\xe2\x9c\x92\xef\xb8\x8f",
|
"black_nib" = "✒️",
|
||||||
"pen" = "\xf0\x9f\x96\x8a",
|
"pen" = "🖊",
|
||||||
"paintbrush" = "\xf0\x9f\x96\x8c",
|
"paintbrush" = "🖌",
|
||||||
"crayon" = "\xf0\x9f\x96\x8d",
|
"crayon" = "🖍",
|
||||||
"book" = "\xf0\x9f\x93\x97",
|
"book" = "📖",
|
||||||
"books" = "\xf0\x9f\x93\x9a",
|
"books" = "📚",
|
||||||
"ledger" = "\xf0\x9f\x93\x92",
|
"ledger" = "📒",
|
||||||
"notebook" = "\xf0\x9f\x93\x93",
|
"notebook" = "📓",
|
||||||
"scroll" = "\xf0\x9f\x93\x9c",
|
"scroll" = "📜",
|
||||||
"page_with_curl" = "\xf0\x9f\x93\x84",
|
"page_with_curl" = "📄",
|
||||||
"newspaper" = "\xf0\x9f\x93\xb0",
|
"newspaper" = "📰",
|
||||||
"chart_with_upwards_trend" = "\xf0\x9f\x93\x88",
|
"chart_with_upwards_trend" = "📈",
|
||||||
"chart_with_downwards_trend" = "\xf0\x9f\x93\x89",
|
"chart_with_downwards_trend" = "📉",
|
||||||
"bar_chart" = "\xf0\x9f\x93\x8a",
|
"bar_chart" = "📊",
|
||||||
"calendar" = "\xf0\x9f\x93\x85",
|
"calendar" = "📅",
|
||||||
"date" = "\xf0\x9f\x93\x85",
|
"date" = "📅",
|
||||||
"hourglass" = "\xe2\x8f\xb3",
|
"hourglass" = "⌛",
|
||||||
"hourglass_flowing_sand" = "\xe2\x8f\xb3",
|
"hourglass_flowing_sand" = "⏳",
|
||||||
"clock" = "\xf0\x9f\x95\x90",
|
"clock" = "🕐",
|
||||||
"alarm_clock" = "\xe2\x8f\xb0",
|
"alarm_clock" = "⏰",
|
||||||
"stopwatch" = "\xe2\x8f\xb1",
|
"stopwatch" = "⏱",
|
||||||
"watch" = "\xe2\x8c\x9a",
|
"watch" = "⌚",
|
||||||
"moneybag" = "\xf0\x9f\x92\xb0",
|
"moneybag" = "💰",
|
||||||
"yen" = "\xf0\x9f\x92\xb4",
|
"yen" = "💴",
|
||||||
"dollar" = "\xf0\x9f\x92\xb5",
|
"dollar" = "💵",
|
||||||
"euro" = "\xf0\x9f\x92\xb6",
|
"euro" = "💶",
|
||||||
"pound" = "\xf0\x9f\x92\xb7",
|
"pound" = "💷",
|
||||||
"money_with_wings" = "\xf0\x9f\x92\xb8",
|
"money_with_wings" = "💸",
|
||||||
"credit_card" = "\xf0\x9f\x92\xb3",
|
"credit_card" = "💳",
|
||||||
"gem" = "\xf0\x9f\x92\x8e",
|
"gem" = "💎",
|
||||||
"bomb" = "\xf0\x9f\x92\xa3",
|
"bomb" = "💣",
|
||||||
"gift" = "\xf0\x9f\x8e\x81",
|
"gift" = "🎁",
|
||||||
"balloon" = "\xf0\x9f\x8e\x88",
|
"balloon" = "🎈",
|
||||||
"tada" = "\xf0\x9f\x8e\x89",
|
"tada" = "🎉",
|
||||||
"confetti_ball" = "\xf0\x9f\x8e\x8a",
|
"confetti_ball" = "🎊",
|
||||||
"package" = "\xf0\x9f\x93\xa6",
|
"package" = "📦",
|
||||||
"mailbox" = "\xf0\x9f\x93\xab",
|
"mailbox" = "📫",
|
||||||
"inbox_tray" = "\xf0\x9f\x93\xa5",
|
"inbox_tray" = "📥",
|
||||||
"outbox_tray" = "\xf0\x9f\x93\xa4",
|
"outbox_tray" = "📤",
|
||||||
"email" = "\xe2\x9c\x89\xef\xb8\x8f",
|
"email" = "✉️",
|
||||||
"envelope" = "\xe2\x9c\x89\xef\xb8\x8f",
|
"envelope" = "✉️",
|
||||||
"incoming_envelope" = "\xf0\x9f\x93\xa8",
|
"incoming_envelope" = "📨",
|
||||||
|
|
||||||
// Activities
|
// Activities
|
||||||
"soccer" = "\xe2\x9a\xbd",
|
"soccer" = "⚽",
|
||||||
"basketball" = "\xf0\x9f\x8f\x80",
|
"basketball" = "🏀",
|
||||||
"football" = "\xf0\x9f\x8f\x88",
|
"football" = "🏈",
|
||||||
"baseball" = "\xe2\x9a\xbe",
|
"baseball" = "⚾",
|
||||||
"tennis" = "\xf0\x9f\x8e\xbe",
|
"tennis" = "🎾",
|
||||||
"8ball" = "\xf0\x9f\x8e\xb1",
|
"8ball" = "🎱",
|
||||||
"bowling" = "\xf0\x9f\x8e\xb3",
|
"bowling" = "🎳",
|
||||||
"video_game" = "\xf0\x9f\x8e\xae",
|
"video_game" = "🎮",
|
||||||
"dart" = "\xf0\x9f\x8e\xaf",
|
"dart" = "🎯",
|
||||||
"game_die" = "\xf0\x9f\x8e\xb2",
|
"game_die" = "🎲",
|
||||||
"slot_machine" = "\xf0\x9f\x8e\xb0",
|
"slot_machine" = "🎰",
|
||||||
"cards" = "\xf0\x9f\x83\x8f",
|
"cards" = "🃏",
|
||||||
"black_joker" = "\xf0\x9f\x83\x9f",
|
"black_joker" = "🃟",
|
||||||
"mahjong" = "\xf0\x9f\x80\x84",
|
"mahjong" = "🀄",
|
||||||
"musical_note" = "\xf0\x9f\x8e\xb5",
|
"musical_note" = "🎵",
|
||||||
"notes" = "\xf0\x9f\x8e\xb6",
|
"notes" = "🎶",
|
||||||
"saxophone" = "\xf0\x9f\x8e\xb7",
|
"saxophone" = "🎷",
|
||||||
"guitar" = "\xf0\x9f\x8e\xb8",
|
"guitar" = "🎸",
|
||||||
"musical_keyboard" = "\xf0\x9f\x8e\xb9",
|
"musical_keyboard" = "🎹",
|
||||||
"trumpet" = "\xf0\x9f\x8e\xba",
|
"trumpet" = "🎺",
|
||||||
"violin" = "\xf0\x9f\x8e\xbb",
|
"violin" = "🎻",
|
||||||
"drum" = "\xf0\x9f\xa5\x81",
|
"drum" = "🥁",
|
||||||
"headphones" = "\xf0\x9f\x8e\xa7",
|
"headphones" = "🎧",
|
||||||
"microphone" = "\xf0\x9f\x8e\xa4",
|
"microphone" = "🎤",
|
||||||
"level_slider" = "\xf0\x9f\x8e\x9a",
|
"level_slider" = "🎚",
|
||||||
"control_knobs" = "\xf0\x9f\x8e\x9b",
|
"control_knobs" = "🎛",
|
||||||
"ticket" = "\xf0\x9f\x8e\xab",
|
"ticket" = "🎫",
|
||||||
"art" = "\xf0\x9f\x8e\xa8",
|
"art" = "🎨",
|
||||||
"circus_tent" = "\xf0\x9f\x8e\xaa",
|
"circus_tent" = "🎪",
|
||||||
"theater_masks" = "\xf0\x9f\x8e\xad",
|
"theater_masks" = "🎭",
|
||||||
"clapper" = "\xf0\x9f\x8e\xac",
|
"clapper" = "🎬",
|
||||||
|
|
||||||
// Travel
|
// Travel
|
||||||
"car" = "\xf0\x9f\x9a\x97",
|
"car" = "🚗",
|
||||||
"taxi" = "\xf0\x9f\x9a\x95",
|
"taxi" = "🚕",
|
||||||
"bus" = "\xf0\x9f\x9a\x8d",
|
"bus" = "🚍",
|
||||||
"train" = "\xf0\x9f\x9a\x86",
|
"train" = "🚆",
|
||||||
"metro" = "\xf0\x9f\x9a\x87",
|
"metro" = "🚇",
|
||||||
"light_rail" = "\xf0\x9f\x9a\x88",
|
"light_rail" = "🚈",
|
||||||
"tram" = "\xf0\x9f\x9a\x8a",
|
"tram" = "🚊",
|
||||||
"bike" = "\xf0\x9f\x9a\xb2",
|
"bike" = "🚲",
|
||||||
"motorcycle" = "\xf0\x9f\x8f\x8d",
|
"motorcycle" = "🏍",
|
||||||
"airplane" = "\xe2\x9c\x88\xef\xb8\x8f",
|
"airplane" = "✈️",
|
||||||
"helicopter" = "\xf0\x9f\x9a\x81",
|
"helicopter" = "🚁",
|
||||||
"boat" = "\xe2\x9b\xb5",
|
"boat" = "⛵",
|
||||||
"sailboat" = "\xe2\x9b\xb5",
|
"sailboat" = "⛵",
|
||||||
"ship" = "\xf0\x9f\x9a\xa2",
|
"ship" = "🚢",
|
||||||
"fuelpump" = "\xe2\x9b\xbd",
|
"fuelpump" = "⛽",
|
||||||
"construction" = "\xf0\x9f\x9a\xa7",
|
"construction" = "🚧",
|
||||||
"house" = "\xf0\x9f\x8f\xa0",
|
"house" = "🏠",
|
||||||
"house_with_garden" = "\xf0\x9f\x8f\xa1",
|
"house_with_garden" = "🏡",
|
||||||
"office" = "\xf0\x9f\x8f\xa2",
|
"office" = "🏢",
|
||||||
"post_office" = "\xf0\x9f\x8f\xa3",
|
"post_office" = "🏣",
|
||||||
"hospital" = "\xf0\x9f\x8f\xa5",
|
"hospital" = "🏥",
|
||||||
"bank" = "\xf0\x9f\x8f\xa6",
|
"bank" = "🏦",
|
||||||
"hotel" = "\xf0\x9f\x8f\xa8",
|
"hotel" = "🏨",
|
||||||
"school" = "\xf0\x9f\x8f\xab",
|
"school" = "🏫",
|
||||||
"department_store" = "\xf0\x9f\x8f\xac",
|
"department_store" = "🏬",
|
||||||
"church" = "\xe2\x9b\xaa",
|
"church" = "⛪",
|
||||||
"castle" = "\xf0\x9f\x8f\xb0",
|
"castle" = "🏰",
|
||||||
"factory" = "\xf0\x9f\x8f\xad",
|
"factory" = "🏭",
|
||||||
"tokyo_tower" = "\xf0\x9f\x97\xbc",
|
"tokyo_tower" = "🗼",
|
||||||
"statue_of_liberty" = "\xf0\x9f\x97\xbd",
|
"statue_of_liberty" = "🗽",
|
||||||
"fountain" = "\xe2\x9b\xb2",
|
"fountain" = "⛲",
|
||||||
"tent" = "\xe2\x9b\xba",
|
"tent" = "⛺",
|
||||||
"mountain" = "\xf0\x9f\x8f\x94",
|
"mountain" = "⛰️",
|
||||||
"snow_capped_mountain" = "\xf0\x9f\x8f\x94",
|
"snow_capped_mountain" = "🏔",
|
||||||
"beach" = "\xf0\x9f\x8f\x96",
|
"beach" = "🏖",
|
||||||
"camping" = "\xf0\x9f\x8f\x95",
|
"camping" = "🏕",
|
||||||
"world_map" = "\xf0\x9f\x97\xba",
|
"world_map" = "🗺",
|
||||||
"japan" = "\xf0\x9f\x97\xbe",
|
"japan" = "🗾",
|
||||||
};
|
}
|
||||||
|
|
||||||
expand_emoji :: proc(text: string) -> string {
|
expand_emoji :: proc(text: string) -> string {
|
||||||
if len(EMOJIS) == 0 {
|
assert(len(EMOJIS) > 0)
|
||||||
return text
|
|
||||||
}
|
|
||||||
|
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make()
|
||||||
defer strings.builder_destroy(&sb)
|
defer strings.builder_destroy(&sb)
|
||||||
@@ -403,11 +401,15 @@ expand_emoji :: proc(text: string) -> string {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
shortcode := remaining[colon + 1 : colon + 1 + end]
|
shortcode := remaining[colon + 1:colon + 1 + end]
|
||||||
|
|
||||||
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 != '-' {
|
||||||
valid = false
|
valid = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -432,3 +434,4 @@ expand_emoji :: proc(text: string) -> string {
|
|||||||
|
|
||||||
return strings.to_string(sb)
|
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
|
found = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if !found {
|
if found {
|
||||||
|
return strings.to_string(sb)
|
||||||
|
} else {
|
||||||
return html
|
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.
|
// 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.
|
// 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) {
|
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, "<>")
|
||||||
|
}
|
||||||
|
|
||||||
+47
-9
@@ -48,6 +48,7 @@ Node :: struct {
|
|||||||
indent: string,
|
indent: string,
|
||||||
first_child: int,
|
first_child: int,
|
||||||
child_count: int,
|
child_count: int,
|
||||||
|
content: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
// node_span returns the number of flat-array entries a node occupies:
|
// node_span returns the number of flat-array entries a node occupies:
|
||||||
@@ -103,7 +104,7 @@ parse :: proc(
|
|||||||
return {}, terr
|
return {}, terr
|
||||||
}
|
}
|
||||||
|
|
||||||
tmpl.nodes, err = parse_tokens(tokens[:], allocator)
|
tmpl.nodes, err = parse_tokens(tokens[:], source, allocator)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
delete(tmpl.nodes)
|
delete(tmpl.nodes)
|
||||||
return {}, err
|
return {}, err
|
||||||
@@ -148,6 +149,7 @@ render :: proc(
|
|||||||
|
|
||||||
parse_tokens :: proc(
|
parse_tokens :: proc(
|
||||||
tokens: []Token,
|
tokens: []Token,
|
||||||
|
source: string,
|
||||||
allocator := context.allocator,
|
allocator := context.allocator,
|
||||||
) -> (
|
) -> (
|
||||||
nodes: [dynamic]Node,
|
nodes: [dynamic]Node,
|
||||||
@@ -155,7 +157,7 @@ parse_tokens :: proc(
|
|||||||
) {
|
) {
|
||||||
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
nodes = make([dynamic]Node, 0, len(tokens), allocator)
|
||||||
pos := 0
|
pos := 0
|
||||||
err = parse_section(tokens, &pos, &nodes, "")
|
err = parse_section(tokens, &pos, &nodes, "", source)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -164,6 +166,7 @@ parse_section :: proc(
|
|||||||
pos: ^int,
|
pos: ^int,
|
||||||
nodes: ^[dynamic]Node,
|
nodes: ^[dynamic]Node,
|
||||||
end_tag: string,
|
end_tag: string,
|
||||||
|
source: string,
|
||||||
) -> Render_Error {
|
) -> Render_Error {
|
||||||
for pos^ < len(tokens) {
|
for pos^ < len(tokens) {
|
||||||
tok := tokens[pos^]
|
tok := tokens[pos^]
|
||||||
@@ -187,18 +190,28 @@ parse_section :: proc(
|
|||||||
case .Section_Open:
|
case .Section_Open:
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
idx := len(nodes)
|
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})
|
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].first_child = idx + 1
|
||||||
nodes[idx].child_count = len(nodes) - idx - 1
|
nodes[idx].child_count = len(nodes) - idx - 1
|
||||||
|
nodes[idx].content = source[content_start:close_pos]
|
||||||
|
|
||||||
case .Inverted_Open:
|
case .Inverted_Open:
|
||||||
pos^ += 1
|
pos^ += 1
|
||||||
idx := len(nodes)
|
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})
|
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].first_child = idx + 1
|
||||||
nodes[idx].child_count = len(nodes) - idx - 1
|
nodes[idx].child_count = len(nodes) - idx - 1
|
||||||
|
nodes[idx].content = source[content_start:close_pos]
|
||||||
|
|
||||||
case .Section_Close:
|
case .Section_Close:
|
||||||
if end_tag != "" && tok.value == end_tag {
|
if end_tag != "" && tok.value == end_tag {
|
||||||
@@ -236,7 +249,7 @@ parse_section :: proc(
|
|||||||
nodes,
|
nodes,
|
||||||
Node{kind = .Parent, key = tok.value, indent = tok.indent, first_child = -1},
|
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].first_child = idx + 1
|
||||||
nodes[idx].child_count = len(nodes) - idx - 1
|
nodes[idx].child_count = len(nodes) - idx - 1
|
||||||
|
|
||||||
@@ -247,7 +260,7 @@ parse_section :: proc(
|
|||||||
nodes,
|
nodes,
|
||||||
Node{kind = .Block, key = tok.value, indent = tok.indent, first_child = -1},
|
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].first_child = idx + 1
|
||||||
nodes[idx].child_count = len(nodes) - idx - 1
|
nodes[idx].child_count = len(nodes) - idx - 1
|
||||||
}
|
}
|
||||||
@@ -447,17 +460,42 @@ render_nodes :: proc(
|
|||||||
|
|
||||||
case .Variable:
|
case .Variable:
|
||||||
val := resolve_name(node.key, ctx[:])
|
val := resolve_name(node.key, ctx[:])
|
||||||
write_value(b, val, escape = true)
|
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
|
i += 1
|
||||||
|
|
||||||
case .Unescaped:
|
case .Unescaped:
|
||||||
val := resolve_name(node.key, ctx[:])
|
val := resolve_name(node.key, ctx[:])
|
||||||
write_value(b, val, escape = false)
|
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
|
i += 1
|
||||||
|
|
||||||
case .Section:
|
case .Section:
|
||||||
val := resolve_name(node.key, ctx[:])
|
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]
|
children := all_nodes[node.first_child:node.first_child + node.child_count]
|
||||||
elem_info, count, data := list_info(val)
|
elem_info, count, data := list_info(val)
|
||||||
if elem_info != nil {
|
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
|
||||||
|
}
|
||||||
|
|
||||||
+41
-65
@@ -24,18 +24,12 @@ Year_Section :: struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Base_Data :: struct {
|
Base_Data :: struct {
|
||||||
now: datetime.DateTime,
|
now: datetime.DateTime,
|
||||||
author: string,
|
author: string,
|
||||||
params: json.Value,
|
params: json.Value,
|
||||||
body: string,
|
body: string,
|
||||||
title: string,
|
title: string,
|
||||||
og_site_name: string,
|
og: Open_Graph,
|
||||||
og_description: string,
|
|
||||||
og_image: string,
|
|
||||||
og_url: string,
|
|
||||||
og_title: string,
|
|
||||||
og_type: string,
|
|
||||||
is_article: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Page_Data :: struct {
|
Page_Data :: struct {
|
||||||
@@ -43,20 +37,17 @@ Page_Data :: struct {
|
|||||||
page_title: string,
|
page_title: string,
|
||||||
date_iso: string,
|
date_iso: string,
|
||||||
date_display: string,
|
date_display: string,
|
||||||
is_post: bool,
|
|
||||||
og_section: string,
|
|
||||||
og_published: string,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Home_Data :: struct {
|
Home_Data :: struct {
|
||||||
using base: Base_Data,
|
using base: Base_Data,
|
||||||
list_pages: [dynamic]Page_Context,
|
pages: [dynamic]Page_Context,
|
||||||
}
|
}
|
||||||
|
|
||||||
Section_Data :: struct {
|
Section_Data :: struct {
|
||||||
using base: Base_Data,
|
using base: Base_Data,
|
||||||
page_title: string,
|
page_title: string,
|
||||||
year_sections: [dynamic]Year_Section,
|
by_year: [dynamic]Year_Section,
|
||||||
}
|
}
|
||||||
|
|
||||||
build_page_context :: proc(page: Page) -> Page_Context {
|
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 {
|
strip_html_tags :: proc(s: string, allocator := context.allocator) -> string {
|
||||||
sb := strings.builder_make()
|
sb := strings.builder_make(allocator)
|
||||||
defer strings.builder_destroy(&sb)
|
defer strings.builder_destroy(&sb)
|
||||||
|
|
||||||
in_tag := false
|
in_tag := false
|
||||||
@@ -95,22 +86,15 @@ strip_html_tags :: proc(s: string) -> string {
|
|||||||
return strings.to_string(sb)
|
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 {
|
load_template :: proc(vfs: ^VFS, virtual_path: string) -> mustache.Template {
|
||||||
data, ok := vfs_get(vfs, virtual_path)
|
data, ok := vfs_get(vfs, virtual_path)
|
||||||
if !ok {
|
if !ok {
|
||||||
log.warnf("thor: template %s not found", virtual_path)
|
log.warnf("template %s not found", virtual_path)
|
||||||
return mustache.Template{}
|
return mustache.Template{}
|
||||||
}
|
}
|
||||||
tpl, err := mustache.parse(string(data))
|
tpl, err := mustache.parse(string(data))
|
||||||
if err != nil {
|
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
|
return tpl
|
||||||
}
|
}
|
||||||
@@ -133,7 +117,7 @@ get_template :: proc(
|
|||||||
chain[n] = "base"; n += 1
|
chain[n] = "base"; n += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..<n {
|
for i in 0 ..< n {
|
||||||
candidate := chain[i]
|
candidate := chain[i]
|
||||||
if cached, ok := cache[candidate]; ok {
|
if cached, ok := cache[candidate]; ok {
|
||||||
return cached
|
return cached
|
||||||
@@ -145,11 +129,11 @@ get_template :: proc(
|
|||||||
return tpl
|
return tpl
|
||||||
}
|
}
|
||||||
if candidate != chain[n - 1] {
|
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{}
|
return mustache.Template{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +154,7 @@ render_template :: proc(
|
|||||||
) -> string {
|
) -> string {
|
||||||
result, err := mustache.render(content_tpl, data, partials)
|
result, err := mustache.render(content_tpl, data, partials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.eprintfln("thor: mustache error: %v", err)
|
fmt.eprintfln("mustache error: %v", err)
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
@@ -192,12 +176,10 @@ render_site :: proc(site: ^Site) {
|
|||||||
|
|
||||||
// Build base data once
|
// Build base data once
|
||||||
base := Base_Data {
|
base := Base_Data {
|
||||||
now = now,
|
now = now,
|
||||||
author = site.author,
|
author = site.author,
|
||||||
params = site.params,
|
params = site.params,
|
||||||
og_site_name = site.title,
|
og = og_init(site^),
|
||||||
og_description = site.description,
|
|
||||||
og_image = fmt.tprintf("%s/avatar.jpg", site.base_url),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find home page
|
// Find home page
|
||||||
@@ -310,13 +292,7 @@ render_page_html :: proc(
|
|||||||
data.body = page.body_html
|
data.body = page.body_html
|
||||||
data.date_iso = page.date
|
data.date_iso = page.date
|
||||||
data.date_display = format_date(page.date)
|
data.date_display = format_date(page.date)
|
||||||
data.is_post = is_article
|
data.og = og_for_page(site^, page, base.og)
|
||||||
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
|
|
||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,11 +317,11 @@ render_home_html :: proc(
|
|||||||
}
|
}
|
||||||
data.title = site.title
|
data.title = site.title
|
||||||
data.body = home.body_html
|
data.body = home.body_html
|
||||||
data.list_pages = list_pages
|
data.pages = list_pages
|
||||||
data.og_url = fmt.tprintf("%s/", site.base_url)
|
data.og.url = fmt.tprintf("%s/", site.base_url)
|
||||||
data.og_title = site.title
|
data.og.title = site.title
|
||||||
data.og_type = "website"
|
data.og.type = "website"
|
||||||
data.is_article = false
|
data.og.is_article = false
|
||||||
return render_template(content_tpl, data, partials)
|
return render_template(content_tpl, data, partials)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,8 +334,8 @@ render_section :: proc(
|
|||||||
partials: map[string]mustache.Template,
|
partials: map[string]mustache.Template,
|
||||||
base: Base_Data,
|
base: Base_Data,
|
||||||
) -> string {
|
) -> string {
|
||||||
year_sections := make([dynamic]Year_Section)
|
by_year := make([dynamic]Year_Section)
|
||||||
defer delete(year_sections)
|
defer delete(by_year)
|
||||||
current_year := ""
|
current_year := ""
|
||||||
for page in site.pages {
|
for page in site.pages {
|
||||||
if page.section != section || page._is_index {
|
if page.section != section || page._is_index {
|
||||||
@@ -367,10 +343,10 @@ render_section :: proc(
|
|||||||
}
|
}
|
||||||
year := get_year(page.date)
|
year := get_year(page.date)
|
||||||
if year != current_year {
|
if year != current_year {
|
||||||
append(&year_sections, Year_Section{year = year})
|
append(&by_year, Year_Section{year = year})
|
||||||
current_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 {
|
data := Section_Data {
|
||||||
@@ -380,16 +356,16 @@ render_section :: proc(
|
|||||||
data.body = section_index.body_html
|
data.body = section_index.body_html
|
||||||
data.page_title = section_index.title
|
data.page_title = section_index.title
|
||||||
data.title = fmt.tprintf("%s | %s", section_index.title, site.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 {
|
} else {
|
||||||
data.page_title = capitalize(section)
|
data.page_title = capitalize(section)
|
||||||
data.title = fmt.tprintf("%s | %s", capitalize(section), site.title)
|
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.by_year = by_year
|
||||||
data.og_url = fmt.tprintf("%s/%s/", site.base_url, section)
|
data.og.url = fmt.tprintf("%s/%s/", site.base_url, section)
|
||||||
data.og_type = "website"
|
data.og.type = "website"
|
||||||
data.is_article = false
|
data.og.is_article = false
|
||||||
return render_template(content_tpl, data, partials)
|
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))
|
tpl, err := mustache.parse(string(data))
|
||||||
if err != nil {
|
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
|
continue
|
||||||
}
|
}
|
||||||
partials[key] = tpl
|
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)
|
dir := fmt.tprintf("%s/%s", output_dir, rel)
|
||||||
if err := os.make_directory_all(dir); err != nil && err != .Exist {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,7 +442,7 @@ write_page :: proc(output_dir: string, permalink: string, html: string) {
|
|||||||
|
|
||||||
write_file :: proc(path: string, html: string) {
|
write_file :: proc(path: string, html: string) {
|
||||||
if err := os.write_entire_file_from_string(path, html); err != nil {
|
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")
|
found, ok := find_config("thor.json")
|
||||||
if ok {
|
if ok {
|
||||||
path = found
|
path = found
|
||||||
log.debugf("thor: using config %s", path)
|
log.debugf("using config %s", path)
|
||||||
} else {
|
} else {
|
||||||
path = "./thor.json"
|
path = "./thor.json"
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ load_config_file :: proc(
|
|||||||
|
|
||||||
unmarshal_err := json.unmarshal_string(string(data), config, allocator = allocator)
|
unmarshal_err := json.unmarshal_string(string(data), config, allocator = allocator)
|
||||||
if unmarshal_err != nil {
|
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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user