export.mjs
135 lines 4.6 KB
Raw
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor ⚠ breaking 6 days ago
1 /**
2 * Export note(s) to file or directory. Formats: md, html. Provenance. SPEC §4.1.
3 */
4
5 import fs from 'fs';
6 import path from 'path';
7 import yaml from 'js-yaml';
8 import { readNote } from './vault.mjs';
9
10 /**
11 * Escape HTML for minimal HTML export.
12 * @param {string} s
13 * @returns {string}
14 */
15 function escapeHtml(s) {
16 return String(s)
17 .replace(/&/g, '&')
18 .replace(/</g, '&lt;')
19 .replace(/>/g, '&gt;')
20 .replace(/"/g, '&quot;');
21 }
22
23 /**
24 * Export one note to HTML (minimal wrapper).
25 * @param {{ path: string, frontmatter: object, body: string }} note
26 * @returns {string}
27 */
28 function noteToHtml(note) {
29 const title = note.frontmatter?.title || note.path;
30 const body = note.body || '';
31 const bodyEscaped = escapeHtml(body).replace(/\n/g, '<br>\n');
32 return `<!DOCTYPE html>
33 <html lang="en">
34 <head><meta charset="utf-8"><title>${escapeHtml(title)}</title></head>
35 <body>
36 <pre>${bodyEscaped}</pre>
37 </body>
38 </html>`;
39 }
40
41 /**
42 * Export notes to output path (file or directory). Records provenance.
43 * @param {string} vaultPath - Absolute vault root
44 * @param {string[]} relativePaths - Vault-relative paths to export
45 * @param {string} outputPath - File path or directory path
46 * @param {{ format?: 'md'|'html' }} options
47 * @returns {{ exported: { path: string, output: string }[], provenance: string }}
48 */
49 export function exportNotes(vaultPath, relativePaths, outputPath, options = {}) {
50 const format = options.format || 'md';
51 const resolvedOutput = path.resolve(outputPath);
52 const exists = fs.existsSync(resolvedOutput);
53 const isDir =
54 relativePaths.length > 1 ||
55 (exists && fs.statSync(resolvedOutput).isDirectory()) ||
56 (!exists && !resolvedOutput.endsWith('.md') && !resolvedOutput.endsWith('.html'));
57 const exported = [];
58
59 for (const rel of relativePaths) {
60 let note;
61 try {
62 note = readNote(vaultPath, rel);
63 } catch (e) {
64 throw new Error(`Export failed: ${e.message}`);
65 }
66
67 const base = path.basename(rel, '.md') || path.basename(rel);
68 const outPath = isDir
69 ? path.join(resolvedOutput, format === 'html' ? `${base}.html` : `${base}.md`)
70 : resolvedOutput;
71
72 const dir = path.dirname(outPath);
73 if (!fs.existsSync(dir)) {
74 fs.mkdirSync(dir, { recursive: true });
75 }
76
77 if (format === 'html') {
78 const html = noteToHtml(note);
79 fs.writeFileSync(outPath, html, 'utf8');
80 } else {
81 const frontmatterWithProvenance = {
82 ...note.frontmatter,
83 source_notes: relativePaths,
84 };
85 const y = yaml.dump(frontmatterWithProvenance, { lineWidth: -1, noRefs: true }).trimEnd();
86 fs.writeFileSync(outPath, `---\n${y}\n---\n${note.body || ''}`, 'utf8');
87 }
88
89 exported.push({ path: rel, output: outPath });
90 }
91
92 const provenance = relativePaths.length ? `Exported from vault paths: ${relativePaths.join(', ')}` : '';
93 return { exported, provenance };
94 }
95
96 /**
97 * Export one note to in-memory content (for API download). No disk write.
98 * @param {string} vaultPath - Absolute vault root
99 * @param {string} relativePath - Vault-relative path to the note
100 * @param {{ format?: 'md'|'html' }} options
101 * @returns {{ content: string, filename: string }}
102 */
103 export function exportNoteToContent(vaultPath, relativePath, options = {}) {
104 const format = options.format || 'md';
105 const note = readNote(vaultPath, relativePath);
106 return exportNoteRecordToContent(
107 { body: note.body, frontmatter: note.frontmatter && typeof note.frontmatter === 'object' ? note.frontmatter : {} },
108 relativePath,
109 { format },
110 );
111 }
112
113 /**
114 * Export one note to in-memory content from a note record (e.g. canister/JSON). No disk read.
115 * @param {{ body: string, frontmatter?: object }} note
116 * @param {string} relativePath - Vault-relative path (used for filename and source_notes)
117 * @param {{ format?: 'md'|'html' }} options
118 * @returns {{ content: string, filename: string }}
119 */
120 export function exportNoteRecordToContent(note, relativePath, options = {}) {
121 const format = options.format || 'md';
122 const rawFm = note?.frontmatter;
123 const fm =
124 rawFm && typeof rawFm === 'object' && !Array.isArray(rawFm) ? { ...rawFm } : {};
125 const base = path.basename(relativePath, '.md') || path.basename(relativePath);
126 const filename = format === 'html' ? `${base}.html` : `${base}.md`;
127 const forHtml = { path: relativePath, body: note.body || '', frontmatter: fm };
128 if (format === 'html') {
129 return { content: noteToHtml(forHtml), filename };
130 }
131 const frontmatterWithProvenance = { ...fm, source_notes: [relativePath] };
132 const y = yaml.dump(frontmatterWithProvenance, { lineWidth: -1, noRefs: true }).trimEnd();
133 const content = `---\n${y}\n---\n${note.body || ''}`;
134 return { content, filename };
135 }
File History 1 commit
sha256:41d741fb345c4abdb640838aa3d847de02ccffd7a39fce04894e743e683b50d0 fix(security): pin patched transitive deps to clear Dependa… Human minor 6 days ago