@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 +11 -0
- package/package.json +2 -2
- package/src/book.mjs +45 -5
- package/src/book.test.mjs +41 -0
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.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
// ---------------------------------------------------------------------------
|