@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/src/extract-tags.js
CHANGED
|
@@ -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
|
-
|
|
21
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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
|
-
|
|
23
|
+
assert.equal(extractFrontmatter('---\ntitle: foo\nno closing delimiter'), null);
|
|
23
24
|
});
|
|
24
25
|
|
|
25
26
|
test('extractFrontmatter: handles empty frontmatter block', () => {
|
|
26
|
-
|
|
27
|
+
assert.equal(extractFrontmatter('---\n\n---\nBody.'), '');
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
test('extractFrontmatter: does not include body content', () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
47
|
-
|
|
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
|
-
|
|
52
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
67
|
+
assert.deepEqual(parseListItems('title: My Post\n', 'tags'), []);
|
|
62
68
|
});
|
|
63
69
|
|
|
64
70
|
test('parseListItems: returns empty array for empty file', () => {
|
|
65
|
-
|
|
71
|
+
assert.deepEqual(parseListItems('', 'tags'), []);
|
|
66
72
|
});
|
|
67
73
|
|
|
68
74
|
test('parseListItems: handles multiple spaces before list items', () => {
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
98
|
+
assert.equal(generatePrefixes('Go'), 'go');
|
|
90
99
|
});
|
|
91
100
|
|
|
92
101
|
test('generatePrefixes: single word with no abbreviation returns lowercase string', () => {
|
|
93
|
-
|
|
102
|
+
assert.equal(generatePrefixes('JavaScript'), 'javascript');
|
|
94
103
|
});
|
|
95
104
|
|
|
96
105
|
test('generatePrefixes: multi-word returns array including concatenated form', () => {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
131
|
+
const result = generatePrefixes('Open Source');
|
|
132
|
+
|
|
133
|
+
assert.ok(result.includes('oss'));
|
|
121
134
|
});
|