@chriswiegman/hugo-tools 0.2.0 → 0.2.1

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/src/book.mjs CHANGED
@@ -22,42 +22,44 @@
22
22
  * - This script uses global fetch (Node 18+).
23
23
  */
24
24
 
25
- import fs from "node:fs/promises";
26
- import { existsSync, readFileSync } from "node:fs";
27
- import path from "node:path";
25
+ import fs from 'node:fs/promises';
26
+ import { existsSync, readFileSync } from 'node:fs';
27
+ import path from 'node:path';
28
28
 
29
29
  // -------------------- Config --------------------
30
30
 
31
31
  const PROJECT_ROOT = process.cwd();
32
32
 
33
33
  let _userConfig = {};
34
+
34
35
  try {
35
- const cfgPath = path.join(PROJECT_ROOT, ".hugo-tools.json");
36
- if (existsSync(cfgPath)) {
37
- _userConfig = JSON.parse(readFileSync(cfgPath, "utf8"));
38
- }
36
+ const cfgPath = path.join(PROJECT_ROOT, '.hugo-tools.json');
37
+
38
+ if (existsSync(cfgPath)) {
39
+ _userConfig = JSON.parse(readFileSync(cfgPath, 'utf8'));
40
+ }
39
41
  } catch {} // eslint-disable-line no-empty
40
42
 
41
- const OUTPUT_ROOT = path.join(PROJECT_ROOT, _userConfig.booksDir ?? "content/books");
43
+ const OUTPUT_ROOT = path.join(PROJECT_ROOT, _userConfig.booksDir ?? 'content/books');
42
44
 
43
45
  // -------------------- Utilities --------------------
44
46
 
45
47
  function usageAndExit() {
46
- console.error("Usage: node scripts/book.mjs path/to/goodreads.csv");
47
- process.exit(1);
48
+ console.error('Usage: node scripts/book.mjs path/to/goodreads.csv');
49
+ process.exit(1);
48
50
  }
49
51
 
50
52
  function slugify(s) {
51
- return (
52
- (s || "")
53
- .trim()
54
- .toLowerCase()
55
- .normalize("NFKD")
56
- .replace(/[\u0300-\u036f]/g, "")
57
- .replace(/[^a-z0-9\s-]/g, "")
58
- .replace(/[\s_-]+/g, "-")
59
- .replace(/^-+|-+$/g, "") || "unknown"
60
- );
53
+ return (
54
+ (s || '')
55
+ .trim()
56
+ .toLowerCase()
57
+ .normalize('NFKD')
58
+ .replace(/[\u0300-\u036f]/g, '')
59
+ .replace(/[^a-z0-9\s-]/g, '')
60
+ .replace(/[\s_-]+/g, '-')
61
+ .replace(/^-+|-+$/g, '') || 'unknown'
62
+ );
61
63
  }
62
64
 
63
65
  /**
@@ -65,47 +67,48 @@ function slugify(s) {
65
67
  * Good enough for Goodreads exports.
66
68
  */
67
69
  function parseCSV(text) {
68
- const rows = [];
69
- let row = [];
70
- let field = "";
71
- let inQuotes = false;
72
-
73
- for (let i = 0; i < text.length; i++) {
74
- const c = text[i];
75
- const next = text[i + 1];
76
-
77
- if (inQuotes) {
78
- if (c === '"' && next === '"') {
79
- field += '"';
80
- i++;
81
- } else if (c === '"') {
82
- inQuotes = false;
83
- } else {
84
- field += c;
85
- }
86
- } else {
87
- if (c === '"') inQuotes = true;
88
- else if (c === ",") {
89
- row.push(field);
90
- field = "";
91
- } else if (c === "\r") {
92
- // ignore
93
- } else if (c === "\n") {
94
- row.push(field);
95
- rows.push(row);
96
- row = [];
97
- field = "";
98
- } else {
99
- field += c;
100
- }
101
- }
102
- }
103
-
104
- if (field.length > 0 || row.length > 0) {
105
- row.push(field);
106
- rows.push(row);
107
- }
108
- return rows;
70
+ const rows = [];
71
+ let row = [];
72
+ let field = '';
73
+ let inQuotes = false;
74
+
75
+ for (let i = 0; i < text.length; i++) {
76
+ const c = text[i];
77
+ const next = text[i + 1];
78
+
79
+ if (inQuotes) {
80
+ if (c === '"' && next === '"') {
81
+ field += '"';
82
+ i++;
83
+ } else if (c === '"') {
84
+ inQuotes = false;
85
+ } else {
86
+ field += c;
87
+ }
88
+ } else {
89
+ if (c === '"') inQuotes = true;
90
+ else if (c === ',') {
91
+ row.push(field);
92
+ field = '';
93
+ } else if (c === '\r') {
94
+ // ignore
95
+ } else if (c === '\n') {
96
+ row.push(field);
97
+ rows.push(row);
98
+ row = [];
99
+ field = '';
100
+ } else {
101
+ field += c;
102
+ }
103
+ }
104
+ }
105
+
106
+ if (field.length > 0 || row.length > 0) {
107
+ row.push(field);
108
+ rows.push(row);
109
+ }
110
+
111
+ return rows;
109
112
  }
110
113
 
111
114
  /**
@@ -114,105 +117,123 @@ function parseCSV(text) {
114
117
  * Normalize to digits/X only.
115
118
  */
116
119
  function cleanIsbn(s) {
117
- const raw = String(s || "").trim();
118
- if (!raw) return "";
120
+ const raw = String(s || '').trim();
121
+
122
+ if (!raw) return '';
119
123
 
120
- // strip leading '=' and surrounding quotes
121
- const stripped = raw.replace(/^=+/, "").replace(/^"+|"+$/g, "");
122
- return stripped.replace(/[^0-9Xx]/g, "").toUpperCase();
124
+ // strip leading '=' and surrounding quotes
125
+ const stripped = raw.replace(/^=+/, '').replace(/^"+|"+$/g, '');
126
+
127
+ return stripped.replace(/[^0-9Xx]/g, '').toUpperCase();
123
128
  }
124
129
 
125
130
  function toISODate(dateStr) {
126
- const s = (dateStr || "").trim();
127
- if (!s) return null;
128
-
129
- if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
130
-
131
- // YYYY/MM/DD (your export uses this)
132
- let m = s.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
133
- if (m) {
134
- const year = Number(m[1]);
135
- const month = Number(m[2]);
136
- const day = Number(m[3]);
137
- if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
138
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
139
- }
140
- }
141
-
142
- // M/D/YYYY
143
- m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/);
144
- if (m) {
145
- let month = Number(m[1]);
146
- let day = Number(m[2]);
147
- let year = Number(m[3]);
148
- if (year < 100) year += 2000;
149
- if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
150
- return `${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
151
- }
152
- }
153
-
154
- // last resort
155
- const d = new Date(s);
156
- if (!Number.isNaN(d.getTime())) {
157
- const yyyy = d.getFullYear();
158
- const mm = String(d.getMonth() + 1).padStart(2, "0");
159
- const dd = String(d.getDate()).padStart(2, "0");
160
- return `${yyyy}-${mm}-${dd}`;
161
- }
162
-
163
- return null;
131
+ const s = (dateStr || '').trim();
132
+
133
+ if (!s) return null;
134
+
135
+ if (/^\d{4}-\d{2}-\d{2}$/.test(s)) return s;
136
+
137
+ // YYYY/MM/DD (your export uses this)
138
+ let m = s.match(/^(\d{4})\/(\d{1,2})\/(\d{1,2})$/);
139
+
140
+ if (m) {
141
+ const year = Number(m[1]);
142
+ const month = Number(m[2]);
143
+ const day = Number(m[3]);
144
+
145
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
146
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
147
+ }
148
+ }
149
+
150
+ // M/D/YYYY
151
+ m = s.match(/^(\d{1,2})\/(\d{1,2})\/(\d{2}|\d{4})$/);
152
+
153
+ if (m) {
154
+ const month = Number(m[1]);
155
+ const day = Number(m[2]);
156
+ let year = Number(m[3]);
157
+
158
+ if (year < 100) year += 2000;
159
+
160
+ if (month >= 1 && month <= 12 && day >= 1 && day <= 31) {
161
+ return `${year}-${String(month).padStart(2, '0')}-${String(day).padStart(2, '0')}`;
162
+ }
163
+ }
164
+
165
+ // last resort
166
+ const d = new Date(s);
167
+
168
+ if (!Number.isNaN(d.getTime())) {
169
+ const yyyy = d.getFullYear();
170
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
171
+ const dd = String(d.getDate()).padStart(2, '0');
172
+
173
+ return `${yyyy}-${mm}-${dd}`;
174
+ }
175
+
176
+ return null;
164
177
  }
165
178
 
166
179
  function clampRating(n) {
167
- if (!Number.isFinite(n)) return 0;
168
- if (n < 0) return 0;
169
- if (n > 5) return 5;
170
- return Math.trunc(n);
180
+ if (!Number.isFinite(n)) return 0;
181
+
182
+ if (n < 0) return 0;
183
+
184
+ if (n > 5) return 5;
185
+
186
+ return Math.trunc(n);
171
187
  }
172
188
 
173
189
  function escapeYAMLString(s) {
174
- return String(s ?? "").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
190
+ return String(s ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
175
191
  }
176
192
 
177
193
  function uniqSortedDates(dates) {
178
- const set = new Set((dates || []).map((d) => String(d).trim()).filter(Boolean));
179
- return Array.from(set).sort(); // YYYY-MM-DD sorts lexicographically
194
+ const set = new Set((dates || []).map((d) => String(d).trim()).filter(Boolean));
195
+
196
+ return Array.from(set).sort(); // YYYY-MM-DD sorts lexicographically
180
197
  }
181
198
 
182
199
  function keyFor(title, author) {
183
- return `${title.trim().toLowerCase()}|${author.trim().toLowerCase()}`;
200
+ return `${title.trim().toLowerCase()}|${author.trim().toLowerCase()}`;
184
201
  }
185
202
 
186
203
  function goodreadsBookUrl(bookId) {
187
- return bookId ? `https://www.goodreads.com/book/show/${bookId}` : "";
204
+ return bookId ? `https://www.goodreads.com/book/show/${bookId}` : '';
188
205
  }
189
206
 
190
207
  function openLibraryIsbnUrl(isbn) {
191
- return isbn ? `https://openlibrary.org/search?isbn=${encodeURIComponent(isbn)}` : "";
208
+ return isbn ? `https://openlibrary.org/search?isbn=${encodeURIComponent(isbn)}` : '';
192
209
  }
193
210
 
194
211
  function amazonSearchLink({ isbn13, isbn10, title, author }) {
195
- // Search by ISBN13 (most specific) > ISBN10 > title+author — search pages never 404.
196
- const query = isbn13 || isbn10 || `${title} ${author}`;
197
- return `https://www.amazon.com/s?k=${encodeURIComponent(query)}`;
212
+ // Search by ISBN13 (most specific) > ISBN10 > title+author — search pages never 404.
213
+ const query = isbn13 || isbn10 || `${title} ${author}`;
214
+
215
+ return `https://www.amazon.com/s?k=${encodeURIComponent(query)}`;
198
216
  }
199
217
 
200
218
  /**
201
219
  * Use "Author l-f" (e.g. "Baldacci, David") → folder "baldacci-david"
202
220
  */
203
221
  function authorDirFromAuthorLF(authorLF) {
204
- const a = (authorLF || "").trim();
205
- if (!a) return "unknown";
206
-
207
- if (a.includes(",")) {
208
- const [last, first] = a.split(",", 2).map((x) => (x || "").trim());
209
- const lastSlug = slugify(last);
210
- const firstSlug = slugify(first);
211
- if (lastSlug && firstSlug && firstSlug !== "unknown") return `${lastSlug}-${firstSlug}`;
212
- return lastSlug || firstSlug || "unknown";
213
- }
214
-
215
- return slugify(a);
222
+ const a = (authorLF || '').trim();
223
+
224
+ if (!a) return 'unknown';
225
+
226
+ if (a.includes(',')) {
227
+ const [last, first] = a.split(',', 2).map((x) => (x || '').trim());
228
+ const lastSlug = slugify(last);
229
+ const firstSlug = slugify(first);
230
+
231
+ if (lastSlug && firstSlug && firstSlug !== 'unknown') return `${lastSlug}-${firstSlug}`;
232
+
233
+ return lastSlug || firstSlug || 'unknown';
234
+ }
235
+
236
+ return slugify(a);
216
237
  }
217
238
 
218
239
  /**
@@ -225,45 +246,50 @@ function authorDirFromAuthorLF(authorLF) {
225
246
  * - finished: "YYYY-MM-DD" (legacy scalar; included as one date)
226
247
  */
227
248
  function parseExistingMarkdown(md) {
228
- const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
229
- if (!m) return { finished: [] };
230
-
231
- const fm = m[1];
232
- const lines = fm.split("\n");
233
- const finished = [];
234
-
235
- // list style
236
- for (let i = 0; i < lines.length; i++) {
237
- if (lines[i].trim() === "finished:") {
238
- for (let j = i + 1; j < lines.length; j++) {
239
- const line = lines[j];
240
- if (/^[A-Za-z0-9_-]+:/.test(line)) break; // new top-level key
241
- const mm = line.match(/^\s*-\s*"?(\d{4}-\d{2}-\d{2})"?\s*$/);
242
- if (mm) finished.push(mm[1]);
243
- }
244
- break;
245
- }
246
- }
247
-
248
- // scalar legacy style
249
- for (const line of lines) {
250
- const mm = line.match(/^finished:\s*"?(\d{4}-\d{2}-\d{2})"?\s*$/);
251
- if (mm) {
252
- finished.push(mm[1]);
253
- break;
254
- }
255
- }
256
-
257
- return { finished: uniqSortedDates(finished) };
249
+ const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
250
+
251
+ if (!m) return { finished: [] };
252
+
253
+ const fm = m[1];
254
+ const lines = fm.split('\n');
255
+ const finished = [];
256
+
257
+ // list style
258
+ for (let i = 0; i < lines.length; i++) {
259
+ if (lines[i].trim() === 'finished:') {
260
+ for (let j = i + 1; j < lines.length; j++) {
261
+ const line = lines[j];
262
+
263
+ if (/^[A-Za-z0-9_-]+:/.test(line)) break; // new top-level key
264
+
265
+ const mm = line.match(/^\s*-\s*"?(\d{4}-\d{2}-\d{2})"?\s*$/);
266
+
267
+ if (mm) finished.push(mm[1]);
268
+ }
269
+ break;
270
+ }
271
+ }
272
+
273
+ // scalar legacy style
274
+ for (const line of lines) {
275
+ const mm = line.match(/^finished:\s*"?(\d{4}-\d{2}-\d{2})"?\s*$/);
276
+
277
+ if (mm) {
278
+ finished.push(mm[1]);
279
+ break;
280
+ }
281
+ }
282
+
283
+ return { finished: uniqSortedDates(finished) };
258
284
  }
259
285
 
260
286
  async function fileExists(p) {
261
- try {
262
- await fs.access(p);
263
- return true;
264
- } catch {
265
- return false;
266
- }
287
+ try {
288
+ await fs.access(p);
289
+ return true;
290
+ } catch {
291
+ return false;
292
+ }
267
293
  }
268
294
 
269
295
  // -------------------- Patch finished block in existing file --------------------
@@ -273,37 +299,39 @@ async function fileExists(p) {
273
299
  * Leaves all other front matter (title, author, rating, links, body) untouched.
274
300
  */
275
301
  function replaceFinishedBlock(md, newFinished) {
276
- const block = ["finished:", ...newFinished.map((d) => ` - "${d}"`)];
277
- const lines = md.split("\n");
278
-
279
- // Find the "finished:" line
280
- const finishedIdx = lines.findIndex((l) => /^finished:\s*$/.test(l));
281
-
282
- if (finishedIdx !== -1) {
283
- // List form: "finished:\n - ...\n - ..."
284
- // Find where the list ends (first line that doesn't start with whitespace, after the key)
285
- let endIdx = finishedIdx + 1;
286
- while (endIdx < lines.length && /^\s/.test(lines[endIdx])) {
287
- endIdx++;
288
- }
289
- return [...lines.slice(0, finishedIdx), ...block, ...lines.slice(endIdx)].join("\n");
290
- }
291
-
292
- // Scalar form: 'finished: "YYYY-MM-DD"' or 'finished: YYYY-MM-DD'
293
- const scalarIdx = lines.findIndex((l) => /^finished:\s+"?\d{4}-\d{2}-\d{2}"?\s*$/.test(l));
294
- if (scalarIdx !== -1) {
295
- return [...lines.slice(0, scalarIdx), ...block, ...lines.slice(scalarIdx + 1)].join("\n");
296
- }
297
-
298
- console.warn(" WARNING: could not locate 'finished:' block — file left unchanged");
299
- return md;
302
+ const block = ['finished:', ...newFinished.map((d) => ` - "${d}"`)];
303
+ const lines = md.split('\n');
304
+
305
+ // Find the "finished:" line
306
+ const finishedIdx = lines.findIndex((l) => /^finished:\s*$/.test(l));
307
+
308
+ if (finishedIdx !== -1) {
309
+ // List form: "finished:\n - ...\n - ..."
310
+ // Find where the list ends (first line that doesn't start with whitespace, after the key)
311
+ let endIdx = finishedIdx + 1;
312
+
313
+ while (endIdx < lines.length && /^\s/.test(lines[endIdx])) {
314
+ endIdx++;
315
+ }
316
+ return [...lines.slice(0, finishedIdx), ...block, ...lines.slice(endIdx)].join('\n');
317
+ }
318
+
319
+ // Scalar form: 'finished: "YYYY-MM-DD"' or 'finished: YYYY-MM-DD'
320
+ const scalarIdx = lines.findIndex((l) => /^finished:\s+"?\d{4}-\d{2}-\d{2}"?\s*$/.test(l));
321
+
322
+ if (scalarIdx !== -1) {
323
+ return [...lines.slice(0, scalarIdx), ...block, ...lines.slice(scalarIdx + 1)].join('\n');
324
+ }
325
+
326
+ console.warn(' WARNING: could not locate \'finished:\' block — file left unchanged');
327
+ return md;
300
328
  }
301
329
 
302
330
  /**
303
331
  * Update the title field in front matter, leaving everything else untouched.
304
332
  */
305
333
  function updateTitleInFrontMatter(md, newTitle) {
306
- return md.replace(/^title:.*$/m, `title: "${escapeYAMLString(newTitle)}"`);
334
+ return md.replace(/^title:.*$/m, `title: "${escapeYAMLString(newTitle)}"`);
307
335
  }
308
336
 
309
337
  // -------------------- Existing-file index --------------------
@@ -313,8 +341,9 @@ function updateTitleInFrontMatter(md, newTitle) {
313
341
  * Matches: goodreads: "https://www.goodreads.com/book/show/12345678"
314
342
  */
315
343
  function extractGoodreadsId(md) {
316
- const m = md.match(/goodreads:\s*"https:\/\/www\.goodreads\.com\/book\/show\/(\d+)"/);
317
- return m ? m[1] : null;
344
+ const m = md.match(/goodreads:\s*"https:\/\/www\.goodreads\.com\/book\/show\/(\d+)"/);
345
+
346
+ return m ? m[1] : null;
318
347
  }
319
348
 
320
349
  /**
@@ -322,17 +351,19 @@ function extractGoodreadsId(md) {
322
351
  * Checks the openlibrary URL (reliable) and the amazon URL (only when it looks like a bare ISBN).
323
352
  */
324
353
  function extractFileIsbns(md) {
325
- const isbns = new Set();
354
+ const isbns = new Set();
355
+
356
+ // openlibrary: "https://openlibrary.org/search?isbn=9780062694430"
357
+ const ol = md.match(/openlibrary:\s*"https:\/\/openlibrary\.org\/search\?isbn=([0-9X]+)"/i);
358
+
359
+ if (ol) isbns.add(ol[1].toUpperCase());
326
360
 
327
- // openlibrary: "https://openlibrary.org/search?isbn=9780062694430"
328
- const ol = md.match(/openlibrary:\s*"https:\/\/openlibrary\.org\/search\?isbn=([0-9X]+)"/i);
329
- if (ol) isbns.add(ol[1].toUpperCase());
361
+ // amazon: "https://www.amazon.com/s?k=9780062694430" — only when the query is a bare ISBN
362
+ const am = md.match(/amazon:\s*"https:\/\/www\.amazon\.com\/s\?k=([0-9X]{10,13})"/i);
330
363
 
331
- // amazon: "https://www.amazon.com/s?k=9780062694430" — only when the query is a bare ISBN
332
- const am = md.match(/amazon:\s*"https:\/\/www\.amazon\.com\/s\?k=([0-9X]{10,13})"/i);
333
- if (am) isbns.add(am[1].toUpperCase());
364
+ if (am) isbns.add(am[1].toUpperCase());
334
365
 
335
- return Array.from(isbns);
366
+ return Array.from(isbns);
336
367
  }
337
368
 
338
369
  /**
@@ -340,50 +371,56 @@ function extractFileIsbns(md) {
340
371
  * These are used to match incoming CSV rows even when the title (and thus slug) has changed.
341
372
  */
342
373
  async function buildExistingIndex(outputRoot) {
343
- const byGoodreadsId = new Map(); // bookId → absolute filePath
344
- const byIsbn = new Map(); // isbn (normalized) → absolute filePath
345
-
346
- let dirEntries;
347
- try {
348
- dirEntries = await fs.readdir(outputRoot, { withFileTypes: true });
349
- } catch {
350
- return { byGoodreadsId, byIsbn };
351
- }
352
-
353
- for (const entry of dirEntries) {
354
- if (!entry.isDirectory()) continue;
355
- const dirPath = path.join(outputRoot, entry.name);
356
-
357
- let fileEntries;
358
- try {
359
- fileEntries = await fs.readdir(dirPath, { withFileTypes: true });
360
- } catch {
361
- continue;
362
- }
363
-
364
- for (const fileEntry of fileEntries) {
365
- if (!fileEntry.isFile() || !fileEntry.name.endsWith(".md")) continue;
366
- const filePath = path.join(dirPath, fileEntry.name);
367
-
368
- let content;
369
- try {
370
- content = await fs.readFile(filePath, "utf8");
371
- } catch {
372
- continue;
373
- }
374
-
375
- const gid = extractGoodreadsId(content);
376
- if (gid && !byGoodreadsId.has(gid)) {
377
- byGoodreadsId.set(gid, filePath);
378
- }
379
-
380
- for (const isbn of extractFileIsbns(content)) {
381
- if (!byIsbn.has(isbn)) byIsbn.set(isbn, filePath);
382
- }
383
- }
384
- }
385
-
386
- return { byGoodreadsId, byIsbn };
374
+ const byGoodreadsId = new Map(); // bookId → absolute filePath
375
+ const byIsbn = new Map(); // isbn (normalized) → absolute filePath
376
+
377
+ let dirEntries;
378
+
379
+ try {
380
+ dirEntries = await fs.readdir(outputRoot, { withFileTypes: true });
381
+ } catch {
382
+ return { byGoodreadsId, byIsbn };
383
+ }
384
+
385
+ for (const entry of dirEntries) {
386
+ if (!entry.isDirectory()) continue;
387
+
388
+ const dirPath = path.join(outputRoot, entry.name);
389
+
390
+ let fileEntries;
391
+
392
+ try {
393
+ fileEntries = await fs.readdir(dirPath, { withFileTypes: true });
394
+ } catch {
395
+ continue;
396
+ }
397
+
398
+ for (const fileEntry of fileEntries) {
399
+ if (!fileEntry.isFile() || !fileEntry.name.endsWith('.md')) continue;
400
+
401
+ const filePath = path.join(dirPath, fileEntry.name);
402
+
403
+ let content;
404
+
405
+ try {
406
+ content = await fs.readFile(filePath, 'utf8');
407
+ } catch {
408
+ continue;
409
+ }
410
+
411
+ const gid = extractGoodreadsId(content);
412
+
413
+ if (gid && !byGoodreadsId.has(gid)) {
414
+ byGoodreadsId.set(gid, filePath);
415
+ }
416
+
417
+ for (const isbn of extractFileIsbns(content)) {
418
+ if (!byIsbn.has(isbn)) byIsbn.set(isbn, filePath);
419
+ }
420
+ }
421
+ }
422
+
423
+ return { byGoodreadsId, byIsbn };
387
424
  }
388
425
 
389
426
  /**
@@ -391,335 +428,359 @@ async function buildExistingIndex(outputRoot) {
391
428
  * Returns the absolute path if found, or null.
392
429
  */
393
430
  async function findExistingFile(b, outPath, index) {
394
- if (b.bookId && index.byGoodreadsId.has(b.bookId)) {
395
- return index.byGoodreadsId.get(b.bookId);
396
- }
397
- if (b.isbn13 && index.byIsbn.has(b.isbn13)) {
398
- return index.byIsbn.get(b.isbn13);
399
- }
400
- if (b.isbn10 && index.byIsbn.has(b.isbn10)) {
401
- return index.byIsbn.get(b.isbn10);
402
- }
403
- if (await fileExists(outPath)) {
404
- return outPath;
405
- }
406
- return null;
431
+ if (b.bookId && index.byGoodreadsId.has(b.bookId)) {
432
+ return index.byGoodreadsId.get(b.bookId);
433
+ }
434
+
435
+ if (b.isbn13 && index.byIsbn.has(b.isbn13)) {
436
+ return index.byIsbn.get(b.isbn13);
437
+ }
438
+
439
+ if (b.isbn10 && index.byIsbn.has(b.isbn10)) {
440
+ return index.byIsbn.get(b.isbn10);
441
+ }
442
+
443
+ if (await fileExists(outPath)) {
444
+ return outPath;
445
+ }
446
+
447
+ return null;
407
448
  }
408
449
 
409
450
  /**
410
451
  * Remove a directory if it is empty (best-effort; ignores errors).
411
452
  */
412
453
  async function removeIfEmpty(dirPath) {
413
- try {
414
- const entries = await fs.readdir(dirPath);
415
- if (entries.length === 0) await fs.rmdir(dirPath);
416
- } catch {
417
- // ignore
418
- }
454
+ try {
455
+ const entries = await fs.readdir(dirPath);
456
+
457
+ if (entries.length === 0) await fs.rmdir(dirPath);
458
+ } catch {
459
+ // ignore
460
+ }
419
461
  }
420
462
 
421
463
  // -------------------- Writing front matter --------------------
422
464
 
423
465
  function toFrontMatter({ title, author, rating, finished, links }) {
424
- const lines = [];
425
- lines.push("---");
426
- lines.push(`title: "${escapeYAMLString(title)}"`);
427
- lines.push(`author: "${escapeYAMLString(author)}"`);
428
- lines.push(`rating: ${rating}`);
429
- lines.push("finished:");
430
- for (const d of finished) lines.push(` - "${escapeYAMLString(d)}"`);
431
-
432
- lines.push("links:");
433
- lines.push(` amazon: "${escapeYAMLString(links.amazon)}"`);
434
- lines.push(` openlibrary: "${escapeYAMLString(links.openlibrary)}"`);
435
- lines.push(` goodreads: "${escapeYAMLString(links.goodreads)}"`);
436
- lines.push("---");
437
- return lines.join("\n");
466
+ const lines = [];
467
+
468
+ lines.push('---');
469
+ lines.push(`title: "${escapeYAMLString(title)}"`);
470
+ lines.push(`author: "${escapeYAMLString(author)}"`);
471
+ lines.push(`rating: ${rating}`);
472
+ lines.push('finished:');
473
+ for (const d of finished) lines.push(` - "${escapeYAMLString(d)}"`);
474
+
475
+ lines.push('links:');
476
+ lines.push(` amazon: "${escapeYAMLString(links.amazon)}"`);
477
+ lines.push(` openlibrary: "${escapeYAMLString(links.openlibrary)}"`);
478
+ lines.push(` goodreads: "${escapeYAMLString(links.goodreads)}"`);
479
+ lines.push('---');
480
+ return lines.join('\n');
438
481
  }
439
482
 
440
483
  // -------------------- Main --------------------
441
484
 
442
485
  const HUGO_CONFIGS = [
443
- 'hugo.toml', 'hugo.yaml', 'hugo.yml', 'hugo.json',
444
- 'config.toml', 'config.yaml', 'config.yml', 'config.json',
445
- 'config',
486
+ 'hugo.toml', 'hugo.yaml', 'hugo.yml', 'hugo.json',
487
+ 'config.toml', 'config.yaml', 'config.yml', 'config.json',
488
+ 'config',
446
489
  ];
447
490
 
448
491
  async function main() {
449
- if (!HUGO_CONFIGS.some(name => existsSync(path.join(PROJECT_ROOT, name)))) {
450
- process.stderr.write('Error: no Hugo config file found. Run this command from the root of your Hugo site.\n');
451
- process.exit(1);
452
- }
453
-
454
- const csvPath = process.argv[2];
455
- if (!csvPath) usageAndExit();
456
-
457
- const csvText = await fs.readFile(csvPath, "utf8");
458
- const rows = parseCSV(csvText);
459
-
460
- if (rows.length < 2) {
461
- console.error("CSV appears empty or missing header row.");
462
- process.exit(1);
463
- }
464
-
465
- const header = rows[0].map((h) => (h ?? "").trim());
466
- const idx = (name) => header.indexOf(name);
467
-
468
- // Columns from your sample
469
- const iBookId = idx("Book Id");
470
- const iTitle = idx("Title");
471
- const iAuthor = idx("Author");
472
- const iAuthorLF = idx("Author l-f");
473
- const iIsbn = idx("ISBN");
474
- const iIsbn13 = idx("ISBN13");
475
- const iRating = idx("My Rating");
476
- const iDateRead = idx("Date Read");
477
- const iDateAdded = idx("Date Added");
478
- const iShelf = idx("Exclusive Shelf");
479
- const iReview = idx("My Review");
480
- const iReadCount = idx("Read Count");
481
-
482
- const required = [
483
- ["Book Id", iBookId],
484
- ["Title", iTitle],
485
- ["Author", iAuthor],
486
- ["Author l-f", iAuthorLF],
487
- ["ISBN", iIsbn],
488
- ["ISBN13", iIsbn13],
489
- ["My Rating", iRating],
490
- ["Date Read", iDateRead],
491
- ["Date Added", iDateAdded],
492
- ["Exclusive Shelf", iShelf],
493
- ["My Review", iReview],
494
- ["Read Count", iReadCount],
495
- ];
496
- const missing = required.filter(([, i]) => i === -1).map(([n]) => n);
497
- if (missing.length) {
498
- console.error(`CSV is missing expected columns: ${missing.join(", ")}`);
499
- process.exit(1);
500
- }
501
-
502
- // Aggregate by Title+Author, collecting finished dates from CSV rows
503
- const books = new Map();
504
-
505
- let seenRows = 0;
506
- let importedRows = 0;
507
-
508
- for (let r = 1; r < rows.length; r++) {
509
- seenRows++;
510
- const row = rows[r];
511
-
512
- const bookId = String(row[iBookId] || "").trim();
513
- const title = String(row[iTitle] || "").trim();
514
- const author = String(row[iAuthor] || "").trim();
515
- const authorLF = String(row[iAuthorLF] || "").trim();
516
- if (!title || !author) continue;
517
-
518
- // Only import read shelf so Date Added fallback doesn't imply "finished" for to-read/currently-reading
519
- const shelf = String(row[iShelf] || "").trim().toLowerCase();
520
- if (shelf !== "read") continue;
521
-
522
- importedRows++;
523
-
524
- const rating = clampRating(Number(String(row[iRating] || "").trim() || 0));
525
- const review = String(row[iReview] || "").trim();
526
- const readCount = Number(String(row[iReadCount] || "").trim() || 0);
527
-
528
- const isbn10 = cleanIsbn(row[iIsbn]);
529
- const isbn13 = cleanIsbn(row[iIsbn13]);
530
-
531
- const dateReadISO = toISODate(String(row[iDateRead] || ""));
532
- const dateAddedISO = toISODate(String(row[iDateAdded] || ""));
533
- const chosen = dateReadISO || dateAddedISO || null;
534
-
535
- const k = keyFor(title, author);
536
- if (!books.has(k)) {
537
- books.set(k, {
538
- bookId,
539
- title,
540
- author,
541
- authorLF,
542
- rating,
543
- isbn10,
544
- isbn13,
545
- readCount,
546
- finished: [],
547
- review: review || "",
548
- });
549
- }
550
-
551
- const b = books.get(k);
552
-
553
- // Keep "best" metadata across duplicate rows
554
- if (rating > b.rating) b.rating = rating;
555
- if (!b.review && review) b.review = review;
556
- if (!b.isbn13 && isbn13) b.isbn13 = isbn13;
557
- if (!b.isbn10 && isbn10) b.isbn10 = isbn10;
558
- if (!b.bookId && bookId) b.bookId = bookId;
559
- if (chosen) b.finished.push(chosen);
560
- if (Number.isFinite(readCount) && readCount > (b.readCount || 0)) b.readCount = readCount;
561
- }
562
-
563
- const bookList = Array.from(books.values());
564
-
565
- console.log(
566
- `Parsed rows: ${seenRows}, importing read-shelf rows: ${importedRows}, unique books: ${bookList.length}`
567
- );
568
-
569
- // Build index of existing files for smart matching (by Goodreads ID and ISBN)
570
- console.log("Indexing existing book files...");
571
- const index = await buildExistingIndex(OUTPUT_ROOT);
572
- console.log(
573
- ` Indexed ${index.byGoodreadsId.size} Goodreads IDs, ${index.byIsbn.size} ISBNs`
574
- );
575
-
576
- // ---- Write markdown ----
577
- // - New files: write everything from CSV data.
578
- // - Existing files matched by Goodreads ID or ISBN: if the title/slug changed, rename
579
- // the file and update the title in front matter; always merge new finished dates.
580
- // - Existing files with no changes: skip.
581
- let written = 0;
582
- let createdNew = 0;
583
- let mergedExisting = 0;
584
- let renamedFiles = 0;
585
- let skippedUnchanged = 0;
586
-
587
- for (const b of bookList) {
588
- const authorDir = authorDirFromAuthorLF(b.authorLF);
589
- const filename = `${slugify(b.title)}.md`;
590
- const outPath = path.join(OUTPUT_ROOT, authorDir, filename);
591
-
592
- const existingPath = await findExistingFile(b, outPath, index);
593
-
594
- if (existingPath) {
595
- const titleChanged = existingPath !== outPath;
596
-
597
- // Read the existing file
598
- const existingContent = await fs.readFile(existingPath, "utf8");
599
- const { finished: existingFinished } = parseExistingMarkdown(existingContent);
600
- const finishedMerged = uniqSortedDates([...existingFinished, ...b.finished]);
601
-
602
- const hasNewDates =
603
- finishedMerged.length !== existingFinished.length ||
492
+ if (!HUGO_CONFIGS.some((name) => existsSync(path.join(PROJECT_ROOT, name)))) {
493
+ process.stderr.write('Error: no Hugo config file found. Run this command from the root of your Hugo site.\n');
494
+ process.exit(1);
495
+ }
496
+
497
+ const csvPath = process.argv[2];
498
+
499
+ if (!csvPath) usageAndExit();
500
+
501
+ const csvText = await fs.readFile(csvPath, 'utf8');
502
+ const rows = parseCSV(csvText);
503
+
504
+ if (rows.length < 2) {
505
+ console.error('CSV appears empty or missing header row.');
506
+ process.exit(1);
507
+ }
508
+
509
+ const header = rows[0].map((h) => (h ?? '').trim());
510
+ const idx = (name) => header.indexOf(name);
511
+
512
+ // Columns from your sample
513
+ const iBookId = idx('Book Id');
514
+ const iTitle = idx('Title');
515
+ const iAuthor = idx('Author');
516
+ const iAuthorLF = idx('Author l-f');
517
+ const iIsbn = idx('ISBN');
518
+ const iIsbn13 = idx('ISBN13');
519
+ const iRating = idx('My Rating');
520
+ const iDateRead = idx('Date Read');
521
+ const iDateAdded = idx('Date Added');
522
+ const iShelf = idx('Exclusive Shelf');
523
+ const iReview = idx('My Review');
524
+ const iReadCount = idx('Read Count');
525
+
526
+ const required = [
527
+ ['Book Id', iBookId],
528
+ ['Title', iTitle],
529
+ ['Author', iAuthor],
530
+ ['Author l-f', iAuthorLF],
531
+ ['ISBN', iIsbn],
532
+ ['ISBN13', iIsbn13],
533
+ ['My Rating', iRating],
534
+ ['Date Read', iDateRead],
535
+ ['Date Added', iDateAdded],
536
+ ['Exclusive Shelf', iShelf],
537
+ ['My Review', iReview],
538
+ ['Read Count', iReadCount],
539
+ ];
540
+ const missing = required.filter(([, i]) => i === -1).map(([n]) => n);
541
+
542
+ if (missing.length) {
543
+ console.error(`CSV is missing expected columns: ${missing.join(', ')}`);
544
+ process.exit(1);
545
+ }
546
+
547
+ // Aggregate by Title+Author, collecting finished dates from CSV rows
548
+ const books = new Map();
549
+
550
+ let seenRows = 0;
551
+ let importedRows = 0;
552
+
553
+ for (let r = 1; r < rows.length; r++) {
554
+ seenRows++;
555
+ const row = rows[r];
556
+
557
+ const bookId = String(row[iBookId] || '').trim();
558
+ const title = String(row[iTitle] || '').trim();
559
+ const author = String(row[iAuthor] || '').trim();
560
+ const authorLF = String(row[iAuthorLF] || '').trim();
561
+
562
+ if (!title || !author) continue;
563
+
564
+ // Only import read shelf so Date Added fallback doesn't imply "finished" for to-read/currently-reading
565
+ const shelf = String(row[iShelf] || '').trim().toLowerCase();
566
+
567
+ if (shelf !== 'read') continue;
568
+
569
+ importedRows++;
570
+
571
+ const rating = clampRating(Number(String(row[iRating] || '').trim() || 0));
572
+ const review = String(row[iReview] || '').trim();
573
+ const readCount = Number(String(row[iReadCount] || '').trim() || 0);
574
+
575
+ const isbn10 = cleanIsbn(row[iIsbn]);
576
+ const isbn13 = cleanIsbn(row[iIsbn13]);
577
+
578
+ const dateReadISO = toISODate(String(row[iDateRead] || ''));
579
+ const dateAddedISO = toISODate(String(row[iDateAdded] || ''));
580
+ const chosen = dateReadISO || dateAddedISO || null;
581
+
582
+ const k = keyFor(title, author);
583
+
584
+ if (!books.has(k)) {
585
+ books.set(k, {
586
+ bookId,
587
+ title,
588
+ author,
589
+ authorLF,
590
+ rating,
591
+ isbn10,
592
+ isbn13,
593
+ readCount,
594
+ finished: [],
595
+ review: review || '',
596
+ });
597
+ }
598
+
599
+ const b = books.get(k);
600
+
601
+ // Keep "best" metadata across duplicate rows
602
+ if (rating > b.rating) b.rating = rating;
603
+
604
+ if (!b.review && review) b.review = review;
605
+
606
+ if (!b.isbn13 && isbn13) b.isbn13 = isbn13;
607
+
608
+ if (!b.isbn10 && isbn10) b.isbn10 = isbn10;
609
+
610
+ if (!b.bookId && bookId) b.bookId = bookId;
611
+
612
+ if (chosen) b.finished.push(chosen);
613
+
614
+ if (Number.isFinite(readCount) && readCount > (b.readCount || 0)) b.readCount = readCount;
615
+ }
616
+
617
+ const bookList = Array.from(books.values());
618
+
619
+ console.log(
620
+ `Parsed rows: ${seenRows}, importing read-shelf rows: ${importedRows}, unique books: ${bookList.length}`,
621
+ );
622
+
623
+ // Build index of existing files for smart matching (by Goodreads ID and ISBN)
624
+ console.log('Indexing existing book files...');
625
+ const index = await buildExistingIndex(OUTPUT_ROOT);
626
+
627
+ console.log(
628
+ ` Indexed ${index.byGoodreadsId.size} Goodreads IDs, ${index.byIsbn.size} ISBNs`,
629
+ );
630
+
631
+ // ---- Write markdown ----
632
+ // - New files: write everything from CSV data.
633
+ // - Existing files matched by Goodreads ID or ISBN: if the title/slug changed, rename
634
+ // the file and update the title in front matter; always merge new finished dates.
635
+ // - Existing files with no changes: skip.
636
+ let written = 0;
637
+ let createdNew = 0;
638
+ let mergedExisting = 0;
639
+ let renamedFiles = 0;
640
+ let skippedUnchanged = 0;
641
+
642
+ for (const b of bookList) {
643
+ const authorDir = authorDirFromAuthorLF(b.authorLF);
644
+ const filename = `${slugify(b.title)}.md`;
645
+ const outPath = path.join(OUTPUT_ROOT, authorDir, filename);
646
+
647
+ const existingPath = await findExistingFile(b, outPath, index);
648
+
649
+ if (existingPath) {
650
+ const titleChanged = existingPath !== outPath;
651
+
652
+ // Read the existing file
653
+ const existingContent = await fs.readFile(existingPath, 'utf8');
654
+ const { finished: existingFinished } = parseExistingMarkdown(existingContent);
655
+ const finishedMerged = uniqSortedDates([...existingFinished, ...b.finished]);
656
+
657
+ const hasNewDates =
658
+ finishedMerged.length !== existingFinished.length ||
604
659
  finishedMerged.some((d, i) => d !== existingFinished[i]);
605
660
 
606
- if (!titleChanged && !hasNewDates) {
607
- skippedUnchanged++;
608
- continue;
609
- }
610
-
611
- // Apply patches
612
- let patched = existingContent;
613
- if (hasNewDates) {
614
- patched = replaceFinishedBlock(patched, finishedMerged);
615
- }
616
- if (titleChanged) {
617
- patched = updateTitleInFrontMatter(patched, b.title);
618
- }
619
-
620
- if (titleChanged) {
621
- // Write to new path, remove old path, clean up empty author dir
622
- await fs.mkdir(path.dirname(outPath), { recursive: true });
623
- await fs.writeFile(outPath, patched, "utf8");
624
- await fs.unlink(existingPath);
625
- await removeIfEmpty(path.dirname(existingPath));
626
-
627
- const relOld = path.relative(OUTPUT_ROOT, existingPath);
628
- const relNew = path.relative(OUTPUT_ROOT, outPath);
629
- console.log(` RENAMED: ${relOld}`);
630
- console.log(` --> ${relNew}`);
631
- renamedFiles++;
632
- } else {
633
- await fs.writeFile(existingPath, patched, "utf8");
634
- }
635
-
636
- written++;
637
- mergedExisting++;
638
- } else {
639
- // New file — write full content from CSV data
640
- createdNew++;
641
-
642
- const finishedMerged = uniqSortedDates(b.finished);
643
- const isbnBest = b.isbn13 || b.isbn10;
644
-
645
- const links = {
646
- amazon: amazonSearchLink({ isbn13: b.isbn13, isbn10: b.isbn10, title: b.title, author: b.author }),
647
- openlibrary: openLibraryIsbnUrl(isbnBest),
648
- goodreads: goodreadsBookUrl(b.bookId),
649
- };
650
-
651
- const fm = toFrontMatter({
652
- title: b.title,
653
- author: b.author,
654
- rating: b.rating,
655
- finished: finishedMerged,
656
- links,
657
- });
658
-
659
- const bodyToWrite = b.review ? `\n\n${b.review}\n` : "\n";
660
-
661
- await fs.mkdir(path.dirname(outPath), { recursive: true });
662
- await fs.writeFile(outPath, fm + bodyToWrite, "utf8");
663
- written++;
664
- }
665
- }
666
-
667
- if (written > 0) {
668
- const libraryPath = path.join(PROJECT_ROOT, "content", "library.md");
669
- try {
670
- const libraryContent = await fs.readFile(libraryPath, "utf8");
671
- const now = new Date().toISOString().replace(/\.\d{3}Z$/, "+00:00");
672
- const updated = libraryContent.replace(
673
- /^date:.*$/m,
674
- `date: ${now}`
675
- );
676
- await fs.writeFile(libraryPath, updated, "utf8");
677
- console.log(` Updated library.md date to ${now}`);
678
- } catch (err) {
679
- console.warn(` WARNING: could not update library.md: ${err.message}`);
680
- }
681
- }
682
-
683
- console.log(
684
- `Done.\n` +
661
+ if (!titleChanged && !hasNewDates) {
662
+ skippedUnchanged++;
663
+ continue;
664
+ }
665
+
666
+ // Apply patches
667
+ let patched = existingContent;
668
+
669
+ if (hasNewDates) {
670
+ patched = replaceFinishedBlock(patched, finishedMerged);
671
+ }
672
+
673
+ if (titleChanged) {
674
+ patched = updateTitleInFrontMatter(patched, b.title);
675
+ }
676
+
677
+ if (titleChanged) {
678
+ // Write to new path, remove old path, clean up empty author dir
679
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
680
+ await fs.writeFile(outPath, patched, 'utf8');
681
+ await fs.unlink(existingPath);
682
+ await removeIfEmpty(path.dirname(existingPath));
683
+
684
+ const relOld = path.relative(OUTPUT_ROOT, existingPath);
685
+ const relNew = path.relative(OUTPUT_ROOT, outPath);
686
+
687
+ console.log(` RENAMED: ${relOld}`);
688
+ console.log(` --> ${relNew}`);
689
+ renamedFiles++;
690
+ } else {
691
+ await fs.writeFile(existingPath, patched, 'utf8');
692
+ }
693
+
694
+ written++;
695
+ mergedExisting++;
696
+ } else {
697
+ // New file — write full content from CSV data
698
+ createdNew++;
699
+
700
+ const finishedMerged = uniqSortedDates(b.finished);
701
+ const isbnBest = b.isbn13 || b.isbn10;
702
+
703
+ const links = {
704
+ amazon: amazonSearchLink({ isbn13: b.isbn13, isbn10: b.isbn10, title: b.title, author: b.author }),
705
+ openlibrary: openLibraryIsbnUrl(isbnBest),
706
+ goodreads: goodreadsBookUrl(b.bookId),
707
+ };
708
+
709
+ const fm = toFrontMatter({
710
+ title: b.title,
711
+ author: b.author,
712
+ rating: b.rating,
713
+ finished: finishedMerged,
714
+ links,
715
+ });
716
+
717
+ const bodyToWrite = b.review ? `\n\n${b.review}\n` : '\n';
718
+
719
+ await fs.mkdir(path.dirname(outPath), { recursive: true });
720
+ await fs.writeFile(outPath, fm + bodyToWrite, 'utf8');
721
+ written++;
722
+ }
723
+ }
724
+
725
+ if (written > 0) {
726
+ const libraryPath = path.join(PROJECT_ROOT, 'content', 'library.md');
727
+
728
+ try {
729
+ const libraryContent = await fs.readFile(libraryPath, 'utf8');
730
+ const now = new Date().toISOString().replace(/\.\d{3}Z$/, '+00:00');
731
+ const updated = libraryContent.replace(
732
+ /^date:.*$/m,
733
+ `date: ${now}`,
734
+ );
735
+
736
+ await fs.writeFile(libraryPath, updated, 'utf8');
737
+ console.log(` Updated library.md date to ${now}`);
738
+ } catch (err) {
739
+ console.warn(` WARNING: could not update library.md: ${err.message}`);
740
+ }
741
+ }
742
+
743
+ console.log(
744
+ 'Done.\n' +
685
745
  ` Books written: ${written}\n` +
686
746
  ` New files created: ${createdNew}\n` +
687
747
  ` Existing files updated: ${mergedExisting}\n` +
688
748
  ` Files renamed: ${renamedFiles}\n` +
689
749
  ` Existing files skipped: ${skippedUnchanged}\n` +
690
- ` Content output: ${OUTPUT_ROOT}\n`
691
- );
750
+ ` Content output: ${OUTPUT_ROOT}\n`,
751
+ );
692
752
  }
693
753
 
694
754
  // Only run main() when executed directly as a script (not imported by tests)
695
755
  const isMain =
696
- path.resolve(new URL(import.meta.url).pathname) === path.resolve(process.argv[1] ?? "");
756
+ path.resolve(new URL(import.meta.url).pathname) === path.resolve(process.argv[1] ?? '');
757
+
697
758
  if (isMain) {
698
- main().catch((err) => {
699
- console.error(err);
700
- process.exit(1);
701
- });
759
+ main().catch((err) => {
760
+ console.error(err);
761
+ process.exit(1);
762
+ });
702
763
  }
703
764
 
704
765
  export {
705
- slugify,
706
- cleanIsbn,
707
- toISODate,
708
- clampRating,
709
- escapeYAMLString,
710
- uniqSortedDates,
711
- authorDirFromAuthorLF,
712
- keyFor,
713
- goodreadsBookUrl,
714
- openLibraryIsbnUrl,
715
- amazonSearchLink,
716
- parseCSV,
717
- parseExistingMarkdown,
718
- replaceFinishedBlock,
719
- updateTitleInFrontMatter,
720
- extractGoodreadsId,
721
- extractFileIsbns,
722
- toFrontMatter,
723
- buildExistingIndex,
724
- findExistingFile,
766
+ slugify,
767
+ cleanIsbn,
768
+ toISODate,
769
+ clampRating,
770
+ escapeYAMLString,
771
+ uniqSortedDates,
772
+ authorDirFromAuthorLF,
773
+ keyFor,
774
+ goodreadsBookUrl,
775
+ openLibraryIsbnUrl,
776
+ amazonSearchLink,
777
+ parseCSV,
778
+ parseExistingMarkdown,
779
+ replaceFinishedBlock,
780
+ updateTitleInFrontMatter,
781
+ extractGoodreadsId,
782
+ extractFileIsbns,
783
+ toFrontMatter,
784
+ buildExistingIndex,
785
+ findExistingFile,
725
786
  };