Add Gruvbox share theme + blog system for Trilium (anonymized)
Blog generator, publish manager, share scripts, theme CSS, highlight.js bundles, font, favicon, templates and generated-reference outputs, plus an English installation guide.
This commit is contained in:
@@ -0,0 +1,240 @@
|
|||||||
|
# Trilium Share – Gruvbox Blog System
|
||||||
|
|
||||||
|
A complete **public-share blog system + Gruvbox theme** for [TriliumNext](https://github.com/TriliumNext/Trilium).
|
||||||
|
It turns a normal Trilium note tree into a themed, searchable blog with categories, an RSS feed,
|
||||||
|
a sitemap, a digital garden (growth states) and client-side rendering of Mermaid, Excalidraw and
|
||||||
|
mind-map notes.
|
||||||
|
|
||||||
|
Everything is driven by Trilium's built-in **Public Share** feature plus a few frontend/backend scripts –
|
||||||
|
no external dependencies (except `highlight.js`, vendored in `lib/`).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Gruvbox light/dark theme for the share, with a theme toggle in the header.
|
||||||
|
- Blog index, category pages, sub-categories, tags and tag overview.
|
||||||
|
- Article metadata: date, category, summary, tags, prev/next navigation, reading time.
|
||||||
|
- **Digital Garden**: `state` labels (Seed → Evergreen, Draft, Review) rendered as badges and
|
||||||
|
colour-coded growth tables.
|
||||||
|
- **Search** (client-side over a generated index), **RSS feed** and **sitemap.xml**.
|
||||||
|
- **Widget rendering** for Mermaid, Excalidraw (canvas) and MindElixir (mind map) notes, with a
|
||||||
|
collapsible, highlighted "Source" block and copy button.
|
||||||
|
- Code highlighting (highlight.js + line numbers) and copy buttons on all code boxes.
|
||||||
|
- Optional "Buy me a coffee" button with locally-hosted Cookie font.
|
||||||
|
- `blog_generator` (backend) builds index/feed/sitemap/search-data; `blog_publish_manager` moves
|
||||||
|
notes between an `Unpublished` staging area and the live blog.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
|
||||||
|
| Path | Content | Trilium note type / mime |
|
||||||
|
|---|---|---|
|
||||||
|
| `blog_generator.js` | backend script that generates index, feed, sitemap, search data, category & tag pages | code, `application/javascript;env=backend` |
|
||||||
|
| `blog_publish_manager.js` | backend script that mirrors notes between `Unpublished` and the blog | code, `application/javascript;env=backend` |
|
||||||
|
| `theme/blog_share_theme.css` | the Gruvbox share theme | code, `text/css` |
|
||||||
|
| `theme/favicon.svg` | default site favicon | code (SVG) |
|
||||||
|
| `theme/cookie.woff2` | "Cookie" font for the Buy me a coffee button | file, `font/woff2` |
|
||||||
|
| `lib/highlight.js` | vendored highlight.js | code, `application/javascript` |
|
||||||
|
| `lib/hljs_line_numbers.js` | highlight.js line-numbers plugin | code |
|
||||||
|
| `scripts/*.js` | frontend share snippets (content is pasted **verbatim** into a code note, including the `<script>`/`<style>` wrapper) | code |
|
||||||
|
| `templates/blog_article.html` | article template with promoted labels | text |
|
||||||
|
| `examples/*` | sample category logo + menu icon script (ignis8) | code / SVG |
|
||||||
|
| `generated/*` | reference output, **regenerated automatically** by `blog_generator.js` | code / xml |
|
||||||
|
|
||||||
|
> The file content of a `scripts/*` note already contains the `<script>` / `<style>` tags. When you
|
||||||
|
> create the code note, paste the **whole file** into the note content.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- TriliumNext server with **Public Share** enabled (Options → Public access).
|
||||||
|
- A public hostname (optional but recommended, e.g. for `sitemap.xml` / OG tags).
|
||||||
|
- Basic familiarity with Trilium attributes (labels & relations).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 1. Create the note structure
|
||||||
|
|
||||||
|
```
|
||||||
|
root
|
||||||
|
└── Blog (book)
|
||||||
|
├── Richard's Blog (book) ← blog root, the shared note
|
||||||
|
│ ├── .blog (book) ← system notes (scripts, css, data)
|
||||||
|
│ ├── Astrophotography (book) ← example category
|
||||||
|
│ ├── Electronic (book)
|
||||||
|
│ ├── Leathercraft (book)
|
||||||
|
│ ├── Digital Garden (book) ← growth-state garden category
|
||||||
|
│ ├── Tags (text) ← auto-filled tag overview
|
||||||
|
│ ├── About Me (text)
|
||||||
|
│ └── Impressum (text)
|
||||||
|
├── Unpublished (book) ← staging area
|
||||||
|
├── blog_generator (code, backend)
|
||||||
|
└── blog_publish_manager (code, backend)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can use any titles; the generator finds notes by the constants at the top of
|
||||||
|
`blog_generator.js` (`BLOG_ROOT_NOTE_ID`, `BLOG_SYSTEM_NOTE_ID`, `TAGS_SECTION_TITLE`).
|
||||||
|
|
||||||
|
### 2. Create the code notes
|
||||||
|
|
||||||
|
Under **`.blog`** create one code note per file, using the file name as the note title
|
||||||
|
(optional, but matches the generator):
|
||||||
|
|
||||||
|
| Note title | File | mime |
|
||||||
|
|---|---|---|
|
||||||
|
| `blog_share_theme.css` | `theme/blog_share_theme.css` | `text/css` |
|
||||||
|
| `favicon` | `theme/favicon.svg` | – (SVG) |
|
||||||
|
| `Cookie` | `theme/cookie.woff2` | `font/woff2` (file note) |
|
||||||
|
| `highlight.js` | `lib/highlight.js` | `application/javascript` |
|
||||||
|
| `hljs line numbers plugin` | `lib/hljs_line_numbers.js` | `text/javascript` |
|
||||||
|
| `hide home title` | `scripts/hide_home_title.js` | `text/html` |
|
||||||
|
| `garden h1 green` | `scripts/garden_h1_green.js` | `text/html` |
|
||||||
|
| `table column hover` | `scripts/table_column_hover.js` | `text/html` |
|
||||||
|
| `blog card click` | `scripts/blog_card_click.js` | `text/html` |
|
||||||
|
| `garden row click` | `scripts/garden_row_click.js` | `text/html` |
|
||||||
|
| `code note highlight` | `scripts/code_note_highlight.js` | `text/html` |
|
||||||
|
| `code copy & lines` | `scripts/code_copy_lines.js` | `text/html` |
|
||||||
|
| `title state badge` | `scripts/title_state_badge.js` | `text/html` |
|
||||||
|
| `bmc button` | `scripts/bmc_button.js` | `text/html` |
|
||||||
|
| `logo share alias` | `scripts/logo_share_alias.js` | `text/html` |
|
||||||
|
| `prev next` | `scripts/prev_next.js` | `text/html` |
|
||||||
|
| `reading time` | `scripts/reading_time.js` | `text/html` |
|
||||||
|
| `search header` | `scripts/search_header.js` | `text/html` |
|
||||||
|
| `note fallback` | `scripts/note_fallback.js` | `text/html` |
|
||||||
|
| `feed.xml` | `generated/feed.xml` | code (regenerated) |
|
||||||
|
| `sitemap.xml` | `generated/sitemap.xml` | code (regenerated) |
|
||||||
|
| `state_map` | `generated/state_map.js` | `text/html` (regenerated) |
|
||||||
|
| `blog_data` | `generated/blog_data.js` | `text/html` (regenerated) |
|
||||||
|
|
||||||
|
Also create, under the `Blog` book: `blog_generator` (from `blog_generator.js`,
|
||||||
|
mime `application/javascript;env=backend`) and `blog_publish_manager` (from
|
||||||
|
`blog_publish_manager.js`, mime `application/javascript;env=backend`).
|
||||||
|
|
||||||
|
### 3. Wire the attributes
|
||||||
|
|
||||||
|
**On the blog root note** (`Richard's Blog`), add:
|
||||||
|
|
||||||
|
| Attribute | Value | Inheritable |
|
||||||
|
|---|---|---|
|
||||||
|
| `shareRoot` | `true` | – |
|
||||||
|
| `shareAlias` | `blog` | – |
|
||||||
|
| `shareIndex` | (empty) | – |
|
||||||
|
| `shareDescription` | e.g. `Project documentation & technical blog` | – |
|
||||||
|
| `shareOpenGraphURL` | `https://YOUR-INSTANCE.example.com/` | – |
|
||||||
|
| `shareOpenGraphColor` | `#83a598` | – |
|
||||||
|
| `iconClass` | `bx bx-edit-alt` | – |
|
||||||
|
| `~shareCss` | → `blog_share_theme.css` | yes |
|
||||||
|
| `~shareFavicon` | → `favicon` | yes |
|
||||||
|
| `~shareHtml` | → `table column hover`, `blog card click`, `garden row click`, `code note highlight`, `code copy & lines`, `state_map`, `title state badge`, `bmc button`, `logo share alias`, `blog_data`, `prev next`, `reading time`, `search header`, `ignis8 menu icon` (optional), `note fallback` | yes |
|
||||||
|
| `~shareHtml` | → `hide home title` | **no** (index only) |
|
||||||
|
| `~runOnNoteContentChange`, `~runOnAttributeChange`, `~runOnNoteCreation`, `~runOnNoteTitleChange`, `~runOnAttributeCreation`, `~runOnBranchDeletion`, `~runOnChildNoteCreation` | → `blog_generator` | yes |
|
||||||
|
| `~runOnAttributeChange`, `~runOnAttributeCreation` | → `blog_publish_manager` | yes |
|
||||||
|
|
||||||
|
**On the script notes** (under `.blog`), add:
|
||||||
|
|
||||||
|
| Note | Attribute |
|
||||||
|
|---|---|
|
||||||
|
| `hide home title` | `shareHtmlLocation=head:end`, `shareHiddenFromTree` |
|
||||||
|
| `garden h1 green` | `shareHtmlLocation=head:end`, `shareHiddenFromTree` |
|
||||||
|
| `state_map` | `shareHtmlLocation=body:start`, `shareHiddenFromTree` |
|
||||||
|
| `title state badge` | `shareHtmlLocation=content:start`, `shareHiddenFromTree` |
|
||||||
|
| every other script | `shareHtmlLocation=body:end`, `shareHiddenFromTree` |
|
||||||
|
| `highlight.js` | `shareHiddenFromTree` |
|
||||||
|
| `feed.xml` | `shareAlias=feed`, `shareRaw`, `shareHiddenFromTree` |
|
||||||
|
| `sitemap.xml` | `shareAlias=sitemap.xml`, `shareRaw`, `shareHiddenFromTree` |
|
||||||
|
| `.blog` (book) | `shareHiddenFromTree`, `viewType=list` |
|
||||||
|
|
||||||
|
**On the Digital Garden category note** (and any other garden category), add:
|
||||||
|
|
||||||
|
| Attribute | Value | Inheritable |
|
||||||
|
|---|---|---|
|
||||||
|
| `blogGarden` | (empty) | – |
|
||||||
|
| `shareAlias` | `digital-garden` | – |
|
||||||
|
| `iconClass` | `bx bx-leaf` | – |
|
||||||
|
| `label:state` | `promoted,alias=Growth Condition,single,select,options=Seed;Seedling;Sprout;Sprouting;Bud;Evergreen;Draft;Review` | yes |
|
||||||
|
| `~shareHtml` | → `garden h1 green` | – |
|
||||||
|
|
||||||
|
> Categories: any top-level note under the blog root (that is not `Tags`, `.blog`, `About Me`,
|
||||||
|
> `Impressum` or `Search`) is treated as a category. `blogGarden` marks it as a growth-state garden.
|
||||||
|
|
||||||
|
### 4. Replace the placeholder note IDs
|
||||||
|
|
||||||
|
Trilium assigns random note IDs on creation, so the scripts that reference notes by ID contain
|
||||||
|
placeholder tokens. Replace every `__TOKEN__` with the real note ID of the corresponding note:
|
||||||
|
|
||||||
|
| Token | Replace with note |
|
||||||
|
|---|---|
|
||||||
|
| `__BLOG_ROOT_ID__` | `Richard's Blog` |
|
||||||
|
| `__BLOG_SYSTEM_ID__` | `.blog` |
|
||||||
|
| `__UNPUBLISHED_ROOT_ID__` | `Unpublished` |
|
||||||
|
| `__BLOG_GENERATOR_ID__` | `blog_generator` |
|
||||||
|
| `__HIDE_HOME_TITLE_ID__` | `hide home title` |
|
||||||
|
| `__HLJS_ID__` | `highlight.js` |
|
||||||
|
| `__HLJS_LN_ID__` | `hljs line numbers plugin` |
|
||||||
|
| `__COOKIE_FONT_ID__` | `Cookie` |
|
||||||
|
| `__TITLE_ICON_ID__` | (only needed for the `ignis8` example) |
|
||||||
|
| `__CATEGORY_NOTE_ID__` | (only needed for the `ignis8` example) |
|
||||||
|
|
||||||
|
Then edit the constants at the top of `blog_generator.js` and `blog_publish_manager.js`:
|
||||||
|
- `BLOG_ROOT_NOTE_ID` / `BLOG_ROOT_ID` → the blog root note ID
|
||||||
|
- `BLOG_SYSTEM_NOTE_ID` → the `.blog` note ID
|
||||||
|
- `UNPUBLISHED_ROOT_ID` → the `Unpublished` note ID
|
||||||
|
- `BLOG_GENERATOR_ID` → the generator note ID
|
||||||
|
- `SHARE_BASE` → your public base URL, e.g. `https://YOUR-INSTANCE.example.com`
|
||||||
|
- `SITE_TITLE`, `SITE_DESCRIPTION` → your blog name/tagline
|
||||||
|
|
||||||
|
### 5. Enable Public Share and generate
|
||||||
|
|
||||||
|
1. On `Richard's Blog` right-click → *Share* → enable sharing.
|
||||||
|
2. Run the generator once: execute the `blog_generator` script (e.g. press play in the script
|
||||||
|
editor, or trigger any attribute/content change). It is also wired to run automatically on
|
||||||
|
every relevant change (`runOn*`), and you can add `run=daily` to it for a nightly run.
|
||||||
|
3. Open the share (options → Public access → URL) – the blog index is generated at the share root.
|
||||||
|
|
||||||
|
### 6. Publish an article
|
||||||
|
|
||||||
|
1. Create a note from the `templates/blog_article.html` content (or clone the template note).
|
||||||
|
It promotes the `publish`, `date`, `category`, `summary`, `shareAlias` and `tags` labels.
|
||||||
|
2. Put it under a category inside `Richard's Blog` and set `publish=true`.
|
||||||
|
3. To keep it private, put it under `Unpublished` instead – `blog_publish_manager` clones it
|
||||||
|
into the matching category (and back) when you flip `publish`.
|
||||||
|
|
||||||
|
### 7. Optional: clean hostname via nginx
|
||||||
|
|
||||||
|
If you want a clean domain instead of `/share`, proxy it and rewrite `/` to `/share/`:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
server {
|
||||||
|
server_name blog.example.com;
|
||||||
|
location / {
|
||||||
|
proxy_pass https://notes.example.com/share/;
|
||||||
|
proxy_set_header Host notes.example.com;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How the generator works
|
||||||
|
|
||||||
|
- Walks the blog root and collects notes with `publish=true`.
|
||||||
|
- Writes the **index** (article cards), **category pages** (sub-category chips + articles,
|
||||||
|
respecting a `<!-- blog:articles -->` marker so your hand-written intro text is kept),
|
||||||
|
**tag pages** and the **tags overview**.
|
||||||
|
- Regenerates `feed.xml` (RSS), `sitemap.xml`, `state_map` (garden states) and `blog_data`
|
||||||
|
(search index used by the header search + prev/next).
|
||||||
|
- Sets `shareDescription` on every published note from its `summary`.
|
||||||
|
|
||||||
|
## Customisation
|
||||||
|
|
||||||
|
- **Theme**: edit `theme/blog_share_theme.css` (Gruvbox light/dark via `html.theme-light/dark`).
|
||||||
|
- **Buy me a coffee**: edit `scripts/bmc_button.js` (link, text, copyright) – the font is
|
||||||
|
referenced via `__COOKIE_FONT_ID__`.
|
||||||
|
- **Category logos**: copy the `examples/` icon + `ignis8_menu_icon.js` pattern to give a
|
||||||
|
category a custom menu icon and title logo.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The vendored `highlight.js` is licensed under BSD-3-Clause (see its header). All other files in
|
||||||
|
this repository are provided as-is – see the original author's repository for details.
|
||||||
@@ -0,0 +1,505 @@
|
|||||||
|
const BLOG_ROOT_NOTE_ID = "__BLOG_ROOT_ID__";
|
||||||
|
const BLOG_SYSTEM_NOTE_ID = "__BLOG_SYSTEM_ID__";
|
||||||
|
const BLOG_ALIAS = "blog";
|
||||||
|
const SHARE_BASE = "https://YOUR-INSTANCE.example.com";
|
||||||
|
const BASE_URL = SHARE_BASE + "/" + BLOG_ALIAS;
|
||||||
|
const SITE_TITLE = "Your Blog";
|
||||||
|
const SITE_DESCRIPTION = "Project documentation & technical blog by the author";
|
||||||
|
const TAGS_SECTION_TITLE = "Tags";
|
||||||
|
|
||||||
|
function isInSubtree(note, targetId) {
|
||||||
|
try {
|
||||||
|
if (note.noteId === targetId) return true;
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
if (isInSubtree(child, targetId)) return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
api.log("blog_generator: isInSubtree error: " + e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isGeneratedNoteId(id) {
|
||||||
|
try {
|
||||||
|
if (id === BLOG_ROOT_NOTE_ID) return true;
|
||||||
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
|
if (systemNote) {
|
||||||
|
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
||||||
|
if (feedNote && id === feedNote.noteId) return true;
|
||||||
|
}
|
||||||
|
const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID);
|
||||||
|
if (blogRoot) {
|
||||||
|
const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE);
|
||||||
|
if (tagsNote && isInSubtree(tagsNote, id)) return true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
api.log("blog_generator: isGeneratedNoteId error: " + e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (api.originEntity && isGeneratedNoteId(api.originEntity.noteId)) {
|
||||||
|
api.log("blog_generator: ignoring self-trigger from generated note");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
run();
|
||||||
|
} catch (e) {
|
||||||
|
api.log("blog_generator: ERROR " + (e && e.stack ? e.stack : e));
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID);
|
||||||
|
if (!blogRoot) {
|
||||||
|
api.log("blog_generator: root note not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const articles = [];
|
||||||
|
|
||||||
|
function visit(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
if (note.noteId !== BLOG_ROOT_NOTE_ID && note.getLabelValue("blogGarden") !== null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const publish = note.getLabelValue("publish");
|
||||||
|
if (note.noteId !== BLOG_ROOT_NOTE_ID && publish === "true") {
|
||||||
|
articles.push(note);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
visit(child, depth + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visit(blogRoot, 0);
|
||||||
|
|
||||||
|
articles.sort((a, b) => {
|
||||||
|
const da = a.getLabelValue("date") || a.dateCreated;
|
||||||
|
const db = b.getLabelValue("date") || b.dateCreated;
|
||||||
|
return (db < da) ? -1 : (db > da ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s)
|
||||||
|
.replace(/&/g, "&")
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagList(a) {
|
||||||
|
return (a.getLabelValue("tags") || "").split(/\s+/).filter(Boolean).map(t => t.replace(/^!+/, ""));
|
||||||
|
}
|
||||||
|
|
||||||
|
function tagHref(t) {
|
||||||
|
return SHARE_BASE + "/" + encodeURIComponent("tag-" + slugifyTag(t));
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderArticles(list) {
|
||||||
|
const lines = [];
|
||||||
|
for (const a of list) {
|
||||||
|
const date = (a.getLabelValue("date") || a.dateCreated).slice(0, 10);
|
||||||
|
const cat = a.getLabelValue("category") || "";
|
||||||
|
const summary = a.getLabelValue("summary") || "";
|
||||||
|
const link = `${SHARE_BASE}/${encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId)}`;
|
||||||
|
const tags = tagList(a);
|
||||||
|
|
||||||
|
lines.push('<article class="blog-post">');
|
||||||
|
lines.push(' <header class="blog-post-header">');
|
||||||
|
lines.push(` <h2 class="blog-post-title"><a href="${escapeHtml(link)}">${escapeHtml(a.title)}</a></h2>`);
|
||||||
|
lines.push(' <div class="blog-post-meta">');
|
||||||
|
if (date) lines.push(` <span class="blog-post-date">${escapeHtml(date)}</span>`);
|
||||||
|
if (cat) lines.push(` <span class="blog-post-category">${escapeHtml(cat)}</span>`);
|
||||||
|
lines.push(' </div>');
|
||||||
|
lines.push(' </header>');
|
||||||
|
if (summary) lines.push(` <p class="blog-post-summary">${escapeHtml(summary)}</p>`);
|
||||||
|
if (tags.length) {
|
||||||
|
lines.push(' <div class="blog-post-tags">' + tags.map(t => `<a class="tag" href="${tagHref(t)}">#${escapeHtml(t)}</a>`).join(" ") + '</div>');
|
||||||
|
}
|
||||||
|
lines.push('</article>');
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateIcon(state) {
|
||||||
|
const s = String(state).toLowerCase();
|
||||||
|
const icons = {
|
||||||
|
"seed": "bx bxs-coffee-bean",
|
||||||
|
"seedling": "bx bxs-coffee-bean",
|
||||||
|
"sprout": "bx bxs-leaf",
|
||||||
|
"sprouting": "bx bxs-leaf",
|
||||||
|
"bud": "bx bxs-leaf",
|
||||||
|
"evergreen": "bx bxs-tree-alt",
|
||||||
|
"draft": "bx bx-edit",
|
||||||
|
"review": "bx bx-search-alt"
|
||||||
|
};
|
||||||
|
return icons[s] || "bx bxs-circle";
|
||||||
|
}
|
||||||
|
|
||||||
|
function stateClass(state) {
|
||||||
|
const s = String(state).toLowerCase();
|
||||||
|
const growth = ["seed", "seedling", "sprout", "sprouting", "bud", "evergreen"];
|
||||||
|
if (growth.includes(s)) return "state-growth";
|
||||||
|
if (s === "draft") return "state-draft";
|
||||||
|
if (s === "review") return "state-review";
|
||||||
|
return "state-unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGardenTable(catNote, list) {
|
||||||
|
let html = '<table class="blog-garden-table">\n<thead><tr><th>Title</th><th>Type</th><th>State</th><th>Date</th></tr></thead>\n<tbody>';
|
||||||
|
for (const a of list) {
|
||||||
|
const link = SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId);
|
||||||
|
const type = a.type || "text";
|
||||||
|
const state = a.getLabelValue("state") || "";
|
||||||
|
const date = (a.getLabelValue("date") || a.dateCreated).slice(0, 10);
|
||||||
|
html += `<tr><td><a href="${escapeHtml(link)}">${escapeHtml(a.title)}</a></td><td><span class="note-type">${escapeHtml(type)}</span></td><td>${state ? `<span class="note-state ${stateClass(state)}"><i class="${stateIcon(state)}"></i>${escapeHtml(state)}</span>` : ""}</td><td>${escapeHtml(date)}</td></tr>`;
|
||||||
|
}
|
||||||
|
html += '</tbody></table>';
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildIndexHtml() {
|
||||||
|
return renderArticles(articles) || '<p>No articles published yet.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFeedXml() {
|
||||||
|
const items = articles.map(a => {
|
||||||
|
const date = a.getLabelValue("date") || a.dateCreated;
|
||||||
|
const alias = a.getLabelValue("shareAlias") || a.noteId;
|
||||||
|
const summary = a.getLabelValue("summary") || "";
|
||||||
|
const pubDate = new Date(date).toUTCString();
|
||||||
|
const link = `${BASE_URL}/${encodeURIComponent(alias)}`;
|
||||||
|
return ` <item>\n <title>${escapeHtml(a.title)}</title>\n <link>${link}</link>\n <guid isPermaLink="false">${escapeHtml(a.noteId)}</guid>\n <pubDate>${pubDate}</pubDate>\n <description>${escapeHtml(summary)}</description>\n </item>`;
|
||||||
|
}).join("\n");
|
||||||
|
|
||||||
|
const now = new Date().toUTCString();
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>\n<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">\n <channel>\n <title>${escapeHtml(SITE_TITLE)}</title>\n <link>${BASE_URL}</link>\n <atom:link href="${SHARE_BASE}/feed" rel="self" type="application/rss+xml"/>\n <description>${escapeHtml(SITE_DESCRIPTION)}</description>\n <language>en</language>\n <lastBuildDate>${now}</lastBuildDate>\n${items}\n </channel>\n</rss>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugifyTag(t) {
|
||||||
|
return t.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildStateMap() {
|
||||||
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
|
if (!systemNote) return;
|
||||||
|
const mapNote = systemNote.getChildNotes().find(n => n.title === "state_map");
|
||||||
|
if (!mapNote) return;
|
||||||
|
const map = {};
|
||||||
|
const seen = new Set();
|
||||||
|
(function walk(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
if (seen.has(child.noteId)) continue;
|
||||||
|
seen.add(child.noteId);
|
||||||
|
const state = child.getLabelValue("state");
|
||||||
|
if (child.getLabelValue("publish") === "true" && state) {
|
||||||
|
map[child.noteId] = state;
|
||||||
|
}
|
||||||
|
walk(child, depth + 1);
|
||||||
|
}
|
||||||
|
})(blogRoot, 0);
|
||||||
|
const html = `<script>window.blogStates = ${JSON.stringify(map)};</script>`;
|
||||||
|
if (mapNote.getContent() !== html) {
|
||||||
|
mapNote.setContent(html);
|
||||||
|
mapNote.save();
|
||||||
|
api.log("blog_generator: state map updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectAllPublished() {
|
||||||
|
const out = [];
|
||||||
|
const seen = new Set();
|
||||||
|
(function walk(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
if (seen.has(child.noteId)) continue;
|
||||||
|
seen.add(child.noteId);
|
||||||
|
if (child.getLabelValue("publish") === "true") out.push(child);
|
||||||
|
walk(child, depth + 1);
|
||||||
|
}
|
||||||
|
})(blogRoot, 0);
|
||||||
|
out.sort((a, b) => {
|
||||||
|
const da = a.getLabelValue("date") || a.dateCreated;
|
||||||
|
const db = b.getLabelValue("date") || b.dateCreated;
|
||||||
|
return (db < da) ? -1 : (db > da ? 1 : 0);
|
||||||
|
});
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSitemap() {
|
||||||
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
|
if (!systemNote) return;
|
||||||
|
const sitemapNote = systemNote.getChildNotes().find(n => n.title === "sitemap.xml");
|
||||||
|
if (!sitemapNote) return;
|
||||||
|
const urls = [SHARE_BASE + "/" + BLOG_ALIAS];
|
||||||
|
for (const a of collectAllPublished()) {
|
||||||
|
urls.push(SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId));
|
||||||
|
}
|
||||||
|
for (const cat of collectCategoryNotes()) {
|
||||||
|
urls.push(SHARE_BASE + "/" + encodeURIComponent(cat.getLabelValue("shareAlias") || cat.noteId));
|
||||||
|
}
|
||||||
|
const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE);
|
||||||
|
if (tagsNote) {
|
||||||
|
for (const t of tagsNote.getChildNotes()) {
|
||||||
|
urls.push(SHARE_BASE + "/" + encodeURIComponent(t.getLabelValue("shareAlias") || t.noteId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const body = urls.map(u => ` <url><loc>${escapeHtml(u)}</loc></url>`).join("\n");
|
||||||
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n${body}\n</urlset>`;
|
||||||
|
if (sitemapNote.getContent() !== xml) {
|
||||||
|
sitemapNote.setContent(xml);
|
||||||
|
sitemapNote.save();
|
||||||
|
api.log("blog_generator: sitemap updated (" + urls.length + " urls)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureShareDescription() {
|
||||||
|
const seen = new Set();
|
||||||
|
(function walk(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
if (seen.has(child.noteId)) continue;
|
||||||
|
seen.add(child.noteId);
|
||||||
|
if (child.getLabelValue("publish") === "true") {
|
||||||
|
const desc = child.getLabelValue("summary") || SITE_DESCRIPTION;
|
||||||
|
if (child.getLabelValue("shareDescription") !== desc) {
|
||||||
|
child.setLabel("shareDescription", desc);
|
||||||
|
child.save();
|
||||||
|
api.log("blog_generator: set shareDescription on " + child.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
walk(child, depth + 1);
|
||||||
|
}
|
||||||
|
})(blogRoot, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBlogData() {
|
||||||
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
|
if (!systemNote) return;
|
||||||
|
const dataNote = systemNote.getChildNotes().find(n => n.title === "blog_data");
|
||||||
|
if (!dataNote) return;
|
||||||
|
const list = collectAllPublished().map(a => ({
|
||||||
|
id: a.noteId,
|
||||||
|
title: a.title,
|
||||||
|
url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId),
|
||||||
|
summary: a.getLabelValue("summary") || "",
|
||||||
|
tags: tagList(a)
|
||||||
|
}));
|
||||||
|
const html = `<script>window.blogData = ${JSON.stringify({ articles: list })};</script>`;
|
||||||
|
if (dataNote.getContent() !== html) {
|
||||||
|
dataNote.setContent(html);
|
||||||
|
dataNote.save();
|
||||||
|
api.log("blog_generator: blog_data updated (" + list.length + " articles)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTags() {
|
||||||
|
const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE);
|
||||||
|
if (!tagsNote) return;
|
||||||
|
|
||||||
|
const map = new Map();
|
||||||
|
for (const a of articles) {
|
||||||
|
for (const t of tagList(a)) {
|
||||||
|
if (!map.has(t)) map.set(t, []);
|
||||||
|
map.get(t).push(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagNames = Array.from(map.keys()).sort();
|
||||||
|
const existing = tagsNote.getChildNotes();
|
||||||
|
const byTitle = {};
|
||||||
|
for (const n of existing) byTitle[n.title] = n;
|
||||||
|
|
||||||
|
for (const tagName of tagNames) {
|
||||||
|
let note = byTitle[tagName];
|
||||||
|
if (!note) {
|
||||||
|
const created = api.createTextNote(tagsNote.noteId, tagName, "");
|
||||||
|
note = created.note ? created.note : created;
|
||||||
|
}
|
||||||
|
const tagged = map.get(tagName).slice();
|
||||||
|
tagged.sort((a, b) => {
|
||||||
|
const da = a.getLabelValue("date") || a.dateCreated;
|
||||||
|
const db = b.getLabelValue("date") || b.dateCreated;
|
||||||
|
return (db < da) ? -1 : (db > da ? 1 : 0);
|
||||||
|
});
|
||||||
|
const html = `<h1>Tag: ${escapeHtml(tagName)}</h1>\n` + (renderArticles(tagged) || '<p>No articles.</p>');
|
||||||
|
if (note.getContent() !== html) {
|
||||||
|
note.setContent(html);
|
||||||
|
note.save();
|
||||||
|
}
|
||||||
|
if (!note.hasRelation("shareHtml", "__HIDE_HOME_TITLE_ID__")) {
|
||||||
|
note.setRelation("shareHtml", "__HIDE_HOME_TITLE_ID__");
|
||||||
|
note.save();
|
||||||
|
}
|
||||||
|
const alias = "tag-" + slugifyTag(tagName);
|
||||||
|
if (note.getLabelValue("shareAlias") !== alias) {
|
||||||
|
note.setLabel("shareAlias", alias);
|
||||||
|
note.save();
|
||||||
|
}
|
||||||
|
if (!note.hasLabel("shareHiddenFromTree")) {
|
||||||
|
note.setLabel("shareHiddenFromTree", "");
|
||||||
|
note.save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags overview page (list all tags as clickable chips)
|
||||||
|
const tagChildren = tagsNote.getChildNotes().slice().sort((a, b) => a.title.localeCompare(b.title));
|
||||||
|
let idxHtml = '<div class="blog-subcategories">';
|
||||||
|
for (const t of tagChildren) {
|
||||||
|
const url = SHARE_BASE + "/" + encodeURIComponent(t.getLabelValue("shareAlias") || t.noteId);
|
||||||
|
idxHtml += `<a class="tag" href="${escapeHtml(url)}">#${escapeHtml(t.title)}</a>`;
|
||||||
|
}
|
||||||
|
idxHtml += '</div>';
|
||||||
|
if (tagsNote.getContent() !== idxHtml) {
|
||||||
|
tagsNote.setContent(idxHtml);
|
||||||
|
tagsNote.save();
|
||||||
|
api.log("blog_generator: tags overview updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const n of existing) {
|
||||||
|
if (!map.has(n.title)) {
|
||||||
|
api.log("blog_generator: removing obsolete tag note " + n.title);
|
||||||
|
n.deleteNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategoryNote(note) {
|
||||||
|
if (note.hasLabel("blogCategory")) return true;
|
||||||
|
const parents = note.getParentNotes();
|
||||||
|
const isTopLevel = parents.some(p => p.noteId === BLOG_ROOT_NOTE_ID);
|
||||||
|
if (!isTopLevel) return false;
|
||||||
|
const sysTitles = ["Tags", ".blog", "Impressum", "About Me", "Search"];
|
||||||
|
if (sysTitles.includes(note.title)) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectCategoryNotes() {
|
||||||
|
const out = [];
|
||||||
|
(function walk(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
if (note.noteId !== BLOG_ROOT_NOTE_ID && isCategoryNote(note)) {
|
||||||
|
out.push(note);
|
||||||
|
}
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
walk(child, depth + 1);
|
||||||
|
}
|
||||||
|
})(blogRoot, 0);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCategories() {
|
||||||
|
const byCategoryLabel = new Map();
|
||||||
|
for (const a of articles) {
|
||||||
|
const c = a.getLabelValue("category") || "";
|
||||||
|
if (!c) continue;
|
||||||
|
if (!byCategoryLabel.has(c)) byCategoryLabel.set(c, []);
|
||||||
|
byCategoryLabel.get(c).push(a);
|
||||||
|
}
|
||||||
|
const marker = "<!-- blog:articles -->";
|
||||||
|
for (const catNote of collectCategoryNotes()) {
|
||||||
|
const current = catNote.getContent() || "";
|
||||||
|
let userText = "";
|
||||||
|
const markerIdx = current.indexOf(marker);
|
||||||
|
if (markerIdx >= 0) {
|
||||||
|
userText = current.slice(0, markerIdx);
|
||||||
|
} else if (!current.trim().startsWith("<article") && !current.trim().startsWith(marker)) {
|
||||||
|
userText = current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = catNote.getChildNotes();
|
||||||
|
const subs = children.filter(c => c.getLabelValue("publish") !== "true");
|
||||||
|
|
||||||
|
const merged = new Map();
|
||||||
|
for (const c of children) {
|
||||||
|
if (c.getLabelValue("publish") === "true") merged.set(c.noteId, c);
|
||||||
|
}
|
||||||
|
for (const a of (byCategoryLabel.get(catNote.title) || [])) {
|
||||||
|
merged.set(a.noteId, a);
|
||||||
|
}
|
||||||
|
const articlesList = Array.from(merged.values()).sort((a, b) => {
|
||||||
|
const da = a.getLabelValue("date") || a.dateCreated;
|
||||||
|
const db = b.getLabelValue("date") || b.dateCreated;
|
||||||
|
return (db < da) ? -1 : (db > da ? 1 : 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
let html = userText;
|
||||||
|
if (html && !html.endsWith("\n")) html += "\n";
|
||||||
|
html += marker + "\n";
|
||||||
|
if (subs.length) {
|
||||||
|
html += '<div class="blog-subcategories">';
|
||||||
|
for (const s of subs) {
|
||||||
|
const url = SHARE_BASE + "/" + encodeURIComponent(s.getLabelValue("shareAlias") || s.noteId);
|
||||||
|
const icon = s.getLabelValue("iconClass");
|
||||||
|
html += `<a class="blog-subcategory" href="${escapeHtml(url)}">`;
|
||||||
|
if (icon) html += `<i class="${escapeHtml(icon)}"></i>`;
|
||||||
|
html += `<span>${escapeHtml(s.title)}</span></a>`;
|
||||||
|
}
|
||||||
|
html += "</div>\n";
|
||||||
|
}
|
||||||
|
if (catNote.hasLabel("blogGarden")) {
|
||||||
|
html += buildGardenTable(catNote, articlesList);
|
||||||
|
} else {
|
||||||
|
html += renderArticles(articlesList) || '<p>No articles yet.</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (catNote.getContent() !== html) {
|
||||||
|
catNote.setContent(html);
|
||||||
|
catNote.save();
|
||||||
|
api.log("blog_generator: category page updated: " + catNote.title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeEmptyCategories() {
|
||||||
|
const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID);
|
||||||
|
if (!blogRoot) return;
|
||||||
|
const toCheck = [];
|
||||||
|
(function collect(note, depth) {
|
||||||
|
if (depth > 20) return;
|
||||||
|
if (note.noteId !== blogRoot.noteId && note.hasLabel("blogCategory")) {
|
||||||
|
toCheck.push(note);
|
||||||
|
}
|
||||||
|
for (const child of note.getChildNotes()) {
|
||||||
|
collect(child, depth + 1);
|
||||||
|
}
|
||||||
|
})(blogRoot, 0);
|
||||||
|
for (const note of toCheck) {
|
||||||
|
if (note.getChildNotes().length === 0) {
|
||||||
|
api.log("blog_generator: removing empty category " + note.title);
|
||||||
|
note.deleteNote();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const indexHtml = buildIndexHtml();
|
||||||
|
if (blogRoot.getContent() !== indexHtml) {
|
||||||
|
blogRoot.setContent(indexHtml);
|
||||||
|
blogRoot.save();
|
||||||
|
api.log("blog_generator: index updated (" + articles.length + " articles)");
|
||||||
|
} else {
|
||||||
|
api.log("blog_generator: index unchanged");
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
|
||||||
|
const feedNote = systemNote.getChildNotes().find(n => n.title === "feed.xml");
|
||||||
|
if (feedNote) {
|
||||||
|
const xml = buildFeedXml();
|
||||||
|
if (feedNote.getContent() !== xml) {
|
||||||
|
feedNote.setContent(xml);
|
||||||
|
feedNote.save();
|
||||||
|
api.log("blog_generator: feed updated");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTags();
|
||||||
|
buildCategories();
|
||||||
|
buildStateMap();
|
||||||
|
buildSitemap();
|
||||||
|
ensureShareDescription();
|
||||||
|
buildBlogData();
|
||||||
|
removeEmptyCategories();
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
const BLOG_ROOT_ID = "__BLOG_ROOT_ID__";
|
||||||
|
const UNPUBLISHED_ROOT_ID = "__UNPUBLISHED_ROOT_ID__";
|
||||||
|
const BLOG_GENERATOR_ID = "__BLOG_GENERATOR_ID__";
|
||||||
|
|
||||||
|
const entity = api.originEntity;
|
||||||
|
if (!entity) return;
|
||||||
|
|
||||||
|
function findOrCreateChild(parentId, title, iconClass) {
|
||||||
|
const parent = api.getNote(parentId);
|
||||||
|
let child = parent.getChildNotes().find(n => n.title === title);
|
||||||
|
if (!child) {
|
||||||
|
const created = api.createTextNote(parentId, title, "");
|
||||||
|
child = created.note ? created.note : created;
|
||||||
|
if (iconClass) child.setLabel("iconClass", iconClass);
|
||||||
|
child.save();
|
||||||
|
return { noteId: child.noteId, created: true };
|
||||||
|
}
|
||||||
|
if (child.hasLabel("blogCategory") && iconClass && child.getLabelValue("iconClass") !== iconClass) {
|
||||||
|
child.setLabel("iconClass", iconClass);
|
||||||
|
child.save();
|
||||||
|
}
|
||||||
|
return { noteId: child.noteId, created: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
function mirrorPath(note, sourceRootId, targetRootId) {
|
||||||
|
const chain = [];
|
||||||
|
let current = note;
|
||||||
|
let guard = 0;
|
||||||
|
while (current && guard++ < 20) {
|
||||||
|
const parents = current.getParentNotes().filter(p => p.noteId === sourceRootId || p.hasAncestor(sourceRootId));
|
||||||
|
if (parents.length === 0) break;
|
||||||
|
const parent = parents[0];
|
||||||
|
if (parent.noteId === sourceRootId) break;
|
||||||
|
chain.unshift(parent);
|
||||||
|
current = parent;
|
||||||
|
}
|
||||||
|
let target = targetRootId;
|
||||||
|
const createdIds = [];
|
||||||
|
for (const folder of chain) {
|
||||||
|
const res = findOrCreateChild(target, folder.title, folder.getLabelValue("iconClass"));
|
||||||
|
if (res.created) createdIds.push(res.noteId);
|
||||||
|
target = res.noteId;
|
||||||
|
}
|
||||||
|
return { target, createdIds };
|
||||||
|
}
|
||||||
|
|
||||||
|
function blogParentsOf(note) {
|
||||||
|
return note.getParentNotes().filter(p => p.noteId === BLOG_ROOT_ID || p.hasAncestor(BLOG_ROOT_ID));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runGenerator() {
|
||||||
|
const gen = api.getNote(BLOG_GENERATOR_ID);
|
||||||
|
if (gen) gen.executeScript();
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugify(s) {
|
||||||
|
return String(s).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAlias(note) {
|
||||||
|
if (note.getLabelValue("shareAlias")) return;
|
||||||
|
const base = slugify(note.title) || "post";
|
||||||
|
let alias = base;
|
||||||
|
let i = 2;
|
||||||
|
while (api.getNotesWithLabel("shareAlias", alias).some(n => n.noteId !== note.noteId)) {
|
||||||
|
alias = base + "-" + i++;
|
||||||
|
}
|
||||||
|
note.setLabel("shareAlias", alias);
|
||||||
|
note.save();
|
||||||
|
api.log("blog_publish_manager: assigned shareAlias '" + alias + "' to '" + note.title + "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity.type === "label" && entity.name === "publish") {
|
||||||
|
const note = api.getNote(entity.noteId);
|
||||||
|
if (!note) return;
|
||||||
|
|
||||||
|
const isPublished = note.getLabelValue("publish") === "true";
|
||||||
|
if (isPublished) ensureAlias(note);
|
||||||
|
const underBlog = note.hasAncestor(BLOG_ROOT_ID);
|
||||||
|
const underUnpublished = note.hasAncestor(UNPUBLISHED_ROOT_ID);
|
||||||
|
|
||||||
|
if (isPublished && underUnpublished && !underBlog) {
|
||||||
|
const res = mirrorPath(note, UNPUBLISHED_ROOT_ID, BLOG_ROOT_ID);
|
||||||
|
api.ensureNoteIsPresentInParent(note.noteId, res.target);
|
||||||
|
for (const id of res.createdIds) {
|
||||||
|
api.getNote(id).setLabel("blogCategory", "");
|
||||||
|
}
|
||||||
|
api.log("blog_publish_manager: cloned note '" + note.title + "' to blog");
|
||||||
|
runGenerator();
|
||||||
|
} else if (!isPublished && underBlog) {
|
||||||
|
if (underUnpublished) {
|
||||||
|
for (const p of blogParentsOf(note)) {
|
||||||
|
api.ensureNoteIsAbsentFromParent(note.noteId, p.noteId);
|
||||||
|
}
|
||||||
|
api.log("blog_publish_manager: removed clone of '" + note.title + "' from blog");
|
||||||
|
} else {
|
||||||
|
const res = mirrorPath(note, BLOG_ROOT_ID, UNPUBLISHED_ROOT_ID);
|
||||||
|
api.ensureNoteIsPresentInParent(note.noteId, res.target);
|
||||||
|
for (const p of blogParentsOf(note)) {
|
||||||
|
api.ensureNoteIsAbsentFromParent(note.noteId, p.noteId);
|
||||||
|
}
|
||||||
|
api.log("blog_publish_manager: moved note '" + note.title + "' back to unpublished");
|
||||||
|
}
|
||||||
|
runGenerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="100%" height="100%">
|
||||||
|
<!-- =========================================================================
|
||||||
|
IGNIS-8 Emblem: Simplified Fire & Wireless Remote Favicon Shield
|
||||||
|
Palette: 100% Strict Gruvbox Dark Match
|
||||||
|
Style: Vibrant, Geometric, Cyber-Tactical, High-Contrast
|
||||||
|
|
||||||
|
OUTSIDE TRANSPARENT, INSIDE DARK GRAY.
|
||||||
|
|
||||||
|
Previously a borderless rectangle in Gruvbox bg0 (#282828) sat under
|
||||||
|
the entire image. The emblem thus carried a gray plate that remained
|
||||||
|
visible next to the frame — in the light GitHub theme as a gray box,
|
||||||
|
in the header of the control view as an edge against its own
|
||||||
|
background.
|
||||||
|
|
||||||
|
The gray now sits exclusively INSIDE the border/outline: It is the
|
||||||
|
fill of exactly the same rectangle that also carries the green
|
||||||
|
contour. The fill edge thereby lies under the inner half of the
|
||||||
|
contour and is not visible; beyond the contour the area stays free.
|
||||||
|
|
||||||
|
For the maskable PWA purpose that is unsuitable — there the home
|
||||||
|
screen cuts its own shape out of the image and needs a full-surface
|
||||||
|
plate. This version therefore lives separately in
|
||||||
|
favicon-maskable.svg, see manifest.json.
|
||||||
|
========================================================================= -->
|
||||||
|
|
||||||
|
<defs>
|
||||||
|
<!-- Silhouette of the emblem: the OUTER EDGE of the border/outline.
|
||||||
|
x/y 12 and 488 x 488 result from the frame rectangle (24, 464)
|
||||||
|
plus half the contour width (12); the radius correspondingly 48 + 12.
|
||||||
|
|
||||||
|
The clipping is not decoration. The shield group extends roughly
|
||||||
|
10 units beyond the outer edge at the bottom — on a gray background
|
||||||
|
that would look like a shield overlapping the frame, without a
|
||||||
|
background it would be a freely floating yellow tip. -->
|
||||||
|
<clipPath id="ignis8-frame">
|
||||||
|
<rect x="12" y="12" width="488" height="488" rx="60" />
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
|
|
||||||
|
<g clip-path="url(#ignis8-frame)">
|
||||||
|
|
||||||
|
<!-- 1. Border/outline (Gruvbox Bright Green #b8bb26) with dark gray
|
||||||
|
inner surface (Gruvbox bg0). Both on ONE element so that the
|
||||||
|
fill edge necessarily lies under the contour. -->
|
||||||
|
<rect x="24" y="24" width="464" height="464" rx="48"
|
||||||
|
fill="#282828"
|
||||||
|
stroke="#b8bb26"
|
||||||
|
stroke-width="24" />
|
||||||
|
|
||||||
|
<!-- 2. Radio waves in the upper right corner (Gruvbox Bright Green #b8bb26) -->
|
||||||
|
<g id="wireless-corner" fill="none" stroke="#b8bb26" stroke-width="16" stroke-linecap="round">
|
||||||
|
<!-- Signal center (corner point) -->
|
||||||
|
<circle cx="430" cy="82" r="12" fill="#b8bb26" stroke="none" />
|
||||||
|
<!-- First radio wave -->
|
||||||
|
<path d="M 370 82 A 60 60 0 0 0 430 142" />
|
||||||
|
<!-- Second radio wave -->
|
||||||
|
<path d="M 310 82 A 120 120 0 0 0 430 202" stroke-width="18" />
|
||||||
|
<!-- Third radio wave -->
|
||||||
|
<path d="M 250 82 A 180 180 0 0 0 430 262" stroke-width="20" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
3. Shield & fire group (Slightly offset toward the lower left,
|
||||||
|
to perfectly balance the radio waves at the top right)
|
||||||
|
-->
|
||||||
|
<g id="shield-core-group" transform="translate(-35, 35) scale(0.92)">
|
||||||
|
|
||||||
|
<!-- Fire brigade shield (Gruvbox Dark Hard #1d2021) with yellow border (#fabd2f) -->
|
||||||
|
<path d="M 256 120
|
||||||
|
C 200 110, 150 135, 110 155
|
||||||
|
C 95 245, 95 335, 120 395
|
||||||
|
C 145 455, 210 492, 256 507
|
||||||
|
C 302 492, 367 455, 392 395
|
||||||
|
C 417 335, 417 245, 402 155
|
||||||
|
C 362 135, 312 110, 256 120 Z"
|
||||||
|
fill="#1d2021"
|
||||||
|
stroke="#fabd2f"
|
||||||
|
stroke-width="20"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Vibrant, intensely glowing fire (Pure Gruvbox accents)
|
||||||
|
Outer: Bright Red (#fb4934) | Middle: Bright Orange (#fe8019) | Inner: Bright Yellow (#fabd2f)
|
||||||
|
-->
|
||||||
|
<g id="vibrant-fire">
|
||||||
|
<!-- Outer flame (Gruvbox Red) -->
|
||||||
|
<path d="M 256 180
|
||||||
|
C 190 250, 150 290, 150 360
|
||||||
|
C 150 415, 190 435, 256 435
|
||||||
|
C 322 435, 362 415, 362 360
|
||||||
|
C 362 290, 322 250, 256 180 Z"
|
||||||
|
fill="#fb4934" />
|
||||||
|
|
||||||
|
<!-- Middle flame (Gruvbox Orange) -->
|
||||||
|
<path d="M 256 215
|
||||||
|
C 205 275, 175 310, 175 370
|
||||||
|
C 175 415, 205 425, 256 425
|
||||||
|
C 307 425, 337 415, 337 370
|
||||||
|
C 337 310, 307 275, 256 215 Z"
|
||||||
|
fill="#fe8019" />
|
||||||
|
|
||||||
|
<!-- Inner flame (Gruvbox Yellow) -->
|
||||||
|
<path d="M 256 250
|
||||||
|
C 220 295, 200 325, 200 375
|
||||||
|
C 200 410, 222 415, 256 415
|
||||||
|
C 290 415, 312 410, 312 375
|
||||||
|
C 312 325, 292 295, 256 250 Z"
|
||||||
|
fill="#fabd2f" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- The "8" as an endless ribbon (Gruvbox Light-Cream #fbf1c7) -->
|
||||||
|
<g id="number-eight">
|
||||||
|
<!-- Dark mask for optical separation from the bright fire (#1d2021) -->
|
||||||
|
<path d="M 256 310
|
||||||
|
C 228 310, 228 250, 256 250
|
||||||
|
C 284 250, 284 310, 256 310
|
||||||
|
C 220 310, 220 395, 256 395
|
||||||
|
C 292 395, 292 310, 256 310"
|
||||||
|
fill="none"
|
||||||
|
stroke="#1d2021"
|
||||||
|
stroke-width="56"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
|
||||||
|
<!-- Foreground loop -->
|
||||||
|
<path d="M 256 310
|
||||||
|
C 228 310, 228 250, 256 250
|
||||||
|
C 284 250, 284 310, 256 310
|
||||||
|
C 220 310, 220 395, 256 395
|
||||||
|
C 292 395, 292 310, 256 310"
|
||||||
|
fill="none"
|
||||||
|
stroke="#fbf1c7"
|
||||||
|
stroke-width="26"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round" />
|
||||||
|
</g>
|
||||||
|
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 5.7 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 560">
|
||||||
|
<g transform="translate(7, -36)">
|
||||||
|
<g transform="translate(-35, 35) scale(0.92)" fill="none" stroke="#83a598" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M 256 120 C 200 110, 150 135, 110 155 C 95 245, 95 335, 120 395 C 145 455, 210 492, 256 507 C 302 492, 367 455, 392 395 C 417 335, 417 245, 402 155 C 362 135, 312 110, 256 120 Z" stroke-width="20"/>
|
||||||
|
<path d="M 256 180 C 190 250, 150 290, 150 360 C 150 415, 190 435, 256 435 C 322 435, 362 415, 362 360 C 362 290, 322 250, 256 180 Z" stroke-width="15"/>
|
||||||
|
<path d="M 256 310 C 228 310, 228 250, 256 250 C 284 250, 284 310, 256 310 C 220 310, 220 395, 256 395 C 292 395, 292 310, 256 310" stroke-width="24"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 780 B |
@@ -0,0 +1,9 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 420 560">
|
||||||
|
<g transform="translate(7, -36)">
|
||||||
|
<g transform="translate(-35, 35) scale(0.92)" fill="none" stroke="#076678" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M 256 120 C 200 110, 150 135, 110 155 C 95 245, 95 335, 120 395 C 145 455, 210 492, 256 507 C 302 492, 367 455, 392 395 C 417 335, 417 245, 402 155 C 362 135, 312 110, 256 120 Z" stroke-width="20"/>
|
||||||
|
<path d="M 256 180 C 190 250, 150 290, 150 360 C 150 415, 190 435, 256 435 C 322 435, 362 415, 362 360 C 362 290, 322 250, 256 180 Z" stroke-width="15"/>
|
||||||
|
<path d="M 256 310 C 228 310, 228 250, 256 250 C 284 250, 284 310, 256 310 C 220 310, 220 395, 256 395 C 292 395, 292 310, 256 310" stroke-width="24"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 780 B |
@@ -0,0 +1,37 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var titleSvg = "api/notes/__TITLE_ICON_ID__/download";
|
||||||
|
|
||||||
|
var a = document.querySelector('#menu a[href="./ignis8"]');
|
||||||
|
if (a) {
|
||||||
|
var icon = a.querySelector(".tn-icon");
|
||||||
|
if (icon) {
|
||||||
|
var s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||||
|
s.setAttribute("class", "tn-icon tn-image tn-image-svg");
|
||||||
|
s.setAttribute("viewBox", "0 0 420 560");
|
||||||
|
s.setAttribute("fill", "none");
|
||||||
|
s.setAttribute("stroke", "currentColor");
|
||||||
|
s.setAttribute("stroke-linecap", "round");
|
||||||
|
s.setAttribute("stroke-linejoin", "round");
|
||||||
|
s.setAttribute("aria-hidden", "true");
|
||||||
|
s.innerHTML = '<g transform="translate(7, -36)"><g transform="translate(-35, 35) scale(0.92)">' +
|
||||||
|
'<path d="M 256 120 C 200 110, 150 135, 110 155 C 95 245, 95 335, 120 395 C 145 455, 210 492, 256 507 C 302 492, 367 455, 392 395 C 417 335, 417 245, 402 155 C 362 135, 312 110, 256 120 Z" stroke-width="20"/>' +
|
||||||
|
'<path d="M 256 180 C 190 250, 150 290, 150 360 C 150 415, 190 435, 256 435 C 322 435, 362 415, 362 360 C 362 290, 322 250, 256 180 Z" stroke-width="15"/>' +
|
||||||
|
'<path d="M 256 310 C 228 310, 228 250, 256 250 C 284 250, 284 310, 256 310 C 220 310, 220 395, 256 395 C 292 395, 292 310, 256 310" stroke-width="24"/>' +
|
||||||
|
'</g></g>';
|
||||||
|
icon.replaceWith(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.body.getAttribute("data-note-id") === "__CATEGORY_NOTE_ID__") {
|
||||||
|
var title = document.getElementById("title");
|
||||||
|
if (title) {
|
||||||
|
var t = document.createElement("img");
|
||||||
|
t.className = "title-image";
|
||||||
|
t.src = titleSvg;
|
||||||
|
t.alt = "ignis8";
|
||||||
|
title.insertBefore(t, title.firstChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<script>window.blogData = {"articles":[]};</script>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||||
|
<channel>
|
||||||
|
<title>Your Blog</title>
|
||||||
|
<link>https://YOUR-INSTANCE.example.com/blog</link>
|
||||||
|
<atom:link href="https://YOUR-INSTANCE.example.com/feed" rel="self" type="application/rss+xml"/>
|
||||||
|
<description>Project documentation & technical blog</description>
|
||||||
|
<language>en</language>
|
||||||
|
<lastBuildDate>REPLACED-BY-GENERATOR</lastBuildDate>
|
||||||
|
</channel>
|
||||||
|
</rss>
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url><loc>https://YOUR-INSTANCE.example.com/blog</loc></url>
|
||||||
|
</urlset>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<script>window.blogStates = {};</script>
|
||||||
+1213
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
!function(r,o){"use strict";var e,a="hljs-ln",l="hljs-ln-line",h="hljs-ln-code",s="hljs-ln-numbers",c="hljs-ln-n",m="data-line-number",i=/\r\n|\r|\n/g;function u(e){for(var n=e.toString(),t=e.anchorNode;"TD"!==t.nodeName;)t=t.parentNode;for(var r=e.focusNode;"TD"!==r.nodeName;)r=r.parentNode;var o=parseInt(t.dataset.lineNumber),i=parseInt(r.dataset.lineNumber);if(o==i)return n;var a,l=t.textContent,s=r.textContent;for(i<o&&(a=o,o=i,i=a,a=l,l=s,s=a);0!==n.indexOf(l);)l=l.slice(1);for(;-1===n.lastIndexOf(s);)s=s.slice(0,-1);for(var c=l,u=function(e){for(var n=e;"TABLE"!==n.nodeName;)n=n.parentNode;return n}(t),d=o+1;d<i;++d){var f=p('.{0}[{1}="{2}"]',[h,m,d]);c+="\n"+u.querySelector(f).textContent}return c+="\n"+s}function n(e){try{var n=o.querySelectorAll("code.hljs,code.nohighlight");for(var t in n)n.hasOwnProperty(t)&&(n[t].classList.contains("nohljsln")||d(n[t],e))}catch(e){r.console.error("LineNumbers error: ",e)}}function d(e,n){"object"==typeof e&&r.setTimeout(function(){e.innerHTML=f(e,n)},0)}function f(e,n){var t,r,o=(t=e,{singleLine:function(e){return!!e.singleLine&&e.singleLine}(r=(r=n)||{}),startFrom:function(e,n){var t=1;isFinite(n.startFrom)&&(t=n.startFrom);var r=function(e,n){return e.hasAttribute(n)?e.getAttribute(n):null}(e,"data-ln-start-from");return null!==r&&(t=function(e,n){if(!e)return n;var t=Number(e);return isFinite(t)?t:n}(r,1)),t}(t,r)});return function e(n){var t=n.childNodes;for(var r in t){var o;t.hasOwnProperty(r)&&(o=t[r],0<(o.textContent.trim().match(i)||[]).length&&(0<o.childNodes.length?e(o):v(o.parentNode)))}}(e),function(e,n){var t=g(e);""===t[t.length-1].trim()&&t.pop();if(1<t.length||n.singleLine){for(var r="",o=0,i=t.length;o<i;o++)r+=p('<tr><td class="{0} {1}" {3}="{5}"><div class="{2}" {3}="{5}"></div></td><td class="{0} {4}" {3}="{5}">{6}</td></tr>',[l,s,c,m,h,o+n.startFrom,0<t[o].length?t[o]:" "]);return p('<table class="{0}">{1}</table>',[a,r])}return e}(e.innerHTML,o)}function v(e){var n=e.className;if(/hljs-/.test(n)){for(var t=g(e.innerHTML),r=0,o="";r<t.length;r++){o+=p('<span class="{0}">{1}</span>\n',[n,0<t[r].length?t[r]:" "])}e.innerHTML=o.trim()}}function g(e){return 0===e.length?[]:e.split(i)}function p(e,t){return e.replace(/\{(\d+)\}/g,function(e,n){return void 0!==t[n]?t[n]:e})}r.hljs?(r.hljs.initLineNumbersOnLoad=function(e){"interactive"===o.readyState||"complete"===o.readyState?n(e):r.addEventListener("DOMContentLoaded",function(){n(e)})},r.hljs.lineNumbersBlock=d,r.hljs.lineNumbersBlockSync=function(e,n){if("object"!=typeof e)return;e.innerHTML=f(e,n)},r.hljs.lineNumbersValue=function(e,n){if("string"!=typeof e)return;var t=document.createElement("code");return t.innerHTML=e,f(t,n)},(e=o.createElement("style")).type="text/css",e.innerHTML=p(".{0}{border-collapse:collapse}.{0} td{padding:0}.{1}:before{content:attr({2})}",[a,c,m]),o.getElementsByTagName("head")[0].appendChild(e)):r.console.error("highlight.js not detected!"),document.addEventListener("copy",function(e){var n,t=window.getSelection();!function(e){for(var n=e;n;){if(n.className&&-1!==n.className.indexOf("hljs-ln-code"))return 1;n=n.parentNode}}(t.anchorNode)||(n=-1!==window.navigator.userAgent.indexOf("Edge")?u(t):t.toString(),e.clipboardData.setData("text/plain",n),e.preventDefault())})}(window,document);
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
var card = e.target.closest(".blog-post");
|
||||||
|
if (!card) return;
|
||||||
|
if (e.target.closest("a")) return;
|
||||||
|
var link = card.querySelector(".blog-post-title a");
|
||||||
|
if (!link) return;
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.href = link.getAttribute("href");
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var pane = document.getElementById("left-pane");
|
||||||
|
if (!pane) return;
|
||||||
|
|
||||||
|
var items = [];
|
||||||
|
var menu = document.getElementById("menu");
|
||||||
|
if (menu) {
|
||||||
|
var links = menu.querySelectorAll('a[href="./aboutme"], a[href="./impressum"]');
|
||||||
|
links.forEach(function (a) {
|
||||||
|
var li = a.closest("li");
|
||||||
|
if (li) items.push(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var rssLink = '<a class="bmc-link" href="./feed"><i class="tn-icon bx bx-rss"></i><span>RSS Feed</span></a>';
|
||||||
|
|
||||||
|
var nav = document.createElement("nav");
|
||||||
|
nav.id = "bmc-links";
|
||||||
|
var inner = "";
|
||||||
|
items.forEach(function (li) {
|
||||||
|
var a = li.querySelector("a");
|
||||||
|
if (a) {
|
||||||
|
var icon = a.querySelector(".tn-icon");
|
||||||
|
var iconHtml = icon ? icon.outerHTML : "";
|
||||||
|
var text = (a.querySelector("span") || a).textContent.trim();
|
||||||
|
if (text === "Impressum") {
|
||||||
|
inner += rssLink;
|
||||||
|
}
|
||||||
|
inner += '<a class="bmc-link" href="' + a.getAttribute("href") + '">' + iconHtml + '<span>' + text + '</span></a>';
|
||||||
|
}
|
||||||
|
li.remove();
|
||||||
|
});
|
||||||
|
nav.innerHTML = inner;
|
||||||
|
pane.appendChild(nav);
|
||||||
|
|
||||||
|
var wrap = document.createElement("div");
|
||||||
|
wrap.id = "bmc-wrap";
|
||||||
|
wrap.innerHTML =
|
||||||
|
'<a class="bmc-btn" href="https://www.buymeacoffee.com/YOUR-USERNAME" target="_blank" rel="noopener noreferrer">' +
|
||||||
|
'<span class="bmc-emoji">\u2615</span><span class="bmc-text">Buy me a coffee</span></a>' +
|
||||||
|
'<div class="bmc-copyright">\u00a9 ' + new Date().getFullYear() + ' Your Name \u00b7 Powered by <a class="bmc-powered" href="https://triliumnotes.org"><svg class="powered-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256" aria-hidden="true"><style> .st0{fill:#95C980;} .st1{fill:#72B755;} .st2{fill:#4FA52B;} .st3{fill:#EE8C89;} .st4{fill:#E96562;} .st5{fill:#E33F3B;} .st6{fill:#EFB075;} .st7{fill:#E99547;} .st8{fill:#E47B19;} </style><g><path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/><path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/><path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/><path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/><path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/><path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/><path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/><path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/><path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/></g></svg> Trilium</a></div>';
|
||||||
|
pane.appendChild(wrap);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function codeText(code) {
|
||||||
|
var ln = code.querySelectorAll(".hljs-ln .hljs-ln-code");
|
||||||
|
if (ln.length) {
|
||||||
|
var parts = [];
|
||||||
|
for (var i = 0; i < ln.length; i++) parts.push(ln[i].textContent);
|
||||||
|
return parts.join("\n");
|
||||||
|
}
|
||||||
|
return code.textContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("#content pre").forEach(function (pre) {
|
||||||
|
if (pre.classList.contains("code-processed")) return;
|
||||||
|
var code = pre.querySelector("code");
|
||||||
|
if (!code) return;
|
||||||
|
pre.classList.add("code-processed");
|
||||||
|
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.className = "code-copy-btn";
|
||||||
|
btn.type = "button";
|
||||||
|
btn.textContent = "Copy";
|
||||||
|
btn.addEventListener("click", function (e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
var text = codeText(code);
|
||||||
|
function copied() {
|
||||||
|
btn.textContent = "Copied!";
|
||||||
|
setTimeout(function () { btn.textContent = "Copy"; }, 1500);
|
||||||
|
}
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(copied);
|
||||||
|
} else {
|
||||||
|
var ta = document.createElement("textarea");
|
||||||
|
ta.value = text;
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
copied();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pre.appendChild(btn);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var codes = document.querySelectorAll("#content pre code:not(.language-mermaid)");
|
||||||
|
if (!codes.length) return;
|
||||||
|
|
||||||
|
function apply() {
|
||||||
|
if (!window.hljs) return;
|
||||||
|
codes.forEach(function (c) {
|
||||||
|
c.classList.add("hljs");
|
||||||
|
hljs.highlightElement(c);
|
||||||
|
if (window.hljs.lineNumbersBlock) {
|
||||||
|
hljs.lineNumbersBlock(c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlugin() {
|
||||||
|
if (window.hljs && window.hljs.lineNumbersBlock) { apply(); return; }
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.src = "api/notes/__HLJS_LN_ID__/download";
|
||||||
|
s.onload = apply;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (window.hljs) { loadPlugin(); return; }
|
||||||
|
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.src = "api/notes/__HLJS_ID__/download";
|
||||||
|
s.onload = loadPlugin;
|
||||||
|
document.head.appendChild(s);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<style>
|
||||||
|
html.theme-dark #content h1 {
|
||||||
|
background-color: rgba(184, 187, 38, 0.80) !important;
|
||||||
|
border-bottom-color: rgba(184, 187, 38, 0.95) !important;
|
||||||
|
}
|
||||||
|
html.theme-light #content h1 {
|
||||||
|
background-color: rgba(184, 187, 38, 0.55) !important;
|
||||||
|
border-bottom-color: rgba(184, 187, 38, 0.8) !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (e.defaultPrevented) return;
|
||||||
|
var row = e.target.closest(".blog-garden-table tbody tr");
|
||||||
|
if (!row) return;
|
||||||
|
if (e.target.closest("a")) return;
|
||||||
|
var link = row.querySelector("a");
|
||||||
|
if (!link) return;
|
||||||
|
e.preventDefault();
|
||||||
|
window.location.href = link.getAttribute("href");
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<style>#content h1#title { display: none; }</style>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var logo = document.getElementById("header-logo");
|
||||||
|
if (!logo) return;
|
||||||
|
var h = logo.getAttribute("href");
|
||||||
|
if (h && h.indexOf("__BLOG_ROOT_ID__") !== -1) {
|
||||||
|
logo.setAttribute("href", "./blog");
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById("content");
|
||||||
|
if (!content) return;
|
||||||
|
var m = document.body.className.match(/type-([\w-]+)/);
|
||||||
|
var type = m ? m[1] : "";
|
||||||
|
var widgetTypes = ["mermaid", "canvas", "mindMap", "webView", "relationMap", "spreadsheet", "search", "geo"];
|
||||||
|
if (widgetTypes.indexOf(type) === -1) return;
|
||||||
|
if (content.querySelector("svg, iframe, canvas, .excalidraw, .mindmap, .mermaid")) return;
|
||||||
|
|
||||||
|
var clone = content.cloneNode(true);
|
||||||
|
clone.querySelectorAll("script, style, nav, a, .blog-prevnext, .reading-time").forEach(function (el) { el.remove(); });
|
||||||
|
var raw = (clone.textContent || "").trim();
|
||||||
|
if (!raw) return;
|
||||||
|
|
||||||
|
function makePre(text) {
|
||||||
|
var pre = document.createElement("pre");
|
||||||
|
pre.className = "note-fallback";
|
||||||
|
var code = document.createElement("code");
|
||||||
|
code.className = "language-json";
|
||||||
|
code.textContent = text;
|
||||||
|
pre.appendChild(code);
|
||||||
|
applyHighlight(code);
|
||||||
|
return pre;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hljsLoading = false;
|
||||||
|
function applyHighlight(codeEl) {
|
||||||
|
if (window.hljs) {
|
||||||
|
codeEl.classList.add("hljs");
|
||||||
|
try { hljs.highlightElement(codeEl); } catch (e) {}
|
||||||
|
if (window.hljs.lineNumbersBlock) {
|
||||||
|
try { hljs.lineNumbersBlock(codeEl); } catch (e) {}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (hljsLoading) {
|
||||||
|
setTimeout(function () { applyHighlight(codeEl); }, 200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
hljsLoading = true;
|
||||||
|
var s = document.createElement("script");
|
||||||
|
s.src = "api/notes/__HLJS_ID__/download";
|
||||||
|
s.onload = function () {
|
||||||
|
if (window.hljs && window.hljs.lineNumbersBlock) { applyHighlight(codeEl); return; }
|
||||||
|
var p = document.createElement("script");
|
||||||
|
p.src = "api/notes/__HLJS_LN_ID__/download";
|
||||||
|
p.onload = function () { applyHighlight(codeEl); };
|
||||||
|
document.head.appendChild(p);
|
||||||
|
};
|
||||||
|
document.head.appendChild(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "mermaid") {
|
||||||
|
content.querySelectorAll("img, hr").forEach(function (el) { el.remove(); });
|
||||||
|
var nativePre = content.querySelector("pre");
|
||||||
|
if (!nativePre) return;
|
||||||
|
var src = nativePre.textContent;
|
||||||
|
if (!src.trim()) return;
|
||||||
|
var nativeDetails = nativePre.closest("details");
|
||||||
|
|
||||||
|
var wrapper = document.createElement("div");
|
||||||
|
wrapper.className = "mermaid-render";
|
||||||
|
var diagram = document.createElement("div");
|
||||||
|
diagram.className = "mermaid-diagram";
|
||||||
|
var details = document.createElement("details");
|
||||||
|
details.className = "mermaid-source";
|
||||||
|
var summary = document.createElement("summary");
|
||||||
|
summary.textContent = "Source";
|
||||||
|
var pre = document.createElement("pre");
|
||||||
|
var code = document.createElement("code");
|
||||||
|
code.textContent = src;
|
||||||
|
pre.appendChild(code);
|
||||||
|
addCopyButton(pre, src);
|
||||||
|
details.appendChild(summary);
|
||||||
|
details.appendChild(pre);
|
||||||
|
wrapper.appendChild(diagram);
|
||||||
|
wrapper.appendChild(details);
|
||||||
|
(nativeDetails || nativePre).replaceWith(wrapper);
|
||||||
|
applyHighlight(code);
|
||||||
|
|
||||||
|
var MMD_DARK = {
|
||||||
|
background: "transparent",
|
||||||
|
textColor: "#ebdbb2",
|
||||||
|
titleColor: "#fbf1c7",
|
||||||
|
lineColor: "#a89984",
|
||||||
|
edgeLabelBackground: "#3c3836",
|
||||||
|
nodeTextColor: "#fbf1c7",
|
||||||
|
primaryColor: "#458588", primaryTextColor: "#fbf1c7", primaryBorderColor: "#458588",
|
||||||
|
secondaryColor: "#b8bb26", secondaryTextColor: "#1d2021", secondaryBorderColor: "#b8bb26",
|
||||||
|
tertiaryColor: "#d3869b", tertiaryTextColor: "#1d2021", tertiaryBorderColor: "#d3869b"
|
||||||
|
};
|
||||||
|
var MMD_LIGHT = {
|
||||||
|
background: "transparent",
|
||||||
|
textColor: "#3c3836",
|
||||||
|
titleColor: "#282828",
|
||||||
|
lineColor: "#928374",
|
||||||
|
edgeLabelBackground: "#ebdbb2",
|
||||||
|
nodeTextColor: "#1d2021",
|
||||||
|
primaryColor: "#83a598", primaryTextColor: "#1d2021", primaryBorderColor: "#076678",
|
||||||
|
secondaryColor: "#98971a", secondaryTextColor: "#fbf1c7", secondaryBorderColor: "#79740e",
|
||||||
|
tertiaryColor: "#b16286", tertiaryTextColor: "#fbf1c7", tertiaryBorderColor: "#8f3f71"
|
||||||
|
};
|
||||||
|
function mermaidConfig() {
|
||||||
|
var light = document.documentElement.classList.contains("theme-light");
|
||||||
|
return { startOnLoad: false, theme: "base", themeVariables: light ? MMD_LIGHT : MMD_DARK };
|
||||||
|
}
|
||||||
|
function renderMermaid() {
|
||||||
|
import("/assets/mermaid.core-BUJU3YJZ.js").then(function (mod) {
|
||||||
|
var mermaid = mod.default;
|
||||||
|
mermaid.initialize(mermaidConfig());
|
||||||
|
return mermaid.render("mm-" + Date.now(), src);
|
||||||
|
}).then(function (res) {
|
||||||
|
diagram.innerHTML = res.svg;
|
||||||
|
diagram.querySelectorAll(".node rect, .cluster rect").forEach(function (r) {
|
||||||
|
var rad = Math.min(8, Math.min(parseFloat(r.getAttribute("width") || 50), parseFloat(r.getAttribute("height") || 50)) * 0.15);
|
||||||
|
r.setAttribute("rx", rad);
|
||||||
|
r.setAttribute("ry", rad);
|
||||||
|
});
|
||||||
|
}).catch(function () {
|
||||||
|
diagram.textContent = src;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderMermaid();
|
||||||
|
var themeInput = document.querySelector(".theme-selection input");
|
||||||
|
if (themeInput) {
|
||||||
|
themeInput.addEventListener("change", renderMermaid);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "webView") {
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.className = "note-fallback note-fallback-link";
|
||||||
|
p.innerHTML = "Embedded page: <a href=\"" + raw + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + raw + "</a>";
|
||||||
|
content.appendChild(p);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var NS = "http://www.w3.org/2000/svg";
|
||||||
|
function svgEl(name, attrs) {
|
||||||
|
var n = document.createElementNS(NS, name);
|
||||||
|
for (var k in attrs) n.setAttribute(k, attrs[k]);
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fallbackCopy(text, done) {
|
||||||
|
var ta = document.createElement("textarea");
|
||||||
|
ta.value = text;
|
||||||
|
ta.style.position = "fixed";
|
||||||
|
ta.style.opacity = "0";
|
||||||
|
document.body.appendChild(ta);
|
||||||
|
ta.select();
|
||||||
|
try { document.execCommand("copy"); } catch (e) {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
function addCopyButton(pre, text) {
|
||||||
|
var btn = document.createElement("button");
|
||||||
|
btn.className = "code-copy-btn";
|
||||||
|
btn.type = "button";
|
||||||
|
btn.textContent = "Copy";
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
var done = function () {
|
||||||
|
btn.textContent = "Copied";
|
||||||
|
setTimeout(function () { btn.textContent = "Copy"; }, 1500);
|
||||||
|
};
|
||||||
|
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||||
|
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text, done); });
|
||||||
|
} else {
|
||||||
|
fallbackCopy(text, done);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pre.appendChild(btn);
|
||||||
|
}
|
||||||
|
function makeSourceDetails(text) {
|
||||||
|
var details = document.createElement("details");
|
||||||
|
details.className = "mermaid-source";
|
||||||
|
var summary = document.createElement("summary");
|
||||||
|
summary.textContent = "Source";
|
||||||
|
var pre = document.createElement("pre");
|
||||||
|
var code = document.createElement("code");
|
||||||
|
code.className = "language-json";
|
||||||
|
code.textContent = text;
|
||||||
|
pre.appendChild(code);
|
||||||
|
addCopyButton(pre, text);
|
||||||
|
details.appendChild(summary);
|
||||||
|
details.appendChild(pre);
|
||||||
|
applyHighlight(code);
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- MindMap (MindElixir data) ---------- */
|
||||||
|
var MIND_DARK = [
|
||||||
|
{ bg: "#458588", bd: "#3a6b70", text: "#fbf1c7" },
|
||||||
|
{ bg: "#b8bb26", bd: "#98971a", text: "#1d2021" },
|
||||||
|
{ bg: "#d3869b", bd: "#b16286", text: "#1d2021" },
|
||||||
|
{ bg: "#8ec07c", bd: "#689d6a", text: "#1d2021" },
|
||||||
|
{ bg: "#fe8019", bd: "#d65d0e", text: "#1d2021" }
|
||||||
|
];
|
||||||
|
var MIND_LIGHT = [
|
||||||
|
{ bg: "#83a598", bd: "#458588", text: "#1d2021" },
|
||||||
|
{ bg: "#98971a", bd: "#79740e", text: "#fbf1c7" },
|
||||||
|
{ bg: "#b16286", bd: "#8f3f71", text: "#fbf1c7" },
|
||||||
|
{ bg: "#427b58", bd: "#3a6350", text: "#fbf1c7" },
|
||||||
|
{ bg: "#af3a03", bd: "#8f3003", text: "#fbf1c7" }
|
||||||
|
];
|
||||||
|
function mindPalette() {
|
||||||
|
return document.documentElement.classList.contains("theme-light") ? MIND_LIGHT : MIND_DARK;
|
||||||
|
}
|
||||||
|
function mindEdgeColor() {
|
||||||
|
return document.documentElement.classList.contains("theme-light") ? "#928374" : "#a89984";
|
||||||
|
}
|
||||||
|
function mindSize(topic, fontSize) {
|
||||||
|
var lines = String(topic || "").split("\n");
|
||||||
|
var w = 0;
|
||||||
|
for (var i = 0; i < lines.length; i++) w = Math.max(w, lines[i].length * fontSize * 0.62);
|
||||||
|
return { w: Math.max(w + 24, 56), h: Math.max(lines.length * (fontSize + 6) + 10, 32) };
|
||||||
|
}
|
||||||
|
function mindSubHeight(n, depth) {
|
||||||
|
n.depth = depth;
|
||||||
|
var fs = depth === 0 ? 16 : 14;
|
||||||
|
n.box = mindSize(n.topic, fs);
|
||||||
|
if (n.children && n.children.length) {
|
||||||
|
var sum = 0;
|
||||||
|
for (var i = 0; i < n.children.length; i++) sum += mindSubHeight(n.children[i], depth + 1);
|
||||||
|
n.childSpan = sum + (n.children.length - 1) * 18;
|
||||||
|
n.total = Math.max(n.box.h, n.childSpan);
|
||||||
|
} else {
|
||||||
|
n.total = n.box.h;
|
||||||
|
}
|
||||||
|
return n.total;
|
||||||
|
}
|
||||||
|
function mindPlace(n, x, side, yTop) {
|
||||||
|
n.x = x - n.box.w / 2;
|
||||||
|
n.y = yTop + (n.total - n.box.h) / 2;
|
||||||
|
if (!n.children || !n.children.length) return;
|
||||||
|
var cursor = yTop;
|
||||||
|
for (var i = 0; i < n.children.length; i++) {
|
||||||
|
var c = n.children[i];
|
||||||
|
var childSide = side === 0 ? (i % 2 === 0 ? -1 : 1) : side;
|
||||||
|
var childX = x + childSide * (n.box.w / 2 + 46 + c.box.w / 2);
|
||||||
|
mindPlace(c, childX, childSide, cursor);
|
||||||
|
cursor += c.total + 18;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function renderMindMap(nodeData) {
|
||||||
|
var wrap = document.createElement("div");
|
||||||
|
wrap.className = "mindmap-render";
|
||||||
|
var root = nodeData;
|
||||||
|
mindSubHeight(root, 0);
|
||||||
|
mindPlace(root, 0, 0, -root.total / 2);
|
||||||
|
var minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
|
||||||
|
(function walk(n) {
|
||||||
|
minX = Math.min(minX, n.x); maxX = Math.max(maxX, n.x + n.box.w);
|
||||||
|
minY = Math.min(minY, n.y); maxY = Math.max(maxY, n.y + n.box.h);
|
||||||
|
if (n.children) n.children.forEach(walk);
|
||||||
|
})(root);
|
||||||
|
var pad = 34;
|
||||||
|
var MIND = mindPalette();
|
||||||
|
var EDGE = mindEdgeColor();
|
||||||
|
var W = maxX - minX + pad * 2, H = maxY - minY + pad * 2;
|
||||||
|
var svg = svgEl("svg", { class: "mindmap-svg", viewBox: (minX - pad) + " " + (minY - pad) + " " + W + " " + H, width: W, height: H });
|
||||||
|
function draw(n) {
|
||||||
|
var col = MIND[Math.min(n.depth, MIND.length - 1)];
|
||||||
|
var g = svgEl("g");
|
||||||
|
g.appendChild(svgEl("rect", { x: n.x, y: n.y, width: n.box.w, height: n.box.h, rx: 8, fill: col.bg, stroke: col.bd, "stroke-width": 1.5 }));
|
||||||
|
var lines = String(n.topic || "").split("\n");
|
||||||
|
var fs = n.depth === 0 ? 16 : 14;
|
||||||
|
var lh = fs + 5;
|
||||||
|
for (var i = 0; i < lines.length; i++) {
|
||||||
|
var t = svgEl("text", { x: n.x + n.box.w / 2, y: n.y + n.box.h / 2 - ((lines.length - 1) * lh) / 2 + i * lh, "text-anchor": "middle", "dominant-baseline": "central", "font-size": fs, fill: col.text, "font-family": "sans-serif" });
|
||||||
|
t.textContent = lines[i];
|
||||||
|
g.appendChild(t);
|
||||||
|
}
|
||||||
|
svg.appendChild(g);
|
||||||
|
if (n.children) n.children.forEach(function (c) {
|
||||||
|
var sy = n.y + n.box.h / 2, ey = c.y + c.box.h / 2;
|
||||||
|
var sx = c.x > n.x ? n.x + n.box.w : n.x;
|
||||||
|
var ex = c.x > n.x ? c.x : c.x + c.box.w;
|
||||||
|
var dir = ex >= sx ? 1 : -1;
|
||||||
|
var dx = Math.max(26, Math.min(56, Math.abs(ex - sx) * 0.4));
|
||||||
|
var lead = dx * 0.5;
|
||||||
|
svg.appendChild(svgEl("path", {
|
||||||
|
d: "M " + sx + " " + sy +
|
||||||
|
" L " + (sx + dir * lead) + " " + sy +
|
||||||
|
" C " + (sx + dir * dx) + " " + sy + " " + (ex - dir * dx) + " " + ey + " " + (ex - dir * lead) + " " + ey +
|
||||||
|
" L " + ex + " " + ey,
|
||||||
|
fill: "none", stroke: EDGE, "stroke-width": 2.5,
|
||||||
|
"stroke-linecap": "round", "stroke-linejoin": "round"
|
||||||
|
}));
|
||||||
|
draw(c);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
draw(root);
|
||||||
|
wrap.appendChild(svg);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- Canvas (Excalidraw) ---------- */
|
||||||
|
function renderExcalidraw(elements) {
|
||||||
|
var wrap = document.createElement("div");
|
||||||
|
wrap.className = "excalidraw-render";
|
||||||
|
var els = elements.filter(function (e) { return e && !e.isDeleted; });
|
||||||
|
if (!els.length) { wrap.textContent = "Empty canvas"; return wrap; }
|
||||||
|
var pad = 40;
|
||||||
|
var minX = 1e9, minY = 1e9, maxX = -1e9, maxY = -1e9;
|
||||||
|
els.forEach(function (e) {
|
||||||
|
minX = Math.min(minX, e.x); minY = Math.min(minY, e.y);
|
||||||
|
maxX = Math.max(maxX, e.x + e.width); maxY = Math.max(maxY, e.y + e.height);
|
||||||
|
});
|
||||||
|
var W = maxX - minX + pad * 2, H = maxY - minY + pad * 2;
|
||||||
|
var svg = svgEl("svg", { class: "excalidraw-svg", viewBox: (minX - pad) + " " + (minY - pad) + " " + W + " " + H, width: W, height: H });
|
||||||
|
els.forEach(function (e) {
|
||||||
|
var g = svgEl("g");
|
||||||
|
if (e.angle) g.setAttribute("transform", "rotate(" + (e.angle * 180 / Math.PI) + " " + (e.x + e.width / 2) + " " + (e.y + e.height / 2) + ")");
|
||||||
|
if (typeof e.opacity === "number" && e.opacity < 100) g.setAttribute("opacity", e.opacity / 100);
|
||||||
|
var sc = e.strokeColor || "#282828", bg = e.backgroundColor || "transparent", sw = Math.max(1, e.strokeWidth || 2);
|
||||||
|
var filled = bg && bg !== "transparent";
|
||||||
|
var fillOp = e.fillStyle === "solid" ? 1 : 0.35;
|
||||||
|
function strokeShape(shape) {
|
||||||
|
if (filled) { shape.setAttribute("fill", bg); shape.setAttribute("fill-opacity", fillOp); }
|
||||||
|
else shape.setAttribute("fill", "none");
|
||||||
|
shape.setAttribute("stroke", sc); shape.setAttribute("stroke-width", sw);
|
||||||
|
g.appendChild(shape);
|
||||||
|
}
|
||||||
|
if (e.type === "rectangle") {
|
||||||
|
strokeShape(svgEl("rect", { x: e.x, y: e.y, width: e.width, height: e.height, rx: Math.min(8, Math.min(e.width, e.height) * 0.08) }));
|
||||||
|
} else if (e.type === "ellipse") {
|
||||||
|
strokeShape(svgEl("ellipse", { cx: e.x + e.width / 2, cy: e.y + e.height / 2, rx: e.width / 2, ry: e.height / 2 }));
|
||||||
|
} else if (e.type === "diamond") {
|
||||||
|
var cx = e.x + e.width / 2, cy = e.y + e.height / 2;
|
||||||
|
strokeShape(svgEl("polygon", { points: cx + "," + e.y + " " + (e.x + e.width) + "," + cy + " " + cx + "," + (e.y + e.height) + " " + e.x + "," + cy }));
|
||||||
|
} else if (e.type === "line" || e.type === "arrow") {
|
||||||
|
var pts = e.points || [];
|
||||||
|
if (!pts.length) return;
|
||||||
|
var coords = pts.map(function (pt) { return (e.x + pt[0]) + "," + (e.y + pt[1]); }).join(" ");
|
||||||
|
strokeShape(svgEl("polyline", { points: coords }));
|
||||||
|
if (e.type === "arrow" && pts.length > 1) {
|
||||||
|
var last = pts[pts.length - 1], prev = pts[pts.length - 2];
|
||||||
|
var ang = Math.atan2(last[1] - prev[1], last[0] - prev[0]);
|
||||||
|
var ax = e.x + last[0], ay = e.y + last[1], al = 9 + sw;
|
||||||
|
g.appendChild(svgEl("polygon", { points: (ax + Math.cos(ang) * al) + "," + (ay + Math.sin(ang) * al) + " " + (ax + Math.cos(ang + 2.6) * al * 0.6) + "," + (ay + Math.sin(ang + 2.6) * al * 0.6) + " " + (ax + Math.cos(ang - 2.6) * al * 0.6) + "," + (ay + Math.sin(ang - 2.6) * al * 0.6), fill: sc, stroke: sc }));
|
||||||
|
}
|
||||||
|
} else if (e.type === "freedraw") {
|
||||||
|
var fpts = e.points || [];
|
||||||
|
if (!fpts.length) return;
|
||||||
|
strokeShape(svgEl("path", { d: "M " + fpts.map(function (pt) { return (e.x + pt[0]) + "," + (e.y + pt[1]); }).join(" L "), "stroke-linecap": "round", "stroke-linejoin": "round" }));
|
||||||
|
} else if (e.type === "text") {
|
||||||
|
var fs = e.fontSize || 20;
|
||||||
|
var ff = e.fontFamily === 2 ? "sans-serif" : (e.fontFamily === 3 ? "monospace" : "'Comic Sans MS','Comic Sans',cursive");
|
||||||
|
var ta = e.textAlign === "left" ? "start" : (e.textAlign === "right" ? "end" : "middle");
|
||||||
|
var x = ta === "middle" ? e.x + e.width / 2 : e.x;
|
||||||
|
var y = e.verticalAlign === "top" ? e.y + fs * 0.8 : e.y + e.height / 2 + fs * 0.35;
|
||||||
|
var lines = String(e.text || "").split("\n");
|
||||||
|
var t = svgEl("text", { x: x, y: y, "font-size": fs, fill: sc, "font-family": ff, "text-anchor": ta });
|
||||||
|
for (var li = 0; li < lines.length; li++) {
|
||||||
|
var ts = document.createElementNS(NS, "tspan");
|
||||||
|
ts.setAttribute("x", x);
|
||||||
|
ts.setAttribute("dy", li === 0 ? 0 : fs * 1.2);
|
||||||
|
ts.textContent = lines[li];
|
||||||
|
t.appendChild(ts);
|
||||||
|
}
|
||||||
|
g.appendChild(t);
|
||||||
|
}
|
||||||
|
svg.appendChild(g);
|
||||||
|
});
|
||||||
|
wrap.appendChild(svg);
|
||||||
|
return wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* content per Download-Endpoint holen */
|
||||||
|
var noteId = document.body.getAttribute("data-note-id");
|
||||||
|
var box = document.createElement("div");
|
||||||
|
var prevnext = content.querySelector(".blog-prevnext");
|
||||||
|
if (prevnext) { content.insertBefore(box, prevnext); }
|
||||||
|
else { content.appendChild(box); }
|
||||||
|
|
||||||
|
function renderWidget() {
|
||||||
|
fetch("api/notes/" + noteId + "/download")
|
||||||
|
.then(function (r) { return r.text(); })
|
||||||
|
.then(function (text) {
|
||||||
|
var pretty = text, data = null;
|
||||||
|
try { data = JSON.parse(text); pretty = JSON.stringify(data, null, 2); } catch (e) {}
|
||||||
|
box.innerHTML = "";
|
||||||
|
var rendered = null;
|
||||||
|
if (data && type === "mindMap" && data.nodeData) rendered = renderMindMap(data.nodeData);
|
||||||
|
else if (data && type === "canvas" && Array.isArray(data.elements)) rendered = renderExcalidraw(data.elements);
|
||||||
|
if (rendered) {
|
||||||
|
rendered.appendChild(makeSourceDetails(pretty));
|
||||||
|
box.appendChild(rendered);
|
||||||
|
} else {
|
||||||
|
box.appendChild(makePre(pretty));
|
||||||
|
box.appendChild(makeSourceDetails(pretty));
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function () {
|
||||||
|
box.textContent = "Content not available.";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
renderWidget();
|
||||||
|
var widgetThemeInput = document.querySelector(".theme-selection input");
|
||||||
|
if (widgetThemeInput) {
|
||||||
|
widgetThemeInput.addEventListener("change", renderWidget);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var data = window.blogData;
|
||||||
|
if (!data || !data.articles || !data.articles.length) return;
|
||||||
|
var id = document.body.getAttribute("data-note-id");
|
||||||
|
var list = data.articles;
|
||||||
|
var idx = -1;
|
||||||
|
for (var i = 0; i < list.length; i++) { if (list[i].id === id) { idx = i; break; } }
|
||||||
|
if (idx < 0) return;
|
||||||
|
var builtin = document.querySelector("#content-footer .navigation");
|
||||||
|
if (builtin) builtin.style.display = "none";
|
||||||
|
var content = document.getElementById("content");
|
||||||
|
if (!content) return;
|
||||||
|
var prev = idx + 1 < list.length ? list[idx + 1] : null;
|
||||||
|
var next = idx > 0 ? list[idx - 1] : null;
|
||||||
|
if (!prev && !next) return;
|
||||||
|
var box = document.createElement("nav");
|
||||||
|
box.className = "blog-prevnext";
|
||||||
|
var inner = "";
|
||||||
|
if (prev) {
|
||||||
|
inner += '<a class="blog-pn prev" href="' + prev.url + '"><span class="blog-pn-label">Previous</span><span class="blog-pn-title">' + prev.title + '</span></a>';
|
||||||
|
} else {
|
||||||
|
inner += '<span class="blog-pn empty"></span>';
|
||||||
|
}
|
||||||
|
if (next) {
|
||||||
|
inner += '<a class="blog-pn next" href="' + next.url + '"><span class="blog-pn-label">Next</span><span class="blog-pn-title">' + next.title + '</span></a>';
|
||||||
|
} else {
|
||||||
|
inner += '<span class="blog-pn empty"></span>';
|
||||||
|
}
|
||||||
|
box.innerHTML = inner;
|
||||||
|
content.appendChild(box);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var content = document.getElementById("content");
|
||||||
|
if (!content) return;
|
||||||
|
if (!document.body.classList.contains("type-text")) return;
|
||||||
|
if (content.querySelector(".blog-post") || content.querySelector(".blog-garden-table") || content.querySelector(".blog-subcategories") || content.querySelector(".blog-search")) return;
|
||||||
|
var clone = content.cloneNode(true);
|
||||||
|
clone.querySelectorAll("script, style, pre, code, nav, a, .blog-prevnext").forEach(function (el) { el.remove(); });
|
||||||
|
var words = (clone.textContent || "").trim().split(/\s+/).filter(Boolean).length;
|
||||||
|
var mins = Math.max(1, Math.round(words / 200));
|
||||||
|
var title = document.getElementById("title");
|
||||||
|
if (!title) return;
|
||||||
|
var p = document.createElement("div");
|
||||||
|
p.className = "reading-time";
|
||||||
|
p.textContent = mins + " min read";
|
||||||
|
title.parentNode.insertBefore(p, title.nextSibling);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var old = document.querySelector(".search-item .search-input");
|
||||||
|
if (!old) return;
|
||||||
|
var box = old.closest(".search-item") || old.parentNode;
|
||||||
|
|
||||||
|
var input = document.createElement("input");
|
||||||
|
input.type = "text";
|
||||||
|
input.className = "blog-search-header-input";
|
||||||
|
input.placeholder = old.placeholder || "Search...";
|
||||||
|
old.replaceWith(input);
|
||||||
|
|
||||||
|
var overlay = document.createElement("div");
|
||||||
|
overlay.className = "blog-search-overlay";
|
||||||
|
box.appendChild(overlay);
|
||||||
|
|
||||||
|
function render(q) {
|
||||||
|
q = (q || "").toLowerCase().trim();
|
||||||
|
if (!q) { overlay.innerHTML = ""; overlay.classList.remove("open"); return; }
|
||||||
|
var articles = (window.blogData && window.blogData.articles) || [];
|
||||||
|
var hits = articles.filter(function (a) {
|
||||||
|
var hay = (a.title + " " + (a.summary || "") + " " + (a.tags || []).join(" ")).toLowerCase();
|
||||||
|
return hay.indexOf(q) !== -1;
|
||||||
|
});
|
||||||
|
if (!hits.length) {
|
||||||
|
overlay.innerHTML = '<div class="blog-search-none">No results.</div>';
|
||||||
|
} else {
|
||||||
|
overlay.innerHTML = hits.slice(0, 8).map(function (a) {
|
||||||
|
return '<a class="blog-search-overlay-hit" href="' + a.url + '"><span class="t">' + a.title + '</span>' +
|
||||||
|
(a.summary ? '<span class="s">' + a.summary + '</span>' : '') + '</a>';
|
||||||
|
}).join("");
|
||||||
|
}
|
||||||
|
overlay.classList.add("open");
|
||||||
|
}
|
||||||
|
|
||||||
|
var t;
|
||||||
|
input.addEventListener("input", function () {
|
||||||
|
clearTimeout(t);
|
||||||
|
t = setTimeout(function () { render(input.value); }, 150);
|
||||||
|
});
|
||||||
|
input.addEventListener("focus", function () { if (input.value.trim()) render(input.value); });
|
||||||
|
input.addEventListener("blur", function () { setTimeout(function () { overlay.classList.remove("open"); }, 150); });
|
||||||
|
input.addEventListener("keydown", function (e) { if (e.key === "Escape") { overlay.classList.remove("open"); } });
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (!box.contains(e.target)) overlay.classList.remove("open");
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
function clearColHover(table) {
|
||||||
|
table.querySelectorAll(".col-hover").forEach(function (el) {
|
||||||
|
el.classList.remove("col-hover");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
document.addEventListener("mouseover", function (e) {
|
||||||
|
var cell = e.target.closest("td, th");
|
||||||
|
if (!cell) return;
|
||||||
|
var table = cell.closest("table");
|
||||||
|
if (!table) return;
|
||||||
|
clearColHover(table);
|
||||||
|
var idx = cell.cellIndex;
|
||||||
|
table.querySelectorAll("tr").forEach(function (tr) {
|
||||||
|
var c = tr.children[idx];
|
||||||
|
if (c) c.classList.add("col-hover");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
document.addEventListener("mouseout", function (e) {
|
||||||
|
var table = e.target.closest("table");
|
||||||
|
if (table && !table.contains(e.relatedTarget)) {
|
||||||
|
clearColHover(table);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
var states = window.blogStates || {};
|
||||||
|
var id = document.body.getAttribute("data-note-id");
|
||||||
|
if (!id || !states[id]) return;
|
||||||
|
var state = states[id];
|
||||||
|
var s = String(state).toLowerCase();
|
||||||
|
document.body.classList.add("is-garden");
|
||||||
|
function inject() {
|
||||||
|
var title = document.getElementById("title");
|
||||||
|
if (!title) return;
|
||||||
|
var icons = {
|
||||||
|
"seed": "bx bxs-coffee-bean",
|
||||||
|
"seedling": "bx bxs-coffee-bean",
|
||||||
|
"sprout": "bx bxs-leaf",
|
||||||
|
"sprouting": "bx bxs-leaf",
|
||||||
|
"bud": "bx bxs-leaf",
|
||||||
|
"evergreen": "bx bxs-tree-alt",
|
||||||
|
"draft": "bx bx-edit",
|
||||||
|
"review": "bx bx-search-alt"
|
||||||
|
};
|
||||||
|
var growth = ["seed", "seedling", "sprout", "sprouting", "bud", "evergreen"];
|
||||||
|
var cls;
|
||||||
|
if (growth.indexOf(s) >= 0) cls = "state-growth";
|
||||||
|
else if (s === "draft") cls = "state-draft";
|
||||||
|
else if (s === "review") cls = "state-review";
|
||||||
|
else cls = "state-unknown";
|
||||||
|
var badge = document.createElement("span");
|
||||||
|
badge.className = "note-state " + cls;
|
||||||
|
badge.innerHTML = '<i class="' + (icons[s] || "bx bxs-circle") + '"></i>' + state;
|
||||||
|
title.appendChild(badge);
|
||||||
|
}
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", inject);
|
||||||
|
} else {
|
||||||
|
inject();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<p>Fill in the metadata below, then write your article body in this note.</p><h2>Metadata</h2>
|
||||||
|
<ul>
|
||||||
|
<li><strong>Published</strong> – set this to true to publish the article on the blog index and in the RSS feed.</li>
|
||||||
|
<li><strong>URL Slug</strong> – optional short URL alias, e.g. <code>mpi400-keyboard-matrix</code>. Leave empty to use the note ID.</li>
|
||||||
|
<li><strong>Category</strong> – e.g. <code>Astrophotography</code>, <code>Electronic</code>, <code>Projects</code>.</li>
|
||||||
|
<li><strong>Date</strong> – article date in <code>YYYY-MM-DD</code> format.</li>
|
||||||
|
<li><strong>Summary</strong> – one or two sentences shown on the blog index and in the RSS feed.</li>
|
||||||
|
<li><strong>Tags</strong> – space-separated keywords, e.g. <code>mount onstep</code>.</li>
|
||||||
|
</ul><h2>Placement</h2>
|
||||||
|
<p>Put this note under a category folder inside <code>Blog Content</code> (e.g. <code>Projects</code>, <code>Astrophotography</code>, <code>Electronic</code>).</p><hr><h2>Article</h2>
|
||||||
|
<p>Write your article here…</p>
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><rect width="32" height="32" rx="6" fill="#282828"/><text x="16" y="24" font-size="22" text-anchor="middle">💻</text></svg>
|
||||||
|
After Width: | Height: | Size: 185 B |
Reference in New Issue
Block a user