@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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
|
|
19
|
+
## 0.2.1 - 2026-07-04
|
|
20
|
+
|
|
21
|
+
### Chores
|
|
22
|
+
|
|
23
|
+
* Add Eslint file formatting for easier development
|
|
24
|
+
* Correct all linting issues with new formatter.
|
|
25
|
+
|
|
8
26
|
## 0.2.0 - 2026-05-09
|
|
9
27
|
|
|
10
28
|
### Features
|
package/README.md
CHANGED
|
@@ -2,12 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
A collection of CLI tools for Hugo bloggers. Run them directly from the root of your Hugo site via `npx` or install globally.
|
|
4
4
|
|
|
5
|
+
> **Never invoke these with `node path/to/script.js`.** Every command below is registered as a proper CLI via the `bin` field in `package.json`, so once installed you run it by name (`draft`, `publish`, ...) — see [Installation](#installation) for the three ways to do that.
|
|
6
|
+
|
|
5
7
|
## Requirements
|
|
6
8
|
|
|
7
9
|
- Node.js 18 or later
|
|
8
10
|
|
|
9
11
|
## Installation
|
|
10
12
|
|
|
13
|
+
Whichever route you pick, you're done as soon as you can run a bare command name (`draft`, `publish`, ...) — you should never need to reference a file path under `node_modules/` directly.
|
|
14
|
+
|
|
11
15
|
**Run without installing (recommended):**
|
|
12
16
|
|
|
13
17
|
```sh
|
|
@@ -27,7 +31,14 @@ npx -p @chriswiegman/hugo-tools publish content/drafts/my-post.md now
|
|
|
27
31
|
npm install --save-dev @chriswiegman/hugo-tools
|
|
28
32
|
```
|
|
29
33
|
|
|
30
|
-
Then run commands via `npx` (no `-p` flag needed once installed)
|
|
34
|
+
Then run commands via `npx` (no `-p` flag needed once installed):
|
|
35
|
+
|
|
36
|
+
```sh
|
|
37
|
+
npx draft
|
|
38
|
+
npx publish content/drafts/my-post.md now
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
...or add them as scripts in your `package.json` so `npm run publish` works too:
|
|
31
42
|
|
|
32
43
|
```json
|
|
33
44
|
{
|
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",
|
|
@@ -45,8 +45,9 @@
|
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@eslint/js": "^10.0.1",
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
48
|
+
"@stylistic/eslint-plugin": "^5.10.0",
|
|
49
|
+
"changie": "^1.25.0",
|
|
50
|
+
"eslint": "^10.7.0",
|
|
51
|
+
"globals": "^17.7.0"
|
|
51
52
|
}
|
|
52
|
-
}
|
|
53
|
+
}
|
package/src/add-tags.js
CHANGED
|
@@ -16,128 +16,131 @@ const DATA_FILE = path.join(process.cwd(), config.vscodeDir, 'tags-categories.js
|
|
|
16
16
|
* Display menu and get user selection
|
|
17
17
|
*/
|
|
18
18
|
async function getSelection(prompt, options) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
console.log(`\n${prompt}`);
|
|
26
|
+
options.forEach((opt, idx) => {
|
|
27
|
+
console.log(` ${idx + 1}. ${opt}`);
|
|
28
|
+
});
|
|
29
|
+
console.log(' 0. Done/Skip');
|
|
30
|
+
|
|
31
|
+
rl.question('\nEnter number: ', (answer) => {
|
|
32
|
+
rl.close();
|
|
33
|
+
const num = parseInt(answer, 10);
|
|
34
|
+
|
|
35
|
+
if (num === 0) {
|
|
36
|
+
resolve(null);
|
|
37
|
+
} else if (num > 0 && num <= options.length) {
|
|
38
|
+
resolve(options[num - 1]);
|
|
39
|
+
} else {
|
|
40
|
+
console.log('Invalid selection');
|
|
41
|
+
resolve(null);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Get multiple selections
|
|
48
49
|
*/
|
|
49
50
|
async function getMultipleSelections(prompt, options) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
51
|
+
const selections = [];
|
|
52
|
+
let done = false;
|
|
53
|
+
|
|
54
|
+
while (!done) {
|
|
55
|
+
const remaining = options.filter((opt) => !selections.includes(opt));
|
|
56
|
+
|
|
57
|
+
if (remaining.length === 0) {
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const selection = await getSelection(
|
|
62
|
+
selections.length === 0 ? prompt : `${prompt} (Selected: ${selections.join(', ')})`,
|
|
63
|
+
remaining,
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (selection === null) {
|
|
67
|
+
done = true;
|
|
68
|
+
} else {
|
|
69
|
+
selections.push(selection);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return selections;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
/**
|
|
75
77
|
* Format frontmatter list
|
|
76
78
|
*/
|
|
77
79
|
function formatList(items) {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
if (items.length === 0) {
|
|
81
|
+
return ' -';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return items.map((item) => ` - ${item}`).join('\n');
|
|
82
85
|
}
|
|
83
86
|
|
|
84
87
|
/**
|
|
85
88
|
* Main execution
|
|
86
89
|
*/
|
|
87
90
|
async function main() {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
91
|
+
config.requireHugoSite();
|
|
92
|
+
|
|
93
|
+
console.log('=== Hugo Draft Tags & Categories Helper ===\n');
|
|
94
|
+
|
|
95
|
+
// Check if data file exists
|
|
96
|
+
if (!fs.existsSync(DATA_FILE)) {
|
|
97
|
+
console.error(`Error: Data file not found at ${DATA_FILE}`);
|
|
98
|
+
console.error('Run "extract-tags" first to generate the data file.');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Load data
|
|
103
|
+
const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
|
|
104
|
+
|
|
105
|
+
console.log(`Loaded ${data.categories.length} categories and ${data.tags.length} tags`);
|
|
106
|
+
console.log(`(Generated from ${data.stats.totalFiles} posts on ${new Date(data.stats.generatedAt).toLocaleDateString()})\n`);
|
|
107
|
+
|
|
108
|
+
// Get category selection
|
|
109
|
+
const category = await getSelection(
|
|
110
|
+
'Select a category:',
|
|
111
|
+
data.categories,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
// Get tag selections
|
|
115
|
+
const tags = await getMultipleSelections(
|
|
116
|
+
'Select tags (you can select multiple):',
|
|
117
|
+
data.tags,
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// Generate output
|
|
121
|
+
console.log('\n=== Generated Frontmatter ===\n');
|
|
122
|
+
|
|
123
|
+
if (category) {
|
|
124
|
+
console.log('categories:');
|
|
125
|
+
console.log(` - ${category}`);
|
|
126
|
+
} else {
|
|
127
|
+
console.log('categories:');
|
|
128
|
+
console.log(' -');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
console.log('tags:');
|
|
132
|
+
console.log(formatList(tags));
|
|
133
|
+
|
|
134
|
+
console.log('\n=== Copy the above into your draft frontmatter ===\n');
|
|
132
135
|
}
|
|
133
136
|
|
|
134
137
|
// Handle Ctrl+C gracefully
|
|
135
138
|
process.on('SIGINT', () => {
|
|
136
|
-
|
|
137
|
-
|
|
139
|
+
console.log('\n\nCancelled.');
|
|
140
|
+
process.exit(0);
|
|
138
141
|
});
|
|
139
142
|
|
|
140
|
-
main().catch(err => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
+
main().catch((err) => {
|
|
144
|
+
console.error('Error:', err);
|
|
145
|
+
process.exit(1);
|
|
143
146
|
});
|