feat(embeds): geo/gpx/3D/YouTube/media keyword system + docs, sanitized mirrors

- 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
This commit is contained in:
trilium-share-gruvbox
2026-09-28 14:53:51 +02:00
parent 9e456bbaaa
commit 2818c40f96
10 changed files with 933 additions and 28 deletions
+216 -3
View File
@@ -51,7 +51,8 @@ if (api.originEntity && isGeneratedNoteId(api.originEntity.noteId)) {
// (avoids full rebuild on every snippet/CSS edit, image upload, self-set shareAlias, ...)
const GEN_RELEVANT_LABELS = new Set([
"publish", "date", "category", "tags", "summary", "state",
"blogGarden", "blogCategory", "blogNoArticles", "iconClass"
"blogGarden", "blogCategory", "blogNoArticles", "blogArticleList", "iconClass",
"geolocation", "geo:position", "geoShape", "viewType", "map:style", "map:scale"
]);
const GEN_SKIP_RELATIONS = new Set([
"shareHtml", "shareCss", "shareTemplate", "shareFavicon", "shareOpenGraphImage",
@@ -71,7 +72,14 @@ function shouldRebuild() {
}
if (o.type === "label" && !GEN_RELEVANT_LABELS.has(o.name)) return false;
if (o.type === "relation" && GEN_SKIP_RELATIONS.has(o.name)) return false;
if (o.type === "branch") return false; // publish/unpublish covers clone+move via label events
if (o.type === "branch") {
// Reordering route points (drag in tree) must refresh geo_data
try {
const bn = o.getNote ? o.getNote() : null;
if (bn && (bn.hasLabel("geolocation") || bn.hasLabel("geo:position") || bn.hasLabel("geoShape"))) return true;
} catch (e) {}
return false; // publish/unpublish covers clone+move via label events
}
return true;
}
if (!shouldRebuild()) {
@@ -437,6 +445,19 @@ function run() {
} catch (e) { return ""; }
}
function isInGardenTree(note) {
try {
for (const p of note.getParentNotes()) {
let cur = p;
for (let d = 0; cur && d < 8; d++) {
if (cur.hasLabel && cur.hasLabel("blogGarden")) return true;
cur = cur.getParentNotes()[0];
}
}
} catch (e) {}
return false;
}
function buildBlogData() {
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
if (!systemNote) return;
@@ -448,8 +469,9 @@ function run() {
url: SHARE_BASE + "/" + encodeURIComponent(a.getLabelValue("shareAlias") || a.noteId),
summary: a.getLabelValue("summary") || "",
tags: tagList(a),
category: a.getLabelValue("category") || "",
category: a.getLabelValue("category") || (isInGardenTree(a) ? "Digital Garden" : ""),
date: (a.getLabelValue("date") || a.dateCreated).slice(0, 10),
created: (a.dateCreated || "").slice(0, 10),
dateText: dateText(a.getLabelValue("date") || a.dateCreated),
text: plainText(a)
}));
@@ -461,6 +483,151 @@ function run() {
}
}
// Published notes with viewType=geoMap get their child geolocations exported
// as window.geoData; the "geo map render" snippet turns them into Leaflet maps.
function buildGeoData() {
const systemNote = api.getNote(BLOG_SYSTEM_NOTE_ID);
if (!systemNote) return;
const books = [];
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("viewType") === "geoMap" && child.getLabelValue("publish") === "true") books.push(child);
walk(child, depth + 1);
}
})(blogRoot, 0);
let dataNote = systemNote.getChildNotes().find(n => n.title === "geo_data");
if (!books.length) {
if (dataNote && dataNote.getContent() !== "") {
dataNote.setContent("");
dataNote.save();
api.log("blog_generator: geo_data cleared");
}
return;
}
if (!dataNote) {
const created = api.createTextNote(systemNote.noteId, "geo_data", "");
dataNote = created.note ? created.note : created;
}
if (!blogRoot.hasRelation("shareHtml", dataNote.noteId)) {
blogRoot.setInheritableRelation("shareHtml", dataNote.noteId);
blogRoot.save();
api.log("blog_generator: geo_data attached to blog root");
}
const maps = books.map(b => {
const pts = [];
let kids = b.getChildNotes();
try {
if (typeof api.getBranchFromChildAndParent === "function") {
kids = kids.slice().sort((c1, c2) => {
const br1 = api.getBranchFromChildAndParent(c1.noteId, b.noteId);
const br2 = api.getBranchFromChildAndParent(c2.noteId, b.noteId);
return (br1 ? br1.position : 0) - (br2 ? br2.position : 0);
});
} else {
api.log("blog_generator: getBranchFromChildAndParent unavailable, using natural order");
}
} catch (e) {
api.log("blog_generator: geo sort failed (" + e + "), using natural order");
}
function parsePairs(s) {
const out = [];
for (const pair of String(s).split(/\s+/)) {
const pm = pair.match(/^(-?\d+(?:\.\d+)?),\s*(-?\d+(?:\.\d+)?)$/);
if (pm) out.push({ lat: parseFloat(pm[1]), lng: parseFloat(pm[2]) });
}
return out;
}
const tracks = [];
for (const c of kids) {
const published = c.getLabelValue("publish") === "true";
const purl = published ? SHARE_BASE + "/" + encodeURIComponent(c.getLabelValue("shareAlias") || c.noteId) : "";
const summary = c.getLabelValue("summary") || "";
const markerCoords = [];
let shapes = [];
try { shapes = c.getLabels("geoShape"); } catch (e) {}
for (const s of shapes) {
const sm = String(s.value).match(/^\s*(point|line|polygon)\s*:\s*([\s\S]*)$/i);
if (!sm) continue;
const cs = parsePairs(sm[2]);
if (!cs.length) continue;
if (sm[1].toLowerCase() === "point") markerCoords.push(...cs);
else tracks.push({ title: c.title, coords: cs.map(p => [p.lng, p.lat]) });
}
const g = c.getLabelValue("geolocation") || c.getLabelValue("geo:position") || "";
markerCoords.push(...parsePairs(g));
markerCoords.forEach((p, i) => {
pts.push({ title: markerCoords.length > 1 ? c.title + " (" + (i + 1) + ")" : c.title, lat: p.lat, lng: p.lng, url: purl, summary: summary });
});
if (c.type === "file" && (/\.gpx$/i.test(c.title || "") || /\.gpx$/i.test(c.getLabelValue("originalFileName") || ""))) {
tracks.push({ title: c.title.replace(/\.gpx$/i, ""), url: "api/notes/" + c.noteId + "/download" });
}
}
return {
id: b.noteId,
title: b.title,
style: b.getLabelValue("map:style") || "",
points: pts,
tracks: tracks
};
});
const gpxIndex = [];
(function walkGpx(note, depth) {
if (depth > 20) return;
for (const child of note.getChildNotes()) {
if (child.type === "file" && (/\.gpx$/i.test(child.title || "") || /\.gpx$/i.test(child.getLabelValue("originalFileName") || ""))) {
gpxIndex.push({ id: child.noteId, title: child.title.replace(/\.gpx$/i, "") });
}
walkGpx(child, depth + 1);
}
})(blogRoot, 0);
const cadIndex = [];
(function walkCad(note, depth) {
if (depth > 20) return;
for (const child of note.getChildNotes()) {
if (child.type === "file") {
const cm = String(child.title || "").match(/\.(stl|step|stp)$/i)
|| String(child.getLabelValue("originalFileName") || "").match(/\.(stl|step|stp)$/i);
if (cm) {
cadIndex.push({ id: child.noteId, title: child.title.replace(/\.(stl|step|stp)$/i, ""), format: cm[1].toLowerCase() });
}
}
walkCad(child, depth + 1);
}
})(blogRoot, 0);
const mediaIndex = [];
(function walkMedia(note, depth) {
if (depth > 20) return;
for (const child of note.getChildNotes()) {
if (child.type === "file") {
const titleStr = String(child.title || "");
const origStr = String(child.getLabelValue("originalFileName") || "");
const wm = titleStr.match(/\.(mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|flac|aac|opus)$/i)
|| origStr.match(/\.(mp4|webm|mov|m4v|mp3|m4a|ogg|oga|wav|flac|aac|opus)$/i);
if (wm) {
const fmt = wm[1].toLowerCase();
mediaIndex.push({
id: child.noteId,
title: titleStr.replace(/\.[a-z0-9]+$/i, "") || origStr.replace(/\.[a-z0-9]+$/i, ""),
format: fmt,
kind: ["mp4", "webm", "mov", "m4v"].indexOf(fmt) >= 0 ? "video" : "audio"
});
}
}
walkMedia(child, depth + 1);
}
})(blogRoot, 0);
const html = `<script>window.geoData = ${JSON.stringify({ maps, gpx: gpxIndex, cad: cadIndex, media: mediaIndex, v: 6 })};</script>`;
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 = "<!-- blog:child-articles -->";
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 += `<h2>${escapeHtml(heading)}</h2>\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();
}