@redpanda-data/docs-extensions-and-macros 5.49.0 → 5.50.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/package.json +1 -1
- package/tools/lint-strings/index.js +65 -3
- package/tools/lint-strings/rules/common.js +9 -2
- package/tools/lint-strings/surfaces/properties.js +3 -0
- package/tools/lint-strings/surfaces/rpk.js +9 -1
- package/tools/property-extractor/README.adoc +10 -9
- package/tools/rpk-docs/generate-rpk-docs.js +38 -4
package/package.json
CHANGED
|
@@ -116,7 +116,16 @@ function lintStrings (options) {
|
|
|
116
116
|
// still exists at HEAD, in any touched file, was edited or moved, not
|
|
117
117
|
// removed.
|
|
118
118
|
const removedFiles = removedBySurface[surfaceName] ? [...removedBySurface[surfaceName].keys()] : []
|
|
119
|
-
const
|
|
119
|
+
const touchedFiles = new Set([...files.keys(), ...removedFiles])
|
|
120
|
+
// A surface whose extraction covers the whole surface anyway (the
|
|
121
|
+
// properties extractor parses every config file on each run) supplies
|
|
122
|
+
// context from all of it, so a property in an untouched file that
|
|
123
|
+
// names, or is named by, a changed one is still on its page. Review
|
|
124
|
+
// scope stays the touched files either way.
|
|
125
|
+
const everything = surface.contextScope === 'surface' ? surface.extract({ repo: repoPath, log }) : null
|
|
126
|
+
const head = everything
|
|
127
|
+
? everything.filter((d) => touchedFiles.has(d.file))
|
|
128
|
+
: surface.extract({ repo: repoPath, files: touchedFiles, log })
|
|
120
129
|
headBySurface[surfaceName] = head
|
|
121
130
|
const declarations = []
|
|
122
131
|
for (const decl of head) {
|
|
@@ -128,7 +137,7 @@ function lintStrings (options) {
|
|
|
128
137
|
continue
|
|
129
138
|
}
|
|
130
139
|
declarations.push(decl)
|
|
131
|
-
pending.push(pendingEntry(decl))
|
|
140
|
+
pending.push({ ...pendingEntry(decl), context: pageContext(decl, everything || head) })
|
|
132
141
|
}
|
|
133
142
|
results.push(runRules(declarations, rulesFor(surface), { skipRules, onlyRules }))
|
|
134
143
|
}
|
|
@@ -186,6 +195,59 @@ function touches (decl, lineSet) {
|
|
|
186
195
|
return spansOf(decl).some(([start, end]) => spanIntersects(start, end, lineSet))
|
|
187
196
|
}
|
|
188
197
|
|
|
198
|
+
const CONTEXT_LIMIT = 15
|
|
199
|
+
const CONTEXT_STRING_LIMIT = 600
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* The other strings a reader sees alongside this one, so a review can judge
|
|
203
|
+
* a string against its page rather than on its own: a flag's usage next to
|
|
204
|
+
* its command's Long text and examples, a property that names another
|
|
205
|
+
* property, the fields around an API field.
|
|
206
|
+
*
|
|
207
|
+
* - rpk: every other string declared in the same file, which is the
|
|
208
|
+
* command's Short, Long and flags (rpk keeps one command per file).
|
|
209
|
+
* - api: the declarations nearest by line, which are the enclosing message
|
|
210
|
+
* or rpc and its neighbors.
|
|
211
|
+
* - properties and metrics: declarations whose string names this one, or
|
|
212
|
+
* that this string names, plus (metrics) its neighbors in the same group.
|
|
213
|
+
*
|
|
214
|
+
* Drawn from declarations already extracted for the diff, so it costs no
|
|
215
|
+
* extra extraction. Deterministic, capped, and order-stable.
|
|
216
|
+
*/
|
|
217
|
+
function pageContext (decl, all) {
|
|
218
|
+
const others = all.filter((d) => d !== decl && d.string != null)
|
|
219
|
+
const byDistance = (d) => Math.abs((d.line_start || 0) - (decl.line_start || 0))
|
|
220
|
+
const sameFile = others.filter((d) => d.file === decl.file)
|
|
221
|
+
let picked
|
|
222
|
+
if (decl.surface === 'rpk') {
|
|
223
|
+
// The command's Short and Long always, then the flags nearest this
|
|
224
|
+
// declaration, so a changed flag late in a long command keeps its
|
|
225
|
+
// neighbors when the cap applies. Shown in source order.
|
|
226
|
+
const isText = (d) => d.meta && (d.meta.kind === 'short' || d.meta.kind === 'long')
|
|
227
|
+
const text = sameFile.filter(isText)
|
|
228
|
+
const flags = sameFile.filter((d) => !isText(d)).sort((a, b) => byDistance(a) - byDistance(b))
|
|
229
|
+
picked = [...text, ...flags].slice(0, CONTEXT_LIMIT)
|
|
230
|
+
.sort((a, b) => (a.line_start || 0) - (b.line_start || 0))
|
|
231
|
+
} else if (decl.surface === 'api') {
|
|
232
|
+
picked = sameFile.sort((a, b) => byDistance(a) - byDistance(b)).slice(0, 6)
|
|
233
|
+
} else {
|
|
234
|
+
const mentions = (text, name) => Boolean(text && name) &&
|
|
235
|
+
new RegExp(`(^|[^A-Za-z0-9_])${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}($|[^A-Za-z0-9_])`).test(text)
|
|
236
|
+
const related = others.filter((d) => mentions(d.string, decl.name) || mentions(decl.string, d.name))
|
|
237
|
+
const neighbors = decl.surface === 'metrics'
|
|
238
|
+
? sameFile.filter((d) => !related.includes(d)).sort((a, b) => byDistance(a) - byDistance(b)).slice(0, 2)
|
|
239
|
+
: []
|
|
240
|
+
picked = [...related, ...neighbors]
|
|
241
|
+
}
|
|
242
|
+
return picked.slice(0, CONTEXT_LIMIT).map((d) => ({
|
|
243
|
+
name: d.name,
|
|
244
|
+
kind: (d.meta && d.meta.kind) || d.surface,
|
|
245
|
+
file: d.file,
|
|
246
|
+
line_start: d.line_start,
|
|
247
|
+
string: d.string.length > CONTEXT_STRING_LIMIT ? `${d.string.slice(0, CONTEXT_STRING_LIMIT)}...` : d.string
|
|
248
|
+
}))
|
|
249
|
+
}
|
|
250
|
+
|
|
189
251
|
/**
|
|
190
252
|
* Stable identity for one version of one doc string. A PR review records the
|
|
191
253
|
* fingerprints it has seen, and a later push skips any declaration whose
|
|
@@ -467,7 +529,7 @@ function runCli (options) {
|
|
|
467
529
|
process.exitCode = options.strict && result.summary.errors > 0 ? 1 : 0
|
|
468
530
|
}
|
|
469
531
|
|
|
470
|
-
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints }
|
|
532
|
+
module.exports = { lintStrings, formatHuman, runCli, SURFACES, rulesFor, fingerprint, readFingerprints, pageContext }
|
|
471
533
|
|
|
472
534
|
// Direct usage: node tools/lint-strings --repo <path> [--surface a,b]
|
|
473
535
|
// [--diff <base>] [--format json|human] [--strict]
|
|
@@ -100,7 +100,9 @@ function maskNonProse (text) {
|
|
|
100
100
|
const CODE_TOKEN_PATTERNS = Object.freeze([
|
|
101
101
|
{ id: 'identifier', re: /\b[a-z][a-z0-9]*(?:_[a-z0-9]+)+\b/g },
|
|
102
102
|
{ id: 'flag', re: /(?<![\w-])--[a-z][a-z0-9]*(?:-[a-z0-9]+)*\b/g },
|
|
103
|
-
|
|
103
|
+
// A path may not end in a period, so "served from /v1/brokers." reports
|
|
104
|
+
// `/v1/brokers`, not the sentence's full stop with it.
|
|
105
|
+
{ id: 'path', re: /(?<![\w/])\/[a-z][a-zA-Z0-9_.-]*(?:\/[a-zA-Z0-9_.{}-]*[a-zA-Z0-9_{}-])+\/?/g }
|
|
104
106
|
])
|
|
105
107
|
|
|
106
108
|
/**
|
|
@@ -276,7 +278,12 @@ const COMMON_RULES = [
|
|
|
276
278
|
description: 'Code value or field name in prose without inline code',
|
|
277
279
|
severity: 'warning',
|
|
278
280
|
check: (decl) => {
|
|
279
|
-
|
|
281
|
+
// A surface whose generator formats some code values itself declares
|
|
282
|
+
// which (convention.auto_inline_code); those are not findings there.
|
|
283
|
+
const auto = (decl.convention && decl.convention.auto_inline_code) || null
|
|
284
|
+
const bare = findBareCodeTokens(decl.string, decl.name).filter((b) => !auto ||
|
|
285
|
+
!((auto.kinds || []).includes(b.kind) ||
|
|
286
|
+
(b.kind === 'path' && (auto.path_prefixes || []).some((prefix) => b.token.startsWith(prefix)))))
|
|
280
287
|
if (bare.length === 0) return []
|
|
281
288
|
const list = bare.map((b) => `\`${b.token}\``).join(', ')
|
|
282
289
|
return [{
|
|
@@ -245,6 +245,9 @@ module.exports = {
|
|
|
245
245
|
// The extractor pairs each .cc with its .h, so a merge-base extraction needs
|
|
246
246
|
// the whole directory, not only the files that lost lines.
|
|
247
247
|
baseScope: 'directory',
|
|
248
|
+
// extract() runs the extractor over the whole repo and filters after, so
|
|
249
|
+
// page context can use every property at no extra cost.
|
|
250
|
+
contextScope: 'surface',
|
|
248
251
|
convention: CONVENTION,
|
|
249
252
|
extract,
|
|
250
253
|
mapExtractorJson,
|
|
@@ -37,7 +37,15 @@ const CONVENTION = {
|
|
|
37
37
|
case: 'sentence',
|
|
38
38
|
terminal_period: false,
|
|
39
39
|
verbatim_asciidoc: false,
|
|
40
|
-
transformer: 'formatDescription'
|
|
40
|
+
transformer: 'formatDescription',
|
|
41
|
+
// Code values formatDescription wraps in inline code by itself, so a bare
|
|
42
|
+
// one in the source is not a finding. Long is a Go raw string and cannot
|
|
43
|
+
// hold a backtick at all, which is why the generator does this. Keep in
|
|
44
|
+
// step with the auto-backtick passes in tools/rpk-docs/generate-rpk-docs.js.
|
|
45
|
+
auto_inline_code: {
|
|
46
|
+
kinds: ['flag'],
|
|
47
|
+
path_prefixes: ['/etc/', '/var/', '/usr/', '/home/', '/tmp/', '~/.']
|
|
48
|
+
}
|
|
41
49
|
}
|
|
42
50
|
|
|
43
51
|
// Composite literal only: gofmt writes `cobra.Command{` with no space, while
|
|
@@ -1231,7 +1231,12 @@ breaks. Two files feed it, both mirrors of the `docs` repo:
|
|
|
1231
1231
|
fields the rendering and link layers read. `cloud_supported` is the point: it is what
|
|
1232
1232
|
makes the "no Cloud link to a property Cloud does not publish" assertion real rather
|
|
1233
1233
|
than assumed. Descriptions are deliberately absent, because the corpus test drives prose from
|
|
1234
|
-
the overrides file, and a full copy would be 695 KB of text that rots.
|
|
1234
|
+
the overrides file, and a full copy would be 695 KB of text that rots. The exceptions are
|
|
1235
|
+
overrides that act on the source description instead of replacing it: links with no prose of
|
|
1236
|
+
their own, which apply to the source description, and includes with no description of their
|
|
1237
|
+
own, which the generator appends to it. The snapshot keeps the source description for those
|
|
1238
|
+
properties. `.github/scripts/derive-property-snapshot.js` makes that call from the overrides
|
|
1239
|
+
file, and both the recipe below and `property-corpus-drift.sh` use it.
|
|
1235
1240
|
|
|
1236
1241
|
Keep the overrides copy a faithful mirror. It previously doubled as
|
|
1237
1242
|
`property-docs-overrides.test.js`'s fixture, someone hand-edited an `admin` entry into
|
|
@@ -1264,17 +1269,13 @@ TAG="$(git -C "$DOCS" ls-tree origin/main --name-only -- modules/reference/attac
|
|
|
1264
1269
|
echo "Refreshing against $TAG"
|
|
1265
1270
|
|
|
1266
1271
|
git -C "$DOCS" show "origin/main:modules/reference/attachments/redpanda-properties-${TAG}.json" \
|
|
1272
|
+
> /tmp/redpanda-properties.json
|
|
1273
|
+
node .github/scripts/derive-property-snapshot.js /tmp/redpanda-properties.json \
|
|
1274
|
+
__tests__/docs-data/property-overrides.json \
|
|
1267
1275
|
| node -e '
|
|
1268
1276
|
const fs = require("fs");
|
|
1269
|
-
const src = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
1270
|
-
const keep = ["name","config_scope","type","cloud_supported","cloud_editable",
|
|
1271
|
-
"cloud_readonly","cloud_byoc_only","is_deprecated","nullable"];
|
|
1272
|
-
const properties = {};
|
|
1273
|
-
for (const [k, v] of Object.entries(src.properties || {})) {
|
|
1274
|
-
properties[k] = Object.fromEntries(keep.filter((f) => f in v).map((f) => [f, v[f]]));
|
|
1275
|
-
}
|
|
1276
1277
|
const out = JSON.parse(fs.readFileSync("__tests__/docs-data/property-snapshot.json", "utf8"));
|
|
1277
|
-
out.properties =
|
|
1278
|
+
out.properties = JSON.parse(fs.readFileSync(0, "utf8"));
|
|
1278
1279
|
out.source_tag = process.argv[1];
|
|
1279
1280
|
fs.writeFileSync("__tests__/docs-data/property-snapshot.json", JSON.stringify(out, null, 1) + "\n");
|
|
1280
1281
|
' "$TAG"
|
|
@@ -1569,6 +1569,26 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1569
1569
|
return placeholder
|
|
1570
1570
|
})
|
|
1571
1571
|
|
|
1572
|
+
// Bare URLs become inline code: endpoints (http://localhost:9644) and links
|
|
1573
|
+
// alike are code values, and an unformatted URL in help prose reads as
|
|
1574
|
+
// broken markup. Not trailing sentence punctuation, and not a URL that is
|
|
1575
|
+
// already an AsciiDoc link macro (url[text]), which backticks would break.
|
|
1576
|
+
//
|
|
1577
|
+
// Protected as a placeholder straight away, before any other pass runs.
|
|
1578
|
+
// Every later pass matches inside a URL it does not know is one: the path
|
|
1579
|
+
// passes backticked /tmp/file out of https://host/tmp/file, the short-flag
|
|
1580
|
+
// pass took -a out of /foo-a/, and the issue-reference pass rewrote #NNNN
|
|
1581
|
+
// anchors into link macros.
|
|
1582
|
+
protectedDesc = protectedDesc.replace(/(?<![`\w/]|\]\()https?:\/\/[^\s`<>"'()[\]]+/g, (url, offset, whole) => {
|
|
1583
|
+
if (whole[offset + url.length] === '[') return url // AsciiDoc link macro
|
|
1584
|
+
const trail = url.match(/[.,;:!?]+$/)
|
|
1585
|
+
const core = trail ? url.slice(0, -trail[0].length) : url
|
|
1586
|
+
if (core.length <= 'https://'.length) return url
|
|
1587
|
+
const placeholder = `__INLINE_CODE_${inlineCode.length}__`
|
|
1588
|
+
inlineCode.push(`\`${decodeHtmlEntities(core)}\``)
|
|
1589
|
+
return placeholder + (trail ? trail[0] : '')
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1572
1592
|
// Known top-level rpk subcommands (for accurate command detection)
|
|
1573
1593
|
const rpkSubcommands = new Set([
|
|
1574
1594
|
'ai', 'check', 'cloud', 'cluster', 'connect', 'container', 'debug',
|
|
@@ -1713,6 +1733,17 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1713
1733
|
}).join('`')
|
|
1714
1734
|
result = result.replace(/(?<!`|-)(-[a-zA-Z])(?![a-zA-Z-])/g, '`$1`')
|
|
1715
1735
|
|
|
1736
|
+
// A bare command followed by its flags ("run rpk topic list -r") is one
|
|
1737
|
+
// command, but the passes above wrap the command path and each flag
|
|
1738
|
+
// separately. Merge flag spans into the command span before them:
|
|
1739
|
+
// `rpk topic list` `-r` -> `rpk topic list -r`. The quoted form never
|
|
1740
|
+
// splits, because its whole span is protected before flags are wrapped.
|
|
1741
|
+
let merged
|
|
1742
|
+
do {
|
|
1743
|
+
merged = result
|
|
1744
|
+
result = result.replace(/`(rpk(?: [a-z][-a-z0-9]*)+(?: -{1,2}[a-zA-Z][-a-zA-Z0-9]*)*)` `(-{1,2}[a-zA-Z][-a-zA-Z0-9]*)`/g, '`$1 $2`')
|
|
1745
|
+
} while (result !== merged)
|
|
1746
|
+
|
|
1716
1747
|
// Add backticks around environment variables
|
|
1717
1748
|
result = result.replace(/(?<!`)(\$[A-Z_][A-Z0-9_]*)/g, '`$1`')
|
|
1718
1749
|
|
|
@@ -1729,12 +1760,15 @@ function formatDescription(desc, customTransformations = null, options = {}) {
|
|
|
1729
1760
|
result = result.replace(/`(\$[A-Z_][A-Z0-9_]*)`(\/(?:[^\s`]*[^\s`).,;:!?])?)/g, '`$1$2`')
|
|
1730
1761
|
|
|
1731
1762
|
// Add backticks around file paths (but not if already backticked)
|
|
1732
|
-
// Must check both the slash and the path aren't already inside backticks
|
|
1733
|
-
|
|
1734
|
-
|
|
1763
|
+
// Must check both the slash and the path aren't already inside backticks.
|
|
1764
|
+
// Like the $HOME merge above, a path may not END in sentence punctuation,
|
|
1765
|
+
// so "...in ~/.config/rpk/rpk.yaml." puts the period outside the span
|
|
1766
|
+
// instead of publishing `~/.config/rpk/rpk.yaml.`.
|
|
1767
|
+
result = result.replace(/(?<![`/])(\/(?:etc|var|usr|home|tmp)\/(?:[^\s,)`]*[^\s,)`.;:!?])?)/g, '`$1`')
|
|
1768
|
+
result = result.replace(/(?<![`/])((?:etc|var|usr|home|tmp)\/(?:[^\s,)`]*[^\s,)`.;:!?])?)/g, '`$1`')
|
|
1735
1769
|
|
|
1736
1770
|
// Add backticks around home directory paths (~/.bashrc, ~/.zshrc, ~/.config/...)
|
|
1737
|
-
result = result.replace(/(?<![`\w])(~\/\.[^\s,;:)`]
|
|
1771
|
+
result = result.replace(/(?<![`\w])(~\/\.(?:[^\s,;:)`]*[^\s,;:)`.!?])?)/g, '`$1`')
|
|
1738
1772
|
|
|
1739
1773
|
// Add backticks around common package names
|
|
1740
1774
|
result = result.replace(/(?<![`\w-])(bash-completion)(?![`\w-])/g, '`$1`')
|