// Collect every image referenced by an Obsidian embed (![[...]]) in the // garden content and copy it to public/assets/ flat by basename — matching // the /assets/ URLs the remark plugin emits. Resolution order: // Digital_Garden first, then the wider vault (some embeds point at files // that live outside the published folder; the live Quartz site 404s those — // we fix them, but loudly, so stray references are visible). import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { CONTENT_DIR, VAULT_ROOT, walkMarkdown, } from '../src/lib/quartz-compat.mjs'; const OUT_DIR = path.join( path.dirname(fileURLToPath(import.meta.url)), '..', 'public', 'assets', ); const EMBED_RE = /!\[\[([^\]|]+?\.(?:png|jpe?g|gif|svg|webp))(?:\|[^\]]*)?\]\]/gi; const MD_IMG_RE = /!\[[^\]]*\]\(([^)\s]+?\.(?:png|jpe?g|gif|svg|webp))\)/gi; function walkFiles(dir) { const out = []; for (const ent of fs.readdirSync(dir, { withFileTypes: true })) { if (ent.name.startsWith('.')) continue; const full = path.join(dir, ent.name); if (ent.isDirectory()) out.push(...walkFiles(full)); else out.push(full); } return out; } // referenced image names (basename, as Obsidian resolves vault-wide) const wanted = new Map(); // lowercase basename -> name as referenced for (const rel of walkMarkdown(CONTENT_DIR)) { const text = fs.readFileSync(path.join(CONTENT_DIR, rel), 'utf8'); for (const m of text.matchAll(EMBED_RE)) { const name = m[1].split('/').pop().trim(); wanted.set(name.toLowerCase(), name); } for (const m of text.matchAll(MD_IMG_RE)) { if (/^(?:[a-z]+:)?\/\//i.test(m[1])) continue; const name = decodeURIComponent(m[1]).split('/').pop().trim(); wanted.set(name.toLowerCase(), name); } } // index every file in the vault by lowercase basename const inDG = new Map(); const inVault = new Map(); for (const f of walkFiles(VAULT_ROOT)) { const key = path.basename(f).toLowerCase(); if (f.startsWith(CONTENT_DIR + path.sep)) { if (!inDG.has(key)) inDG.set(key, f); } else if (!inVault.has(key)) { inVault.set(key, f); } } fs.rmSync(OUT_DIR, { recursive: true, force: true }); fs.mkdirSync(OUT_DIR, { recursive: true }); let copied = 0; let missing = 0; for (const [key, name] of wanted) { const src = inDG.get(key) ?? inVault.get(key); if (!src) { console.warn(`copy-assets: UNRESOLVED image embed: ${name}`); missing++; continue; } if (!inDG.has(key)) { console.warn( `copy-assets: ${name} lives outside Digital_Garden (${path.relative(VAULT_ROOT, src)})`, ); } fs.copyFileSync(src, path.join(OUT_DIR, name)); copied++; } console.log(`copy-assets: ${copied} copied, ${missing} unresolved -> ${OUT_DIR}`); if (missing) process.exitCode = 0; // warn, don't fail the build