import fs from 'node:fs'; import path from 'node:path'; import matter from 'gray-matter'; import { STAGES, normalizeStage } from '../../notes-src/stack/folio.mjs'; const LAYOUTS = ['note', 'page']; /** * Read the top-level *.md files of a NotePlan folder. * Returns every note (published or not); errors are reported for published notes only. * @param {string} dir * @returns {{ notes: Array<{file: string, title: string, body: string, data: object}>, errors: string[] }} */ export function readSource(dir) { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { throw new Error(`NOTES_SOURCE folder not found: ${dir}`); } const notes = []; const errors = []; const files = fs .readdirSync(dir, { withFileTypes: true }) .filter((e) => e.isFile() && e.name.endsWith('.md')) .map((e) => e.name) .sort(); for (const file of files) { const raw = fs.readFileSync(path.join(dir, file), 'utf8'); let parsed; let broken = false; try { parsed = matter(raw, {}); } catch (err) { broken = true; const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/); const block = m ? m[1] : ''; if (/^publish:\s*true\s*$/m.test(block)) { errors.push(`${file}: invalid frontmatter (${String(err.message).split('\n')[0]})`); } parsed = { data: {}, content: m ? raw.slice(m[0].length) : raw }; } const fm = parsed.data || {}; const publish = fm.publish === true; const lines = parsed.content.split('\n'); const h1 = lines.findIndex((l) => /^# \S/.test(l)); let title = path.basename(file, '.md'); let body = parsed.content; if (h1 >= 0) { title = lines[h1].replace(/^#\s+/, '').trim(); lines.splice(h1, 1); body = lines.join('\n'); } else if (publish && !broken) { errors.push(`${file}: missing "# Title" heading`); } body = body.replace(/^(?:[ \t]*\r?\n)+/, ''); let stage = 'found'; if (fm.stage != null) { stage = normalizeStage(fm.stage); if (!STAGES.includes(stage)) { if (publish) errors.push(`${file}: invalid stage "${fm.stage}" (use ${STAGES.join(', ')})`); stage = 'found'; } } let layout = 'note'; if (fm.layout != null) { layout = String(fm.layout).trim().toLowerCase(); if (!LAYOUTS.includes(layout)) { if (publish) errors.push(`${file}: invalid layout "${fm.layout}"`); layout = 'note'; } } const rawTags = Array.isArray(fm.tags) ? fm.tags : fm.tags == null ? [] : [fm.tags]; const tags = rawTags.map((t) => String(t).trim().toLowerCase()).filter(Boolean); const data = { publish, stage, tags, layout }; if (fm.epistemic != null) data.epistemic = String(fm.epistemic); if (fm.slug != null) data.slug = String(fm.slug); if (fm.description != null) data.description = String(fm.description); notes.push({ file, title, body, data }); } return { notes, errors }; }