<%*
/** ======================
* 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 += ``;
%>