@redpanda-data/docs-extensions-and-macros 5.38.3 → 5.38.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "5.38.3",
3
+ "version": "5.38.5",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -127,6 +127,18 @@ function findBareCodeTokens (text, name = null) {
127
127
  return found
128
128
  }
129
129
 
130
+ /**
131
+ * A short window of `text` around the first occurrence of `needle`, for use in
132
+ * rule messages so the writer can see the offending phrase in context.
133
+ */
134
+ function excerpt (text, needle, width = 60) {
135
+ const at = text.toLowerCase().indexOf(needle.toLowerCase())
136
+ if (at === -1) return text.slice(0, width)
137
+ const start = Math.max(0, at - Math.floor(width / 2))
138
+ const slice = text.slice(start, start + width)
139
+ return `${start > 0 ? '...' : ''}${slice}${start + width < text.length ? '...' : ''}`
140
+ }
141
+
130
142
  const COMMON_RULES = [
131
143
  {
132
144
  name: 'empty-description',
@@ -203,6 +215,49 @@ const COMMON_RULES = [
203
215
  return issues
204
216
  }
205
217
  },
218
+ {
219
+ name: 'em-dash',
220
+ description: 'Em dash in prose that ships to docs.redpanda.com',
221
+ severity: 'warning',
222
+ check: (decl) => {
223
+ const text = decl.string || ''
224
+ if (!text.includes('\u2014')) return []
225
+ // The docs style guide takes commas, parentheses or a sentence break
226
+ // instead of em dashes. Reference docs generated from these strings pass
227
+ // prose through unchanged, so an em dash here is published verbatim, and
228
+ // correcting it in the generated partial is undone by the next
229
+ // regeneration.
230
+ const count = (text.match(/\u2014/g) || []).length
231
+ return [{
232
+ message: `Contains ${count} em dash${count === 1 ? '' : 'es'}. Use a comma, parentheses, or a separate sentence: "${excerpt(text, '\u2014')}"`
233
+ }]
234
+ }
235
+ },
236
+ {
237
+ name: 'latin-abbreviation',
238
+ description: 'Latin abbreviation instead of plain English',
239
+ severity: 'warning',
240
+ check: (decl) => {
241
+ const text = decl.string || ''
242
+ if (!text) return []
243
+ const issues = []
244
+ // "e.g." and "i.e." are both ruled out by the style guide's terminology
245
+ // list. Match the abbreviation only, so "e.g" inside a longer token such
246
+ // as a URL or an identifier is left alone.
247
+ for (const [abbr, replacement] of [['e.g.', '"for example"'], ['i.e.', '"that is"']]) {
248
+ // Boundaries on both sides: the abbreviation must not be part of a
249
+ // longer token, so a host or identifier that happens to contain it
250
+ // (https://e.g.example/path) is left alone.
251
+ const pattern = new RegExp(`(^|[^\\w.])${abbr.replace(/\./g, '\\.')}(?=$|[^\\w])`, 'i')
252
+ if (pattern.test(text)) {
253
+ issues.push({
254
+ message: `Uses "${abbr}". Write ${replacement} instead: "${excerpt(text, abbr)}"`
255
+ })
256
+ }
257
+ }
258
+ return issues
259
+ }
260
+ },
206
261
  {
207
262
  name: 'unbalanced-backticks',
208
263
  description: 'Odd number of backticks ships broken markup',
@@ -196,6 +196,33 @@ function mergeOverrides(target, overrides) {
196
196
  return target;
197
197
  }
198
198
 
199
+ /**
200
+ * House-style regressions that a regeneration would reintroduce.
201
+ *
202
+ * Generated prose passes through unchanged by design (see
203
+ * helpers/renderConnectDescription.js), so when a writer corrects an em dash or
204
+ * an "e.g." in a published partial instead of upstream, the next regeneration
205
+ * silently puts it back. Comparing the published text with the incoming text
206
+ * catches exactly that case: a style issue the published file does not have and
207
+ * the new one does. Same rules the doc-strings lint applies at the source, so
208
+ * the two agree on what counts as a regression.
209
+ *
210
+ * @param {string} published current on-disk partial
211
+ * @param {string} incoming regenerated partial
212
+ * @returns {string[]} rule names whose hit count increases
213
+ */
214
+ function styleRegressions (published, incoming) {
215
+ return STYLE_REGRESSION_RULES
216
+ .filter(({ count }) => count(incoming) > count(published))
217
+ .map(({ label }) => label);
218
+ }
219
+
220
+ const STYLE_REGRESSION_RULES = [
221
+ { label: 'em dash', count: (text) => (text.match(/\u2014/g) || []).length },
222
+ { label: '"e.g."', count: (text) => (text.match(/(^|[^\w.])e\.g\./gi) || []).length },
223
+ { label: '"i.e."', count: (text) => (text.match(/(^|[^\w.])i\.e\./gi) || []).length }
224
+ ];
225
+
199
226
  /**
200
227
  * Resolves $ref references in an object by replacing them with their definitions.
201
228
  * Supports JSON Pointer style references like "#/definitions/client_certs".
@@ -448,6 +475,7 @@ async function generateRpcnConnectorDocs(options) {
448
475
  let partialsWritten = 0;
449
476
  const descriptionReports = [];
450
477
  const lostSectionWarnings = [];
478
+ const styleRegressionWarnings = [];
451
479
  // typeDir/name keys handled this run; used after the loop to blank
452
480
  // description partials orphaned by a connector deleted upstream.
453
481
  const visitedDescriptionPartials = new Set();
@@ -487,6 +515,12 @@ async function generateRpcnConnectorDocs(options) {
487
515
  if (fieldsOut.trim()) {
488
516
  const fPath = path.join(fieldsOutRoot, type, `${name}.adoc`);
489
517
  fs.mkdirSync(path.dirname(fPath), { recursive: true });
518
+ if (fs.existsSync(fPath)) {
519
+ const regressions = styleRegressions(fs.readFileSync(fPath, 'utf8'), fieldsOut);
520
+ if (regressions.length) {
521
+ styleRegressionWarnings.push({ partial: path.relative(process.cwd(), fPath), issues: regressions });
522
+ }
523
+ }
490
524
  fs.writeFileSync(fPath, fieldsOut);
491
525
  if (!writeFullDrafts) {
492
526
  partialsWritten++;
@@ -587,7 +621,12 @@ async function generateRpcnConnectorDocs(options) {
587
621
  // because it left the upstream description or because a human
588
622
  // added it here -- so warn before overwriting instead of
589
623
  // dropping it without a trace.
590
- const lost = lostMetadataSections(fs.readFileSync(dPath, 'utf8'), descriptionOut);
624
+ const publishedDescription = fs.readFileSync(dPath, 'utf8');
625
+ const regressions = styleRegressions(publishedDescription, descriptionOut);
626
+ if (regressions.length) {
627
+ styleRegressionWarnings.push({ partial: path.relative(process.cwd(), dPath), issues: regressions });
628
+ }
629
+ const lost = lostMetadataSections(publishedDescription, descriptionOut);
591
630
  if (lost.length) {
592
631
  lostSectionWarnings.push({ partial: path.relative(process.cwd(), dPath), sections: lost });
593
632
  console.warn(
@@ -938,6 +977,7 @@ async function generateRpcnConnectorDocs(options) {
938
977
  draftFiles,
939
978
  descriptionReports,
940
979
  lostSectionWarnings,
980
+ styleRegressionWarnings,
941
981
  orphanSweepSkipped,
942
982
  descriptionBackfill
943
983
  };
@@ -164,6 +164,29 @@ function renderLostSectionWarnings(warnings) {
164
164
  return lines;
165
165
  }
166
166
 
167
+ /**
168
+ * Render the house-style regression block.
169
+ *
170
+ * A regeneration reintroducing an em dash or an "e.g." that the published
171
+ * partial does not have means the correction was made in the generated file
172
+ * instead of upstream, so every release undoes it again. Surfacing it in the
173
+ * summary is what tells the reviewer the diff contains reverted copy, which
174
+ * the schema delta above never shows.
175
+ * @param {Array<{partial: string, issues: string[]}>} warnings
176
+ * @returns {string[]} lines (empty when there is nothing to warn about)
177
+ */
178
+ function renderStyleRegressionWarnings(warnings) {
179
+ if (!Array.isArray(warnings) || warnings.length === 0) return [];
180
+ const lines = [];
181
+ lines.push('> [!WARNING]');
182
+ lines.push('> **This update reintroduces house-style issues that the published docs do not have.** The fix belongs upstream in the Connect source, or in `docs-data/overrides.json`: correcting the generated partial is undone by the next regeneration.');
183
+ for (const w of warnings) {
184
+ lines.push(`> - \`${w.partial}\` adds: ${w.issues.join(', ')}`);
185
+ }
186
+ lines.push('');
187
+ return lines;
188
+ }
189
+
167
190
  function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, draftedConnectors = null) {
168
191
  const lines = [];
169
192
 
@@ -176,6 +199,7 @@ function generateMultiVersionPRSummary(masterDiff, binaryAnalysis = null, drafte
176
199
  lines.push('<!-- PR_SUMMARY_START -->');
177
200
  lines.push('');
178
201
  lines.push(...renderLostSectionWarnings(masterDiff?.lostSectionWarnings));
202
+ lines.push(...renderStyleRegressionWarnings(masterDiff?.styleRegressionWarnings));
179
203
  lines.push(...renderDescriptionReports(masterDiff?.descriptionReports));
180
204
  lines.push('## Redpanda Connect Documentation Update');
181
205
  lines.push('');
@@ -583,6 +607,7 @@ function generatePRSummary(diffData, binaryAnalysis = null, draftedConnectors =
583
607
  lines.push('<!-- PR_SUMMARY_START -->');
584
608
  lines.push('');
585
609
  lines.push(...renderLostSectionWarnings(diffData.lostSectionWarnings));
610
+ lines.push(...renderStyleRegressionWarnings(diffData.styleRegressionWarnings));
586
611
  lines.push(...renderDescriptionReports(diffData.descriptionReports));
587
612
 
588
613
  // Detect if this is a master diff
@@ -1364,13 +1389,17 @@ function printPRSummary(diffData, binaryAnalysis = null, draftedConnectors = nul
1364
1389
  * something each call site has to remember.
1365
1390
  *
1366
1391
  * @param {object} result generator result
1367
- * @param {{descriptionReports: Array, lostSectionWarnings: Array}} into accumulators, mutated in place
1368
- * @returns {{descriptionReports: Array, lostSectionWarnings: Array}} the same accumulators
1392
+ * @param {{descriptionReports: Array, lostSectionWarnings: Array, styleRegressionWarnings?: Array}} into accumulators, mutated in place
1393
+ * @returns {{descriptionReports: Array, lostSectionWarnings: Array, styleRegressionWarnings: Array}} the same accumulators
1369
1394
  */
1370
1395
  function collectGeneratorReports (result, into) {
1371
1396
  if (!result) return into;
1372
1397
  into.descriptionReports.push(...(result.descriptionReports || []));
1373
1398
  into.lostSectionWarnings.push(...(result.lostSectionWarnings || []));
1399
+ // Tolerate an accumulator created before this key existed, so an older call
1400
+ // site collects what it knows about instead of throwing.
1401
+ if (!into.styleRegressionWarnings) into.styleRegressionWarnings = [];
1402
+ into.styleRegressionWarnings.push(...(result.styleRegressionWarnings || []));
1374
1403
  return into;
1375
1404
  }
1376
1405
 
@@ -1381,5 +1410,6 @@ module.exports = {
1381
1410
  printPRSummary,
1382
1411
  renderDescriptionReports,
1383
1412
  renderLostSectionWarnings,
1413
+ renderStyleRegressionWarnings,
1384
1414
  truncateToSentence
1385
1415
  };
@@ -500,7 +500,18 @@ function updateWhatsNew ({ dataDir, oldVersion, newVersion, binaryAnalysis }) {
500
500
  const firstMatch = versionHeading.exec(contentWithoutOldSection)
501
501
  const insertIdx = firstMatch ? firstMatch.index : contentWithoutOldSection.length
502
502
 
503
- const updated = contentWithoutOldSection.slice(0, insertIdx) + section + '\n' + contentWithoutOldSection.slice(insertIdx)
503
+ // Entries in this file are separated by two blank lines. Normalize both
504
+ // boundaries instead of appending a newline to whatever is already there:
505
+ // the insertion point already carries the separator that preceded the
506
+ // previous first entry, so adding to it gave every new entry one more blank
507
+ // line than the entries below it, which then had to be corrected by hand
508
+ // after each release. Mirrors the rpk what's-new fix in #285.
509
+ const before = contentWithoutOldSection.slice(0, insertIdx).replace(/\n*$/, '')
510
+ const after = contentWithoutOldSection.slice(insertIdx).replace(/^\n*/, '')
511
+ const entry = section.replace(/^\n*/, '').replace(/\n*$/, '')
512
+ const updated = after
513
+ ? `${before}\n\n\n${entry}\n\n\n${after}`
514
+ : `${before}\n\n\n${entry}\n`
504
515
 
505
516
  if (startIdx !== -1) {
506
517
  console.log(`♻️ whats-new.adoc: replaced section for Version ${diff.comparison.newVersion}`)
@@ -1236,13 +1247,14 @@ async function handleRpcnConnectorDocs (options) {
1236
1247
  let partialsWritten, partialFiles
1237
1248
  const descriptionReports = []
1238
1249
  const lostSectionWarnings = []
1250
+ const styleRegressionWarnings = []
1239
1251
  // Both generator call sites (partials and drafts) feed the same PR summary,
1240
1252
  // so they collect through one function. Pushed inline, the draft call site
1241
1253
  // silently dropped descriptionReports for months: the structure reports for
1242
1254
  // newly drafted connectors, the ones most likely to need an upstream fix,
1243
1255
  // never reached the summary.
1244
1256
  const { collectGeneratorReports } = require('./pr-summary-formatter.js')
1245
- const collect = (result) => collectGeneratorReports(result, { descriptionReports, lostSectionWarnings })
1257
+ const collect = (result) => collectGeneratorReports(result, { descriptionReports, lostSectionWarnings, styleRegressionWarnings })
1246
1258
 
1247
1259
  try {
1248
1260
  const result = await generateRpcnConnectorDocs({
@@ -2109,22 +2121,24 @@ async function handleRpcnConnectorDocs (options) {
2109
2121
 
2110
2122
  // Generate PR summary
2111
2123
  try {
2112
- const { printPRSummary, renderLostSectionWarnings, renderDescriptionReports } = require('./pr-summary-formatter.js')
2124
+ const { printPRSummary, renderLostSectionWarnings, renderStyleRegressionWarnings, renderDescriptionReports } = require('./pr-summary-formatter.js')
2113
2125
  // Use master diff if available, otherwise use single diff. Content-loss
2114
2126
  // warnings and structure reports ride the diff object so they land in the
2115
2127
  // PR summary body instead of dying in the collapsed workflow log.
2116
2128
  const summaryDiff = masterDiff || diffJson
2117
2129
  if (summaryDiff) {
2118
2130
  if (lostSectionWarnings.length) summaryDiff.lostSectionWarnings = lostSectionWarnings
2131
+ if (styleRegressionWarnings.length) summaryDiff.styleRegressionWarnings = styleRegressionWarnings
2119
2132
  if (descriptionReports.length) summaryDiff.descriptionReports = descriptionReports
2120
2133
  printPRSummary(summaryDiff, binaryAnalysis, draftFiles, masterDiff ? true : false)
2121
- } else if (lostSectionWarnings.length || descriptionReports.length) {
2134
+ } else if (lostSectionWarnings.length || styleRegressionWarnings.length || descriptionReports.length) {
2122
2135
  // No diff to summarize (no prior version, or versions match), but this
2123
2136
  // run still produced content-loss warnings or description-structure
2124
2137
  // reports that a reviewer needs to see -- print them on their own
2125
2138
  // instead of losing them because there was nothing to diff against.
2126
2139
  const lines = [
2127
2140
  ...renderLostSectionWarnings(lostSectionWarnings),
2141
+ ...renderStyleRegressionWarnings(styleRegressionWarnings),
2128
2142
  ...renderDescriptionReports(descriptionReports)
2129
2143
  ]
2130
2144
  console.log('\n' + lines.join('\n') + '\n')
@@ -1718,7 +1718,15 @@ function formatDescription(desc, customTransformations = null, options = {}) {
1718
1718
 
1719
1719
  // Merge backticked environment variables with adjacent paths
1720
1720
  // e.g., `$HOME`/.local/bin → `$HOME/.local/bin`
1721
- result = result.replace(/`(\$[A-Z_][A-Z0-9_]*)`(\/[^\s`]+)/g, '`$1$2`')
1721
+ // The continuation excludes a trailing ) . , ; : ! ? so sentence punctuation
1722
+ // immediately after the path (no space) lands outside the code span instead
1723
+ // of being swallowed into it, e.g. "...(defaults to `$HOME/.local/bin)`" or
1724
+ // "...saved in `$HOME/.local/bin.` This" -- both published on
1725
+ // rpk-plugin-install.adoc before this fix. The continuation past the slash
1726
+ // is optional, so a bare trailing slash ($HOME/, $PWD/ -- both real in the
1727
+ // rpk source) still merges into `$HOME/` instead of staying split as
1728
+ // `$HOME`/.
1729
+ result = result.replace(/`(\$[A-Z_][A-Z0-9_]*)`(\/(?:[^\s`]*[^\s`).,;:!?])?)/g, '`$1$2`')
1722
1730
 
1723
1731
  // Add backticks around file paths (but not if already backticked)
1724
1732
  // Must check both the slash and the path aren't already inside backticks