From 2818c40f969d89f052bbf85f2da02618ec575604 Mon Sep 17 00:00:00 2001 From: trilium-share-gruvbox Date: Mon, 28 Sep 2026 14:53:51 +0200 Subject: [PATCH] feat(embeds): geo/gpx/3D/YouTube/media keyword system + docs, sanitized mirrors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new snippet geo_map_render.js: [map] inline geomaps (tree order, tint, theme-following), [gpx] stats card + elevation profile with hover cursor (km from start, d-height, vertical guide), [yt] privacy click-to-load facade, [video]/[audio] inline players fed from the new geoData indexes - 3d_model_viewer.js: [cad] keyword + title/ID resolution via geoData.cad, sizes/captions, real occt-wasm ESM pair (fixes 'STEP worker error'), code-span-safe keyword scan (docs can show examples) - blog_generator.js: buildGeoData() v6 – maps (geolocation + geoShape point/line/polygon + child GPX tracks), gpx/cad/media indexes, branch-change rebuild for route reordering - generated/geo_data.js placeholder; docs/embed-keywords.md (new) + embed-3d-model Option D; README layout/wiring/notes tables updated - license: runtime components (MapLibre ISC, VersaTiles MIT/ODbL, YouTube) - hygiene: removed leaked note IDs and instance domain from theme docs --- AGENTS.md | 6 +- README.md | 24 +- THIRD_PARTY_LICENSES.md | 11 + blog_generator.js | 219 ++++++++++++++- docs/embed-3d-model.md | 18 +- docs/embed-keywords.md | 51 ++++ docs/switch-blog-theme.md | 28 +- generated/geo_data.js | 1 + scripts/3d_model_viewer.js | 62 +++++ scripts/geo_map_render.js | 541 +++++++++++++++++++++++++++++++++++++ 10 files changed, 933 insertions(+), 28 deletions(-) create mode 100644 docs/embed-keywords.md create mode 100644 generated/geo_data.js create mode 100644 scripts/geo_map_render.js diff --git a/AGENTS.md b/AGENTS.md index caf0023..f5e8483 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -26,7 +26,7 @@ CSS file (Gruvbox, light + dark, theme toggle without reload). | `scripts/*.js` | one code note each (`.blog`), attached via `~shareHtml` | frontend snippets (verbatim content incl. ``; + if (dataNote.getContent() !== html) { + dataNote.setContent(html); + dataNote.save(); + api.log("blog_generator: geo_data updated (" + maps.length + " maps)"); + } + } + function buildTags() { const tagsNote = blogRoot.getChildNotes().find(n => n.title === TAGS_SECTION_TITLE); if (!tagsNote) return; @@ -634,6 +801,50 @@ function run() { } } + // Opt-in (label "blogArticleList"): append a generated list of the note's + // published child notes after the user's own content. Idempotent via marker; + // placing the marker yourself controls where the list appears. The label + // value (if any, not "true") becomes the list heading. + function buildChildArticleLists() { + const marker = ""; + 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.hasLabel("blogArticleList")) { + const kids = child.getChildNotes().filter(c => c.getLabelValue("publish") === "true"); + kids.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 current = child.getContent() || ""; + const idx = current.indexOf(marker); + const userText = idx >= 0 ? current.slice(0, idx) : current; + let out; + if (kids.length) { + out = userText; + if (out && !out.endsWith("\n")) out += "\n"; + out += marker + "\n"; + const heading = child.getLabelValue("blogArticleList") || ""; + if (heading && heading !== "true") out += `

${escapeHtml(heading)}

\n`; + out += renderArticles(kids); + } else { + out = idx >= 0 ? current.replace(marker + "\n", "").replace(marker, "") : current; + } + if (out !== current) { + child.setContent(out); + child.save(); + api.log("blog_generator: child article list updated: " + child.title + " (" + kids.length + ")"); + } + } + walk(child, depth + 1); + } + })(blogRoot, 0); + } + function removeEmptyCategories() { const blogRoot = api.getNote(BLOG_ROOT_NOTE_ID); if (!blogRoot) return; @@ -683,10 +894,12 @@ function run() { buildTags(); buildCategories(); + buildChildArticleLists(); buildStateMap(); buildSitemap(); ensureSeoMeta(); buildBlogData(); + buildGeoData(); hideThumbChildren(); removeEmptyCategories(); } \ No newline at end of file diff --git a/docs/embed-3d-model.md b/docs/embed-3d-model.md index bc0e040..292b29b 100644 --- a/docs/embed-3d-model.md +++ b/docs/embed-3d-model.md @@ -7,7 +7,7 @@ available space instead of the readable content width. **Inline viewers embedded articles** (Options B and C) are limited to the content width (`model-viewer-inline`). The caption below the model always shows ` · · ` (e.g. `Wrench.stl · 711 KB · 14.6k triangles`). -There are three ways to embed a model, depending on how you store the file. +There are four ways to embed a model, depending on how you store the file. ## Option A – Model as its own note (file note) @@ -47,6 +47,22 @@ exists as its own (published) file note — the model stays in one place. > STEP tessellation is set to `linearDeflection: 0.4, angularDeflection: 0.2` > (≈480k triangles for the ignis battery module; tuned for mobile). +## Option D – `[cad]` keyword (editor-safe, no HTML or links) + +Plain-text keyword, robust against editor re-saves (unlike raw HTML embeds, which CKEditor strips): + +``` +[cad ] # e.g. [cad base] or [cad __MODEL_FILE_NOTE_ID__] +[cad |small] # small | medium | large (viewer height) +[cad |Own caption] # free text as caption +``` + +The key resolves through the **`cad` index** in `window.geoData` (built by `blog_generator.js`, +scans the blog tree for `.stl` / `.step` / `.stp` file notes; the extension may live in the +note title **or** `#originalFileName`). The model file note does **not** need its own `publish` +label — it only has to be inside the blog subtree. See `docs/embed-keywords.md` for the whole +keyword family (`map`, `gpx`, `cad`, `yt`, `video`, `audio`). + ## Requirements - The `3d model viewer` script must be wired: `~shareHtml → 3d model viewer` diff --git a/docs/embed-keywords.md b/docs/embed-keywords.md new file mode 100644 index 0000000..a28e96d --- /dev/null +++ b/docs/embed-keywords.md @@ -0,0 +1,51 @@ +# Inline embed keywords (map / gpx / cad / yt / video / audio) + +The share renders small **text keywords** into interactive widgets, client-side, from two +`shareHtml` snippets: `scripts/geo_map_render.js` (`map`, `gpx`, `yt`, `video`, `audio`) and +`scripts/3d_model_viewer.js` (`cad`). Keywords are plain text, so they survive editor re-saves — +no raw HTML needed. Put one keyword **on its own line** (recommended) or inline in a sentence. + +A keyword is ignored when it sits inside `code`/`pre` (that is how this file shows examples). + +## Syntax + +``` +[map ] # geomap, optional: |small|medium|large +[gpx ] # track stats card + elevation profile, optional: |Custom title +[cad ] # 3D model viewer, optional: |small|medium|large or |Caption +[yt ] # YouTube click-to-load facade, optional: |Caption +[video ] # inline video player, optional: |Caption +[audio ] # inline audio player, optional: |Caption +``` + +`` is either the **note ID** (12 chars) or the **note title / file name** +(case-insensitive; file extension optional). + +## Requirements per keyword + +| Keyword | References | Notes | +|---|---|---| +| `map` | a geoMap book note (`#viewType=geoMap` + `#publish=true`) | points via child `#geolocation` / `#geo:position` / `#geoShape` or child `.gpx` files; tree order = route order; reorder children in Trilium → auto refresh (`~runOnBranchChange`) | +| `gpx` | any `.gpx` **file note** in the blog tree | stats: distance, ascent/descent (0.5 m deadband), elevation range, duration, avg speed, pace (needs `