@redpanda-data/docs-extensions-and-macros 5.51.2 → 5.53.0
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/bin/doc-tools-mcp.js +4 -0
- package/bin/doc-tools.js +119 -0
- package/bin/mcp-tools/rpcn-docs.js +1 -0
- package/extensions/REFERENCE.adoc +19 -4
- package/extensions/generate-rp-connect-categories.js +76 -25
- package/extensions/generate-rp-connect-info.js +150 -40
- package/extensions/modify-connect-tag-playbook.js +183 -17
- package/extensions/util/connect-catalog.js +153 -0
- package/macros/rp-connect-components.js +23 -8
- package/package.json +1 -1
- package/tools/lint-strings/diff.js +4 -1
- package/tools/lint-strings/engine.js +5 -0
- package/tools/lint-strings/fixtures/connect/lint_connect.go +1 -1
- package/tools/lint-strings/fixtures/connect/repo/go.mod.txt +5 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/cli/lint.go +19 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/azure/common.go +20 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/azure/docs.go +8 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/azure/input_thing.go +32 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/azure/output_thing.go +12 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/widget/bloblang.go +21 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/impl/widget/scanner.go +12 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/plugins/info.csv +3 -0
- package/tools/lint-strings/fixtures/connect/repo/internal/retries/retries.go +13 -0
- package/tools/lint-strings/go-index.js +1143 -0
- package/tools/lint-strings/index.js +28 -2
- package/tools/lint-strings/rules/common.js +10 -2
- package/tools/lint-strings/surfaces/connect.js +1484 -137
- package/tools/lint-strings/surfaces/rpk.js +1 -0
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +23 -13
- package/tools/redpanda-connect/report-delta.js +1 -0
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +19 -13
- package/tools/redpanda-connect/templates/connector.hbs +20 -2
- package/tools/redpanda-release-notes/generate-release-notes.js +242 -0
- package/tools/redpanda-release-notes/parse-release-body.js +241 -0
- package/tools/redpanda-release-notes/render-release-notes.js +70 -0
- package/tools/rpk-docs/helpers/index.js +32 -35
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
// Parse a Self-Managed rpchangelog release body into a structured, de-noised model.
|
|
2
|
+
//
|
|
3
|
+
// Input is the raw GitHub Release body a release captain pastes into the
|
|
4
|
+
// streaming-enterprise release (the output of `rpchangelog.py rel`): markdown
|
|
5
|
+
// with `## Features` / `## Bug Fixes` / `## Improvements` sections, each a list
|
|
6
|
+
// of `*` bullets carrying `by @author in [#PR](url)` suffixes and, sometimes,
|
|
7
|
+
// a leading `[#issue](url)` reference.
|
|
8
|
+
//
|
|
9
|
+
// This module does ONLY the deterministic, no-judgment work: split into
|
|
10
|
+
// sections, join wrapped bullet lines, strip source noise, unescape backticks,
|
|
11
|
+
// de-duplicate backport repeats, and normalize category order/case. It does NOT
|
|
12
|
+
// rewrite voice, add `Area::` labels, normalize units, phrase CVEs, or drop
|
|
13
|
+
// bodyless entries — those need editorial judgment and belong to the curation
|
|
14
|
+
// step that runs on the generated page.
|
|
15
|
+
|
|
16
|
+
const LOG_TAG = '[release-notes]';
|
|
17
|
+
|
|
18
|
+
// Canonical page order and headings: Features, then Bug fixes, then Improvements
|
|
19
|
+
// (improvements last — they are usually internal detail, least relevant to a
|
|
20
|
+
// reader scanning what changed). This matches rpchangelog's raw source order
|
|
21
|
+
// (Features -> Bug Fixes -> Improvements), so the reorder only recases the
|
|
22
|
+
// "Bug Fixes" heading. It deliberately differs from ADP, which puts fixes last.
|
|
23
|
+
const KIND_FEATURE = 'feature';
|
|
24
|
+
const KIND_IMPROVEMENT = 'improvement';
|
|
25
|
+
const KIND_FIX = 'fix';
|
|
26
|
+
|
|
27
|
+
const PAGE_ORDER = [KIND_FEATURE, KIND_FIX, KIND_IMPROVEMENT];
|
|
28
|
+
const PAGE_TITLE = {
|
|
29
|
+
[KIND_FEATURE]: 'Features',
|
|
30
|
+
[KIND_IMPROVEMENT]: 'Improvements',
|
|
31
|
+
[KIND_FIX]: 'Bug fixes',
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Maps a source `##` heading to a canonical kind by keyword, mirroring ADP's
|
|
36
|
+
* `sectionKind`. Returns null for a heading that is not one of the three
|
|
37
|
+
* release-note categories (for example rpchangelog's `--show-extra` `None` /
|
|
38
|
+
* `Unclear` sections), so the caller can ignore it.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} heading - The heading text after the `##` marker.
|
|
41
|
+
* @return {string|null} One of the KIND_* constants, or null if unrecognized.
|
|
42
|
+
*/
|
|
43
|
+
function sectionKind(heading) {
|
|
44
|
+
const h = heading.toLowerCase();
|
|
45
|
+
if (h.includes('feature')) return KIND_FEATURE;
|
|
46
|
+
if (h.includes('improvement')) return KIND_IMPROVEMENT;
|
|
47
|
+
if (h.includes('fix')) return KIND_FIX;
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Strips the trailing attribution blocks rpchangelog appends to every bullet:
|
|
53
|
+
* ` by @author` optionally followed by ` in [#PR](url)`, repeated once per PR
|
|
54
|
+
* when a change was aggregated from several PRs (the backport case). Works from
|
|
55
|
+
* the end of the string so it only ever removes trailing attribution, never a
|
|
56
|
+
* legitimate mid-sentence "by".
|
|
57
|
+
*
|
|
58
|
+
* @param {string} text - A single bullet's text.
|
|
59
|
+
* @return {string} The text with trailing attribution removed.
|
|
60
|
+
*/
|
|
61
|
+
function stripAttribution(text) {
|
|
62
|
+
const trailing = /\s+by\s+@[A-Za-z0-9][A-Za-z0-9._-]*(?:\s+in\s+\[#\d+\]\([^)]*\))?\s*$/i;
|
|
63
|
+
let out = text;
|
|
64
|
+
while (trailing.test(out)) {
|
|
65
|
+
out = out.replace(trailing, '');
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Strips a leading `[#issue](url)` reference that rpchangelog prepends when a
|
|
72
|
+
* PR body declared `Fixes: #N`. Only the leading reference is removed; links
|
|
73
|
+
* inside the sentence are left alone.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} text - A single bullet's text.
|
|
76
|
+
* @return {string} The text with a leading issue reference removed.
|
|
77
|
+
*/
|
|
78
|
+
function stripLeadingIssueRef(text) {
|
|
79
|
+
return text.replace(/^\s*\[#\d+\]\([^)]*\)\s+/, '');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Cleans one bullet: remove attribution and leading issue ref, unescape
|
|
84
|
+
* backslash-escaped backticks (`\`` -> `` ` ``) that survive from the PR body,
|
|
85
|
+
* collapse whitespace introduced by joining wrapped lines, and apply the two
|
|
86
|
+
* context-free mechanical normalizations — AsciiDoc brace escaping and the
|
|
87
|
+
* standard-mandated unit notation. House-style spelling and voice are left to
|
|
88
|
+
* the editorial curation step.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} raw - The joined raw bullet text.
|
|
91
|
+
* @return {string} The cleaned entry text.
|
|
92
|
+
*/
|
|
93
|
+
function cleanEntry(raw) {
|
|
94
|
+
let text = raw.replace(/\s+/g, ' ').trim();
|
|
95
|
+
text = stripLeadingIssueRef(text);
|
|
96
|
+
text = stripAttribution(text);
|
|
97
|
+
text = text.replace(/\\`/g, '`');
|
|
98
|
+
text = normalizeUnits(text);
|
|
99
|
+
text = escapeAsciiDocBraces(text);
|
|
100
|
+
return text.trim();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Rewrites the source's underscore data-size notation to the readable unit the
|
|
105
|
+
* standard requires (`4_MiB` -> `4 MiB`). Only a digit followed by `_` and a
|
|
106
|
+
* size unit is touched, so identifiers such as `s3_fifo` or `last_offset_delta`
|
|
107
|
+
* are left alone.
|
|
108
|
+
*
|
|
109
|
+
* @param {string} text - The entry text.
|
|
110
|
+
* @return {string} The text with data-size units normalized.
|
|
111
|
+
*/
|
|
112
|
+
function normalizeUnits(text) {
|
|
113
|
+
return text.replace(/(\d)_((?:[KMGT]i?)?B)\b/g, '$1 $2');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Escapes AsciiDoc attribute-reference braces so a literal `{id}` in the source
|
|
118
|
+
* renders as text rather than an (empty) attribute substitution. Braces already
|
|
119
|
+
* escaped are left as-is. AsciiDoc substitutes attributes even inside inline
|
|
120
|
+
* monospace, so this applies everywhere in the entry, including within backticks.
|
|
121
|
+
*
|
|
122
|
+
* @param {string} text - The entry text.
|
|
123
|
+
* @return {string} The text with unescaped braces escaped.
|
|
124
|
+
*/
|
|
125
|
+
function escapeAsciiDocBraces(text) {
|
|
126
|
+
return text.replace(/(?<!\\)([{}])/g, '\\$1');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parses a raw rpchangelog release body into de-noised, de-duplicated entries
|
|
131
|
+
* grouped by canonical category in page order.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} body - The raw GitHub Release body markdown.
|
|
134
|
+
* @return {{sections: Array<{kind: string, title: string, entries: string[]}>}}
|
|
135
|
+
* Sections in page order (Features, Bug fixes, Improvements); a category with
|
|
136
|
+
* no entries is omitted.
|
|
137
|
+
*/
|
|
138
|
+
function parseReleaseBody(body) {
|
|
139
|
+
if (typeof body !== 'string' || body.trim() === '') {
|
|
140
|
+
throw new Error('Release body is empty.');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Drop HTML comments up front (rpchangelog strips them from PR bodies, but a
|
|
144
|
+
// pasted body may still carry some).
|
|
145
|
+
const cleaned = body.replace(/<!--[\s\S]*?-->/g, '');
|
|
146
|
+
|
|
147
|
+
const buckets = {
|
|
148
|
+
[KIND_FEATURE]: [],
|
|
149
|
+
[KIND_IMPROVEMENT]: [],
|
|
150
|
+
[KIND_FIX]: [],
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
let currentKind = null; // kind of the section we are inside, or null to ignore
|
|
154
|
+
let current = null; // the bullet text being accumulated, or null between bullets
|
|
155
|
+
|
|
156
|
+
const flush = () => {
|
|
157
|
+
if (current !== null && currentKind && buckets[currentKind]) {
|
|
158
|
+
const entry = cleanEntry(current);
|
|
159
|
+
if (entry) buckets[currentKind].push(entry);
|
|
160
|
+
}
|
|
161
|
+
current = null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const lines = cleaned.split(/\r?\n/);
|
|
165
|
+
for (const line of lines) {
|
|
166
|
+
const heading = line.match(/^##\s+(.+?)\s*$/);
|
|
167
|
+
if (heading) {
|
|
168
|
+
flush();
|
|
169
|
+
currentKind = sectionKind(heading[1]);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// The changelog footer ends the release notes; nothing after it is an entry.
|
|
174
|
+
if (/^\s*\*\*(?:Partial|Full)\s+Changelog\*\*/i.test(line)) {
|
|
175
|
+
flush();
|
|
176
|
+
currentKind = null;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (currentKind === null) continue;
|
|
181
|
+
|
|
182
|
+
const bullet = line.match(/^\s*[*-]\s+(.*)$/);
|
|
183
|
+
if (bullet) {
|
|
184
|
+
flush();
|
|
185
|
+
current = bullet[1];
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (line.trim() === '') {
|
|
190
|
+
// Blank line: entries in this format are contiguous, so a blank marks the
|
|
191
|
+
// end of the current bullet rather than a continuation.
|
|
192
|
+
flush();
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// A non-blank, non-bullet line continues the current wrapped bullet.
|
|
197
|
+
if (current !== null) {
|
|
198
|
+
current += ' ' + line.trim();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
flush();
|
|
202
|
+
|
|
203
|
+
const sections = [];
|
|
204
|
+
for (const kind of PAGE_ORDER) {
|
|
205
|
+
const seen = new Set();
|
|
206
|
+
const entries = [];
|
|
207
|
+
for (const entry of buckets[kind]) {
|
|
208
|
+
// De-duplicate backport repeats: the same change aggregated from several
|
|
209
|
+
// PRs yields identical text once attribution is stripped.
|
|
210
|
+
const key = entry.toLowerCase();
|
|
211
|
+
if (seen.has(key)) continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
entries.push(entry);
|
|
214
|
+
}
|
|
215
|
+
if (entries.length > 0) {
|
|
216
|
+
sections.push({ kind, title: PAGE_TITLE[kind], entries });
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (sections.length === 0) {
|
|
221
|
+
console.warn(`${LOG_TAG} WARN: no Features/Improvements/Bug fixes entries found in the release body.`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { sections };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
parseReleaseBody,
|
|
229
|
+
// Exported for unit tests and reuse.
|
|
230
|
+
sectionKind,
|
|
231
|
+
stripAttribution,
|
|
232
|
+
stripLeadingIssueRef,
|
|
233
|
+
cleanEntry,
|
|
234
|
+
normalizeUnits,
|
|
235
|
+
escapeAsciiDocBraces,
|
|
236
|
+
PAGE_ORDER,
|
|
237
|
+
PAGE_TITLE,
|
|
238
|
+
KIND_FEATURE,
|
|
239
|
+
KIND_IMPROVEMENT,
|
|
240
|
+
KIND_FIX,
|
|
241
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Render a parsed release model into a candidate AsciiDoc release-notes section.
|
|
2
|
+
//
|
|
3
|
+
// Output is a `== vX.Y.Z (date)` section with `=== Features` / `=== Bug fixes`
|
|
4
|
+
// / `=== Improvements` subsections of plain `*` bullets. Entries are emitted as
|
|
5
|
+
// bullets carrying the de-noised source prose verbatim: this is a CANDIDATE for
|
|
6
|
+
// the curation step, which rewrites voice, converts bullets to `Area::`
|
|
7
|
+
// definition-list items, normalizes units, and phrases CVEs. This renderer adds
|
|
8
|
+
// no `Area::` labels and makes no editorial change.
|
|
9
|
+
|
|
10
|
+
const LOG_TAG = '[release-notes]';
|
|
11
|
+
|
|
12
|
+
const VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
13
|
+
const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Normalizes a tag or version to a bare `X.Y.Z` (strips a leading `v`).
|
|
17
|
+
*
|
|
18
|
+
* @param {string} version - A version or tag, such as `v26.2.3` or `26.2.3`.
|
|
19
|
+
* @return {string} The bare `X.Y.Z` version.
|
|
20
|
+
*/
|
|
21
|
+
function normalizeVersion(version) {
|
|
22
|
+
return String(version || '').trim().replace(/^v/i, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Renders one release section as AsciiDoc.
|
|
27
|
+
*
|
|
28
|
+
* @param {Object} options
|
|
29
|
+
* @param {string} options.version - The release version (`v26.2.3` or `26.2.3`).
|
|
30
|
+
* @param {string} options.date - The release date as `YYYY-MM-DD` (authored; the
|
|
31
|
+
* captain confirms it on the PR).
|
|
32
|
+
* @param {Array<{title: string, entries: string[]}>} options.sections - Sections
|
|
33
|
+
* in page order, as produced by parseReleaseBody.
|
|
34
|
+
* @return {string} The AsciiDoc section, ending with a single trailing newline.
|
|
35
|
+
* @throws {Error} If the version or date is malformed, or there are no sections.
|
|
36
|
+
*/
|
|
37
|
+
function renderReleaseSection({ version, date, sections }) {
|
|
38
|
+
const v = normalizeVersion(version);
|
|
39
|
+
if (!VERSION_RE.test(v)) {
|
|
40
|
+
throw new Error(`Invalid version: ${JSON.stringify(version)}. Expected X.Y.Z (optionally v-prefixed).`);
|
|
41
|
+
}
|
|
42
|
+
if (!DATE_RE.test(String(date || '').trim())) {
|
|
43
|
+
throw new Error(`Invalid date: ${JSON.stringify(date)}. Expected YYYY-MM-DD.`);
|
|
44
|
+
}
|
|
45
|
+
if (!Array.isArray(sections) || sections.length === 0) {
|
|
46
|
+
throw new Error('No release-note sections to render.');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const lines = [`== v${v} (${String(date).trim()})`, ''];
|
|
50
|
+
for (const section of sections) {
|
|
51
|
+
if (!section.entries || section.entries.length === 0) continue;
|
|
52
|
+
lines.push(`=== ${section.title}`, '');
|
|
53
|
+
for (const entry of section.entries) {
|
|
54
|
+
lines.push(`* ${entry}`);
|
|
55
|
+
}
|
|
56
|
+
lines.push('');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Trim the trailing blank line to exactly one terminating newline.
|
|
60
|
+
while (lines.length > 0 && lines[lines.length - 1] === '') lines.pop();
|
|
61
|
+
return lines.join('\n') + '\n';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
renderReleaseSection,
|
|
66
|
+
normalizeVersion,
|
|
67
|
+
VERSION_RE,
|
|
68
|
+
DATE_RE,
|
|
69
|
+
LOG_TAG,
|
|
70
|
+
};
|
|
@@ -305,9 +305,9 @@ function isPluginCommand(commandPath, pluginVersions) {
|
|
|
305
305
|
}
|
|
306
306
|
|
|
307
307
|
/**
|
|
308
|
-
* Convert ALL CAPS section name to
|
|
308
|
+
* Convert ALL CAPS section name to sentence case for display
|
|
309
309
|
* @param {string} name - ALL CAPS section name
|
|
310
|
-
* @returns {string}
|
|
310
|
+
* @returns {string} Sentence case name
|
|
311
311
|
*/
|
|
312
312
|
function sectionTitle(name) {
|
|
313
313
|
if (!name) return ''
|
|
@@ -317,18 +317,41 @@ function sectionTitle(name) {
|
|
|
317
317
|
'id', 'ip', 'api', 'url', 'uri', 'cpu', 'gpu', 'ram', 'io',
|
|
318
318
|
'tls', 'ssl', 'mtls', 'sasl', 'sso', 'oauth', 'oidc', 'jwt',
|
|
319
319
|
'json', 'yaml', 'xml', 'csv', 'http', 'https', 'grpc', 'rpc',
|
|
320
|
-
'aws', 'gcp', '
|
|
320
|
+
'aws', 'gcp', 's3', 'eos'
|
|
321
321
|
])
|
|
322
322
|
|
|
323
|
-
//
|
|
323
|
+
// Proper nouns that keep their initial capital
|
|
324
324
|
const properNouns = new Set([
|
|
325
|
-
'redpanda', 'kafka', 'kubernetes', '
|
|
325
|
+
'redpanda', 'kafka', 'kubernetes', 'azure'
|
|
326
326
|
])
|
|
327
327
|
|
|
328
|
+
// Multi-word proper nouns, capitalized only as the full phrase
|
|
329
|
+
// ("SCHEMA REGISTRY" -> "Schema Registry", but "SCHEMA FORMAT" -> "Schema format")
|
|
330
|
+
const properPhrases = [['schema', 'registry']]
|
|
331
|
+
|
|
332
|
+
const capitalize = word => word.charAt(0).toUpperCase() + word.slice(1)
|
|
333
|
+
|
|
328
334
|
// Split on spaces/hyphens but keep & as its own token
|
|
329
335
|
// "PRODUCER ID & EPOCH" -> ["producer", "id", "&", "epoch"]
|
|
330
336
|
const words = name.toLowerCase().split(/[\s-]+/)
|
|
331
337
|
|
|
338
|
+
const inPhrase = new Set()
|
|
339
|
+
for (const phrase of properPhrases) {
|
|
340
|
+
for (let i = 0; i + phrase.length <= words.length; i++) {
|
|
341
|
+
if (phrase.every((part, j) => words[i + j] === part)) {
|
|
342
|
+
phrase.forEach((_, j) => inPhrase.add(i + j))
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Sentence case: capitalize the first word, acronyms, and proper nouns;
|
|
348
|
+
// lowercase everything else
|
|
349
|
+
const formatPart = (part, isFirst, index) => {
|
|
350
|
+
if (acronyms.has(part)) return part.toUpperCase()
|
|
351
|
+
if (isFirst || properNouns.has(part) || inPhrase.has(index)) return capitalize(part)
|
|
352
|
+
return part
|
|
353
|
+
}
|
|
354
|
+
|
|
332
355
|
return words.map((word, index) => {
|
|
333
356
|
// Preserve ampersand as-is
|
|
334
357
|
if (word === '&') {
|
|
@@ -337,38 +360,12 @@ function sectionTitle(name) {
|
|
|
337
360
|
|
|
338
361
|
// Handle words with slashes (e.g., "enabled/disabled")
|
|
339
362
|
if (word.includes('/')) {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return part.toUpperCase()
|
|
344
|
-
}
|
|
345
|
-
if (index === 0 && partIndex === 0) {
|
|
346
|
-
return part.charAt(0).toUpperCase() + part.slice(1)
|
|
347
|
-
}
|
|
348
|
-
if (properNouns.has(part)) {
|
|
349
|
-
return part.charAt(0).toUpperCase() + part.slice(1)
|
|
350
|
-
}
|
|
351
|
-
return part
|
|
352
|
-
}).join('/')
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
// Acronyms: ALL CAPS
|
|
356
|
-
if (acronyms.has(word)) {
|
|
357
|
-
return word.toUpperCase()
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// First word: always capitalize
|
|
361
|
-
if (index === 0) {
|
|
362
|
-
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Proper nouns: capitalize
|
|
366
|
-
if (properNouns.has(word)) {
|
|
367
|
-
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
363
|
+
return word.split('/')
|
|
364
|
+
.map((part, partIndex) => formatPart(part, index === 0 && partIndex === 0, index))
|
|
365
|
+
.join('/')
|
|
368
366
|
}
|
|
369
367
|
|
|
370
|
-
|
|
371
|
-
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
368
|
+
return formatPart(word, index === 0, index)
|
|
372
369
|
}).join(' ')
|
|
373
370
|
}
|
|
374
371
|
|