@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.
@@ -4,21 +4,21 @@ import { createRequire } from 'node:module';
4
4
 
5
5
  const require = createRequire(import.meta.url);
6
6
  const {
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,
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
- return Array.from({ length: n }, (_, i) => `word${i}`).join(' ');
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
- title = '',
38
- description = '',
39
- categories = null, // null → placeholder " -", array → real values
40
- tags = null, // null → placeholder " -", array → real values
41
- body = '',
42
- publishDate,
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
- 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
- if (tags === null) {
62
- lines.push(' -');
63
- } else {
64
- for (const t of tags) lines.push(` - ${t}`);
65
- }
66
-
67
- if (publishDate !== undefined) lines.push(`publishDate: ${publishDate}`);
68
- lines.push('---');
69
- if (body) lines.push('', body);
70
- return lines.join('\n');
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
- const { fm, body, had } = splitFrontMatter('---\ntitle: foo\n---\nbody text');
83
- assert.equal(had, true);
84
- assert.ok(fm.includes('title: foo'));
85
- assert.ok(body.includes('body text'));
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
- const { had, body } = splitFrontMatter('just body text');
90
- assert.equal(had, false);
91
- assert.equal(body, 'just body text');
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
- const { fm, had } = splitFrontMatter('---\n---\nbody');
96
- assert.equal(had, true);
97
- assert.equal(fm, '');
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
- const { body } = splitFrontMatter('---\nfoo: bar\n---\nline1\nline2');
102
- assert.ok(body.includes('line1'));
103
- assert.ok(body.includes('line2'));
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
- // Regression: when closing --- immediately follows a list item, the \n
108
- // before --- must remain in fm so extractListValues can match.
109
- const text = '---\ncategories:\n - Personal\n---\nbody';
110
- const { fm } = splitFrontMatter(text);
111
- assert.ok(fm.endsWith('\n'), 'fm should end with newline');
112
- assert.ok(fm.includes(' - Personal'));
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
- assert.equal(slugify('Hello World'), 'hello-world');
129
+ assert.equal(slugify('Hello World'), 'hello-world');
121
130
  });
122
131
 
123
132
  test('slugify: strips special characters', () => {
124
- assert.equal(slugify("It's Great!"), 'its-great');
133
+ assert.equal(slugify('It\'s Great!'), 'its-great');
125
134
  });
126
135
 
127
136
  test('slugify: collapses multiple hyphens', () => {
128
- assert.equal(slugify('a--b---c'), 'a-b-c');
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
- assert.equal(slugify(''), 'post');
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
- assert.equal(parseTitleFromFm('title: "My Post"'), 'My Post');
149
+ assert.equal(parseTitleFromFm('title: "My Post"'), 'My Post');
141
150
  });
142
151
 
143
152
  test('parseTitleFromFm: returns unquoted title', () => {
144
- assert.equal(parseTitleFromFm('title: My Post'), 'My Post');
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
- assert.equal(parseTitleFromFm('draft: true\n'), null);
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
- assert.equal(parseTitleFromFm('title: ""\n'), null);
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
- assert.equal(titleFromBasename('my-draft-post'), 'My Draft Post');
169
+ assert.equal(titleFromBasename('my-draft-post'), 'My Draft Post');
161
170
  });
162
171
 
163
172
  test('titleFromBasename: underscore-separated becomes Title Case', () => {
164
- assert.equal(titleFromBasename('my_draft_post'), 'My Draft Post');
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
- const fm = 'tags:\n - foo\n - bar\n';
173
- assert.deepEqual(extractListValues(fm, 'tags'), ['foo', 'bar']);
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
- const fm = 'tags:\n - "foo"\n - \'bar\'\n';
178
- assert.deepEqual(extractListValues(fm, 'tags'), ['foo', 'bar']);
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
- assert.deepEqual(extractListValues('title: foo\n', 'tags'), []);
193
+ assert.deepEqual(extractListValues('title: foo\n', 'tags'), []);
183
194
  });
184
195
 
185
196
  test('extractListValues: returns [] for archetype placeholder list ( -)', () => {
186
- assert.deepEqual(extractListValues('tags:\n -\n', 'tags'), []);
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
- assert.equal(hasCategory('categories:\n - Technology\n', 'Technology'), true);
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
- assert.equal(hasCategory('category:\n - Personal\n', 'personal'), true);
209
+ assert.equal(hasCategory('category:\n - Personal\n', 'personal'), true);
199
210
  });
200
211
 
201
212
  test('hasCategory: is case-insensitive', () => {
202
- assert.equal(hasCategory('categories:\n - PERSONAL\n', 'personal'), true);
213
+ assert.equal(hasCategory('categories:\n - PERSONAL\n', 'personal'), true);
203
214
  });
204
215
 
205
216
  test('hasCategory: returns false for placeholder list', () => {
206
- assert.equal(hasCategory('categories:\n -\n', 'personal'), false);
217
+ assert.equal(hasCategory('categories:\n -\n', 'personal'), false);
207
218
  });
208
219
 
209
220
  test('hasCategory: returns false when not present', () => {
210
- assert.equal(hasCategory('categories:\n - Technology\n', 'personal'), false);
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
- assert.equal(getScalarValue('date: 2026-05-04\n', 'date'), '2026-05-04');
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
- assert.equal(getScalarValue('title: foo\n', 'date'), null);
233
+ assert.equal(getScalarValue('title: foo\n', 'date'), null);
223
234
  });
224
235
 
225
236
  test('ensureLine: adds a new key when absent', () => {
226
- const result = ensureLine('title: foo\n', 'draft', 'false');
227
- assert.ok(result.includes('draft: false'));
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
- const result = ensureLine('draft: true\n', 'draft', 'false');
232
- assert.ok(result.includes('draft: false'));
233
- assert.ok(!result.includes('draft: true'));
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
- const result = removeKey('publishDate: 2026-01-01\ntitle: foo\n', 'publishDate');
238
- assert.ok(!result.includes('publishDate'));
239
- assert.ok(result.includes('title: foo'));
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
- const result = removeKey('tags:\n - foo\n - bar\ntitle: x\n', 'tags');
244
- assert.ok(!result.includes('tags:'));
245
- assert.ok(result.includes('title: x'));
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
- const result = removeKey('tags:\n -\ntitle: x\n', 'tags');
250
- assert.ok(!result.includes('tags:'));
251
- assert.ok(result.includes('title: x'));
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
- assert.equal(chicagoAt8AM('2026-01-15'), '2026-01-15T08:00:00-06:00');
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
- assert.equal(chicagoAt8AM('2026-07-15'), '2026-07-15T08:00:00-05:00');
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
- const text = makeDraft({ body: words(10) });
272
- const { typeDir } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
273
- assert.equal(typeDir, 'notes');
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
- const text = makeDraft({ categories: ['Personal'], body: words(10) });
278
- const { typeDir } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
279
- assert.equal(typeDir, 'notes');
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
- const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['JavaScript'], body: words(10) });
284
- const { typeDir } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
285
- assert.equal(typeDir, 'posts');
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
- const text = makeDraft({ title: 'Long Post', categories: ['Technology'], tags: ['Go'], body: words(200) });
290
- const { typeDir } = processContent(text, 'now', 'long-post', NOW_STAMP, '', DRAFT_PATH);
291
- assert.equal(typeDir, 'posts');
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
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
300
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
301
- assert.ok(updated.includes('draft: false'));
302
- assert.ok(!updated.includes('draft: true'));
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
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
307
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
308
- assert.ok(updated.includes(`date: ${NOW_STAMP}`));
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
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
313
- const { updated } = processContent(text, 'later', 'my-post', '', LATER_STAMP, DRAFT_PATH);
314
- assert.ok(updated.includes(`date: ${LATER_STAMP}`));
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
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], publishDate: '2026-01-01', body: words(10) });
319
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
320
- assert.ok(!updated.includes('publishDate'));
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
- const text = makeDraft({ title: 'Hello World', categories: ['Tech'], tags: ['go'], body: words(10) });
325
- const { slug } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
326
- assert.equal(slug, 'hello-world');
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
- const body = 'This is the body. ' + words(5);
331
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body });
332
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
333
- assert.ok(updated.includes('This is the body.'));
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
- const text = makeDraft({ title: 'My Post', description: 'Existing desc', categories: ['Tech'], tags: ['go'], body: words(10) });
342
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
343
- assert.ok(updated.includes('description: "Existing desc"'));
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
- // Archetype provides description: "" — processContent should not overwrite it
348
- const text = makeDraft({ title: 'My Post', categories: ['Tech'], tags: ['go'], body: words(10) });
349
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
350
- assert.ok(updated.includes('description:'));
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
- const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['go'], body: words(10) });
355
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
356
- assert.ok(updated.includes('categories:'));
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
- const text = makeDraft({ title: 'My Post', categories: ['Technology'], tags: ['go'], body: words(10) });
361
- const { updated } = processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH);
362
- assert.ok(updated.includes('tags:'));
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
- const text = makeDraft({ body: words(5) });
371
- const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
372
- assert.ok(!updated.includes('tags:'));
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
- const text = makeDraft({ categories: ['Personal'], body: words(5) });
377
- const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
378
- assert.ok(!updated.includes('categories:'));
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
- const text = makeDraft({ body: words(5) });
383
- const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
384
- assert.ok(!updated.includes('description:'));
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
- const text = makeDraft({ body: words(5) });
389
- const { updated } = processContent(text, 'now', 'a-note', NOW_STAMP, '', DRAFT_PATH);
390
- assert.ok(!updated.includes('images:'));
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
- const { typeDir } = processContent(words(5), 'now', 'quick-note', NOW_STAMP, '', DRAFT_PATH);
399
- assert.equal(typeDir, 'notes');
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
- const text = makeDraft({ body: words(200) });
408
- assert.throws(
409
- () => processContent(text, 'now', 'long-post', NOW_STAMP, '', DRAFT_PATH),
410
- /notes must be under 200 words/
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
- // title: "" is the archetype default — parseTitleFromFm returns null
416
- const text = makeDraft({ categories: ['Technology'], tags: ['go'], body: words(10) });
417
- assert.throws(
418
- () => processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH),
419
- /must have a title/
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
- // tags: - (placeholder) counts as no tags
425
- const text = makeDraft({ title: 'My Post', categories: ['Technology'], body: words(10) });
426
- assert.throws(
427
- () => processContent(text, 'now', 'my-post', NOW_STAMP, '', DRAFT_PATH),
428
- /must have at least one tag/
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
- assert.equal(stripQuotes('"hello"'), 'hello');
475
+ assert.equal(stripQuotes('"hello"'), 'hello');
438
476
  });
439
477
 
440
478
  test('stripQuotes: strips single quotes', () => {
441
- assert.equal(stripQuotes("'hello'"), 'hello');
479
+ assert.equal(stripQuotes('\'hello\''), 'hello');
442
480
  });
443
481
 
444
482
  test('stripQuotes: returns value unchanged when no quotes', () => {
445
- assert.equal(stripQuotes('hello'), 'hello');
483
+ assert.equal(stripQuotes('hello'), 'hello');
446
484
  });
447
485
 
448
486
  test('stripQuotes: preserves internal spaces', () => {
449
- assert.equal(stripQuotes('"hello world"'), 'hello world');
487
+ assert.equal(stripQuotes('"hello world"'), 'hello world');
450
488
  });
451
489
 
452
490
  test('stripQuotes: trims surrounding whitespace', () => {
453
- assert.equal(stripQuotes(' hello '), 'hello');
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
- const result = ensurePlaceholderList('title: foo\n', 'tags');
462
- assert.ok(result.includes('tags:\n -\n'));
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
- const fm = 'tags:\n - go\n';
467
- const result = ensurePlaceholderList(fm, 'tags');
468
- assert.equal(result, fm);
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
- const fm = 'tags:\n -\n';
473
- const result = ensurePlaceholderList(fm, 'tags');
474
- assert.equal(result, fm);
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
- assert.equal(formatChicagoDateTitle('2026-05-04T10:00:00-05:00'), 'Monday, 04 May, 2026');
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
- // 2026-07-15 is a Wednesday
487
- assert.equal(formatChicagoDateTitle('2026-07-15T08:00:00-05:00'), 'Wednesday, 15 July, 2026');
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
- assert.equal(formatChicagoTime('2026-05-04T10:30:00-05:00'), '10:30');
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
- assert.equal(formatChicagoTime('2026-05-04T08:05:00-05:00'), '08:05');
536
+ assert.equal(formatChicagoTime('2026-05-04T08:05:00-05:00'), '08:05');
496
537
  });