@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.
@@ -17,218 +17,229 @@ const SNIPPETS_FILE = path.join(process.cwd(), config.vscodeDir, 'markdown.code-
17
17
  * Extracts frontmatter from a markdown file
18
18
  */
19
19
  function extractFrontmatter(content) {
20
- const match = content.match(/^---\n([\s\S]*?)\n---/);
21
- return match ? match[1] : null;
20
+ const match = content.match(/^---\n([\s\S]*?)\n---/);
21
+
22
+ return match ? match[1] : null;
22
23
  }
23
24
 
24
25
  /**
25
26
  * Parses YAML-style list items from frontmatter
26
27
  */
27
28
  function parseListItems(frontmatter, key) {
28
- const items = [];
29
- const blockRegex = new RegExp(`^${key}:\\s*\\n((\\s*-\\s*.+\\n)+)`, 'm');
30
- const match = frontmatter.match(blockRegex);
31
-
32
- if (match) {
33
- const lines = match[1].split('\n');
34
- for (const line of lines) {
35
- const itemMatch = line.match(/^\s*-\s*(.+)\s*$/);
36
- if (itemMatch) {
37
- const value = itemMatch[1].trim().replace(/^["']|["']$/g, '');
38
- if (value) {
39
- items.push(value);
40
- }
41
- }
42
- }
43
- }
44
-
45
- return items;
29
+ const items = [];
30
+ const blockRegex = new RegExp(`^${key}:\\s*\\n((\\s*-\\s*.+\\n)+)`, 'm');
31
+ const match = frontmatter.match(blockRegex);
32
+
33
+ if (match) {
34
+ const lines = match[1].split('\n');
35
+
36
+ for (const line of lines) {
37
+ const itemMatch = line.match(/^\s*-\s*(.+)\s*$/);
38
+
39
+ if (itemMatch) {
40
+ const value = itemMatch[1].trim().replace(/^["']|["']$/g, '');
41
+
42
+ if (value) {
43
+ items.push(value);
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return items;
46
50
  }
47
51
 
48
52
  /**
49
53
  * Recursively finds all markdown files
50
54
  */
51
55
  function findMarkdownFiles(dir) {
52
- const files = [];
53
- const entries = fs.readdirSync(dir, { withFileTypes: true });
54
-
55
- for (const entry of entries) {
56
- const fullPath = path.join(dir, entry.name);
57
- if (entry.isDirectory()) {
58
- files.push(...findMarkdownFiles(fullPath));
59
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
60
- files.push(fullPath);
61
- }
62
- }
63
-
64
- return files;
56
+ const files = [];
57
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
58
+
59
+ for (const entry of entries) {
60
+ const fullPath = path.join(dir, entry.name);
61
+
62
+ if (entry.isDirectory()) {
63
+ files.push(...findMarkdownFiles(fullPath));
64
+ } else if (entry.isFile() && entry.name.endsWith('.md')) {
65
+ files.push(fullPath);
66
+ }
67
+ }
68
+
69
+ return files;
65
70
  }
66
71
 
67
72
  /**
68
73
  * Main extraction logic
69
74
  */
70
75
  function extractTagsAndCategories() {
71
- const categories = new Set();
72
- const tags = new Set();
73
- const categoryCount = {};
74
- const tagCount = {};
75
-
76
- const files = findMarkdownFiles(CONTENT_DIR);
77
- console.log(`Found ${files.length} markdown files`);
78
-
79
- for (const file of files) {
80
- const content = fs.readFileSync(file, 'utf8');
81
- const frontmatter = extractFrontmatter(content);
82
-
83
- if (!frontmatter) continue;
84
-
85
- // Extract categories
86
- const fileCategories = parseListItems(frontmatter, 'categories');
87
- for (const cat of fileCategories) {
88
- categories.add(cat);
89
- categoryCount[cat] = (categoryCount[cat] || 0) + 1;
90
- }
91
-
92
- // Extract tags
93
- const fileTags = parseListItems(frontmatter, 'tags');
94
- for (const tag of fileTags) {
95
- tags.add(tag);
96
- tagCount[tag] = (tagCount[tag] || 0) + 1;
97
- }
98
- }
99
-
100
- // Sort by usage count (most used first)
101
- const sortedCategories = Array.from(categories).sort((a, b) => {
102
- return (categoryCount[b] || 0) - (categoryCount[a] || 0);
103
- });
104
-
105
- const sortedTags = Array.from(tags).sort((a, b) => {
106
- return (tagCount[b] || 0) - (tagCount[a] || 0);
107
- });
108
-
109
- return {
110
- categories: sortedCategories,
111
- tags: sortedTags,
112
- stats: {
113
- categoryCount,
114
- tagCount,
115
- totalCategories: categories.size,
116
- totalTags: tags.size,
117
- totalFiles: files.length,
118
- generatedAt: new Date().toISOString()
119
- }
120
- };
76
+ const categories = new Set();
77
+ const tags = new Set();
78
+ const categoryCount = {};
79
+ const tagCount = {};
80
+
81
+ const files = findMarkdownFiles(CONTENT_DIR);
82
+
83
+ console.log(`Found ${files.length} markdown files`);
84
+
85
+ for (const file of files) {
86
+ const content = fs.readFileSync(file, 'utf8');
87
+ const frontmatter = extractFrontmatter(content);
88
+
89
+ if (!frontmatter) continue;
90
+
91
+ // Extract categories
92
+ const fileCategories = parseListItems(frontmatter, 'categories');
93
+
94
+ for (const cat of fileCategories) {
95
+ categories.add(cat);
96
+ categoryCount[cat] = (categoryCount[cat] || 0) + 1;
97
+ }
98
+
99
+ // Extract tags
100
+ const fileTags = parseListItems(frontmatter, 'tags');
101
+
102
+ for (const tag of fileTags) {
103
+ tags.add(tag);
104
+ tagCount[tag] = (tagCount[tag] || 0) + 1;
105
+ }
106
+ }
107
+
108
+ // Sort by usage count (most used first)
109
+ const sortedCategories = Array.from(categories).sort((a, b) => {
110
+ return (categoryCount[b] || 0) - (categoryCount[a] || 0);
111
+ });
112
+
113
+ const sortedTags = Array.from(tags).sort((a, b) => {
114
+ return (tagCount[b] || 0) - (tagCount[a] || 0);
115
+ });
116
+
117
+ return {
118
+ categories: sortedCategories,
119
+ tags: sortedTags,
120
+ stats: {
121
+ categoryCount,
122
+ tagCount,
123
+ totalCategories: categories.size,
124
+ totalTags: tags.size,
125
+ totalFiles: files.length,
126
+ generatedAt: new Date().toISOString(),
127
+ },
128
+ };
121
129
  }
122
130
 
123
131
  /**
124
132
  * Generate snippet prefixes for a tag/category
125
133
  */
126
134
  function generatePrefixes(name) {
127
- const lower = name.toLowerCase();
128
- const prefixes = [lower];
129
-
130
- // Add concatenated version for multi-word items (e.g., "Digital Life" -> "digitallife")
131
- if (name.includes(' ')) {
132
- const concat = lower.replace(/\s+/g, '');
133
- if (concat !== lower) {
134
- prefixes.push(concat);
135
- }
136
- }
137
-
138
- // Add common abbreviations
139
- const abbrevMap = {
140
- 'Technology': ['tech'],
141
- 'Personal': ['pers'],
142
- 'WordPress': ['wp'],
143
- 'Infrastructure': ['infra'],
144
- 'Open Source': ['oss'],
145
- 'Reflection': ['reflect'],
146
- 'Web Development': ['webdev', 'web'],
147
- 'Development': ['dev'],
148
- 'Digital Life': ['digital'],
149
- 'Social Media': ['social'],
150
- 'Education': ['edu'],
151
- 'Self-hosting': ['selfhost'],
152
- 'Content Management Systems': ['cms', 'contentmanagement']
153
- };
154
-
155
- if (abbrevMap[name]) {
156
- prefixes.push(...abbrevMap[name]);
157
- }
158
-
159
- // Return single string if only one prefix, otherwise return array
160
- return prefixes.length === 1 ? prefixes[0] : prefixes;
135
+ const lower = name.toLowerCase();
136
+ const prefixes = [lower];
137
+
138
+ // Add concatenated version for multi-word items (e.g., "Digital Life" -> "digitallife")
139
+ if (name.includes(' ')) {
140
+ const concat = lower.replace(/\s+/g, '');
141
+
142
+ if (concat !== lower) {
143
+ prefixes.push(concat);
144
+ }
145
+ }
146
+
147
+ // Add common abbreviations
148
+ const abbrevMap = {
149
+ 'Technology': ['tech'],
150
+ 'Personal': ['pers'],
151
+ 'WordPress': ['wp'],
152
+ 'Infrastructure': ['infra'],
153
+ 'Open Source': ['oss'],
154
+ 'Reflection': ['reflect'],
155
+ 'Web Development': ['webdev', 'web'],
156
+ 'Development': ['dev'],
157
+ 'Digital Life': ['digital'],
158
+ 'Social Media': ['social'],
159
+ 'Education': ['edu'],
160
+ 'Self-hosting': ['selfhost'],
161
+ 'Content Management Systems': ['cms', 'contentmanagement'],
162
+ };
163
+
164
+ if (abbrevMap[name]) {
165
+ prefixes.push(...abbrevMap[name]);
166
+ }
167
+
168
+ // Return single string if only one prefix, otherwise return array
169
+ return prefixes.length === 1 ? prefixes[0] : prefixes;
161
170
  }
162
171
 
163
172
  /**
164
173
  * Generate VSCode snippets from tags and categories
165
174
  */
166
175
  function generateSnippets(data) {
167
- const snippets = {};
168
-
169
- // Generate category snippets
170
- for (const category of data.categories) {
171
- snippets[`Category: ${category}`] = {
172
- prefix: generatePrefixes(category),
173
- body: category,
174
- description: `Category: ${category}`
175
- };
176
- }
177
-
178
- // Generate tag snippets
179
- for (const tag of data.tags) {
180
- snippets[`Tag: ${tag}`] = {
181
- prefix: generatePrefixes(tag),
182
- body: tag,
183
- description: `Tag: ${tag}`
184
- };
185
- }
186
-
187
- return snippets;
176
+ const snippets = {};
177
+
178
+ // Generate category snippets
179
+ for (const category of data.categories) {
180
+ snippets[`Category: ${category}`] = {
181
+ prefix: generatePrefixes(category),
182
+ body: category,
183
+ description: `Category: ${category}`,
184
+ };
185
+ }
186
+
187
+ // Generate tag snippets
188
+ for (const tag of data.tags) {
189
+ snippets[`Tag: ${tag}`] = {
190
+ prefix: generatePrefixes(tag),
191
+ body: tag,
192
+ description: `Tag: ${tag}`,
193
+ };
194
+ }
195
+
196
+ return snippets;
188
197
  }
189
198
 
190
199
  /**
191
200
  * Main execution
192
201
  */
193
202
  function main() {
194
- config.requireHugoSite();
195
-
196
- console.log('Extracting tags and categories from published posts...');
197
-
198
- const data = extractTagsAndCategories();
199
-
200
- // Ensure .vscode directory exists
201
- const vscodePath = path.dirname(OUTPUT_FILE);
202
- if (!fs.existsSync(vscodePath)) {
203
- fs.mkdirSync(vscodePath, { recursive: true });
204
- }
205
-
206
- // Write JSON data file
207
- fs.writeFileSync(OUTPUT_FILE, JSON.stringify(data, null, 2));
208
-
209
- // Generate and write snippets file
210
- const snippets = generateSnippets(data);
211
- fs.writeFileSync(SNIPPETS_FILE, JSON.stringify(snippets, null, 2));
212
-
213
- console.log('\nResults:');
214
- console.log(` Categories: ${data.stats.totalCategories}`);
215
- console.log(` Tags: ${data.stats.totalTags}`);
216
- console.log(` Files processed: ${data.stats.totalFiles}`);
217
- console.log(`\nOutput written to:`);
218
- console.log(` - ${OUTPUT_FILE}`);
219
- console.log(` - ${SNIPPETS_FILE}`);
220
- console.log('\nMost used categories:');
221
- data.categories.slice(0, 5).forEach(cat => {
222
- console.log(` - ${cat} (${data.stats.categoryCount[cat]} posts)`);
223
- });
224
- console.log('\nMost used tags:');
225
- data.tags.slice(0, 10).forEach(tag => {
226
- console.log(` - ${tag} (${data.stats.tagCount[tag]} posts)`);
227
- });
203
+ config.requireHugoSite();
204
+
205
+ console.log('Extracting tags and categories from published posts...');
206
+
207
+ const data = extractTagsAndCategories();
208
+
209
+ // Ensure .vscode directory exists
210
+ const vscodePath = path.dirname(OUTPUT_FILE);
211
+
212
+ if (!fs.existsSync(vscodePath)) {
213
+ fs.mkdirSync(vscodePath, { recursive: true });
214
+ }
215
+
216
+ // Write JSON data file
217
+ fs.writeFileSync(OUTPUT_FILE, JSON.stringify(data, null, 2));
218
+
219
+ // Generate and write snippets file
220
+ const snippets = generateSnippets(data);
221
+
222
+ fs.writeFileSync(SNIPPETS_FILE, JSON.stringify(snippets, null, 2));
223
+
224
+ console.log('\nResults:');
225
+ console.log(` Categories: ${data.stats.totalCategories}`);
226
+ console.log(` Tags: ${data.stats.totalTags}`);
227
+ console.log(` Files processed: ${data.stats.totalFiles}`);
228
+ console.log('\nOutput written to:');
229
+ console.log(` - ${OUTPUT_FILE}`);
230
+ console.log(` - ${SNIPPETS_FILE}`);
231
+ console.log('\nMost used categories:');
232
+ data.categories.slice(0, 5).forEach((cat) => {
233
+ console.log(` - ${cat} (${data.stats.categoryCount[cat]} posts)`);
234
+ });
235
+ console.log('\nMost used tags:');
236
+ data.tags.slice(0, 10).forEach((tag) => {
237
+ console.log(` - ${tag} (${data.stats.tagCount[tag]} posts)`);
238
+ });
228
239
  }
229
240
 
230
241
  if (require.main === module) {
231
- main();
242
+ main();
232
243
  }
233
244
 
234
245
  module.exports = { extractFrontmatter, parseListItems, generatePrefixes, main };
@@ -10,32 +10,35 @@ const { extractFrontmatter, parseListItems, generatePrefixes } = require('./extr
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
12
  test('extractFrontmatter: returns frontmatter content between --- delimiters', () => {
13
- const content = '---\ntitle: My Post\ndraft: true\n---\nBody text here.';
14
- assert.equal(extractFrontmatter(content), 'title: My Post\ndraft: true');
13
+ const content = '---\ntitle: My Post\ndraft: true\n---\nBody text here.';
14
+
15
+ assert.equal(extractFrontmatter(content), 'title: My Post\ndraft: true');
15
16
  });
16
17
 
17
18
  test('extractFrontmatter: returns null when no frontmatter', () => {
18
- assert.equal(extractFrontmatter('Just body text with no delimiters.'), null);
19
+ assert.equal(extractFrontmatter('Just body text with no delimiters.'), null);
19
20
  });
20
21
 
21
22
  test('extractFrontmatter: returns null when only one --- delimiter', () => {
22
- assert.equal(extractFrontmatter('---\ntitle: foo\nno closing delimiter'), null);
23
+ assert.equal(extractFrontmatter('---\ntitle: foo\nno closing delimiter'), null);
23
24
  });
24
25
 
25
26
  test('extractFrontmatter: handles empty frontmatter block', () => {
26
- assert.equal(extractFrontmatter('---\n\n---\nBody.'), '');
27
+ assert.equal(extractFrontmatter('---\n\n---\nBody.'), '');
27
28
  });
28
29
 
29
30
  test('extractFrontmatter: does not include body content', () => {
30
- const content = '---\ntitle: foo\n---\nThis body should not appear.';
31
- const result = extractFrontmatter(content);
32
- assert.ok(!result.includes('This body'));
31
+ const content = '---\ntitle: foo\n---\nThis body should not appear.';
32
+ const result = extractFrontmatter(content);
33
+
34
+ assert.ok(!result.includes('This body'));
33
35
  });
34
36
 
35
37
  test('extractFrontmatter: handles --- appearing in body without confusion', () => {
36
- const content = '---\ntitle: foo\n---\nBody line.\n---\nAnother section.';
37
- const result = extractFrontmatter(content);
38
- assert.equal(result, 'title: foo');
38
+ const content = '---\ntitle: foo\n---\nBody line.\n---\nAnother section.';
39
+ const result = extractFrontmatter(content);
40
+
41
+ assert.equal(result, 'title: foo');
39
42
  });
40
43
 
41
44
  // ---------------------------------------------------------------------------
@@ -43,42 +46,48 @@ test('extractFrontmatter: handles --- appearing in body without confusion', () =
43
46
  // ---------------------------------------------------------------------------
44
47
 
45
48
  test('parseListItems: parses a simple list', () => {
46
- const fm = 'tags:\n - JavaScript\n - Go\n';
47
- assert.deepEqual(parseListItems(fm, 'tags'), ['JavaScript', 'Go']);
49
+ const fm = 'tags:\n - JavaScript\n - Go\n';
50
+
51
+ assert.deepEqual(parseListItems(fm, 'tags'), ['JavaScript', 'Go']);
48
52
  });
49
53
 
50
54
  test('parseListItems: strips double quotes from values', () => {
51
- const fm = 'categories:\n - "Technology"\n - "Open Source"\n';
52
- assert.deepEqual(parseListItems(fm, 'categories'), ['Technology', 'Open Source']);
55
+ const fm = 'categories:\n - "Technology"\n - "Open Source"\n';
56
+
57
+ assert.deepEqual(parseListItems(fm, 'categories'), ['Technology', 'Open Source']);
53
58
  });
54
59
 
55
60
  test('parseListItems: strips single quotes from values', () => {
56
- const fm = "tags:\n - 'JavaScript'\n";
57
- assert.deepEqual(parseListItems(fm, 'tags'), ['JavaScript']);
61
+ const fm = 'tags:\n - \'JavaScript\'\n';
62
+
63
+ assert.deepEqual(parseListItems(fm, 'tags'), ['JavaScript']);
58
64
  });
59
65
 
60
66
  test('parseListItems: returns empty array when key is not present', () => {
61
- assert.deepEqual(parseListItems('title: My Post\n', 'tags'), []);
67
+ assert.deepEqual(parseListItems('title: My Post\n', 'tags'), []);
62
68
  });
63
69
 
64
70
  test('parseListItems: returns empty array for empty file', () => {
65
- assert.deepEqual(parseListItems('', 'tags'), []);
71
+ assert.deepEqual(parseListItems('', 'tags'), []);
66
72
  });
67
73
 
68
74
  test('parseListItems: handles multiple spaces before list items', () => {
69
- const fm = 'tags:\n - foo\n - bar\n';
70
- assert.deepEqual(parseListItems(fm, 'tags'), ['foo', 'bar']);
75
+ const fm = 'tags:\n - foo\n - bar\n';
76
+
77
+ assert.deepEqual(parseListItems(fm, 'tags'), ['foo', 'bar']);
71
78
  });
72
79
 
73
80
  test('parseListItems: does not confuse tags and categories', () => {
74
- const fm = 'categories:\n - Tech\ntags:\n - Go\n';
75
- assert.deepEqual(parseListItems(fm, 'tags'), ['Go']);
76
- assert.deepEqual(parseListItems(fm, 'categories'), ['Tech']);
81
+ const fm = 'categories:\n - Tech\ntags:\n - Go\n';
82
+
83
+ assert.deepEqual(parseListItems(fm, 'tags'), ['Go']);
84
+ assert.deepEqual(parseListItems(fm, 'categories'), ['Tech']);
77
85
  });
78
86
 
79
87
  test('parseListItems: single item list', () => {
80
- const fm = 'tags:\n - solo\n';
81
- assert.deepEqual(parseListItems(fm, 'tags'), ['solo']);
88
+ const fm = 'tags:\n - solo\n';
89
+
90
+ assert.deepEqual(parseListItems(fm, 'tags'), ['solo']);
82
91
  });
83
92
 
84
93
  // ---------------------------------------------------------------------------
@@ -86,36 +95,40 @@ test('parseListItems: single item list', () => {
86
95
  // ---------------------------------------------------------------------------
87
96
 
88
97
  test('generatePrefixes: single word returns a string', () => {
89
- assert.equal(generatePrefixes('Go'), 'go');
98
+ assert.equal(generatePrefixes('Go'), 'go');
90
99
  });
91
100
 
92
101
  test('generatePrefixes: single word with no abbreviation returns lowercase string', () => {
93
- assert.equal(generatePrefixes('JavaScript'), 'javascript');
102
+ assert.equal(generatePrefixes('JavaScript'), 'javascript');
94
103
  });
95
104
 
96
105
  test('generatePrefixes: multi-word returns array including concatenated form', () => {
97
- const result = generatePrefixes('Open Source');
98
- assert.ok(Array.isArray(result));
99
- assert.ok(result.includes('open source'));
100
- assert.ok(result.includes('opensource'));
106
+ const result = generatePrefixes('Open Source');
107
+
108
+ assert.ok(Array.isArray(result));
109
+ assert.ok(result.includes('open source'));
110
+ assert.ok(result.includes('opensource'));
101
111
  });
102
112
 
103
113
  test('generatePrefixes: known abbreviation is included', () => {
104
- const result = generatePrefixes('Technology');
105
- assert.ok(Array.isArray(result));
106
- assert.ok(result.includes('technology'));
107
- assert.ok(result.includes('tech'));
114
+ const result = generatePrefixes('Technology');
115
+
116
+ assert.ok(Array.isArray(result));
117
+ assert.ok(result.includes('technology'));
118
+ assert.ok(result.includes('tech'));
108
119
  });
109
120
 
110
121
  test('generatePrefixes: multi-word with abbreviation includes all forms', () => {
111
- const result = generatePrefixes('Web Development');
112
- assert.ok(result.includes('web development'));
113
- assert.ok(result.includes('webdevelopment'));
114
- assert.ok(result.includes('webdev'));
115
- assert.ok(result.includes('web'));
122
+ const result = generatePrefixes('Web Development');
123
+
124
+ assert.ok(result.includes('web development'));
125
+ assert.ok(result.includes('webdevelopment'));
126
+ assert.ok(result.includes('webdev'));
127
+ assert.ok(result.includes('web'));
116
128
  });
117
129
 
118
130
  test('generatePrefixes: Open Source includes oss abbreviation', () => {
119
- const result = generatePrefixes('Open Source');
120
- assert.ok(result.includes('oss'));
131
+ const result = generatePrefixes('Open Source');
132
+
133
+ assert.ok(result.includes('oss'));
121
134
  });