@chriswiegman/hugo-tools 0.2.1 → 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 CHANGED
@@ -5,6 +5,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
5
5
  adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html),
6
6
  and is generated by [Changie](https://github.com/miniscruff/changie).
7
7
 
8
+ ## 0.2.2 - 2026-07-19
9
+
10
+ ### Bug Fixes
11
+
12
+ * book: update rating from Goodreads only when it differs and Goodreads has an actual rating, so manually-set ratings in markdown aren't overwritten by an unrated (0) Goodreads entry
13
+
14
+ ### Chores
15
+
16
+ * Update project dependencies.
17
+ * Fix no-console warnings during linting.
18
+
8
19
  ## 0.2.1 - 2026-07-04
9
20
 
10
21
  ### Chores
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chriswiegman/hugo-tools",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "A set of tools to make blogging easier with Hugo.",
5
5
  "keywords": [
6
6
  "hugo",
@@ -47,7 +47,7 @@
47
47
  "@eslint/js": "^10.0.1",
48
48
  "@stylistic/eslint-plugin": "^5.10.0",
49
49
  "changie": "^1.25.0",
50
- "eslint": "^10.6.0",
50
+ "eslint": "^10.7.0",
51
51
  "globals": "^17.7.0"
52
52
  }
53
53
  }
package/src/book.mjs CHANGED
@@ -12,7 +12,10 @@
12
12
  * - Existing files are matched by Goodreads Book ID first, then ISBN, then title slug.
13
13
  * - When a match is found at a different path (title changed in Goodreads), the file is
14
14
  * renamed to the new slug and the title in front matter is updated automatically.
15
- * - For an existing book file, finished dates are merged; other fields are left untouched
15
+ * - For an existing book file, finished dates are merged and the rating is updated when the
16
+ * Goodreads rating differs from the file's rating — but only when Goodreads has a rating
17
+ * (> 0). A Goodreads rating of 0 (unrated) never overwrites a rating already in the file,
18
+ * since you may have rated the book only in the markdown. Other fields are left untouched
16
19
  * unless a rename occurs (in which case the title is also updated).
17
20
  *
18
21
  * Usage:
@@ -244,11 +247,13 @@ function authorDirFromAuthorLF(authorLF) {
244
247
  * - "YYYY-MM-DD"
245
248
  * - "YYYY-MM-DD"
246
249
  * - finished: "YYYY-MM-DD" (legacy scalar; included as one date)
250
+ *
251
+ * Also extracts the existing `rating:` value (or null if absent/unparseable).
247
252
  */
248
253
  function parseExistingMarkdown(md) {
249
254
  const m = md.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
250
255
 
251
- if (!m) return { finished: [] };
256
+ if (!m) return { finished: [], rating: null };
252
257
 
253
258
  const fm = m[1];
254
259
  const lines = fm.split('\n');
@@ -280,7 +285,18 @@ function parseExistingMarkdown(md) {
280
285
  }
281
286
  }
282
287
 
283
- return { finished: uniqSortedDates(finished) };
288
+ let rating = null;
289
+
290
+ for (const line of lines) {
291
+ const mm = line.match(/^rating:\s*(\d+)\s*$/);
292
+
293
+ if (mm) {
294
+ rating = Number(mm[1]);
295
+ break;
296
+ }
297
+ }
298
+
299
+ return { finished: uniqSortedDates(finished), rating };
284
300
  }
285
301
 
286
302
  async function fileExists(p) {
@@ -334,6 +350,13 @@ function updateTitleInFrontMatter(md, newTitle) {
334
350
  return md.replace(/^title:.*$/m, `title: "${escapeYAMLString(newTitle)}"`);
335
351
  }
336
352
 
353
+ /**
354
+ * Update the rating field in front matter, leaving everything else untouched.
355
+ */
356
+ function updateRatingInFrontMatter(md, newRating) {
357
+ return md.replace(/^rating:.*$/m, `rating: ${newRating}`);
358
+ }
359
+
337
360
  // -------------------- Existing-file index --------------------
338
361
 
339
362
  /**
@@ -638,6 +661,7 @@ async function main() {
638
661
  let mergedExisting = 0;
639
662
  let renamedFiles = 0;
640
663
  let skippedUnchanged = 0;
664
+ let ratingsUpdated = 0;
641
665
 
642
666
  for (const b of bookList) {
643
667
  const authorDir = authorDirFromAuthorLF(b.authorLF);
@@ -651,14 +675,19 @@ async function main() {
651
675
 
652
676
  // Read the existing file
653
677
  const existingContent = await fs.readFile(existingPath, 'utf8');
654
- const { finished: existingFinished } = parseExistingMarkdown(existingContent);
678
+ const { finished: existingFinished, rating: existingRating } = parseExistingMarkdown(existingContent);
655
679
  const finishedMerged = uniqSortedDates([...existingFinished, ...b.finished]);
656
680
 
657
681
  const hasNewDates =
658
682
  finishedMerged.length !== existingFinished.length ||
659
683
  finishedMerged.some((d, i) => d !== existingFinished[i]);
660
684
 
661
- if (!titleChanged && !hasNewDates) {
685
+ // Only let Goodreads override the file's rating when Goodreads actually has one (> 0).
686
+ // A rating of 0 usually means "not rated on Goodreads" — the file may carry a rating
687
+ // entered by hand there instead, so leave it alone rather than clobbering it with 0.
688
+ const hasRatingChange = b.rating > 0 && existingRating !== null && b.rating !== existingRating;
689
+
690
+ if (!titleChanged && !hasNewDates && !hasRatingChange) {
662
691
  skippedUnchanged++;
663
692
  continue;
664
693
  }
@@ -670,6 +699,15 @@ async function main() {
670
699
  patched = replaceFinishedBlock(patched, finishedMerged);
671
700
  }
672
701
 
702
+ if (hasRatingChange) {
703
+ patched = updateRatingInFrontMatter(patched, b.rating);
704
+ ratingsUpdated++;
705
+
706
+ const rel = path.relative(OUTPUT_ROOT, existingPath);
707
+
708
+ console.log(` RATING UPDATED: ${rel} (${existingRating} -> ${b.rating})`);
709
+ }
710
+
673
711
  if (titleChanged) {
674
712
  patched = updateTitleInFrontMatter(patched, b.title);
675
713
  }
@@ -746,6 +784,7 @@ async function main() {
746
784
  ` New files created: ${createdNew}\n` +
747
785
  ` Existing files updated: ${mergedExisting}\n` +
748
786
  ` Files renamed: ${renamedFiles}\n` +
787
+ ` Ratings updated: ${ratingsUpdated}\n` +
749
788
  ` Existing files skipped: ${skippedUnchanged}\n` +
750
789
  ` Content output: ${OUTPUT_ROOT}\n`,
751
790
  );
@@ -778,6 +817,7 @@ export {
778
817
  parseExistingMarkdown,
779
818
  replaceFinishedBlock,
780
819
  updateTitleInFrontMatter,
820
+ updateRatingInFrontMatter,
781
821
  extractGoodreadsId,
782
822
  extractFileIsbns,
783
823
  toFrontMatter,
package/src/book.test.mjs CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  parseExistingMarkdown,
21
21
  replaceFinishedBlock,
22
22
  updateTitleInFrontMatter,
23
+ updateRatingInFrontMatter,
23
24
  extractGoodreadsId,
24
25
  extractFileIsbns,
25
26
  toFrontMatter,
@@ -433,6 +434,26 @@ test('parseExistingMarkdown: returns empty array for invalid front matter', () =
433
434
  assert.deepEqual(finished, []);
434
435
  });
435
436
 
437
+ test('parseExistingMarkdown: extracts rating', () => {
438
+ const md = sampleMarkdown({ rating: 5 });
439
+ const { rating } = parseExistingMarkdown(md);
440
+
441
+ assert.equal(rating, 5);
442
+ });
443
+
444
+ test('parseExistingMarkdown: returns null rating when field is absent', () => {
445
+ const md = ['---', 'title: "Test"', 'finished:', ' - "2023-01-15"', '---', ''].join('\n');
446
+ const { rating } = parseExistingMarkdown(md);
447
+
448
+ assert.equal(rating, null);
449
+ });
450
+
451
+ test('parseExistingMarkdown: returns null rating for invalid front matter', () => {
452
+ const { rating } = parseExistingMarkdown('no front matter here');
453
+
454
+ assert.equal(rating, null);
455
+ });
456
+
436
457
  // ---------------------------------------------------------------------------
437
458
  // replaceFinishedBlock
438
459
  // ---------------------------------------------------------------------------
@@ -508,6 +529,26 @@ test('updateTitleInFrontMatter: preserves all other front matter', () => {
508
529
  assert.ok(result.includes('goodreads:'));
509
530
  });
510
531
 
532
+ // ---------------------------------------------------------------------------
533
+ // updateRatingInFrontMatter
534
+ // ---------------------------------------------------------------------------
535
+
536
+ test('updateRatingInFrontMatter: replaces rating line', () => {
537
+ const md = sampleMarkdown({ rating: 3 });
538
+ const result = updateRatingInFrontMatter(md, 5);
539
+
540
+ assert.ok(result.includes('rating: 5'));
541
+ assert.ok(!result.includes('rating: 3'));
542
+ });
543
+
544
+ test('updateRatingInFrontMatter: preserves all other front matter', () => {
545
+ const md = sampleMarkdown({ title: 'Keep Me', rating: 2 });
546
+ const result = updateRatingInFrontMatter(md, 4);
547
+
548
+ assert.ok(result.includes('title: "Keep Me"'));
549
+ assert.ok(result.includes('goodreads:'));
550
+ });
551
+
511
552
  // ---------------------------------------------------------------------------
512
553
  // extractGoodreadsId
513
554
  // ---------------------------------------------------------------------------