@chriswiegman/hugo-tools 0.2.0 → 0.2.2
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/CHANGELOG.md +18 -0
- package/README.md +12 -1
- package/package.json +6 -5
- package/src/add-tags.js +103 -100
- package/src/book.mjs +643 -542
- package/src/book.test.mjs +660 -566
- package/src/config-init.js +9 -9
- package/src/config.js +22 -20
- package/src/draft.js +44 -42
- package/src/draft.test.mjs +32 -27
- package/src/extract-tags.js +184 -173
- package/src/extract-tags.test.mjs +56 -43
- package/src/pick-image.js +111 -97
- package/src/pick-image.test.mjs +57 -46
- package/src/publish.js +363 -317
- package/src/publish.test.mjs +242 -201
- package/src/vscode.js +68 -65
package/src/publish.test.mjs
CHANGED
|
@@ -4,21 +4,21 @@ import { createRequire } from 'node:module';
|
|
|
4
4
|
|
|
5
5
|
const require = createRequire(import.meta.url);
|
|
6
6
|
const {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
7
|
+
processContent,
|
|
8
|
+
splitFrontMatter,
|
|
9
|
+
ensureLine,
|
|
10
|
+
ensurePlaceholderList,
|
|
11
|
+
getScalarValue,
|
|
12
|
+
removeKey,
|
|
13
|
+
extractListValues,
|
|
14
|
+
hasCategory,
|
|
15
|
+
parseTitleFromFm,
|
|
16
|
+
titleFromBasename,
|
|
17
|
+
slugify,
|
|
18
|
+
stripQuotes,
|
|
19
|
+
chicagoAt8AM,
|
|
20
|
+
formatChicagoDateTitle,
|
|
21
|
+
formatChicagoTime,
|
|
22
22
|
} = require('./publish.js');
|
|
23
23
|
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
@@ -27,47 +27,51 @@ const {
|
|
|
27
27
|
|
|
28
28
|
// Produces a body string of exactly n space-separated words.
|
|
29
29
|
function words(n) {
|
|
30
|
-
|
|
30
|
+
return Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
// Mirrors the output of `hugo new content/drafts/<file>.md` using the drafts
|
|
34
34
|
// archetype, followed by the perl one-liner that strips the date: line.
|
|
35
35
|
// Callers can override any field; categories/tags default to placeholder lists.
|
|
36
36
|
function makeDraft({
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
37
|
+
title = '',
|
|
38
|
+
description = '',
|
|
39
|
+
categories = null, // null → placeholder " -", array → real values
|
|
40
|
+
tags = null, // null → placeholder " -", array → real values
|
|
41
|
+
body = '',
|
|
42
|
+
publishDate,
|
|
43
43
|
} = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
44
|
+
const lines = [
|
|
45
|
+
'---',
|
|
46
|
+
`title: "${title}"`,
|
|
47
|
+
`description: "${description}"`,
|
|
48
|
+
'draft: true',
|
|
49
|
+
'images:',
|
|
50
|
+
' -',
|
|
51
|
+
'categories:',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (categories === null) {
|
|
55
|
+
lines.push(' -');
|
|
56
|
+
} else {
|
|
57
|
+
for (const c of categories) lines.push(` - ${c}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
lines.push('tags:');
|
|
61
|
+
|
|
62
|
+
if (tags === null) {
|
|
63
|
+
lines.push(' -');
|
|
64
|
+
} else {
|
|
65
|
+
for (const t of tags) lines.push(` - ${t}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (publishDate !== undefined) lines.push(`publishDate: ${publishDate}`);
|
|
69
|
+
|
|
70
|
+
lines.push('---');
|
|
71
|
+
|
|
72
|
+
if (body) lines.push('', body);
|
|
73
|
+
|
|
74
|
+
return lines.join('\n');
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
const NOW_STAMP = '2026-05-04T10:00:00-05:00';
|
|
@@ -79,37 +83,42 @@ const DRAFT_PATH = 'content/drafts/my-post.md';
|
|
|
79
83
|
// ---------------------------------------------------------------------------
|
|
80
84
|
|
|
81
85
|
test('splitFrontMatter: parses standard frontmatter', () => {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
const { fm, body, had } = splitFrontMatter('---\ntitle: foo\n---\nbody text');
|
|
87
|
+
|
|
88
|
+
assert.equal(had, true);
|
|
89
|
+
assert.ok(fm.includes('title: foo'));
|
|
90
|
+
assert.ok(body.includes('body text'));
|
|
86
91
|
});
|
|
87
92
|
|
|
88
93
|
test('splitFrontMatter: returns had=false when no opening ---', () => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
94
|
+
const { had, body } = splitFrontMatter('just body text');
|
|
95
|
+
|
|
96
|
+
assert.equal(had, false);
|
|
97
|
+
assert.equal(body, 'just body text');
|
|
92
98
|
});
|
|
93
99
|
|
|
94
100
|
test('splitFrontMatter: handles empty frontmatter block', () => {
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
const { fm, had } = splitFrontMatter('---\n---\nbody');
|
|
102
|
+
|
|
103
|
+
assert.equal(had, true);
|
|
104
|
+
assert.equal(fm, '');
|
|
98
105
|
});
|
|
99
106
|
|
|
100
107
|
test('splitFrontMatter: body after closing --- is preserved', () => {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const { body } = splitFrontMatter('---\nfoo: bar\n---\nline1\nline2');
|
|
109
|
+
|
|
110
|
+
assert.ok(body.includes('line1'));
|
|
111
|
+
assert.ok(body.includes('line2'));
|
|
104
112
|
});
|
|
105
113
|
|
|
106
114
|
test('splitFrontMatter: trailing newline in fm preserved for list parsing', () => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
115
|
+
// Regression: when closing --- immediately follows a list item, the \n
|
|
116
|
+
// before --- must remain in fm so extractListValues can match.
|
|
117
|
+
const text = '---\ncategories:\n - Personal\n---\nbody';
|
|
118
|
+
const { fm } = splitFrontMatter(text);
|
|
119
|
+
|
|
120
|
+
assert.ok(fm.endsWith('\n'), 'fm should end with newline');
|
|
121
|
+
assert.ok(fm.includes(' - Personal'));
|
|
113
122
|
});
|
|
114
123
|
|
|
115
124
|
// ---------------------------------------------------------------------------
|
|
@@ -117,19 +126,19 @@ test('splitFrontMatter: trailing newline in fm preserved for list parsing', () =
|
|
|
117
126
|
// ---------------------------------------------------------------------------
|
|
118
127
|
|
|
119
128
|
test('slugify: lowercases and hyphenates spaces', () => {
|
|
120
|
-
|
|
129
|
+
assert.equal(slugify('Hello World'), 'hello-world');
|
|
121
130
|
});
|
|
122
131
|
|
|
123
132
|
test('slugify: strips special characters', () => {
|
|
124
|
-
|
|
133
|
+
assert.equal(slugify('It\'s Great!'), 'its-great');
|
|
125
134
|
});
|
|
126
135
|
|
|
127
136
|
test('slugify: collapses multiple hyphens', () => {
|
|
128
|
-
|
|
137
|
+
assert.equal(slugify('a--b---c'), 'a-b-c');
|
|
129
138
|
});
|
|
130
139
|
|
|
131
140
|
test('slugify: falls back to "post" for empty string', () => {
|
|
132
|
-
|
|
141
|
+
assert.equal(slugify(''), 'post');
|
|
133
142
|
});
|
|
134
143
|
|
|
135
144
|
// ---------------------------------------------------------------------------
|
|
@@ -137,19 +146,19 @@ test('slugify: falls back to "post" for empty string', () => {
|
|
|
137
146
|
// ---------------------------------------------------------------------------
|
|
138
147
|
|
|
139
148
|
test('parseTitleFromFm: returns quoted title without quotes', () => {
|
|
140
|
-
|
|
149
|
+
assert.equal(parseTitleFromFm('title: "My Post"'), 'My Post');
|
|
141
150
|
});
|
|
142
151
|
|
|
143
152
|
test('parseTitleFromFm: returns unquoted title', () => {
|
|
144
|
-
|
|
153
|
+
assert.equal(parseTitleFromFm('title: My Post'), 'My Post');
|
|
145
154
|
});
|
|
146
155
|
|
|
147
156
|
test('parseTitleFromFm: returns null when title key is absent', () => {
|
|
148
|
-
|
|
157
|
+
assert.equal(parseTitleFromFm('draft: true\n'), null);
|
|
149
158
|
});
|
|
150
159
|
|
|
151
160
|
test('parseTitleFromFm: returns null for empty quoted title (archetype default)', () => {
|
|
152
|
-
|
|
161
|
+
assert.equal(parseTitleFromFm('title: ""\n'), null);
|
|
153
162
|
});
|
|
154
163
|
|
|
155
164
|
// ---------------------------------------------------------------------------
|
|
@@ -157,11 +166,11 @@ test('parseTitleFromFm: returns null for empty quoted title (archetype default)'
|
|
|
157
166
|
// ---------------------------------------------------------------------------
|
|
158
167
|
|
|
159
168
|
test('titleFromBasename: hyphen-separated becomes Title Case', () => {
|
|
160
|
-
|
|
169
|
+
assert.equal(titleFromBasename('my-draft-post'), 'My Draft Post');
|
|
161
170
|
});
|
|
162
171
|
|
|
163
172
|
test('titleFromBasename: underscore-separated becomes Title Case', () => {
|
|
164
|
-
|
|
173
|
+
assert.equal(titleFromBasename('my_draft_post'), 'My Draft Post');
|
|
165
174
|
});
|
|
166
175
|
|
|
167
176
|
// ---------------------------------------------------------------------------
|
|
@@ -169,21 +178,23 @@ test('titleFromBasename: underscore-separated becomes Title Case', () => {
|
|
|
169
178
|
// ---------------------------------------------------------------------------
|
|
170
179
|
|
|
171
180
|
test('extractListValues: reads YAML list items', () => {
|
|
172
|
-
|
|
173
|
-
|
|
181
|
+
const fm = 'tags:\n - foo\n - bar\n';
|
|
182
|
+
|
|
183
|
+
assert.deepEqual(extractListValues(fm, 'tags'), ['foo', 'bar']);
|
|
174
184
|
});
|
|
175
185
|
|
|
176
186
|
test('extractListValues: strips quotes from items', () => {
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
const fm = 'tags:\n - "foo"\n - \'bar\'\n';
|
|
188
|
+
|
|
189
|
+
assert.deepEqual(extractListValues(fm, 'tags'), ['foo', 'bar']);
|
|
179
190
|
});
|
|
180
191
|
|
|
181
192
|
test('extractListValues: returns [] for key not present', () => {
|
|
182
|
-
|
|
193
|
+
assert.deepEqual(extractListValues('title: foo\n', 'tags'), []);
|
|
183
194
|
});
|
|
184
195
|
|
|
185
196
|
test('extractListValues: returns [] for archetype placeholder list ( -)', () => {
|
|
186
|
-
|
|
197
|
+
assert.deepEqual(extractListValues('tags:\n -\n', 'tags'), []);
|
|
187
198
|
});
|
|
188
199
|
|
|
189
200
|
// ---------------------------------------------------------------------------
|
|
@@ -191,23 +202,23 @@ test('extractListValues: returns [] for archetype placeholder list ( -)', () =>
|
|
|
191
202
|
// ---------------------------------------------------------------------------
|
|
192
203
|
|
|
193
204
|
test('hasCategory: finds match in categories key', () => {
|
|
194
|
-
|
|
205
|
+
assert.equal(hasCategory('categories:\n - Technology\n', 'Technology'), true);
|
|
195
206
|
});
|
|
196
207
|
|
|
197
208
|
test('hasCategory: finds match in category key (singular)', () => {
|
|
198
|
-
|
|
209
|
+
assert.equal(hasCategory('category:\n - Personal\n', 'personal'), true);
|
|
199
210
|
});
|
|
200
211
|
|
|
201
212
|
test('hasCategory: is case-insensitive', () => {
|
|
202
|
-
|
|
213
|
+
assert.equal(hasCategory('categories:\n - PERSONAL\n', 'personal'), true);
|
|
203
214
|
});
|
|
204
215
|
|
|
205
216
|
test('hasCategory: returns false for placeholder list', () => {
|
|
206
|
-
|
|
217
|
+
assert.equal(hasCategory('categories:\n -\n', 'personal'), false);
|
|
207
218
|
});
|
|
208
219
|
|
|
209
220
|
test('hasCategory: returns false when not present', () => {
|
|
210
|
-
|
|
221
|
+
assert.equal(hasCategory('categories:\n - Technology\n', 'personal'), false);
|
|
211
222
|
});
|
|
212
223
|
|
|
213
224
|
// ---------------------------------------------------------------------------
|
|
@@ -215,40 +226,45 @@ test('hasCategory: returns false when not present', () => {
|
|
|
215
226
|
// ---------------------------------------------------------------------------
|
|
216
227
|
|
|
217
228
|
test('getScalarValue: reads a scalar front matter value', () => {
|
|
218
|
-
|
|
229
|
+
assert.equal(getScalarValue('date: 2026-05-04\n', 'date'), '2026-05-04');
|
|
219
230
|
});
|
|
220
231
|
|
|
221
232
|
test('getScalarValue: returns null when key absent', () => {
|
|
222
|
-
|
|
233
|
+
assert.equal(getScalarValue('title: foo\n', 'date'), null);
|
|
223
234
|
});
|
|
224
235
|
|
|
225
236
|
test('ensureLine: adds a new key when absent', () => {
|
|
226
|
-
|
|
227
|
-
|
|
237
|
+
const result = ensureLine('title: foo\n', 'draft', 'false');
|
|
238
|
+
|
|
239
|
+
assert.ok(result.includes('draft: false'));
|
|
228
240
|
});
|
|
229
241
|
|
|
230
242
|
test('ensureLine: overwrites an existing key', () => {
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
243
|
+
const result = ensureLine('draft: true\n', 'draft', 'false');
|
|
244
|
+
|
|
245
|
+
assert.ok(result.includes('draft: false'));
|
|
246
|
+
assert.ok(!result.includes('draft: true'));
|
|
234
247
|
});
|
|
235
248
|
|
|
236
249
|
test('removeKey: removes a scalar key', () => {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
250
|
+
const result = removeKey('publishDate: 2026-01-01\ntitle: foo\n', 'publishDate');
|
|
251
|
+
|
|
252
|
+
assert.ok(!result.includes('publishDate'));
|
|
253
|
+
assert.ok(result.includes('title: foo'));
|
|
240
254
|
});
|
|
241
255
|
|
|
242
256
|
test('removeKey: removes a list key and its items', () => {
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
257
|
+
const result = removeKey('tags:\n - foo\n - bar\ntitle: x\n', 'tags');
|
|
258
|
+
|
|
259
|
+
assert.ok(!result.includes('tags:'));
|
|
260
|
+
assert.ok(result.includes('title: x'));
|
|
246
261
|
});
|
|
247
262
|
|
|
248
263
|
test('removeKey: removes placeholder list', () => {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
264
|
+
const result = removeKey('tags:\n -\ntitle: x\n', 'tags');
|
|
265
|
+
|
|
266
|
+
assert.ok(!result.includes('tags:'));
|
|
267
|
+
assert.ok(result.includes('title: x'));
|
|
252
268
|
});
|
|
253
269
|
|
|
254
270
|
// ---------------------------------------------------------------------------
|
|
@@ -256,11 +272,11 @@ test('removeKey: removes placeholder list', () => {
|
|
|
256
272
|
// ---------------------------------------------------------------------------
|
|
257
273
|
|
|
258
274
|
test('chicagoAt8AM: returns CST offset in winter', () => {
|
|
259
|
-
|
|
275
|
+
assert.equal(chicagoAt8AM('2026-01-15'), '2026-01-15T08:00:00-06:00');
|
|
260
276
|
});
|
|
261
277
|
|
|
262
278
|
test('chicagoAt8AM: returns CDT offset in summer', () => {
|
|
263
|
-
|
|
279
|
+
assert.equal(chicagoAt8AM('2026-07-15'), '2026-07-15T08:00:00-05:00');
|
|
264
280
|
});
|
|
265
281
|
|
|
266
282
|
// ---------------------------------------------------------------------------
|
|
@@ -268,27 +284,31 @@ test('chicagoAt8AM: returns CDT offset in summer', () => {
|
|
|
268
284
|
// ---------------------------------------------------------------------------
|
|
269
285
|
|
|
270
286
|
test('processContent: placeholder categories (no real cats) → note', () => {
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
287
|
+
const text = makeDraft({ body: words(10) });
|
|
288
|
+
const { typeDir } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
289
|
+
|
|
290
|
+
assert.equal(typeDir, 'notes');
|
|
274
291
|
});
|
|
275
292
|
|
|
276
293
|
test('processContent: personal category with short body → note', () => {
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
294
|
+
const text = makeDraft({ categories: ['Personal'], body: words(10) });
|
|
295
|
+
const { typeDir } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
296
|
+
|
|
297
|
+
assert.equal(typeDir, 'notes');
|
|
280
298
|
});
|
|
281
299
|
|
|
282
300
|
test('processContent: non-personal category with tags → post', () => {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['JavaScript'], body: words(10) });
|
|
302
|
+
const { typeDir } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
303
|
+
|
|
304
|
+
assert.equal(typeDir, 'posts');
|
|
286
305
|
});
|
|
287
306
|
|
|
288
307
|
test('processContent: long post (>=200 words) with categories → post', () => {
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
308
|
+
const text = makeDraft({ title: 'Long Post', categories: ['Technology'], tags: ['Go'], body: words(200) });
|
|
309
|
+
const { typeDir } = processContent(text, 'now', 'long-post', NOW_STAMP, '', DRAFT_PATH);
|
|
310
|
+
|
|
311
|
+
assert.equal(typeDir, 'posts');
|
|
292
312
|
});
|
|
293
313
|
|
|
294
314
|
// ---------------------------------------------------------------------------
|
|
@@ -296,41 +316,47 @@ test('processContent: long post (>=200 words) with categories → post', () => {
|
|
|
296
316
|
// ---------------------------------------------------------------------------
|
|
297
317
|
|
|
298
318
|
test('processContent: sets draft: false', () => {
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
319
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
320
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
321
|
+
|
|
322
|
+
assert.ok(updated.includes('draft: false'));
|
|
323
|
+
assert.ok(!updated.includes('draft: true'));
|
|
303
324
|
});
|
|
304
325
|
|
|
305
326
|
test('processContent: sets date to dateStamp in now mode', () => {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
327
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
328
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
329
|
+
|
|
330
|
+
assert.ok(updated.includes(`date: ${NOW_STAMP}`));
|
|
309
331
|
});
|
|
310
332
|
|
|
311
333
|
test('processContent: sets date to publishStamp in later mode', () => {
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
334
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
335
|
+
const { updated } = processContent(text, 'later', 'my-post', '', LATER_STAMP, DRAFT_PATH);
|
|
336
|
+
|
|
337
|
+
assert.ok(updated.includes(`date: ${LATER_STAMP}`));
|
|
315
338
|
});
|
|
316
339
|
|
|
317
340
|
test('processContent: removes publishDate key', () => {
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
341
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], publishDate: '2026-01-01', body: words(10) });
|
|
342
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
343
|
+
|
|
344
|
+
assert.ok(!updated.includes('publishDate'));
|
|
321
345
|
});
|
|
322
346
|
|
|
323
347
|
test('processContent: slug is derived from frontmatter title', () => {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
348
|
+
const text = makeDraft({ title: 'Hello World', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
349
|
+
const { slug } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
350
|
+
|
|
351
|
+
assert.equal(slug, 'hello-world');
|
|
327
352
|
});
|
|
328
353
|
|
|
329
354
|
test('processContent: body content is preserved', () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
355
|
+
const body = 'This is the body. ' + words(5);
|
|
356
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body });
|
|
357
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
358
|
+
|
|
359
|
+
assert.ok(updated.includes('This is the body.'));
|
|
334
360
|
});
|
|
335
361
|
|
|
336
362
|
// ---------------------------------------------------------------------------
|
|
@@ -338,28 +364,32 @@ test('processContent: body content is preserved', () => {
|
|
|
338
364
|
// ---------------------------------------------------------------------------
|
|
339
365
|
|
|
340
366
|
test('processContent: preserves existing description for post', () => {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
367
|
+
const text = makeDraft({ title: 'My Post', description: 'Existing desc', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
368
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
369
|
+
|
|
370
|
+
assert.ok(updated.includes('description: "Existing desc"'));
|
|
344
371
|
});
|
|
345
372
|
|
|
346
373
|
test('processContent: retains placeholder description when post has no description set', () => {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
374
|
+
// Archetype provides description: "" — processContent should not overwrite it
|
|
375
|
+
const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
|
|
376
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
377
|
+
|
|
378
|
+
assert.ok(updated.includes('description:'));
|
|
351
379
|
});
|
|
352
380
|
|
|
353
381
|
test('processContent: categories block is present in published post', () => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
382
|
+
const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['go'], body: words(10) });
|
|
383
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
384
|
+
|
|
385
|
+
assert.ok(updated.includes('categories:'));
|
|
357
386
|
});
|
|
358
387
|
|
|
359
388
|
test('processContent: tags block is present in published post', () => {
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
389
|
+
const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['go'], body: words(10) });
|
|
390
|
+
const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
|
|
391
|
+
|
|
392
|
+
assert.ok(updated.includes('tags:'));
|
|
363
393
|
});
|
|
364
394
|
|
|
365
395
|
// ---------------------------------------------------------------------------
|
|
@@ -367,27 +397,31 @@ test('processContent: tags block is present in published post', () => {
|
|
|
367
397
|
// ---------------------------------------------------------------------------
|
|
368
398
|
|
|
369
399
|
test('processContent: strips tags from note', () => {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
400
|
+
const text = makeDraft({ body: words(5) });
|
|
401
|
+
const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
402
|
+
|
|
403
|
+
assert.ok(!updated.includes('tags:'));
|
|
373
404
|
});
|
|
374
405
|
|
|
375
406
|
test('processContent: strips categories from note', () => {
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
407
|
+
const text = makeDraft({ categories: ['Personal'], body: words(5) });
|
|
408
|
+
const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
409
|
+
|
|
410
|
+
assert.ok(!updated.includes('categories:'));
|
|
379
411
|
});
|
|
380
412
|
|
|
381
413
|
test('processContent: strips description from note', () => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
414
|
+
const text = makeDraft({ body: words(5) });
|
|
415
|
+
const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
416
|
+
|
|
417
|
+
assert.ok(!updated.includes('description:'));
|
|
385
418
|
});
|
|
386
419
|
|
|
387
420
|
test('processContent: strips images from note', () => {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
421
|
+
const text = makeDraft({ body: words(5) });
|
|
422
|
+
const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
|
|
423
|
+
|
|
424
|
+
assert.ok(!updated.includes('images:'));
|
|
391
425
|
});
|
|
392
426
|
|
|
393
427
|
// ---------------------------------------------------------------------------
|
|
@@ -395,8 +429,9 @@ test('processContent: strips images from note', () => {
|
|
|
395
429
|
// ---------------------------------------------------------------------------
|
|
396
430
|
|
|
397
431
|
test('processContent: handles draft with no frontmatter as note', () => {
|
|
398
|
-
|
|
399
|
-
|
|
432
|
+
const { typeDir } = processContent(words(5), 'now', 'quick-note', NOW_STAMP, '', DRAFT_PATH);
|
|
433
|
+
|
|
434
|
+
assert.equal(typeDir, 'notes');
|
|
400
435
|
});
|
|
401
436
|
|
|
402
437
|
// ---------------------------------------------------------------------------
|
|
@@ -404,29 +439,32 @@ test('processContent: handles draft with no frontmatter as note', () => {
|
|
|
404
439
|
// ---------------------------------------------------------------------------
|
|
405
440
|
|
|
406
441
|
test('processContent: throws for 200+ word draft with no real categories', () => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
442
|
+
const text = makeDraft({ body: words(200) });
|
|
443
|
+
|
|
444
|
+
assert.throws(
|
|
445
|
+
() => processContent(text, 'now', 'long-post', NOW_STAMP, '', DRAFT_PATH),
|
|
446
|
+
/notes must be under 200 words/,
|
|
447
|
+
);
|
|
412
448
|
});
|
|
413
449
|
|
|
414
450
|
test('processContent: throws for post without a frontmatter title', () => {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
451
|
+
// title: "" is the archetype default — parseTitleFromFm returns null
|
|
452
|
+
const text = makeDraft({ categories: ['Technology'], tags: ['go'], body: words(10) });
|
|
453
|
+
|
|
454
|
+
assert.throws(
|
|
455
|
+
() => processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH),
|
|
456
|
+
/must have a title/,
|
|
457
|
+
);
|
|
421
458
|
});
|
|
422
459
|
|
|
423
460
|
test('processContent: throws for post with placeholder tags only', () => {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
461
|
+
// tags: - (placeholder) counts as no tags
|
|
462
|
+
const text = makeDraft({ title: 'My Post', categories: ['Technology'], body: words(10) });
|
|
463
|
+
|
|
464
|
+
assert.throws(
|
|
465
|
+
() => processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH),
|
|
466
|
+
/must have at least one tag/,
|
|
467
|
+
);
|
|
430
468
|
});
|
|
431
469
|
|
|
432
470
|
// ---------------------------------------------------------------------------
|
|
@@ -434,23 +472,23 @@ test('processContent: throws for post with placeholder tags only', () => {
|
|
|
434
472
|
// ---------------------------------------------------------------------------
|
|
435
473
|
|
|
436
474
|
test('stripQuotes: strips double quotes', () => {
|
|
437
|
-
|
|
475
|
+
assert.equal(stripQuotes('"hello"'), 'hello');
|
|
438
476
|
});
|
|
439
477
|
|
|
440
478
|
test('stripQuotes: strips single quotes', () => {
|
|
441
|
-
|
|
479
|
+
assert.equal(stripQuotes('\'hello\''), 'hello');
|
|
442
480
|
});
|
|
443
481
|
|
|
444
482
|
test('stripQuotes: returns value unchanged when no quotes', () => {
|
|
445
|
-
|
|
483
|
+
assert.equal(stripQuotes('hello'), 'hello');
|
|
446
484
|
});
|
|
447
485
|
|
|
448
486
|
test('stripQuotes: preserves internal spaces', () => {
|
|
449
|
-
|
|
487
|
+
assert.equal(stripQuotes('"hello world"'), 'hello world');
|
|
450
488
|
});
|
|
451
489
|
|
|
452
490
|
test('stripQuotes: trims surrounding whitespace', () => {
|
|
453
|
-
|
|
491
|
+
assert.equal(stripQuotes(' hello '), 'hello');
|
|
454
492
|
});
|
|
455
493
|
|
|
456
494
|
// ---------------------------------------------------------------------------
|
|
@@ -458,20 +496,23 @@ test('stripQuotes: trims surrounding whitespace', () => {
|
|
|
458
496
|
// ---------------------------------------------------------------------------
|
|
459
497
|
|
|
460
498
|
test('ensurePlaceholderList: adds placeholder when key is absent', () => {
|
|
461
|
-
|
|
462
|
-
|
|
499
|
+
const result = ensurePlaceholderList('title: foo\n', 'tags');
|
|
500
|
+
|
|
501
|
+
assert.ok(result.includes('tags:\n -\n'));
|
|
463
502
|
});
|
|
464
503
|
|
|
465
504
|
test('ensurePlaceholderList: does not add when list with items already present', () => {
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
505
|
+
const fm = 'tags:\n - go\n';
|
|
506
|
+
const result = ensurePlaceholderList(fm, 'tags');
|
|
507
|
+
|
|
508
|
+
assert.equal(result, fm);
|
|
469
509
|
});
|
|
470
510
|
|
|
471
511
|
test('ensurePlaceholderList: does not add when placeholder already present', () => {
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
512
|
+
const fm = 'tags:\n -\n';
|
|
513
|
+
const result = ensurePlaceholderList(fm, 'tags');
|
|
514
|
+
|
|
515
|
+
assert.equal(result, fm);
|
|
475
516
|
});
|
|
476
517
|
|
|
477
518
|
// ---------------------------------------------------------------------------
|
|
@@ -479,18 +520,18 @@ test('ensurePlaceholderList: does not add when placeholder already present', ()
|
|
|
479
520
|
// ---------------------------------------------------------------------------
|
|
480
521
|
|
|
481
522
|
test('formatChicagoDateTitle: formats as Weekday, DD Month, YYYY', () => {
|
|
482
|
-
|
|
523
|
+
assert.equal(formatChicagoDateTitle('2026-05-04T10:00:00-05:00'), 'Monday, 04 May, 2026');
|
|
483
524
|
});
|
|
484
525
|
|
|
485
526
|
test('formatChicagoDateTitle: uses Chicago time (CDT in summer)', () => {
|
|
486
|
-
|
|
487
|
-
|
|
527
|
+
// 2026-07-15 is a Wednesday
|
|
528
|
+
assert.equal(formatChicagoDateTitle('2026-07-15T08:00:00-05:00'), 'Wednesday, 15 July, 2026');
|
|
488
529
|
});
|
|
489
530
|
|
|
490
531
|
test('formatChicagoTime: returns HH:MM in 24-hour format', () => {
|
|
491
|
-
|
|
532
|
+
assert.equal(formatChicagoTime('2026-05-04T10:30:00-05:00'), '10:30');
|
|
492
533
|
});
|
|
493
534
|
|
|
494
535
|
test('formatChicagoTime: pads minutes to two digits', () => {
|
|
495
|
-
|
|
536
|
+
assert.equal(formatChicagoTime('2026-05-04T08:05:00-05:00'), '08:05');
|
|
496
537
|
});
|