/** * Export note(s) to file or directory. Formats: md, html. Provenance. SPEC ยง4.1. */ import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; import { readNote } from './vault.mjs'; /** * Escape HTML for minimal HTML export. * @param {string} s * @returns {string} */ function escapeHtml(s) { return String(s) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"'); } /** * Export one note to HTML (minimal wrapper). * @param {{ path: string, frontmatter: object, body: string }} note * @returns {string} */ function noteToHtml(note) { const title = note.frontmatter?.title || note.path; const body = note.body || ''; const bodyEscaped = escapeHtml(body).replace(/\n/g, '
\n'); return ` ${escapeHtml(title)}
${bodyEscaped}
`; } /** * Export notes to output path (file or directory). Records provenance. * @param {string} vaultPath - Absolute vault root * @param {string[]} relativePaths - Vault-relative paths to export * @param {string} outputPath - File path or directory path * @param {{ format?: 'md'|'html' }} options * @returns {{ exported: { path: string, output: string }[], provenance: string }} */ export function exportNotes(vaultPath, relativePaths, outputPath, options = {}) { const format = options.format || 'md'; const resolvedOutput = path.resolve(outputPath); const exists = fs.existsSync(resolvedOutput); const isDir = relativePaths.length > 1 || (exists && fs.statSync(resolvedOutput).isDirectory()) || (!exists && !resolvedOutput.endsWith('.md') && !resolvedOutput.endsWith('.html')); const exported = []; for (const rel of relativePaths) { let note; try { note = readNote(vaultPath, rel); } catch (e) { throw new Error(`Export failed: ${e.message}`); } const base = path.basename(rel, '.md') || path.basename(rel); const outPath = isDir ? path.join(resolvedOutput, format === 'html' ? `${base}.html` : `${base}.md`) : resolvedOutput; const dir = path.dirname(outPath); if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } if (format === 'html') { const html = noteToHtml(note); fs.writeFileSync(outPath, html, 'utf8'); } else { const frontmatterWithProvenance = { ...note.frontmatter, source_notes: relativePaths, }; const y = yaml.dump(frontmatterWithProvenance, { lineWidth: -1, noRefs: true }).trimEnd(); fs.writeFileSync(outPath, `---\n${y}\n---\n${note.body || ''}`, 'utf8'); } exported.push({ path: rel, output: outPath }); } const provenance = relativePaths.length ? `Exported from vault paths: ${relativePaths.join(', ')}` : ''; return { exported, provenance }; } /** * Export one note to in-memory content (for API download). No disk write. * @param {string} vaultPath - Absolute vault root * @param {string} relativePath - Vault-relative path to the note * @param {{ format?: 'md'|'html' }} options * @returns {{ content: string, filename: string }} */ export function exportNoteToContent(vaultPath, relativePath, options = {}) { const format = options.format || 'md'; const note = readNote(vaultPath, relativePath); return exportNoteRecordToContent( { body: note.body, frontmatter: note.frontmatter && typeof note.frontmatter === 'object' ? note.frontmatter : {} }, relativePath, { format }, ); } /** * Export one note to in-memory content from a note record (e.g. canister/JSON). No disk read. * @param {{ body: string, frontmatter?: object }} note * @param {string} relativePath - Vault-relative path (used for filename and source_notes) * @param {{ format?: 'md'|'html' }} options * @returns {{ content: string, filename: string }} */ export function exportNoteRecordToContent(note, relativePath, options = {}) { const format = options.format || 'md'; const rawFm = note?.frontmatter; const fm = rawFm && typeof rawFm === 'object' && !Array.isArray(rawFm) ? { ...rawFm } : {}; const base = path.basename(relativePath, '.md') || path.basename(relativePath); const filename = format === 'html' ? `${base}.html` : `${base}.md`; const forHtml = { path: relativePath, body: note.body || '', frontmatter: fm }; if (format === 'html') { return { content: noteToHtml(forHtml), filename }; } const frontmatterWithProvenance = { ...fm, source_notes: [relativePath] }; const y = yaml.dump(frontmatterWithProvenance, { lineWidth: -1, noRefs: true }).trimEnd(); const content = `---\n${y}\n---\n${note.body || ''}`; return { content, filename }; }