@redpanda-data/docs-extensions-and-macros 5.51.1 → 5.52.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 +1 -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/rpk-docs/helpers/index.js +33 -36
|
@@ -334,6 +334,11 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
334
334
|
templateDescription,
|
|
335
335
|
templateBloblang,
|
|
336
336
|
writeFullDrafts,
|
|
337
|
+
// False when the connect repo provides the generated partials, config
|
|
338
|
+
// snippets, and Bloblang reference (see the modify-connect-tag-playbook
|
|
339
|
+
// extension). Drafts are still written, and still include the description
|
|
340
|
+
// partial the connect repo publishes for them.
|
|
341
|
+
writePartials = true,
|
|
337
342
|
// Explicit opt-in for the destructive description-partial orphan sweep.
|
|
338
343
|
// Off by default: see ORPHAN_SWEEP_MAX_FRACTION.
|
|
339
344
|
pruneOrphanedDescriptions = false,
|
|
@@ -458,7 +463,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
458
463
|
: path.join(outputRoot, 'components');
|
|
459
464
|
const configExamplesRoot = path.resolve(process.cwd(), 'modules/components/examples');
|
|
460
465
|
|
|
461
|
-
if (!writeFullDrafts) {
|
|
466
|
+
if (!writeFullDrafts && writePartials) {
|
|
462
467
|
fs.mkdirSync(fieldsOutRoot, { recursive: true });
|
|
463
468
|
fs.mkdirSync(examplesOutRoot, { recursive: true });
|
|
464
469
|
fs.mkdirSync(metadataOutRoot, { recursive: true });
|
|
@@ -512,7 +517,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
512
517
|
examplesOut = renderExamplesPartial(item);
|
|
513
518
|
}
|
|
514
519
|
|
|
515
|
-
if (fieldsOut.trim()) {
|
|
520
|
+
if (writePartials && fieldsOut.trim()) {
|
|
516
521
|
const fPath = path.join(fieldsOutRoot, type, `${name}.adoc`);
|
|
517
522
|
fs.mkdirSync(path.dirname(fPath), { recursive: true });
|
|
518
523
|
if (fs.existsSync(fPath)) {
|
|
@@ -532,7 +537,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
532
537
|
// gate, one list. The old denylist named only the two bloblang keys, so
|
|
533
538
|
// any other non-page data key (config today, anything added upstream
|
|
534
539
|
// tomorrow) leaked partials no page can include.
|
|
535
|
-
if (examplesOut.trim() && CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
540
|
+
if (writePartials && examplesOut.trim() && CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
536
541
|
const ePath = path.join(examplesOutRoot, type, `${name}.adoc`);
|
|
537
542
|
fs.mkdirSync(path.dirname(ePath), { recursive: true });
|
|
538
543
|
fs.writeFileSync(ePath, examplesOut);
|
|
@@ -542,7 +547,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
542
547
|
}
|
|
543
548
|
}
|
|
544
549
|
|
|
545
|
-
if (CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
550
|
+
if (writePartials && CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
546
551
|
// Render the metadata partial from the connector's description. Only
|
|
547
552
|
// written when the description contains a `== Metadata` section.
|
|
548
553
|
// Rendered inside the guard, not before it: the loop covers every
|
|
@@ -611,8 +616,8 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
611
616
|
visitedDescriptionPartials.add(`${partialTypeDir}/${name}`);
|
|
612
617
|
const dPath = path.join(descriptionOutRoot, partialTypeDir, `${name}.adoc`);
|
|
613
618
|
if (descriptionOut.trim()) {
|
|
614
|
-
fs.mkdirSync(path.dirname(dPath), { recursive: true });
|
|
615
|
-
if (fs.existsSync(dPath)) {
|
|
619
|
+
if (writePartials) fs.mkdirSync(path.dirname(dPath), { recursive: true });
|
|
620
|
+
if (writePartials && fs.existsSync(dPath)) {
|
|
616
621
|
// Same content-loss check the metadata partial ten lines up has
|
|
617
622
|
// had since #236, for the same reason: regeneration is
|
|
618
623
|
// authoritative, but a section disappearing from a published
|
|
@@ -637,7 +642,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
637
642
|
);
|
|
638
643
|
}
|
|
639
644
|
}
|
|
640
|
-
fs.writeFileSync(dPath, descriptionOut);
|
|
645
|
+
if (writePartials) fs.writeFileSync(dPath, descriptionOut);
|
|
641
646
|
// The partial now exists for this connector, so a drafted page can
|
|
642
647
|
// include its body instead of freezing the summary and description
|
|
643
648
|
// into the page. Gated on the file actually being written:
|
|
@@ -652,7 +657,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
652
657
|
if (typeof item.summary === 'string' && item.summary.trim()) {
|
|
653
658
|
item.hasDescriptionMeta = true;
|
|
654
659
|
}
|
|
655
|
-
if (!writeFullDrafts) {
|
|
660
|
+
if (!writeFullDrafts && writePartials) {
|
|
656
661
|
partialsWritten++;
|
|
657
662
|
partialFiles.push(path.relative(process.cwd(), dPath));
|
|
658
663
|
}
|
|
@@ -683,7 +688,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
683
688
|
descriptionReports.push({ connector: `${partialTypeDir}/${name}`, message: seqMsg });
|
|
684
689
|
console.warn(seqMsg);
|
|
685
690
|
}
|
|
686
|
-
} else if (fs.existsSync(dPath)) {
|
|
691
|
+
} else if (writePartials && fs.existsSync(dPath)) {
|
|
687
692
|
// The upstream description disappeared, but a previously generated
|
|
688
693
|
// partial is still on disk and an already published page may include
|
|
689
694
|
// it. Deleting the file would break that include, so blank it to a
|
|
@@ -732,6 +737,11 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
732
737
|
|
|
733
738
|
// typeDir is already computed at the beginning of the loop
|
|
734
739
|
|
|
740
|
+
// When the connect repo publishes the partials, the draft includes
|
|
741
|
+
// its availability, footnotes, and catalog data instead of freezing
|
|
742
|
+
// categories and the cgo requirement into the page.
|
|
743
|
+
item.connectPartials = !writePartials;
|
|
744
|
+
|
|
735
745
|
let content;
|
|
736
746
|
try {
|
|
737
747
|
content = compiledTemplate(item);
|
|
@@ -767,7 +777,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
767
777
|
}
|
|
768
778
|
|
|
769
779
|
// Bloblang function/method partials (only if includeBloblang is true)
|
|
770
|
-
if (options.includeBloblang) {
|
|
780
|
+
if (writePartials && options.includeBloblang) {
|
|
771
781
|
for (const { key, folder } of bloblangTypes) {
|
|
772
782
|
const items = dataObj[key];
|
|
773
783
|
if (!Array.isArray(items)) continue;
|
|
@@ -877,7 +887,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
877
887
|
// Common/Advanced config snippet YAMLs in modules/components/examples
|
|
878
888
|
const commonConfig = helpers.commonConfig;
|
|
879
889
|
const advancedConfig = helpers.advancedConfig;
|
|
880
|
-
for (const [type, items] of Object.entries(dataObj)) {
|
|
890
|
+
for (const [type, items] of writePartials ? Object.entries(dataObj) : []) {
|
|
881
891
|
if (!Array.isArray(items)) continue;
|
|
882
892
|
for (const item of items) {
|
|
883
893
|
if (!item.name || !item.config || !Array.isArray(item.config.children)) continue;
|
|
@@ -908,7 +918,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
908
918
|
// The sweep is destructive against published content, so collect the
|
|
909
919
|
// candidates first and check the blast radius before writing anything.
|
|
910
920
|
let orphanSweepSkipped = null;
|
|
911
|
-
if (!writeFullDrafts && fs.existsSync(descriptionOutRoot)) {
|
|
921
|
+
if (!writeFullDrafts && writePartials && fs.existsSync(descriptionOutRoot)) {
|
|
912
922
|
const orphans = [];
|
|
913
923
|
let partialsOnDisk = 0;
|
|
914
924
|
for (const typeDirName of fs.readdirSync(descriptionOutRoot)) {
|
|
@@ -966,7 +976,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
966
976
|
// Self-heal published page headers: pages are one-time drafts, so pages
|
|
967
977
|
// created before the template emitted :description: never got one.
|
|
968
978
|
let descriptionBackfill = { backfilled: [], skippedNoSummary: [] };
|
|
969
|
-
if (!writeFullDrafts) {
|
|
979
|
+
if (!writeFullDrafts && writePartials) {
|
|
970
980
|
descriptionBackfill = backfillPageDescriptions(dataObj);
|
|
971
981
|
}
|
|
972
982
|
|
|
@@ -24,6 +24,7 @@ function generateConnectorDiffJson(oldIndex, newIndex, opts = {}) {
|
|
|
24
24
|
type,
|
|
25
25
|
status: raw.status || '',
|
|
26
26
|
version: raw.version || raw.introducedInVersion || '',
|
|
27
|
+
summary: raw.summary || '',
|
|
27
28
|
description: raw.description || '',
|
|
28
29
|
requiresCgo: metadata.requiresCgo || false,
|
|
29
30
|
cloudOnly: metadata.cloudOnly || false,
|
|
@@ -399,7 +399,7 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
|
|
|
399
399
|
for (const comp of regularComponents) {
|
|
400
400
|
const typeLabel = comp.type.charAt(0).toUpperCase() + comp.type.slice(1)
|
|
401
401
|
const statusLabel = comp.status || '-'
|
|
402
|
-
let desc = comp.summary
|
|
402
|
+
let desc = comp.summary ? capToTwoSentences(comp.summary) : (comp.description ? capToTwoSentences(comp.description) : '// TODO: Add description')
|
|
403
403
|
|
|
404
404
|
if (comp.requiresCgo) {
|
|
405
405
|
const cgoNote = '\nNOTE: Requires a cgo-enabled binary. See the xref:install:index.adoc[installation guides] for details.'
|
|
@@ -1243,8 +1243,10 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1243
1243
|
// Main Processing: Handle the latest version (final iteration)
|
|
1244
1244
|
// ========================================================================
|
|
1245
1245
|
|
|
1246
|
-
|
|
1247
|
-
|
|
1246
|
+
// options.partials is false with --no-partials: the connect repo publishes
|
|
1247
|
+
// the generated partials, so this run only updates release-level content.
|
|
1248
|
+
const writePartials = options.partials !== false
|
|
1249
|
+
let partialsWritten = 0, partialFiles = []
|
|
1248
1250
|
const descriptionReports = []
|
|
1249
1251
|
const lostSectionWarnings = []
|
|
1250
1252
|
const styleRegressionWarnings = []
|
|
@@ -1256,7 +1258,10 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1256
1258
|
const { collectGeneratorReports } = require('./pr-summary-formatter.js')
|
|
1257
1259
|
const collect = (result) => collectGeneratorReports(result, { descriptionReports, lostSectionWarnings, styleRegressionWarnings })
|
|
1258
1260
|
|
|
1259
|
-
|
|
1261
|
+
if (!writePartials) {
|
|
1262
|
+
console.log('Skipping connector partials (--no-partials): the connect repo provides them.')
|
|
1263
|
+
} else try {
|
|
1264
|
+
console.log('Generating connector partials...')
|
|
1260
1265
|
const result = await generateRpcnConnectorDocs({
|
|
1261
1266
|
data: dataFile,
|
|
1262
1267
|
overrides: options.overrides,
|
|
@@ -1511,20 +1516,20 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
1511
1516
|
}
|
|
1512
1517
|
}
|
|
1513
1518
|
|
|
1514
|
-
// Publish
|
|
1519
|
+
// Publish the connector data to attachments. The Bloblang playground and
|
|
1520
|
+
// other site tools read this file, so it is published on every run, with
|
|
1521
|
+
// overrides merged in only when an overrides file still exists.
|
|
1515
1522
|
// IMPORTANT: This must run AFTER binary analysis and augmentation to include CGO-only connectors
|
|
1516
|
-
|
|
1523
|
+
{
|
|
1517
1524
|
try {
|
|
1518
|
-
const { mergeOverrides, resolveReferences } = require('./generate-rpcn-connector-docs.js')
|
|
1519
|
-
|
|
1520
1525
|
// Use the augmented newIndex which now includes CGO-only and cloud-only connectors
|
|
1521
1526
|
const mergedData = JSON.parse(JSON.stringify(newIndex))
|
|
1522
1527
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
+
if (options.overrides && fs.existsSync(options.overrides)) {
|
|
1529
|
+
const { mergeOverrides, resolveReferences } = require('./generate-rpcn-connector-docs.js')
|
|
1530
|
+
const ovObj = JSON.parse(fs.readFileSync(options.overrides, 'utf8'))
|
|
1531
|
+
mergeOverrides(mergedData, resolveReferences(ovObj, ovObj))
|
|
1532
|
+
}
|
|
1528
1533
|
|
|
1529
1534
|
const attachmentsRoot = path.resolve(process.cwd(), 'modules/components/attachments')
|
|
1530
1535
|
fs.mkdirSync(attachmentsRoot, { recursive: true })
|
|
@@ -2066,6 +2071,7 @@ async function handleRpcnConnectorDocs (options) {
|
|
|
2066
2071
|
templateDescription: options.templateDescription,
|
|
2067
2072
|
templateIntro: options.templateIntro,
|
|
2068
2073
|
writeFullDrafts: true,
|
|
2074
|
+
writePartials,
|
|
2069
2075
|
cgoOnly: binaryAnalysis?.cgoOnly || [],
|
|
2070
2076
|
cloudOnly: binaryAnalysis?.comparison?.cloudOnly || [],
|
|
2071
2077
|
csvMetadata
|
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
= {{{name}}}
|
|
2
2
|
// tag::single-source[]
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
{{!-- Drafts are written once and never regenerated, so they hold no
|
|
4
|
+
reference facts that change with the source. The type comes from the
|
|
5
|
+
page directory, the support level and Cloud availability from the
|
|
6
|
+
catalog data, and, when the connect repo publishes the partials
|
|
7
|
+
(connectPartials), the categories from its catalog.json and the status
|
|
8
|
+
and cgo flags from the description partial's meta region. --}}
|
|
9
|
+
{{#unless connectPartials}}
|
|
5
10
|
:categories: [{{join categories ", "}}]
|
|
11
|
+
{{/unless}}
|
|
6
12
|
{{#if hasDescriptionMeta}}
|
|
7
13
|
// tag::meta[]
|
|
8
14
|
{{{descriptionInclude this "meta"}}}
|
|
@@ -27,6 +33,10 @@
|
|
|
27
33
|
(cloud-docs .github/description-allowlist.txt). --}}
|
|
28
34
|
|
|
29
35
|
component_type_dropdown::[]
|
|
36
|
+
{{#if connectPartials}}
|
|
37
|
+
|
|
38
|
+
include::connect:components:partial$availability/{{typeDir}}/{{name}}.adoc[opts=optional]
|
|
39
|
+
{{/if}}
|
|
30
40
|
|
|
31
41
|
{{#if hasDescriptionPartial}}
|
|
32
42
|
{{{descriptionInclude this "body"}}}
|
|
@@ -58,6 +68,7 @@ include::components:example$advanced/{{typeDir}}/{{name}}.yaml[]
|
|
|
58
68
|
======
|
|
59
69
|
{{/if}}
|
|
60
70
|
|
|
71
|
+
{{#unless connectPartials}}
|
|
61
72
|
{{#if requiresCgo}}
|
|
62
73
|
== Requirements
|
|
63
74
|
|
|
@@ -71,6 +82,7 @@ You can either xref:install:prebuilt-binary.adoc[download a prebuilt cgo-enabled
|
|
|
71
82
|
{{/if}}
|
|
72
83
|
|
|
73
84
|
{{/if}}
|
|
85
|
+
{{/unless}}
|
|
74
86
|
{{#if (or this.config.children children)}}
|
|
75
87
|
include::connect:components:partial$fields/{{typeDir}}/{{name}}.adoc[]
|
|
76
88
|
{{/if}}
|
|
@@ -78,6 +90,12 @@ include::connect:components:partial$fields/{{typeDir}}/{{name}}.adoc[]
|
|
|
78
90
|
{{#if this.examples}}
|
|
79
91
|
include::connect:components:partial$examples/{{typeDir}}/{{name}}.adoc[]
|
|
80
92
|
{{/if}}
|
|
93
|
+
{{#if connectPartials}}
|
|
94
|
+
{{#if hasDescriptionPartial}}
|
|
95
|
+
|
|
96
|
+
{{{descriptionInclude this "footnotes"}}}
|
|
97
|
+
{{/if}}
|
|
98
|
+
{{/if}}
|
|
81
99
|
|
|
82
100
|
|
|
83
101
|
// end::single-source[]
|
|
@@ -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 ''
|
|
@@ -315,20 +315,43 @@ function sectionTitle(name) {
|
|
|
315
315
|
// Acronyms that should remain ALL CAPS
|
|
316
316
|
const acronyms = new Set([
|
|
317
317
|
'id', 'ip', 'api', 'url', 'uri', 'cpu', 'gpu', 'ram', 'io',
|
|
318
|
-
'tls', 'ssl', 'mtls', 'sasl', 'oauth', 'oidc', 'jwt',
|
|
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
|
|