<%* /** ====================== * Batch Frontmatter → Body Snapshot (Markdown Table, preserves [[wikilinks]]) * ====================== * Configure filters here: */ const TARGET_FOLDER = null; // e.g. "Notes/Projects" or null for whole vault const REQUIRED_TAG = null; // e.g. "publish" (without #) or null to ignore const SKIP_FILES_WITHOUT_FM = true; // skip notes that have no frontmatter const PLACE_AT = "after-first-h1"; // "top" | "bottom" | "after-first-h1" const START = "<!-- YAML-SNAPSHOT:START -->"; const END = "<!-- YAML-SNAPSHOT:END -->"; /** ---------- Helpers ---------- */ function fmtScalar(v) { if (v === null || v === undefined) return 'null'; if (typeof v === 'boolean' || typeof v === 'number') return String(v); const s = String(v); // Leave Obsidian wiki-links exactly as they are if (/^\[\[.*\]\]$/.test(s)) return s; return s; } function flattenOnce(x) { if (Array.isArray(x) && x.length === 1 && Array.isArray(x[0])) return x[0]; return x; } /** Read raw YAML frontmatter for wiki-link preservation */ function extractFrontmatterBlock(text) { if (!text.startsWith('---')) return null; const lines = text.split('\n'); let endIdx = -1; for (let i = 1; i < lines.length; i++) { if (/^---\s*$/.test(lines[i])) { endIdx = i; break; } } if (endIdx === -1) return null; return lines.slice(1, endIdx).join('\n'); } function parseRawTopListsByKey(fmText) { const map = {}; if (!fmText) return map; const lines = fmText.split('\n'); let currentKey = null; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const keyMatch = line.match(/^([^\s][^:]*):\s*(.*)$/); if (keyMatch) { currentKey = keyMatch[1].trim(); if (!map[currentKey]) map[currentKey] = []; continue; } if (currentKey) { const bulletMatch = line.match(/^\s*-\s+(.*)$/); if (bulletMatch) { const rawVal = bulletMatch[1].trim(); map[currentKey].push(rawVal); continue; } if (/^[^\s]/.test(line)) { currentKey = null; } } } return map; } /** Convert frontmatter to table rows */ function toTableRows(obj, rawTopLists = {}, parentKey = null) { const rows = []; if (Array.isArray(obj)) { const arr = flattenOnce(obj); if (!arr.length) return ['_none_']; const rawList = parentKey ? (rawTopLists[parentKey] || []) : []; return arr.map((item, i) => { if (rawList[i]) return rawList[i]; if (Array.isArray(item)) { return flattenOnce(item).map(sub => fmtScalar(sub)).join(', '); } else if (item && typeof item === 'object') { return JSON.stringify(item); } else { return fmtScalar(item); } }); } for (const [k, v] of Object.entries(obj || {})) { if (Array.isArray(v)) { const vals = toTableRows(v, rawTopLists, k); rows.push([`**${k}**`, vals.join(', ')]); } else if (v && typeof v === 'object') { rows.push([`**${k}**`, JSON.stringify(v)]); } else { rows.push([`**${k}**`, fmtScalar(v)]); } } return rows; } /** ---------- Placement helpers ---------- */ function hasRequiredTag(cache, tag) { if (!tag) return true; const allTags = (cache?.tags || []).map(t => t.tag.replace(/^#/, '')); return allTags.includes(tag); } function inTargetFolder(tfile, folder) { if (!folder) return true; return tfile.path.startsWith(folder.endsWith('/') ? folder : folder + '/'); } function injectSnapshot(content, block) { const startIdx = content.indexOf(START); const endIdx = content.indexOf(END); if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { return content.slice(0, startIdx) + block + content.slice(endIdx + END.length); } if (PLACE_AT === "top") return block + "\n\n" + content; if (PLACE_AT === "bottom") return content.trimEnd() + "\n\n" + block + "\n"; if (PLACE_AT === "after-first-h1") { const m = content.match(/^# .+$/m); if (m) { const pos = m.index + m[0].length; return content.slice(0, pos) + "\n\n" + block + "\n\n" + content.slice(pos); } return block + "\n\n" + content; } return block + "\n\n" + content; } /** ---------- Main ---------- */ const files = app.vault.getMarkdownFiles(); let processed = 0, skipped = 0, changed = 0; for (const f of files) { if (!inTargetFolder(f, TARGET_FOLDER)) { skipped++; continue; } const cache = app.metadataCache.getFileCache(f) || {}; if (!hasRequiredTag(cache, REQUIRED_TAG)) { skipped++; continue; } const fm = cache.frontmatter; if (!fm && SKIP_FILES_WITHOUT_FM) { skipped++; continue; } const cur = await app.vault.read(f); const fmRaw = extractFrontmatterBlock(cur); const rawTopLists = parseRawTopListsByKey(fmRaw); const rows = toTableRows(fm || {}, rawTopLists); let table = ''; if (rows.length) { table += `| Key | Value |\n| --- | ----- |\n`; for (const [key, val] of rows) { table += `| ${key} | ${val} |\n`; } } const block = `${START}\n${table}\n${END}`; const next = injectSnapshot(cur, block); if (next !== cur) { await app.vault.modify(f, next); changed++; } processed++; } tR += ``; %>