@chriswiegman/hugo-tools 0.1.2 → 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/CHANGELOG.md +17 -0
- package/README.md +36 -4
- package/package.json +7 -6
- package/src/add-tags.js +103 -100
- package/src/book.mjs +602 -541
- package/src/book.test.mjs +619 -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 +121 -60
- package/src/pick-image.test.mjs +121 -0
- 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,23 @@ 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.1 - 2026-07-04
|
|
9
|
+
|
|
10
|
+
### Chores
|
|
11
|
+
|
|
12
|
+
* Add Eslint file formatting for easier development
|
|
13
|
+
* Correct all linting issues with new formatter.
|
|
14
|
+
|
|
15
|
+
## 0.2.0 - 2026-05-09
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* Rework pick-image to move images into the dated assets directory and insert references directly into content files (frontmatter images array or markdown image tag)
|
|
20
|
+
|
|
21
|
+
### Chores
|
|
22
|
+
|
|
23
|
+
* Update project dependencies.
|
|
24
|
+
|
|
8
25
|
## 0.1.2 - 2026-05-04
|
|
9
26
|
|
|
10
27
|
### 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
|
{
|
|
@@ -196,13 +207,34 @@ Requires `extract-tags` to have been run at least once.
|
|
|
196
207
|
|
|
197
208
|
### `pick-image`
|
|
198
209
|
|
|
199
|
-
|
|
210
|
+
Moves an image into your `imagesDir` under the current year/month and inserts a reference into a Hugo content file.
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
pick-image <image-file> [content-file]
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**What it does:**
|
|
217
|
+
|
|
218
|
+
- Moves `<image-file>` to `assets/images/YYYY/MM/<filename>` (creates the directory if needed)
|
|
219
|
+
- Derives the Hugo URL path by stripping the `assets/` prefix (e.g. `/images/YYYY/MM/filename`)
|
|
220
|
+
- If a `[content-file]` is given:
|
|
221
|
+
- Appends the path to the `images:` array in front matter when that field is present (replacing any placeholder ` -` entry)
|
|
222
|
+
- Otherwise appends a `` markdown tag to the end of the body
|
|
223
|
+
- If no `[content-file]` is given, copies the URL path to the clipboard instead
|
|
224
|
+
|
|
225
|
+
**Examples:**
|
|
200
226
|
|
|
201
227
|
```sh
|
|
202
|
-
|
|
228
|
+
# Move photo.jpg and insert into the currently open draft
|
|
229
|
+
pick-image ~/Downloads/photo.jpg content/drafts/my-post.md
|
|
230
|
+
|
|
231
|
+
# Move photo.jpg and copy the path to the clipboard
|
|
232
|
+
pick-image ~/Downloads/photo.jpg
|
|
203
233
|
```
|
|
204
234
|
|
|
205
|
-
|
|
235
|
+
The VS Code "Pick Image" task prompts for the image path and automatically passes the currently open file as the content target — drag the image file into the terminal prompt to fill the path.
|
|
236
|
+
|
|
237
|
+
Clipboard support (no-content-file mode): macOS (`pbcopy`), Windows (`clip`), Linux (`xclip`, `xsel`, or `wl-copy` — whichever is available).
|
|
206
238
|
|
|
207
239
|
---
|
|
208
240
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chriswiegman/hugo-tools",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A set of tools to make blogging easier with Hugo.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"hugo",
|
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"lint": "eslint src/",
|
|
44
|
-
"test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs"
|
|
44
|
+
"test": "node --test src/book.test.mjs src/publish.test.mjs src/extract-tags.test.mjs src/draft.test.mjs src/pick-image.test.mjs"
|
|
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.6.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
|
});
|