mdboard 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.js +752 -0
- package/bin.js +56 -0
- package/config.js +73 -0
- package/defaults.json +43 -0
- package/index.html +865 -137
- package/init.js +45 -4
- package/package.json +9 -2
- package/scanner.js +491 -0
- package/server.js +269 -542
- package/watcher.js +131 -0
- package/workspace.js +220 -0
- package/yaml.js +129 -0
package/watcher.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard — File watcher
|
|
3
|
+
*
|
|
4
|
+
* Watches project directories for changes to .md, config, and CSS files.
|
|
5
|
+
* Supports watching multiple directories for multi-source workspaces.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
|
|
10
|
+
const watchTimers = new Map();
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Setup recursive watcher on a directory for .md, mdboard.json, and mdboard.css changes.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} dir - Directory to watch recursively
|
|
16
|
+
* @param {object} callbacks - { onChange(filename), onConfigChange(), onCssChange(filename) }
|
|
17
|
+
*/
|
|
18
|
+
function watchDir(dir, callbacks) {
|
|
19
|
+
if (!fs.existsSync(dir)) return;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
fs.watch(dir, { recursive: true }, (eventType, filename) => {
|
|
23
|
+
if (!filename) return;
|
|
24
|
+
|
|
25
|
+
const isMd = filename.endsWith('.md');
|
|
26
|
+
const isConfig = filename === 'mdboard.json' || filename.endsWith('/mdboard.json');
|
|
27
|
+
const isCss = filename === 'mdboard.css' || filename.endsWith('/mdboard.css');
|
|
28
|
+
|
|
29
|
+
if (!isMd && !isConfig && !isCss) return;
|
|
30
|
+
|
|
31
|
+
const key = dir + ':' + filename;
|
|
32
|
+
if (watchTimers.has(key)) clearTimeout(watchTimers.get(key));
|
|
33
|
+
|
|
34
|
+
watchTimers.set(key, setTimeout(() => {
|
|
35
|
+
watchTimers.delete(key);
|
|
36
|
+
if (isConfig && callbacks.onConfigChange) callbacks.onConfigChange();
|
|
37
|
+
if (callbacks.onChange) callbacks.onChange(filename, isCss);
|
|
38
|
+
}, 200));
|
|
39
|
+
});
|
|
40
|
+
} catch {
|
|
41
|
+
console.warn(' Warning: file watching unavailable for ' + dir);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Watch a single file for changes (e.g., workspace.json).
|
|
47
|
+
*
|
|
48
|
+
* @param {string} filePath - Absolute path to file
|
|
49
|
+
* @param {function} onChange - Callback on change
|
|
50
|
+
*/
|
|
51
|
+
function watchFile(filePath, onChange) {
|
|
52
|
+
if (!fs.existsSync(filePath)) return;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
fs.watch(filePath, (eventType) => {
|
|
56
|
+
const key = 'file:' + filePath;
|
|
57
|
+
if (watchTimers.has(key)) clearTimeout(watchTimers.get(key));
|
|
58
|
+
|
|
59
|
+
watchTimers.set(key, setTimeout(() => {
|
|
60
|
+
watchTimers.delete(key);
|
|
61
|
+
if (onChange) onChange();
|
|
62
|
+
}, 200));
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Non-critical
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Setup watchers for project directories and workspace-level config.
|
|
71
|
+
*
|
|
72
|
+
* @param {object} opts
|
|
73
|
+
* @param {string[]} opts.projectDirs - List of project/ directories to watch recursively
|
|
74
|
+
* @param {string[]} opts.rootDirs - List of root directories to watch for mdboard.json/css
|
|
75
|
+
* @param {function} opts.onScan - Called when files change and a rescan is needed
|
|
76
|
+
* @param {function} opts.onConfigReload - Called when mdboard.json changes
|
|
77
|
+
* @param {function} opts.onBroadcast - Called with event data to broadcast via SSE
|
|
78
|
+
* @param {string} [opts.workspacePath] - Path to workspace.json to watch
|
|
79
|
+
* @param {function} [opts.onWorkspaceChange] - Called when workspace.json changes
|
|
80
|
+
*/
|
|
81
|
+
function setupWatchers(opts) {
|
|
82
|
+
const { projectDirs, rootDirs, onScan, onConfigReload, onBroadcast, workspacePath, onWorkspaceChange } = opts;
|
|
83
|
+
|
|
84
|
+
// Watch each project directory recursively
|
|
85
|
+
for (const dir of projectDirs) {
|
|
86
|
+
watchDir(dir, {
|
|
87
|
+
onConfigChange: () => {
|
|
88
|
+
if (onConfigReload) onConfigReload();
|
|
89
|
+
},
|
|
90
|
+
onChange: (filename, isCss) => {
|
|
91
|
+
if (onScan) onScan();
|
|
92
|
+
const eventData = { type: 'update', file: filename, timestamp: new Date().toISOString() };
|
|
93
|
+
if (isCss) eventData.cssReload = true;
|
|
94
|
+
if (onBroadcast) onBroadcast(eventData);
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Watch root directories for workspace-level mdboard.json and mdboard.css
|
|
100
|
+
for (const dir of rootDirs) {
|
|
101
|
+
try {
|
|
102
|
+
fs.watch(dir, (eventType, filename) => {
|
|
103
|
+
if (!filename) return;
|
|
104
|
+
if (filename !== 'mdboard.json' && filename !== 'mdboard.css') return;
|
|
105
|
+
|
|
106
|
+
const key = 'root:' + dir + ':' + filename;
|
|
107
|
+
if (watchTimers.has(key)) clearTimeout(watchTimers.get(key));
|
|
108
|
+
|
|
109
|
+
watchTimers.set(key, setTimeout(() => {
|
|
110
|
+
watchTimers.delete(key);
|
|
111
|
+
if (filename === 'mdboard.json') {
|
|
112
|
+
if (onConfigReload) onConfigReload();
|
|
113
|
+
if (onScan) onScan();
|
|
114
|
+
}
|
|
115
|
+
const eventData = { type: 'update', file: filename, timestamp: new Date().toISOString() };
|
|
116
|
+
if (filename === 'mdboard.css') eventData.cssReload = true;
|
|
117
|
+
if (onBroadcast) onBroadcast(eventData);
|
|
118
|
+
}, 200));
|
|
119
|
+
});
|
|
120
|
+
} catch {
|
|
121
|
+
// Non-critical
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Watch workspace.json
|
|
126
|
+
if (workspacePath && onWorkspaceChange) {
|
|
127
|
+
watchFile(workspacePath, onWorkspaceChange);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
module.exports = { setupWatchers, watchDir, watchFile };
|
package/workspace.js
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard — Workspace loader
|
|
3
|
+
*
|
|
4
|
+
* Handles multi-repo workspace via workspace.json.
|
|
5
|
+
* Resolves source paths, manages remote git clones, and provides
|
|
6
|
+
* author info via git log.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
|
|
14
|
+
const CACHE_DIR = path.join(os.homedir(), '.cache', 'mdboard');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Load and resolve workspace.json.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} projectDir - The workspace root directory
|
|
20
|
+
* @param {string} [explicitPath] - Explicit path to workspace.json
|
|
21
|
+
* @returns {object|null} - Parsed workspace with resolved source paths, or null
|
|
22
|
+
*/
|
|
23
|
+
function loadWorkspace(projectDir, explicitPath) {
|
|
24
|
+
let wsPath = null;
|
|
25
|
+
let ws = null;
|
|
26
|
+
|
|
27
|
+
const candidates = [];
|
|
28
|
+
if (explicitPath) candidates.push(path.resolve(explicitPath));
|
|
29
|
+
if (projectDir) {
|
|
30
|
+
candidates.push(path.join(projectDir, 'workspace.json'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const p of candidates) {
|
|
34
|
+
try {
|
|
35
|
+
const raw = fs.readFileSync(p, 'utf-8');
|
|
36
|
+
ws = JSON.parse(raw);
|
|
37
|
+
wsPath = p;
|
|
38
|
+
break;
|
|
39
|
+
} catch {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!ws || !Array.isArray(ws.sources) || ws.sources.length === 0) return null;
|
|
45
|
+
|
|
46
|
+
const wsDir = path.dirname(wsPath);
|
|
47
|
+
|
|
48
|
+
// Resolve each source
|
|
49
|
+
for (const source of ws.sources) {
|
|
50
|
+
if (!source.name) continue;
|
|
51
|
+
|
|
52
|
+
source.type = source.type || 'local';
|
|
53
|
+
source.label = source.label || source.name;
|
|
54
|
+
source.icon = source.icon || source.name.charAt(0).toUpperCase();
|
|
55
|
+
|
|
56
|
+
if (!source.color) {
|
|
57
|
+
source.color = hashColor(source.name);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (source.readonly == null) {
|
|
61
|
+
source.readonly = source.type === 'remote';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (source.type === 'local') {
|
|
65
|
+
if (source.path) {
|
|
66
|
+
const resolved = path.resolve(wsDir, source.path);
|
|
67
|
+
const root = source.root || 'project';
|
|
68
|
+
source._resolvedPath = path.join(resolved, root);
|
|
69
|
+
source._repoRoot = resolved;
|
|
70
|
+
}
|
|
71
|
+
} else if (source.type === 'remote') {
|
|
72
|
+
const root = source.root || 'project';
|
|
73
|
+
const cacheKey = source.name;
|
|
74
|
+
const cachePath = path.join(CACHE_DIR, cacheKey);
|
|
75
|
+
source._resolvedPath = path.join(cachePath, root);
|
|
76
|
+
source._repoRoot = cachePath;
|
|
77
|
+
source._cachePath = cachePath;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Also resolve the overview (workspace root's project/ dir)
|
|
82
|
+
const overviewPath = path.join(projectDir, 'project');
|
|
83
|
+
if (fs.existsSync(overviewPath)) {
|
|
84
|
+
ws.sources.unshift({
|
|
85
|
+
name: 'overview',
|
|
86
|
+
label: 'Overview',
|
|
87
|
+
icon: '\u2605',
|
|
88
|
+
color: '#5B6EF5',
|
|
89
|
+
type: 'local',
|
|
90
|
+
readonly: false,
|
|
91
|
+
_resolvedPath: overviewPath,
|
|
92
|
+
_repoRoot: projectDir,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ws._path = wsPath;
|
|
97
|
+
ws.settings = ws.settings || {};
|
|
98
|
+
|
|
99
|
+
return ws;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Generate a deterministic hex color from a string.
|
|
104
|
+
*/
|
|
105
|
+
function hashColor(str) {
|
|
106
|
+
const colors = ['#F97316', '#8B5CF6', '#EC4899', '#10B981', '#06B6D4', '#5B6EF5', '#6366F1', '#D4A72C'];
|
|
107
|
+
let hash = 0;
|
|
108
|
+
for (let i = 0; i < str.length; i++) hash = str.charCodeAt(i) + ((hash << 5) - hash);
|
|
109
|
+
return colors[Math.abs(hash) % colors.length];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Sync a single remote source by cloning or pulling.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} source - A source entry with type=remote
|
|
116
|
+
*/
|
|
117
|
+
async function syncRemote(source) {
|
|
118
|
+
if (source.type !== 'remote' || !source.url) return;
|
|
119
|
+
|
|
120
|
+
const cachePath = source._cachePath;
|
|
121
|
+
if (!cachePath) return;
|
|
122
|
+
|
|
123
|
+
const branch = source.branch || 'main';
|
|
124
|
+
|
|
125
|
+
try {
|
|
126
|
+
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
|
127
|
+
|
|
128
|
+
if (fs.existsSync(path.join(cachePath, '.git'))) {
|
|
129
|
+
// Pull latest
|
|
130
|
+
execSync(`git -C "${cachePath}" fetch origin ${branch} --depth=1 && git -C "${cachePath}" reset --hard origin/${branch}`, {
|
|
131
|
+
stdio: 'pipe',
|
|
132
|
+
timeout: 30000,
|
|
133
|
+
});
|
|
134
|
+
} else {
|
|
135
|
+
// Clone
|
|
136
|
+
execSync(`git clone --depth=1 --branch ${branch} "${source.url}" "${cachePath}"`, {
|
|
137
|
+
stdio: 'pipe',
|
|
138
|
+
timeout: 60000,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
source._lastSync = new Date().toISOString();
|
|
143
|
+
source._error = null;
|
|
144
|
+
} catch (err) {
|
|
145
|
+
source._error = err.message;
|
|
146
|
+
console.warn(` Warning: sync failed for ${source.name}: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Sync all remote sources.
|
|
152
|
+
*
|
|
153
|
+
* @param {object[]} sources - Array of source entries
|
|
154
|
+
*/
|
|
155
|
+
async function syncAllRemotes(sources) {
|
|
156
|
+
const remotes = sources.filter(s => s.type === 'remote');
|
|
157
|
+
for (const source of remotes) {
|
|
158
|
+
await syncRemote(source);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Get the git author of the last commit that touched a file.
|
|
164
|
+
*
|
|
165
|
+
* @param {string} repoRoot - Path to git repo root
|
|
166
|
+
* @param {string} relFile - Relative file path
|
|
167
|
+
* @returns {string|null}
|
|
168
|
+
*/
|
|
169
|
+
const authorCache = new Map();
|
|
170
|
+
const AUTHOR_CACHE_MAX = 500;
|
|
171
|
+
|
|
172
|
+
function getGitAuthor(repoRoot, relFile) {
|
|
173
|
+
if (!repoRoot || !relFile) return null;
|
|
174
|
+
|
|
175
|
+
const key = repoRoot + ':' + relFile;
|
|
176
|
+
if (authorCache.has(key)) return authorCache.get(key);
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const author = execSync(
|
|
180
|
+
`git -C "${repoRoot}" log -1 --format="%an" -- "${relFile}"`,
|
|
181
|
+
{ stdio: 'pipe', timeout: 5000 }
|
|
182
|
+
).toString().trim();
|
|
183
|
+
|
|
184
|
+
if (authorCache.size >= AUTHOR_CACHE_MAX) {
|
|
185
|
+
const first = authorCache.keys().next().value;
|
|
186
|
+
authorCache.delete(first);
|
|
187
|
+
}
|
|
188
|
+
authorCache.set(key, author || null);
|
|
189
|
+
return author || null;
|
|
190
|
+
} catch {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* List all cached remote repos.
|
|
197
|
+
*
|
|
198
|
+
* @returns {string[]} - Directory names in cache
|
|
199
|
+
*/
|
|
200
|
+
function listCache() {
|
|
201
|
+
try {
|
|
202
|
+
return fs.readdirSync(CACHE_DIR).filter(e => !e.startsWith('.'));
|
|
203
|
+
} catch {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Clean the entire cache directory.
|
|
210
|
+
*/
|
|
211
|
+
function cleanCache() {
|
|
212
|
+
try {
|
|
213
|
+
fs.rmSync(CACHE_DIR, { recursive: true, force: true });
|
|
214
|
+
return true;
|
|
215
|
+
} catch {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { loadWorkspace, syncRemote, syncAllRemotes, getGitAuthor, listCache, cleanCache, CACHE_DIR };
|
package/yaml.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mdboard — YAML frontmatter parser and serializer
|
|
3
|
+
*
|
|
4
|
+
* Lightweight parser for YAML frontmatter in markdown files.
|
|
5
|
+
* No external dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
function parseFrontmatter(content) {
|
|
9
|
+
const result = { frontmatter: {}, content: '' };
|
|
10
|
+
if (!content.startsWith('---')) return { frontmatter: {}, content };
|
|
11
|
+
|
|
12
|
+
const end = content.indexOf('\n---', 3);
|
|
13
|
+
if (end === -1) return { frontmatter: {}, content };
|
|
14
|
+
|
|
15
|
+
const yaml = content.substring(4, end).trim();
|
|
16
|
+
result.content = content.substring(end + 4).trim();
|
|
17
|
+
result.frontmatter = parseYaml(yaml);
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseYaml(text) {
|
|
22
|
+
const obj = {};
|
|
23
|
+
const lines = text.split('\n');
|
|
24
|
+
let i = 0;
|
|
25
|
+
|
|
26
|
+
while (i < lines.length) {
|
|
27
|
+
const line = lines[i];
|
|
28
|
+
const match = line.match(/^(\w[\w.-]*)\s*:\s*(.*)/);
|
|
29
|
+
|
|
30
|
+
if (!match) { i++; continue; }
|
|
31
|
+
|
|
32
|
+
const key = match[1];
|
|
33
|
+
const rawValue = match[2].trim();
|
|
34
|
+
|
|
35
|
+
if (rawValue === '' || rawValue === '') {
|
|
36
|
+
const nested = {};
|
|
37
|
+
let dashList = null;
|
|
38
|
+
let j = i + 1;
|
|
39
|
+
|
|
40
|
+
while (j < lines.length) {
|
|
41
|
+
const nextLine = lines[j];
|
|
42
|
+
if (!nextLine.match(/^\s/) || nextLine.trim() === '') break;
|
|
43
|
+
|
|
44
|
+
const dashMatch = nextLine.match(/^\s+-\s+(.*)/);
|
|
45
|
+
const nestedMatch = nextLine.match(/^\s+(\w[\w.-]*)\s*:\s*(.*)/);
|
|
46
|
+
|
|
47
|
+
if (dashMatch) {
|
|
48
|
+
if (!dashList) dashList = [];
|
|
49
|
+
dashList.push(parseValue(dashMatch[1].trim()));
|
|
50
|
+
} else if (nestedMatch) {
|
|
51
|
+
nested[nestedMatch[1]] = parseValue(nestedMatch[2].trim());
|
|
52
|
+
}
|
|
53
|
+
j++;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
obj[key] = dashList || (Object.keys(nested).length > 0 ? nested : parseValue(rawValue));
|
|
57
|
+
i = j;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
obj[key] = parseValue(rawValue);
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return obj;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseValue(raw) {
|
|
69
|
+
if (raw === '' || raw === undefined) return null;
|
|
70
|
+
|
|
71
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
|
|
72
|
+
return raw.slice(1, -1);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (raw.startsWith('[') && raw.endsWith(']')) {
|
|
76
|
+
const inner = raw.slice(1, -1).trim();
|
|
77
|
+
if (inner === '') return [];
|
|
78
|
+
return inner.split(',').map(s => parseValue(s.trim()));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (raw === 'true') return true;
|
|
82
|
+
if (raw === 'false') return false;
|
|
83
|
+
if (raw === 'null' || raw === '~') return null;
|
|
84
|
+
if (/^-?\d+(\.\d+)?$/.test(raw)) return Number(raw);
|
|
85
|
+
|
|
86
|
+
return raw;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function serializeValue(v) {
|
|
90
|
+
if (v === null || v === undefined) return '';
|
|
91
|
+
if (typeof v === 'boolean') return v ? 'true' : 'false';
|
|
92
|
+
if (typeof v === 'number') return String(v);
|
|
93
|
+
if (typeof v === 'string') {
|
|
94
|
+
if (/[:#\[\]{}&*!|>'"%@`,\n]/.test(v) || v === '' || v === 'true' || v === 'false' || v === 'null') {
|
|
95
|
+
return '"' + v.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
|
|
96
|
+
}
|
|
97
|
+
return v;
|
|
98
|
+
}
|
|
99
|
+
return String(v);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function serializeYaml(obj) {
|
|
103
|
+
const lines = [];
|
|
104
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
105
|
+
if (key.startsWith('_')) continue;
|
|
106
|
+
if (value === undefined) continue;
|
|
107
|
+
if (value === null) {
|
|
108
|
+
lines.push(key + ':');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (Array.isArray(value)) {
|
|
112
|
+
if (value.length === 0) {
|
|
113
|
+
lines.push(key + ': []');
|
|
114
|
+
} else {
|
|
115
|
+
lines.push(key + ': [' + value.map(v => serializeValue(v)).join(', ') + ']');
|
|
116
|
+
}
|
|
117
|
+
} else if (typeof value === 'object') {
|
|
118
|
+
lines.push(key + ':');
|
|
119
|
+
for (const [k, v] of Object.entries(value)) {
|
|
120
|
+
lines.push(' ' + k + ': ' + serializeValue(v));
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
lines.push(key + ': ' + serializeValue(value));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return lines.join('\n');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
module.exports = { parseFrontmatter, parseYaml, parseValue, serializeYaml, serializeValue };
|