@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.js
CHANGED
|
@@ -25,172 +25,201 @@ const TZ = config.timezone;
|
|
|
25
25
|
// --- Timezone helpers ---
|
|
26
26
|
|
|
27
27
|
function getChicagoOffset(date) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
28
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
29
|
+
timeZone: TZ,
|
|
30
|
+
timeZoneName: 'longOffset',
|
|
31
|
+
}).formatToParts(date);
|
|
32
|
+
|
|
33
|
+
// e.g. "GMT-06:00" → "-06:00"
|
|
34
|
+
return parts.find((p) => p.type === 'timeZoneName').value.replace('GMT', '');
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
function toChicagoISO(date) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
39
|
+
timeZone: TZ,
|
|
40
|
+
year: 'numeric',
|
|
41
|
+
month: '2-digit',
|
|
42
|
+
day: '2-digit',
|
|
43
|
+
hour: '2-digit',
|
|
44
|
+
minute: '2-digit',
|
|
45
|
+
second: '2-digit',
|
|
46
|
+
hour12: false,
|
|
47
|
+
}).formatToParts(date);
|
|
48
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
49
|
+
let hour = get('hour');
|
|
50
|
+
|
|
51
|
+
if (hour === '24') hour = '00';
|
|
52
|
+
|
|
53
|
+
return `${get('year')}-${get('month')}-${get('day')}T${hour}:${get('minute')}:${get('second')}${getChicagoOffset(date)}`;
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
function getChicagoDateParts(date) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
57
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
58
|
+
timeZone: TZ,
|
|
59
|
+
year: 'numeric',
|
|
60
|
+
month: '2-digit',
|
|
61
|
+
day: '2-digit',
|
|
62
|
+
}).formatToParts(date);
|
|
63
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
64
|
+
|
|
65
|
+
return { year: get('year'), month: get('month'), day: get('day') };
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
function chicagoAt8AM(dateStr) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
69
|
+
// Returns ISO 8601 string for 08:00:00 America/Chicago on the given YYYY-MM-DD date.
|
|
70
|
+
// Uses noon UTC to determine the DST-correct offset, then back-computes the UTC time for 08:00 local.
|
|
71
|
+
const [year, month, day] = dateStr.split('-').map(Number);
|
|
72
|
+
const noonUTC = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
73
|
+
const offset = getChicagoOffset(noonUTC); // e.g. "-06:00"
|
|
74
|
+
const match = offset.match(/([+-])(\d{2}):(\d{2})/);
|
|
75
|
+
const offsetSign = match[1] === '+' ? 1 : -1;
|
|
76
|
+
const offsetTotalMins = (parseInt(match[2], 10) * 60 + parseInt(match[3], 10)) * offsetSign;
|
|
77
|
+
// UTCtime = localtime - offset → 08:00 local = (8 - offsetHours) UTC
|
|
78
|
+
const utcHour = 8 - offsetTotalMins / 60;
|
|
79
|
+
const target = new Date(Date.UTC(year, month - 1, day, utcHour, 0, 0));
|
|
80
|
+
|
|
81
|
+
return `${dateStr}T08:00:00${getChicagoOffset(target)}`;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
// --- Front matter helpers ---
|
|
80
85
|
|
|
81
86
|
function escapeRegex(s) {
|
|
82
|
-
|
|
87
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
function splitFrontMatter(text) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
91
|
+
if (!text.startsWith('---')) return { fm: '', body: text, had: false };
|
|
92
|
+
|
|
93
|
+
const m1 = text.match(/^---\s*\n/);
|
|
94
|
+
|
|
95
|
+
if (!m1) return { fm: '', body: text, had: false };
|
|
96
|
+
|
|
97
|
+
const rest = text.slice(m1[0].length);
|
|
98
|
+
const m2 = rest.match(/(?:^|\n)---\s*(?:\n|$)/m);
|
|
99
|
+
|
|
100
|
+
if (!m2) return { fm: '', body: text, had: false };
|
|
101
|
+
|
|
102
|
+
// When the match begins with \n that \n is the last line of fm, not part of the delimiter.
|
|
103
|
+
const fmEnd = m2[0][0] === '\n' ? m2.index + 1 : m2.index;
|
|
104
|
+
const fm = rest.slice(0, fmEnd);
|
|
105
|
+
const body = rest.slice(m2.index + m2[0].length).replace(/^[\r\n]+/, '');
|
|
106
|
+
|
|
107
|
+
return { fm, body, had: true };
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
function ensureLine(fm, key, value) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
111
|
+
const ek = escapeRegex(key);
|
|
112
|
+
const line = `${key}: ${value}`;
|
|
113
|
+
|
|
114
|
+
if (new RegExp(`^${ek}\\s*:.*$`, 'm').test(fm)) {
|
|
115
|
+
return fm.replace(new RegExp(`^${ek}\\s*:.*$`, 'gm'), line);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return fm.trimEnd() + (fm.trim() ? '\n' : '') + line + '\n';
|
|
106
119
|
}
|
|
107
120
|
|
|
108
121
|
function getScalarValue(fm, key) {
|
|
109
|
-
|
|
110
|
-
|
|
122
|
+
const m = fm.match(new RegExp(`^${escapeRegex(key)}\\s*:\\s*(.*?)\\s*$`, 'm'));
|
|
123
|
+
|
|
124
|
+
return m ? m[1].trim() : null;
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
function removeKey(fm, key) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
128
|
+
const ek = escapeRegex(key);
|
|
129
|
+
|
|
130
|
+
fm = fm.replace(new RegExp(`^${ek}[ \\t]*:[ \\t]*\\n(?:[ \\t]*-[ \\t]*.*\\n)+`, 'gm'), '');
|
|
131
|
+
fm = fm.replace(new RegExp(`^${ek}\\s*:.*(?:\\n|$)`, 'gm'), '');
|
|
132
|
+
return fm;
|
|
118
133
|
}
|
|
119
134
|
|
|
120
135
|
function ensurePlaceholderList(fm, key) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
const ek = escapeRegex(key);
|
|
137
|
+
|
|
138
|
+
if (new RegExp(`^${ek}\\s*:\\s*\\n(?:\\s*-\\s*.*\\n)+`, 'm').test(fm)) return fm;
|
|
139
|
+
|
|
140
|
+
return fm.trimEnd() + (fm.trim() ? '\n' : '') + `${key}:\n -\n`;
|
|
124
141
|
}
|
|
125
142
|
|
|
126
143
|
function parseTitleFromFm(fm) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
144
|
+
const m = fm.match(/^title\s*:\s*(.+)/m);
|
|
145
|
+
|
|
146
|
+
if (!m) return null;
|
|
147
|
+
|
|
148
|
+
let raw = m[1].trim();
|
|
149
|
+
|
|
150
|
+
if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith('\'') && raw.endsWith('\''))) {
|
|
151
|
+
raw = raw.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return raw.trim() || null;
|
|
134
155
|
}
|
|
135
156
|
|
|
136
157
|
function titleFromBasename(b) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
158
|
+
return b.replace(/[-_]/g, ' ')
|
|
159
|
+
.split(' ')
|
|
160
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
161
|
+
.join(' ');
|
|
141
162
|
}
|
|
142
163
|
|
|
143
164
|
function slugify(s) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
165
|
+
s = s.trim().toLowerCase()
|
|
166
|
+
.replace(/[\s_]+/g, '-')
|
|
167
|
+
.replace(/[^a-z0-9-]/g, '')
|
|
168
|
+
.replace(/-{2,}/g, '-')
|
|
169
|
+
.replace(/^-+|-+$/g, '');
|
|
170
|
+
return s || 'post';
|
|
150
171
|
}
|
|
151
172
|
|
|
152
173
|
function stripQuotes(val) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
174
|
+
const v = val.trim();
|
|
175
|
+
|
|
176
|
+
if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith('\'') && v.endsWith('\''))) {
|
|
177
|
+
return v.slice(1, -1).trim();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return v;
|
|
158
181
|
}
|
|
159
182
|
|
|
160
183
|
function extractListValues(fm, key) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
184
|
+
const ek = escapeRegex(key);
|
|
185
|
+
const blockMatch = fm.match(new RegExp(`^${ek}\\s*:\\s*\\n((?:\\s*-\\s*.*\\n)+)`, 'm'));
|
|
186
|
+
|
|
187
|
+
if (blockMatch) {
|
|
188
|
+
return blockMatch[1].split('\n')
|
|
189
|
+
.map((line) => line.match(/^\s*-\s*(.+)\s*$/))
|
|
190
|
+
.filter(Boolean)
|
|
191
|
+
.map((m) => stripQuotes(m[1]))
|
|
192
|
+
.filter((v) => v);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const scalar = getScalarValue(fm, key);
|
|
196
|
+
|
|
197
|
+
if (scalar) {
|
|
198
|
+
return scalar.split(',').map(stripQuotes).filter((v) => v);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return [];
|
|
175
202
|
}
|
|
176
203
|
|
|
177
204
|
function hasCategory(fm, name) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
205
|
+
const target = name.trim().toLowerCase();
|
|
206
|
+
|
|
207
|
+
for (const key of ['category', 'categories']) {
|
|
208
|
+
for (const val of extractListValues(fm, key)) {
|
|
209
|
+
if (val.toLowerCase() === target) return true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
185
213
|
}
|
|
186
214
|
|
|
187
215
|
function ghaWarning(msg, file) {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
216
|
+
const esc = (s) => s.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A');
|
|
217
|
+
|
|
218
|
+
if (file) {
|
|
219
|
+
process.stderr.write(`::warning file=${esc(file)},line=1,col=1::${esc(msg)}\n`);
|
|
220
|
+
} else {
|
|
221
|
+
process.stderr.write(`::warning::${esc(msg)}\n`);
|
|
222
|
+
}
|
|
194
223
|
}
|
|
195
224
|
|
|
196
225
|
// --- Date title helpers for notes ---
|
|
@@ -199,230 +228,247 @@ function ghaWarning(msg, file) {
|
|
|
199
228
|
const DATE_TITLE_RE = /^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), \d{2} (January|February|March|April|May|June|July|August|September|October|November|December), \d{4}$/;
|
|
200
229
|
|
|
201
230
|
function formatChicagoDateTitle(isoStr) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
231
|
+
const date = new Date(isoStr);
|
|
232
|
+
const weekday = new Intl.DateTimeFormat('en-US', { timeZone: TZ, weekday: 'long' }).format(date);
|
|
233
|
+
const day = new Intl.DateTimeFormat('en-US', { timeZone: TZ, day: '2-digit' }).format(date);
|
|
234
|
+
const month = new Intl.DateTimeFormat('en-US', { timeZone: TZ, month: 'long' }).format(date);
|
|
235
|
+
const year = new Intl.DateTimeFormat('en-US', { timeZone: TZ, year: 'numeric' }).format(date);
|
|
236
|
+
|
|
237
|
+
return `${weekday}, ${day} ${month}, ${year}`;
|
|
208
238
|
}
|
|
209
239
|
|
|
210
240
|
function formatChicagoTime(isoStr) {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
241
|
+
const date = new Date(isoStr);
|
|
242
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
243
|
+
timeZone: TZ,
|
|
244
|
+
hour: '2-digit',
|
|
245
|
+
minute: '2-digit',
|
|
246
|
+
hour12: false,
|
|
247
|
+
}).formatToParts(date);
|
|
248
|
+
const get = (type) => parts.find((p) => p.type === type).value;
|
|
249
|
+
let hour = get('hour');
|
|
250
|
+
|
|
251
|
+
if (hour === '24') hour = '00';
|
|
252
|
+
|
|
253
|
+
return `${hour}:${get('minute')}`;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
// --- Content processing ---
|
|
225
257
|
|
|
226
258
|
function processContent(text, mode, baseName, dateStamp, publishStamp, draftPath) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
259
|
+
let { fm, body, had } = splitFrontMatter(text); // eslint-disable-line prefer-const
|
|
260
|
+
|
|
261
|
+
if (!had) {
|
|
262
|
+
fm = '';
|
|
263
|
+
body = text.replace(/^[\r\n]+/, '');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const rawTitle = parseTitleFromFm(fm);
|
|
267
|
+
const trimmedBody = body.trim();
|
|
268
|
+
const wordCount = trimmedBody ? trimmedBody.split(/\s+/).length : 0;
|
|
269
|
+
const hasAnyCategory = extractListValues(fm, 'category').length > 0 ||
|
|
237
270
|
extractListValues(fm, 'categories').length > 0;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
271
|
+
const isNote = (hasCategory(fm, 'personal') || !hasAnyCategory) && wordCount < 200;
|
|
272
|
+
|
|
273
|
+
fm = removeKey(fm, 'publishDate');
|
|
274
|
+
|
|
275
|
+
const targetDate = mode === 'now' ? dateStamp : publishStamp;
|
|
276
|
+
|
|
277
|
+
const existingDate = getScalarValue(fm, 'date');
|
|
278
|
+
|
|
279
|
+
if (existingDate !== null && existingDate !== targetDate) {
|
|
280
|
+
const warnMsg = `Draft already had date: ${existingDate} — overriding with generated date: ${targetDate}`;
|
|
281
|
+
|
|
282
|
+
if (process.env.GITHUB_ACTIONS === 'true') {
|
|
283
|
+
ghaWarning(warnMsg, draftPath);
|
|
284
|
+
} else {
|
|
285
|
+
process.stderr.write(`WARN: ${warnMsg}\n`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
fm = ensureLine(fm, 'date', targetDate);
|
|
290
|
+
|
|
291
|
+
let title = rawTitle || titleFromBasename(baseName);
|
|
292
|
+
|
|
293
|
+
if (isNote) {
|
|
294
|
+
if (!rawTitle) {
|
|
295
|
+
title = formatChicagoDateTitle(targetDate);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
if (DATE_TITLE_RE.test(title)) {
|
|
299
|
+
title = `${title} - ${formatChicagoTime(targetDate)}`;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const slug = slugify(title);
|
|
304
|
+
|
|
305
|
+
fm = ensureLine(fm, 'title', `"${title}"`);
|
|
306
|
+
fm = ensureLine(fm, 'draft', 'false');
|
|
307
|
+
|
|
308
|
+
if (!isNote && !fm.match(/^description\s*:/m)) {
|
|
309
|
+
fm = ensureLine(fm, 'description', '""');
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (isNote) {
|
|
313
|
+
for (const key of ['images', 'description', 'tags', 'category', 'categories']) {
|
|
314
|
+
fm = removeKey(fm, key);
|
|
315
|
+
}
|
|
316
|
+
fm = ensureLine(fm, 'date', targetDate);
|
|
317
|
+
} else {
|
|
318
|
+
fm = ensurePlaceholderList(fm, 'categories');
|
|
319
|
+
fm = ensurePlaceholderList(fm, 'tags');
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (!isNote) {
|
|
323
|
+
const looksLikeNote = hasCategory(fm, 'personal') || !hasAnyCategory;
|
|
324
|
+
|
|
325
|
+
if (looksLikeNote) {
|
|
326
|
+
throw new Error(`Error: post has ${wordCount} words — notes must be under 200 words. Add categories and tags to publish as a post instead.`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!rawTitle) {
|
|
330
|
+
throw new Error('Error: post must have a title in front matter before publishing');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (extractListValues(fm, 'tags').length === 0) {
|
|
334
|
+
throw new Error('Error: post must have at least one tag before publishing');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
fm = fm.trimEnd() + '\n';
|
|
339
|
+
const updated = `---\n${fm}---\n\n${body.trimStart()}`;
|
|
340
|
+
|
|
341
|
+
return { slug, typeDir: isNote ? 'notes' : 'posts', updated };
|
|
302
342
|
}
|
|
303
343
|
|
|
304
344
|
// --- Interactive prompt ---
|
|
305
345
|
|
|
306
346
|
async function ask(question) {
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
347
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
348
|
+
|
|
349
|
+
return new Promise((resolve) => {
|
|
350
|
+
rl.question(question, (answer) => {
|
|
351
|
+
rl.close();
|
|
352
|
+
resolve(answer.trim());
|
|
353
|
+
});
|
|
354
|
+
});
|
|
314
355
|
}
|
|
315
356
|
|
|
316
357
|
// --- Main ---
|
|
317
358
|
|
|
318
359
|
async function main() {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
360
|
+
config.requireHugoSite();
|
|
361
|
+
|
|
362
|
+
const draftPath = process.argv[2];
|
|
363
|
+
let mode = process.argv[3] || '';
|
|
364
|
+
|
|
365
|
+
if (!draftPath) {
|
|
366
|
+
console.error('Error: draft path required (e.g. content/drafts/My Draft.md)');
|
|
367
|
+
process.exit(1);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const draftsPrefix = config.draftsDir.replace(/\\/g, '/').replace(/\/$/, '');
|
|
371
|
+
|
|
372
|
+
if (!draftPath.startsWith(draftsPrefix + '/') || !draftPath.endsWith('.md')) {
|
|
373
|
+
console.error(`Error: draft must be under ${config.draftsDir}/*.md`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!fs.existsSync(draftPath)) {
|
|
378
|
+
console.error(`Error: file not found: ${draftPath}`);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!mode) {
|
|
383
|
+
mode = await ask('Choose mode: now | later\n');
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const now = new Date();
|
|
387
|
+
let dateStamp = '';
|
|
388
|
+
let publishStamp = '';
|
|
389
|
+
let year, month, day;
|
|
390
|
+
|
|
391
|
+
if (mode === 'now') {
|
|
392
|
+
dateStamp = toChicagoISO(now);
|
|
393
|
+
({ year, month, day } = getChicagoDateParts(now));
|
|
394
|
+
} else if (mode === 'later') {
|
|
395
|
+
let pubDate = process.argv[4] || '';
|
|
396
|
+
|
|
397
|
+
if (!pubDate) {
|
|
398
|
+
pubDate = await ask('Enter publish date (YYYY-MM-DD):\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!/^\d{4}-\d{2}-\d{2}$/.test(pubDate)) {
|
|
402
|
+
console.error('Error: date must be YYYY-MM-DD');
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
publishStamp = chicagoAt8AM(pubDate);
|
|
407
|
+
year = pubDate.slice(0, 4);
|
|
408
|
+
month = pubDate.slice(5, 7);
|
|
409
|
+
day = pubDate.slice(8, 10);
|
|
410
|
+
} else {
|
|
411
|
+
console.error('Error: mode must be \'now\' or \'later\'');
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const baseName = path.basename(draftPath, '.md');
|
|
416
|
+
const text = fs.readFileSync(draftPath, 'utf8');
|
|
417
|
+
|
|
418
|
+
let slug, typeDir, updated;
|
|
419
|
+
|
|
420
|
+
try {
|
|
421
|
+
({ slug, typeDir, updated } = processContent(
|
|
422
|
+
text, mode, baseName, dateStamp, publishStamp, draftPath,
|
|
423
|
+
));
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(err.message);
|
|
426
|
+
process.exit(1);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const baseDir = typeDir === 'notes' ? config.notesDir : config.postsDir;
|
|
430
|
+
const destDir = path.join(baseDir, year);
|
|
431
|
+
const destPath = path.join(destDir, `${month}-${day}-${slug}.md`);
|
|
432
|
+
|
|
433
|
+
if (fs.existsSync(destPath)) {
|
|
434
|
+
console.error(`Error: destination already exists: ${destPath}`);
|
|
435
|
+
process.exit(1);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
439
|
+
fs.writeFileSync(destPath, updated, 'utf8');
|
|
440
|
+
fs.unlinkSync(draftPath);
|
|
441
|
+
|
|
442
|
+
console.log('Published:');
|
|
443
|
+
console.log(` ${destPath}`);
|
|
398
444
|
}
|
|
399
445
|
|
|
400
446
|
if (require.main === module) {
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
447
|
+
process.on('SIGINT', () => {
|
|
448
|
+
console.log('\nCancelled.');
|
|
449
|
+
process.exit(0);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
main().catch((err) => {
|
|
453
|
+
console.error('Error:', err);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
});
|
|
410
456
|
}
|
|
411
457
|
|
|
412
458
|
module.exports = {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
459
|
+
processContent,
|
|
460
|
+
splitFrontMatter,
|
|
461
|
+
ensureLine,
|
|
462
|
+
ensurePlaceholderList,
|
|
463
|
+
getScalarValue,
|
|
464
|
+
removeKey,
|
|
465
|
+
extractListValues,
|
|
466
|
+
hasCategory,
|
|
467
|
+
parseTitleFromFm,
|
|
468
|
+
titleFromBasename,
|
|
469
|
+
slugify,
|
|
470
|
+
stripQuotes,
|
|
471
|
+
chicagoAt8AM,
|
|
472
|
+
formatChicagoDateTitle,
|
|
473
|
+
formatChicagoTime,
|
|
428
474
|
};
|