@redpanda-data/docs-extensions-and-macros 5.55.0 → 5.56.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.js +315 -7
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +37 -3
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +90 -4
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +2 -2
- package/extensions/url-to-xref.js +92 -22
- package/extensions/validate-attributes.js +10 -1
- package/package.json +2 -1
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +29 -8
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +19 -24
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +1 -1
|
@@ -60,6 +60,9 @@ function rulesFor (surface) {
|
|
|
60
60
|
* @param {Set<string>} [options.reviewedFingerprints] - Diff mode: skip
|
|
61
61
|
* declarations (and removals) whose fingerprint is in this set, so a PR
|
|
62
62
|
* review sees each string once across pushes
|
|
63
|
+
* @param {string[]} [options.reviewerPassages] - Diff mode: passages from
|
|
64
|
+
* human review comments. A changed line found in one counts as written by
|
|
65
|
+
* a reviewer, and a declaration whose changed lines all are is skipped
|
|
63
66
|
* @param {Function} [options.log] - Progress logger (stderr by default)
|
|
64
67
|
* @returns {Object} { findings, summary, unsupported_surfaces }
|
|
65
68
|
*/
|
|
@@ -71,6 +74,7 @@ function lintStrings (options) {
|
|
|
71
74
|
skipRules = [],
|
|
72
75
|
onlyRules = null,
|
|
73
76
|
reviewedFingerprints = null,
|
|
77
|
+
reviewerPassages = null,
|
|
74
78
|
log = (msg) => process.stderr.write(`${msg}\n`)
|
|
75
79
|
} = options
|
|
76
80
|
|
|
@@ -93,6 +97,8 @@ function lintStrings (options) {
|
|
|
93
97
|
const reviewed = reviewedFingerprints || new Set()
|
|
94
98
|
const pending = []
|
|
95
99
|
let alreadyReviewed = 0
|
|
100
|
+
const reviewerWritten = []
|
|
101
|
+
const headLines = new Map()
|
|
96
102
|
let removal = { rawFiles: [], declarations: [] }
|
|
97
103
|
const skipped = []
|
|
98
104
|
let unverifiable = 0
|
|
@@ -138,6 +144,17 @@ function lintStrings (options) {
|
|
|
138
144
|
alreadyReviewed++
|
|
139
145
|
continue
|
|
140
146
|
}
|
|
147
|
+
if (reviewerPassages && reviewerPassages.length) {
|
|
148
|
+
const changed = changedLinesOf(repoPath, decl, files.get(decl.file), headLines)
|
|
149
|
+
const own = passagesInDeclaration(decl, reviewerPassages)
|
|
150
|
+
if (changed.length > 0 && changed.every((l) => writtenByReviewer(l.text, own))) {
|
|
151
|
+
// No wording review, but the published-content check still
|
|
152
|
+
// sees it: a reviewer's wording can describe a behavior change
|
|
153
|
+
// that makes another page wrong.
|
|
154
|
+
reviewerWritten.push(pendingEntry(decl))
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
}
|
|
141
158
|
declarations.push(decl)
|
|
142
159
|
if (decl.meta && decl.meta.unverifiable) unverifiable++
|
|
143
160
|
const detail = surface.reviewContext ? surface.reviewContext(decl) : null
|
|
@@ -181,6 +198,8 @@ function lintStrings (options) {
|
|
|
181
198
|
if (diffBase) {
|
|
182
199
|
merged.declarations = pending
|
|
183
200
|
merged.summary.alreadyReviewed = alreadyReviewed
|
|
201
|
+
merged.summary.reviewerWritten = reviewerWritten.length
|
|
202
|
+
merged.summary.reviewerWrittenDeclarations = reviewerWritten
|
|
184
203
|
}
|
|
185
204
|
merged.summary.removedDeclarations = removal.declarations.map(pendingEntry)
|
|
186
205
|
// Declarations whose text has gaps the extractor could not evaluate
|
|
@@ -468,6 +487,9 @@ function formatHuman (result) {
|
|
|
468
487
|
if (summary.alreadyReviewed) {
|
|
469
488
|
lines.push(`Already reviewed on an earlier push (skipped): ${summary.alreadyReviewed}`)
|
|
470
489
|
}
|
|
490
|
+
if (summary.reviewerWritten) {
|
|
491
|
+
lines.push(`Text a human reviewer suggested on the PR (skipped): ${summary.reviewerWritten}`)
|
|
492
|
+
}
|
|
471
493
|
for (const decl of summary.removedDeclarations || []) {
|
|
472
494
|
lines.push(`Removed or renamed: ${decl.name} (${decl.surface}, ${decl.file})`)
|
|
473
495
|
}
|
|
@@ -520,6 +542,102 @@ function readFingerprints (file) {
|
|
|
520
542
|
return new Set(text.split(/[\s,]+/).filter((token) => /^[0-9a-f]{16}$/.test(token)))
|
|
521
543
|
}
|
|
522
544
|
|
|
545
|
+
// Shorter passages are labels, lead-ins ("Suggest replacing this with:") or
|
|
546
|
+
// fragments that occur in many strings, so matching on them would skip
|
|
547
|
+
// declarations nobody reviewed.
|
|
548
|
+
const MIN_REVIEWER_PASSAGE = 40
|
|
549
|
+
|
|
550
|
+
function normalizeText (text) {
|
|
551
|
+
return String(text == null ? '' : text).replace(/\s+/g, ' ').trim()
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Passages from human review comments that a declaration can be matched
|
|
556
|
+
* against: the body of every ```suggestion block, and every prose paragraph
|
|
557
|
+
* outside a code block. Whitespace is collapsed so a paragraph the author
|
|
558
|
+
* re-wrapped to the file's line length still matches.
|
|
559
|
+
*/
|
|
560
|
+
function reviewerPassagesFrom (bodies) {
|
|
561
|
+
const passages = new Set()
|
|
562
|
+
for (const body of Array.isArray(bodies) ? bodies : []) {
|
|
563
|
+
if (typeof body !== 'string') continue
|
|
564
|
+
const chunks = [...body.matchAll(/```suggestion[^\n]*\n([\s\S]*?)```/g)].map((m) => m[1])
|
|
565
|
+
chunks.push(...body.replace(/```[\s\S]*?(```|$)/g, '\n\n').split(/\n\s*\n/))
|
|
566
|
+
for (const chunk of chunks) {
|
|
567
|
+
const text = normalizeText(chunk)
|
|
568
|
+
if (text.length >= MIN_REVIEWER_PASSAGE) passages.add(text)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
return [...passages]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// A changed line shorter than this ("per topic.", "For example,") occurs in
|
|
575
|
+
// too many paragraphs to say who wrote it, so it never counts as a
|
|
576
|
+
// reviewer's. That errs toward reviewing.
|
|
577
|
+
const MIN_REVIEWER_LINE = 12
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* The lines this PR changed inside a declaration's span, as HEAD has them.
|
|
581
|
+
* Lines with no letters or digits (a closing backtick, a blank line) say
|
|
582
|
+
* nothing about who wrote the string and are left out.
|
|
583
|
+
*/
|
|
584
|
+
function changedLinesOf (repoPath, decl, lineSet, cache) {
|
|
585
|
+
if (!lineSet) return []
|
|
586
|
+
if (!cache.has(decl.file)) {
|
|
587
|
+
let lines = null
|
|
588
|
+
try {
|
|
589
|
+
lines = fs.readFileSync(path.join(repoPath, decl.file), 'utf8').split('\n')
|
|
590
|
+
} catch {}
|
|
591
|
+
cache.set(decl.file, lines)
|
|
592
|
+
}
|
|
593
|
+
const lines = cache.get(decl.file)
|
|
594
|
+
if (!lines) return []
|
|
595
|
+
const out = []
|
|
596
|
+
for (const [start, end] of spansOf(decl)) {
|
|
597
|
+
if (start == null || end == null) continue
|
|
598
|
+
for (let n = start; n <= end; n++) {
|
|
599
|
+
if (!lineSet.has(n) || n > lines.length) continue
|
|
600
|
+
const text = normalizeText(lines[n - 1])
|
|
601
|
+
if (/[A-Za-z0-9]/.test(text)) out.push({ line: n, text })
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
return out
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
* The passages a declaration contains whole. A reviewer's proposed wording
|
|
609
|
+
* lands in the string as written, but a comment that quotes the string to
|
|
610
|
+
* criticize it ("Please change this: ...") carries a lead-in the string
|
|
611
|
+
* does not have, so it never counts as the reviewer writing the string.
|
|
612
|
+
*/
|
|
613
|
+
function passagesInDeclaration (decl, passages) {
|
|
614
|
+
const forms = [normalizeText(decl.string), normalizeText(decl.declaration_text)].filter(Boolean)
|
|
615
|
+
return passages.filter((passage) => forms.some((form) => form.includes(passage)))
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* True when a human reviewer wrote this line on the PR: it appears whole in
|
|
620
|
+
* one of their passages. Matching per line, not per string, means an author
|
|
621
|
+
* who keeps a reviewer's paragraph and rewrites the next one still gets the
|
|
622
|
+
* new paragraph reviewed (streaming-enterprise PR 461 is the case this
|
|
623
|
+
* exists for: the review argued with help text applied from a reviewer).
|
|
624
|
+
*/
|
|
625
|
+
function writtenByReviewer (line, passages) {
|
|
626
|
+
return line.length >= MIN_REVIEWER_LINE && passages.some((passage) => passage.includes(line))
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Read a --reviewer-comments file: a JSON array of comment bodies. A missing
|
|
631
|
+
* or unparseable file yields no passages, so it means "review everything".
|
|
632
|
+
*/
|
|
633
|
+
function readReviewerComments (file) {
|
|
634
|
+
try {
|
|
635
|
+
return reviewerPassagesFrom(JSON.parse(fs.readFileSync(file, 'utf8')))
|
|
636
|
+
} catch {
|
|
637
|
+
return []
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
523
641
|
/**
|
|
524
642
|
* CLI entry point shared by bin/doc-tools.js and direct invocation
|
|
525
643
|
* (node tools/lint-strings --repo <path> ...).
|
|
@@ -536,7 +654,8 @@ function runCli (options) {
|
|
|
536
654
|
diffBase: options.diff || null,
|
|
537
655
|
skipRules: options.skipRules ? String(options.skipRules).split(',').map((s) => s.trim()).filter(Boolean) : [],
|
|
538
656
|
onlyRules: options.onlyRules ? String(options.onlyRules).split(',').map((s) => s.trim()).filter(Boolean) : null,
|
|
539
|
-
reviewedFingerprints: options.reviewed ? readFingerprints(options.reviewed) : null
|
|
657
|
+
reviewedFingerprints: options.reviewed ? readFingerprints(options.reviewed) : null,
|
|
658
|
+
reviewerPassages: options.reviewerComments ? readReviewerComments(options.reviewerComments) : null
|
|
540
659
|
})
|
|
541
660
|
} catch (err) {
|
|
542
661
|
console.error(`Error: ${err.message}`)
|
|
@@ -555,7 +674,7 @@ function runCli (options) {
|
|
|
555
674
|
process.exitCode = options.strict && result.summary.errors > 0 ? 1 : 0
|
|
556
675
|
}
|
|
557
676
|
|
|
558
|
-
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints, pageContext }
|
|
677
|
+
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints, reviewerPassagesFrom, readReviewerComments, pageContext }
|
|
559
678
|
|
|
560
679
|
// Direct usage: node tools/lint-strings --repo <path> [--surface a,b]
|
|
561
680
|
// [--diff <base>] [--format json|human] [--strict]
|
|
@@ -571,10 +690,21 @@ if (require.main === module) {
|
|
|
571
690
|
else if (arg === '--skip-rules') options.skipRules = args[++i]
|
|
572
691
|
else if (arg === '--only-rules') options.onlyRules = args[++i]
|
|
573
692
|
else if (arg === '--reviewed') options.reviewed = args[++i]
|
|
693
|
+
else if (arg === '--reviewer-comments') {
|
|
694
|
+
// A present flag with no operand must not act like an absent one, or
|
|
695
|
+
// `--reviewer-comments --strict` would read --strict as the file and
|
|
696
|
+
// quietly turn strict mode off.
|
|
697
|
+
const operand = args[i + 1]
|
|
698
|
+
if (!operand || operand.startsWith('-')) {
|
|
699
|
+
console.error('--reviewer-comments requires a file path')
|
|
700
|
+
process.exit(2)
|
|
701
|
+
}
|
|
702
|
+
options.reviewerComments = args[++i]
|
|
703
|
+
}
|
|
574
704
|
else if (arg === '--strict') options.strict = true
|
|
575
705
|
else {
|
|
576
706
|
console.error(`Unknown argument: ${arg}`)
|
|
577
|
-
console.error('Usage: node tools/lint-strings --repo <path> [--surface a,b] [--diff <base>] [--format json|human] [--skip-rules a,b] [--only-rules a,b] [--reviewed <file>] [--strict]')
|
|
707
|
+
console.error('Usage: node tools/lint-strings --repo <path> [--surface a,b] [--diff <base>] [--format json|human] [--skip-rules a,b] [--only-rules a,b] [--reviewed <file>] [--reviewer-comments <file>] [--strict]')
|
|
578
708
|
process.exit(2)
|
|
579
709
|
}
|
|
580
710
|
}
|
|
@@ -105,4 +105,169 @@ const VERBATIM_ASCIIDOC_RULES = [
|
|
|
105
105
|
}
|
|
106
106
|
]
|
|
107
107
|
|
|
108
|
-
|
|
108
|
+
/**
|
|
109
|
+
* AsciiDoc preprocessor conditionals in doc strings that publish to two
|
|
110
|
+
* audiences. Connect spec strings are published on the self-managed Connect
|
|
111
|
+
* docs and on the Redpanda Cloud docs, which set the `env-cloud` attribute,
|
|
112
|
+
* and each Connect binary renders the same strings as Markdown with the
|
|
113
|
+
* attribute set only in the Cloud binaries. An author who wraps text whose
|
|
114
|
+
* behavior differs in Cloud has one convention:
|
|
115
|
+
*
|
|
116
|
+
* - `env-cloud` is the only attribute;
|
|
117
|
+
* - block form only: `ifdef::env-cloud[]` or `ifndef::env-cloud[]` alone
|
|
118
|
+
* on a line at column 0, the whole paragraph inside, `endif::[]` alone on
|
|
119
|
+
* a line, a blank line before the opening directive and after the closing
|
|
120
|
+
* one (unless the string starts or ends there);
|
|
121
|
+
* - nothing after the closing bracket, not even whitespace: the Markdown
|
|
122
|
+
* converter in the Connect binaries only treats a line ending in `]` as
|
|
123
|
+
* a directive;
|
|
124
|
+
* - never the single-line form `ifndef::env-cloud[text]`;
|
|
125
|
+
* - never in a Summary, which renders as one line, or in an example
|
|
126
|
+
* summary;
|
|
127
|
+
* - never inside a code fence;
|
|
128
|
+
* - never `ifndef::env-cloud[]` inside `ifdef::env-cloud[]` or the
|
|
129
|
+
* reverse, whose inner text renders for neither audience.
|
|
130
|
+
*
|
|
131
|
+
* Links to pages the other site lacks are handled by the renderers, so a
|
|
132
|
+
* conditional is only for behavior that differs. Not part of
|
|
133
|
+
* VERBATIM_ASCIIDOC_RULES: a surface that publishes to both audiences opts
|
|
134
|
+
* in (connect does); the other verbatim surfaces carry audience conditionals
|
|
135
|
+
* as data, not in the string.
|
|
136
|
+
*/
|
|
137
|
+
const CONDITIONAL_ATTRIBUTE = 'env-cloud'
|
|
138
|
+
const CONDITIONAL_WHY = 'The Cloud docs and the Cloud binaries set env-cloud, and the text inside must read as a whole paragraph in both outputs. See docs/README.md in the connect repo.'
|
|
139
|
+
const CONDITIONAL_TOKEN = /\b(?:ifdef|ifndef|ifeval|endif)::/
|
|
140
|
+
// A directive on its own line: optional indent (reported), kind, attribute
|
|
141
|
+
// list, bracketed text, then optional trailing whitespace (reported).
|
|
142
|
+
const CONDITIONAL_LINE = /^(\s*)(ifdef|ifndef|ifeval|endif)::([^\s[\]]*)\[(.*)\](\s*)$/
|
|
143
|
+
// Listing and literal delimiters, and Markdown fences with an optional
|
|
144
|
+
// language tag (```yaml). Tracked so a directive inside one is reported; the
|
|
145
|
+
// preprocessor still evaluates it there.
|
|
146
|
+
const CODE_FENCE = /^(?:(-{4,}|\.{4,})|(`{3,})([\w.+#-]*))[ \t]*$/
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Which summary a declaration is, or null. Summaries render as one line, and
|
|
150
|
+
* connect's docs test rejects a conditional in any JSON `summary` key, which
|
|
151
|
+
* includes example summaries.
|
|
152
|
+
*/
|
|
153
|
+
function summaryKind (decl) {
|
|
154
|
+
const meta = decl.meta || {}
|
|
155
|
+
if (meta.kind === 'summary' || (meta.kind === 'bloblang' && meta.part === 'summary')) return 'a Summary'
|
|
156
|
+
if (meta.kind === 'example-summary' || meta.kind === 'bloblang-example') return 'an example summary'
|
|
157
|
+
return null
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Advance the code-fence state over one fence line. An AsciiDoc block closes
|
|
162
|
+
* only on a delimiter of the same character and length; a Markdown fence
|
|
163
|
+
* closes on a bare fence at least as long as the opener.
|
|
164
|
+
*/
|
|
165
|
+
function stepFence (fence, match) {
|
|
166
|
+
const [, adoc, ticks, lang] = match
|
|
167
|
+
if (fence === null) return adoc ? { char: adoc[0], length: adoc.length } : { char: '`', length: ticks.length }
|
|
168
|
+
if (adoc) return fence.char === adoc[0] && fence.length === adoc.length ? null : fence
|
|
169
|
+
return fence.char === '`' && !lang && ticks.length >= fence.length ? null : fence
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const CONDITIONAL_RULE = {
|
|
173
|
+
name: 'conditional',
|
|
174
|
+
description: 'AsciiDoc conditional that is not a well-formed env-cloud block',
|
|
175
|
+
severity: 'warning',
|
|
176
|
+
check: (decl) => {
|
|
177
|
+
const text = decl.string || ''
|
|
178
|
+
if (!text || !CONDITIONAL_TOKEN.test(text)) return []
|
|
179
|
+
// CLI help is not AsciiDoc; the surface marks those declarations.
|
|
180
|
+
if (decl.convention && decl.convention.verbatim_asciidoc === false) return []
|
|
181
|
+
const issues = []
|
|
182
|
+
const say = (what, severity) => issues.push({ message: `${what} ${CONDITIONAL_WHY}`, ...(severity ? { severity } : {}) })
|
|
183
|
+
|
|
184
|
+
const summary = summaryKind(decl)
|
|
185
|
+
if (summary) {
|
|
186
|
+
say(`Contains an AsciiDoc conditional in ${summary}. A summary renders as one line, so it cannot hold a conditional block; keep the summary the same for both audiences and put the differing paragraph in the Description.`)
|
|
187
|
+
return issues
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lines = text.split('\n')
|
|
191
|
+
const open = []
|
|
192
|
+
let fence = null
|
|
193
|
+
for (let i = 0; i < lines.length; i++) {
|
|
194
|
+
const line = lines[i]
|
|
195
|
+
const fenceMatch = line.match(CODE_FENCE)
|
|
196
|
+
if (fenceMatch) {
|
|
197
|
+
fence = stepFence(fence, fenceMatch)
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
const m = line.match(CONDITIONAL_LINE)
|
|
201
|
+
if (!m) {
|
|
202
|
+
if (CONDITIONAL_TOKEN.test(line)) {
|
|
203
|
+
say(`"${line.trim().slice(0, 60)}" is not a directive on a line of its own at column 0. Put ifdef::env-cloud[], ifndef::env-cloud[] and endif::[] each on their own line with nothing before them.`)
|
|
204
|
+
}
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
const [, indent, kind, attribute, body, trailing] = m
|
|
208
|
+
const label = `${kind}::${attribute}[${body}]`
|
|
209
|
+
// What is between the brackets decides how Asciidoctor reads the line.
|
|
210
|
+
const bracket = body === '' ? 'empty' : body.trim() === '' ? 'whitespace' : 'text'
|
|
211
|
+
if (fence !== null) {
|
|
212
|
+
// Reported on its own: the form checks below assume prose around
|
|
213
|
+
// the block, and a fence line is not prose.
|
|
214
|
+
say(`${label} is inside a code fence. Keep conditionals out of code fences; wrap the whole paragraph or block instead.`)
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
if (indent) {
|
|
218
|
+
// Asciidoctor and the Connect Markdown converter both read an
|
|
219
|
+
// indented directive as text, so it neither opens nor closes a block
|
|
220
|
+
// here either: the never-closed and no-matching-opener errors below
|
|
221
|
+
// then report the block it leaves unbalanced.
|
|
222
|
+
say(`${label} is indented. A directive starts at column 0, or AsciiDoc and the Connect binaries treat it as text and it does not open or close a block.`)
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
if (trailing) {
|
|
226
|
+
say(`${label} has trailing whitespace after the closing bracket. The Connect binaries only treat a line ending in ] as a directive, so CLI readers would see the directive and the text for the other audience. Remove everything after the ].`, 'error')
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (kind === 'ifeval') {
|
|
230
|
+
say(`${label} uses ifeval. env-cloud is the only attribute a doc string may test, with ifdef::env-cloud[] or ifndef::env-cloud[].`)
|
|
231
|
+
open.push({ label, line: i })
|
|
232
|
+
continue
|
|
233
|
+
}
|
|
234
|
+
if (kind === 'endif') {
|
|
235
|
+
if (attribute && attribute !== CONDITIONAL_ATTRIBUTE) say(`${label} names "${attribute}". env-cloud is the only attribute; close the block with endif::[].`)
|
|
236
|
+
if (bracket !== 'empty') {
|
|
237
|
+
// Asciidoctor rejects an endif with anything in its brackets and
|
|
238
|
+
// leaves the block open, so it does not close one here either: the
|
|
239
|
+
// never-closed error below then reports the block it leaves open.
|
|
240
|
+
say(`${label} has ${bracket === 'text' ? 'text' : 'whitespace'} inside its brackets. Asciidoctor rejects it and leaves the block open, so the text after it is hidden from one audience. Write endif::[] with nothing between the brackets.`, 'error')
|
|
241
|
+
} else if (open.length === 0) say(`${label} has no matching ifdef::env-cloud[] or ifndef::env-cloud[] before it.`, 'error')
|
|
242
|
+
else open.pop()
|
|
243
|
+
if (i < lines.length - 1 && lines[i + 1].trim() !== '') say(`${label} needs a blank line after it, so the paragraph inside ends where the block ends.`)
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
// ifdef / ifndef
|
|
247
|
+
if (attribute !== CONDITIONAL_ATTRIBUTE) say(`${label} tests "${attribute}". env-cloud is the only attribute a doc string may test.`)
|
|
248
|
+
if (bracket === 'whitespace') {
|
|
249
|
+
// Asciidoctor reads any bracket content, whitespace included, as the
|
|
250
|
+
// single-line form, which opens no block: the text that follows shows
|
|
251
|
+
// for both audiences. So it is not pushed as an opener, and the endif
|
|
252
|
+
// that follows reports no matching opener.
|
|
253
|
+
say(`${label} has whitespace inside its brackets. Asciidoctor then reads it as the single-line form, which opens no block, so the text after it shows for both audiences. Write ${kind}::${attribute}[] with nothing between the brackets.`, 'error')
|
|
254
|
+
} else if (bracket === 'text') {
|
|
255
|
+
say(`${label} is the single-line form. Use the block form: the directive alone on its line, the whole paragraph inside, then endif::[] on its own line.`)
|
|
256
|
+
} else {
|
|
257
|
+
if (attribute === CONDITIONAL_ATTRIBUTE) {
|
|
258
|
+
const opposite = kind === 'ifdef' ? 'ifndef' : 'ifdef'
|
|
259
|
+
const outer = open.find((o) => o.kind === opposite && o.attribute === CONDITIONAL_ATTRIBUTE)
|
|
260
|
+
if (outer) say(`${label} is inside ${outer.label}, so the text between them renders for neither audience. Close the outer block with endif::[] before opening the other one.`)
|
|
261
|
+
}
|
|
262
|
+
open.push({ label, kind, attribute, line: i })
|
|
263
|
+
}
|
|
264
|
+
if (i > 0 && lines[i - 1].trim() !== '') say(`${label} needs a blank line before it, so the paragraph inside starts where the block starts.`)
|
|
265
|
+
}
|
|
266
|
+
for (const { label } of open) {
|
|
267
|
+
say(`${label} is never closed with endif::[]. Everything after it is hidden from one audience, or the rest of the page is.`, 'error')
|
|
268
|
+
}
|
|
269
|
+
return issues
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
module.exports = { VERBATIM_ASCIIDOC_RULES, CONDITIONAL_RULE, KNOWN_ATTRIBUTES, MACRO_NAMES, countRawPipes }
|
|
@@ -8,6 +8,7 @@ const { SourceCache } = require('../source-text')
|
|
|
8
8
|
const { splitTopLevelArgs, findBalancedClose, collectGoFiles } = require('../go-source')
|
|
9
9
|
const { GoIndex, statementEnd, straightLineReturn, stripStrings, renderResult, isResolved, unresolvedParts } = require('../go-index')
|
|
10
10
|
const { parseLiteralFields } = require('./rpk')
|
|
11
|
+
const { CONDITIONAL_RULE } = require('../rules/verbatim-asciidoc')
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Connect surface: every published string a Redpanda Connect build defines.
|
|
@@ -18,6 +19,11 @@ const { parseLiteralFields } = require('./rpk')
|
|
|
18
19
|
* the option descriptions of annotated enum fields.
|
|
19
20
|
* - Bloblang plugins (bloblang.PluginSpec): Description, Example summaries,
|
|
20
21
|
* and parameter descriptions.
|
|
22
|
+
* - benthos's internal field API (internal/docs): the description argument
|
|
23
|
+
* of docs.FieldString, docs.FieldObject, docs.FieldBool and the other
|
|
24
|
+
* docs.Field constructors, which document the shared config blocks
|
|
25
|
+
* (http_server, tls, logger, batching, ...) that benthos builds outside
|
|
26
|
+
* the public service API.
|
|
21
27
|
* - `rpk connect` help (urfave/cli): command Usage, UsageText, ArgsUsage and
|
|
22
28
|
* Description, and flag Usage.
|
|
23
29
|
*
|
|
@@ -87,6 +93,17 @@ const BARE_FIELD_CTORS = new Set([
|
|
|
87
93
|
|
|
88
94
|
const SERVICE_PKG = 'github.com/redpanda-data/benthos/v4/public/service'
|
|
89
95
|
const BLOBLANG_PKG = 'github.com/redpanda-data/benthos/v4/public/bloblang'
|
|
96
|
+
const DOCS_PKG = 'github.com/redpanda-data/benthos/v4/internal/docs'
|
|
97
|
+
|
|
98
|
+
// internal/docs constructors with the signature
|
|
99
|
+
// (name, description string, examples ...any). docs.FieldComponent takes
|
|
100
|
+
// no description and is left out.
|
|
101
|
+
const DOCS_FIELD_CTORS = [
|
|
102
|
+
'FieldAnything', 'FieldObject', 'FieldString', 'FieldInterpolatedString', 'FieldBloblang',
|
|
103
|
+
'FieldInt', 'FieldFloat', 'FieldBool', 'FieldURL',
|
|
104
|
+
'FieldInput', 'FieldProcessor', 'FieldOutput', 'FieldBuffer', 'FieldCache',
|
|
105
|
+
'FieldRateLimit', 'FieldMetrics', 'FieldTracer', 'FieldScanner'
|
|
106
|
+
]
|
|
90
107
|
const CLI_PKG = /^github\.com\/urfave\/cli(?:\/v\d+)?$/
|
|
91
108
|
|
|
92
109
|
// Repos whose urfave/cli commands are `rpk connect` help.
|
|
@@ -331,6 +348,7 @@ class ConnectScanner {
|
|
|
331
348
|
file._aliases = {
|
|
332
349
|
service: aliasesFor(file, SERVICE_PKG),
|
|
333
350
|
bloblang: aliasesFor(file, BLOBLANG_PKG),
|
|
351
|
+
docs: aliasesFor(file, DOCS_PKG),
|
|
334
352
|
cli: aliasesFor(file, CLI_PKG)
|
|
335
353
|
}
|
|
336
354
|
}
|
|
@@ -804,6 +822,7 @@ class ConnectScanner {
|
|
|
804
822
|
const skipped = []
|
|
805
823
|
const { service, bloblang, cli } = this.aliases(file)
|
|
806
824
|
if (service.size > 0 || bloblang.size > 0) this.scanSpecs(file, rel, declarations, skipped)
|
|
825
|
+
this.scanDocsFields(file, rel, declarations)
|
|
807
826
|
if (cli.size > 0 && this.cli) this.scanCli(file, rel, declarations)
|
|
808
827
|
return { declarations, skipped }
|
|
809
828
|
}
|
|
@@ -1055,6 +1074,53 @@ class ConnectScanner {
|
|
|
1055
1074
|
this.scanJsFunctions(file, rel, declarations)
|
|
1056
1075
|
}
|
|
1057
1076
|
|
|
1077
|
+
/**
|
|
1078
|
+
* benthos's internal field API: `docs.FieldString(name, description)`
|
|
1079
|
+
* and the other internal/docs constructors. The description is the second
|
|
1080
|
+
* argument, not a `.Description()` call, so scanSpecs never sees it.
|
|
1081
|
+
* Inside internal/docs itself the constructors are unqualified. An empty
|
|
1082
|
+
* description is skipped: internal/docs uses `FieldString("type", "")`
|
|
1083
|
+
* for structural fields that no page documents.
|
|
1084
|
+
*/
|
|
1085
|
+
scanDocsFields (file, rel, declarations) {
|
|
1086
|
+
const { docs } = this.aliases(file)
|
|
1087
|
+
const mod = this.index.gomod.module
|
|
1088
|
+
const inPackage = Boolean(mod) && `${mod}/${this.index.rel(file.dir).split(path.sep).join('/')}` === DOCS_PKG
|
|
1089
|
+
if (docs.size === 0 && !inPackage) return
|
|
1090
|
+
const qual = docs.size > 0 ? `(?:${[...docs].map(escapeRe).join('|')})\\s*\\.\\s*` : ''
|
|
1091
|
+
const prefix = inPackage ? (qual ? `(?:${qual})?` : '') : qual
|
|
1092
|
+
const pattern = new RegExp(`(^|[^\\w.])${prefix}(${DOCS_FIELD_CTORS.join('|')})\\s*\\(`, 'g')
|
|
1093
|
+
const masked = file.masked
|
|
1094
|
+
// String contents blanked, so example YAML or prose that mentions a
|
|
1095
|
+
// constructor never reads as a call.
|
|
1096
|
+
const text = stripStrings(masked)
|
|
1097
|
+
let m
|
|
1098
|
+
while ((m = pattern.exec(text)) !== null) {
|
|
1099
|
+
const start = m.index + m[1].length
|
|
1100
|
+
if (/\bfunc\s*$/.test(text.slice(Math.max(0, start - 10), start))) continue
|
|
1101
|
+
const open = m.index + m[0].length - 1
|
|
1102
|
+
const close = file.pairs.get(open)
|
|
1103
|
+
if (close === undefined) continue
|
|
1104
|
+
const args = splitTopLevelArgs(masked.slice(open + 1, close))
|
|
1105
|
+
if (args.length < 2) continue
|
|
1106
|
+
const argStart = (i) => open + 1 + args.slice(0, i).reduce((n, a) => n + a.length + 1, 0)
|
|
1107
|
+
const result = this.evalAt(file, args[1], argStart(1))
|
|
1108
|
+
if (isResolved(result) && renderResult(result).trim() === '') continue
|
|
1109
|
+
const nameResult = this.evalAt(file, args[0], argStart(0))
|
|
1110
|
+
this.pushDecl(declarations, file, rel, {
|
|
1111
|
+
kind: 'field',
|
|
1112
|
+
name: isResolved(nameResult) ? renderResult(nameResult) : null,
|
|
1113
|
+
nameResult,
|
|
1114
|
+
nameExpr: { text: args[0], offset: argStart(0) },
|
|
1115
|
+
result,
|
|
1116
|
+
valueExpr: { text: args[1], offset: argStart(1) },
|
|
1117
|
+
start,
|
|
1118
|
+
end: close,
|
|
1119
|
+
extra: { ctor: `docs.${m[2]}`, ...this.componentMeta(file, start) }
|
|
1120
|
+
})
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1058
1124
|
/**
|
|
1059
1125
|
* javascript processor functions: `registerVMRunnerFunction(name,
|
|
1060
1126
|
* description).Param(name, type, what)`. The processor renders each one
|
|
@@ -1272,7 +1338,7 @@ class ConnectScanner {
|
|
|
1272
1338
|
extra: {
|
|
1273
1339
|
literal: literal === 'Command' ? 'command' : literal,
|
|
1274
1340
|
// rpk-docs formats CLI help like cobra help; it is not AsciiDoc.
|
|
1275
|
-
skip_rules: ['raw-pipe', 'unknown-attribute', 'broken-macro', 'too-short']
|
|
1341
|
+
skip_rules: ['raw-pipe', 'unknown-attribute', 'broken-macro', 'conditional', 'too-short']
|
|
1276
1342
|
}
|
|
1277
1343
|
}))
|
|
1278
1344
|
}
|
|
@@ -1460,8 +1526,6 @@ const MARKUP_PATTERNS = [
|
|
|
1460
1526
|
[/\n/, 'a line break']
|
|
1461
1527
|
]
|
|
1462
1528
|
|
|
1463
|
-
const ENV_CONDITIONAL = /\b(?:ifdef|ifndef|ifeval|endif)::/
|
|
1464
|
-
|
|
1465
1529
|
// Modules of the Redpanda Connect docs component. A generated partial is
|
|
1466
1530
|
// rendered on the self-managed page and included into Cloud pages, so a
|
|
1467
1531
|
// link needs an explicit module to resolve in both places.
|
|
@@ -1491,15 +1555,11 @@ const RULES = [
|
|
|
1491
1555
|
return [{ message: `ShortDescription contains ${found.join(', ')}. Form-based config editors, such as the one in Redpanda Cloud, show it as plain text, so markup renders literally. Write one plain sentence and keep markup in Description().` }]
|
|
1492
1556
|
}
|
|
1493
1557
|
},
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
if (!decl.string || !ENV_CONDITIONAL.test(decl.string)) return []
|
|
1500
|
-
return [{ message: 'Contains an AsciiDoc conditional (ifdef::, ifndef:: or endif::). The docs generator owns every Cloud conditional in the partials, and a hand-written one nests inside them, so the Cloud copy shows or hides the wrong text. Fix the availability data in internal/plugins/info.csv instead, or ask the docs team to carry the sentence on the page.' }]
|
|
1501
|
-
}
|
|
1502
|
-
},
|
|
1558
|
+
// Spec strings publish to the self-managed and the Cloud docs, and the
|
|
1559
|
+
// Cloud binaries render them with env-cloud set, so an env-cloud block
|
|
1560
|
+
// conditional is the supported way to vary a paragraph between the two.
|
|
1561
|
+
// The rule checks the form; see rules/verbatim-asciidoc.js.
|
|
1562
|
+
CONDITIONAL_RULE,
|
|
1503
1563
|
{
|
|
1504
1564
|
name: 'connect-xref-module',
|
|
1505
1565
|
description: 'xref without a module, or to a module the Connect docs do not have',
|
|
@@ -405,18 +405,22 @@ const RULES = [
|
|
|
405
405
|
},
|
|
406
406
|
{
|
|
407
407
|
name: 'go-field-name-first',
|
|
408
|
-
description: 'Description
|
|
408
|
+
description: 'Description opens with the Go field name or the YAML key instead of what the field does',
|
|
409
409
|
severity: 'warning',
|
|
410
410
|
check: (decl) => {
|
|
411
411
|
const text = (decl.string || '').trim()
|
|
412
412
|
if (!text) return []
|
|
413
413
|
const firstWord = (text.match(/^[A-Za-z0-9_]+/) || [null])[0]
|
|
414
414
|
if (!firstWord) return []
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
}
|
|
419
|
-
|
|
415
|
+
// Case-sensitive on purpose: `RetentionTime is...` on
|
|
416
|
+
// json:"retentionTime" opens with the Go name, and `retentionTime
|
|
417
|
+
// is...` opens with the key. Neither says what the field does.
|
|
418
|
+
const { go_name: goName, json_name: jsonName } = decl.meta
|
|
419
|
+
let opener = null
|
|
420
|
+
if (firstWord === goName) opener = `the Go field name "${goName}"`
|
|
421
|
+
else if (firstWord === jsonName) opener = `the YAML key "${jsonName}"`
|
|
422
|
+
if (!opener) return []
|
|
423
|
+
return [{ message: `Description opens with ${opener}. Start with what the field does, for example "Specifies ...". Don't open with the Go field name "${goName}" or the YAML key "${jsonName}".` }]
|
|
420
424
|
}
|
|
421
425
|
}
|
|
422
426
|
]
|
package/tools/metrics/metrics.py
CHANGED
|
@@ -23,6 +23,16 @@ These output files are stored in a versioned folder under `docs/autogenerated/<v
|
|
|
23
23
|
|
|
24
24
|
logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s")
|
|
25
25
|
|
|
26
|
+
|
|
27
|
+
def generated_marker(generator, source, hint=""):
|
|
28
|
+
"""Same marker shape as cli-utils/generated-marker.js (no version, so a
|
|
29
|
+
release does not rewrite every file); convert-to-markdown reads it."""
|
|
30
|
+
marker = (
|
|
31
|
+
"// This content is autogenerated. Do not edit manually. "
|
|
32
|
+
f"Generated by doc-tools ({generator}) from {source.rstrip('.')}."
|
|
33
|
+
)
|
|
34
|
+
return marker + (f" {hint.strip()}" if hint.strip() else "")
|
|
35
|
+
|
|
26
36
|
def fetch_metrics(url):
|
|
27
37
|
"""Fetch metrics from the given URL."""
|
|
28
38
|
try:
|
|
@@ -123,6 +133,7 @@ def parse_metrics(metrics_text):
|
|
|
123
133
|
def output_asciidoc(metrics, adoc_file):
|
|
124
134
|
"""Output metrics as AsciiDoc."""
|
|
125
135
|
with open(adoc_file, "w") as f:
|
|
136
|
+
f.write(generated_marker("metrics-docs", "the metrics Redpanda exposes, defined in redpanda-data/streaming-enterprise") + "\n\n")
|
|
126
137
|
for name, data in metrics.items():
|
|
127
138
|
f.write(f"=== {name}\n\n")
|
|
128
139
|
f.write(f"{data['description']}\n\n")
|
|
@@ -2,10 +2,18 @@
|
|
|
2
2
|
// writer already has open. The previous pointer was a Confluence page, and
|
|
3
3
|
// Confluence is retired, so it sent every writer who read this line to a dead
|
|
4
4
|
// end. A repo-relative path cannot rot the same way.
|
|
5
|
-
const AUTOGEN_NOTICE = '// This content is autogenerated. Do not edit manually. To change a description, add a link, or scope text to one docs build, edit docs-data/property-overrides.json (fields are documented in docs-data/property-overrides.schema.json and docs-data/README.adoc) and regenerate.\n';
|
|
6
5
|
'use strict';
|
|
7
6
|
|
|
8
7
|
const fs = require('fs');
|
|
8
|
+
const { generatedMarker } = require('../../cli-utils/generated-marker');
|
|
9
|
+
|
|
10
|
+
// The marker's shape is shared by every generator and read by the
|
|
11
|
+
// convert-to-markdown extension, which exports it to the Markdown twins.
|
|
12
|
+
const AUTOGEN_NOTICE = generatedMarker(
|
|
13
|
+
'property-docs',
|
|
14
|
+
'the configuration property definitions in redpanda-data/streaming-enterprise',
|
|
15
|
+
'To change a description, add a link, or scope text to one docs build, edit docs-data/property-overrides.json (fields are documented in docs-data/property-overrides.schema.json and docs-data/README.adoc) and regenerate.'
|
|
16
|
+
) + '\n';
|
|
9
17
|
const bigIntJson = require('../../cli-utils/big-int-json');
|
|
10
18
|
const path = require('path');
|
|
11
19
|
const handlebars = require('handlebars');
|
|
@@ -419,6 +419,39 @@ function normalizeIncludes(propName, raw, warnings) {
|
|
|
419
419
|
return out;
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
/**
|
|
423
|
+
* Remove the inline copies of `includes` that the attachment write-back appended.
|
|
424
|
+
*
|
|
425
|
+
* generate-handlebars-docs.js writes each include back into the description
|
|
426
|
+
* on disk, so the attachment's tooltip still renders it, and leaves `includes`
|
|
427
|
+
* in place. A run that reads that file again (a reused gen/<tag>-properties.json,
|
|
428
|
+
* or generate-docs run twice without re-extraction) sees the directive in the
|
|
429
|
+
* description and in `includes`, and the template rendered it twice. Only the
|
|
430
|
+
* exact shapes the write-back produces are
|
|
431
|
+
* removed, so an include a human put in the prose is left alone.
|
|
432
|
+
*
|
|
433
|
+
* @param {string|undefined} description - Flattened description.
|
|
434
|
+
* @param {Array<{target: string, cloud_only?: boolean, self_managed_only?: boolean}>} items - Normalized includes.
|
|
435
|
+
* @returns {string|undefined}
|
|
436
|
+
*/
|
|
437
|
+
function stripInlineIncludes(description, items) {
|
|
438
|
+
if (typeof description !== 'string' || !description.includes('include::')) return description;
|
|
439
|
+
let text = description;
|
|
440
|
+
// The write-back appends in order, so peel from the end.
|
|
441
|
+
for (const item of [...items].reverse()) {
|
|
442
|
+
const directive = `include::${item.target}`;
|
|
443
|
+
let block = directive;
|
|
444
|
+
if (item.cloud_only) block = `ifdef::env-cloud[]\n${directive}\nendif::[]`;
|
|
445
|
+
else if (item.self_managed_only) block = `ifndef::env-cloud[]\n${directive}\nendif::[]`;
|
|
446
|
+
if (text.endsWith(`\n\n${block}`)) {
|
|
447
|
+
text = text.slice(0, -(block.length + 2));
|
|
448
|
+
} else if (text === block) {
|
|
449
|
+
text = '';
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return text;
|
|
453
|
+
}
|
|
454
|
+
|
|
422
455
|
/**
|
|
423
456
|
* Why the Cloud build cannot reach a link target, or null when it can.
|
|
424
457
|
*
|
|
@@ -471,6 +504,7 @@ function applyPropertyLinks(properties) {
|
|
|
471
504
|
}
|
|
472
505
|
if (prop.includes !== undefined) {
|
|
473
506
|
prop.includes = normalizeIncludes(propName, prop.includes, warnings);
|
|
507
|
+
prop.description = stripInlineIncludes(prop.description, prop.includes);
|
|
474
508
|
}
|
|
475
509
|
if (Array.isArray(prop.admonitions)) {
|
|
476
510
|
prop.admonitions = prop.admonitions.filter((item) => {
|
|
@@ -589,3 +623,4 @@ module.exports.applyLinksToText = applyLinksToText;
|
|
|
589
623
|
module.exports.renderLink = renderLink;
|
|
590
624
|
module.exports.cloudUnreachable = cloudUnreachable;
|
|
591
625
|
module.exports.normalizeIncludes = normalizeIncludes;
|
|
626
|
+
module.exports.stripInlineIncludes = stripInlineIncludes;
|