@redpanda-data/docs-extensions-and-macros 5.13.0 → 5.13.1
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 +39 -0
- package/package.json +1 -1
- package/tools/generate-cli-docs.js +15 -4
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +275 -33
- package/tools/redpanda-connect/helpers/descriptionInclude.js +24 -0
- package/tools/redpanda-connect/helpers/index.js +3 -0
- package/tools/redpanda-connect/helpers/renderConnectDescription.js +125 -0
- package/tools/redpanda-connect/helpers/summaryAttribute.js +22 -0
- package/tools/redpanda-connect/metadata-utils.js +183 -35
- package/tools/redpanda-connect/migrate-descriptions-to-partials.js +275 -0
- package/tools/redpanda-connect/normalize-metadata.js +6 -4
- package/tools/redpanda-connect/pr-summary-formatter.js +51 -0
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +37 -7
- package/tools/redpanda-connect/templates/connector.hbs +17 -4
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +21 -0
- package/tools/redpanda-connect/templates/intro.hbs +1 -1
package/bin/doc-tools.js
CHANGED
|
@@ -772,9 +772,11 @@ automation
|
|
|
772
772
|
.option('--template-fields <path>', 'Fields section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/fields-partials.hbs'))
|
|
773
773
|
.option('--template-examples <path>', 'Examples section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/examples-partials.hbs'))
|
|
774
774
|
.option('--template-metadata <path>', 'Metadata section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/metadata-partials.hbs'))
|
|
775
|
+
.option('--template-description <path>', 'Description section partial template', path.resolve(__dirname, '../tools/redpanda-connect/templates/descriptions-partials.hbs'))
|
|
775
776
|
.option('--template-bloblang <path>', 'Custom Handlebars template for bloblang function/method partials')
|
|
776
777
|
.option('--overrides <path>', 'Optional JSON file with overrides', 'docs-data/overrides.json')
|
|
777
778
|
.option('--include-bloblang', 'Include Bloblang functions and methods in generation')
|
|
779
|
+
.option('--prune-orphaned-descriptions', 'Allow the description-partial orphan sweep to blank more than 10% of the tree. Only use this when the connector dataset is confirmed complete: an incomplete dataset looks identical to a mass upstream deletion and the sweep blanks published content')
|
|
778
780
|
.option('--cloud-version <version>', 'Cloud binary version (default: auto-detect latest)')
|
|
779
781
|
.option('--cgo-version <version>', 'cgo binary version (default: same as cloud-version)')
|
|
780
782
|
.option('--skip-intermediate', 'Skip intermediate release processing (legacy mode - only compare latest vs last documented)')
|
|
@@ -794,6 +796,43 @@ automation
|
|
|
794
796
|
await handleRpcnConnectorDocs(options)
|
|
795
797
|
})
|
|
796
798
|
|
|
799
|
+
/**
|
|
800
|
+
* generate migrate-rpcn-descriptions
|
|
801
|
+
*
|
|
802
|
+
* @description
|
|
803
|
+
* One-time migration that rewires published connector reference pages onto the
|
|
804
|
+
* regenerated description partial: the header `:description:` becomes
|
|
805
|
+
* `include::...[tag=attrs]` and the frozen intro prose becomes
|
|
806
|
+
* `include::...[tag=body]`. Afterwards `generate rpcn-connector-docs` keeps
|
|
807
|
+
* both fresh on every run.
|
|
808
|
+
*
|
|
809
|
+
* @why
|
|
810
|
+
* Connector pages are one-time first drafts, so their summary, "Introduced in
|
|
811
|
+
* version" line and description prose never change again even when the
|
|
812
|
+
* connector does. The body rewire is guarded: a page whose intro carries
|
|
813
|
+
* anything the partial does not reproduce is reported and left for a human,
|
|
814
|
+
* because rewriting it would delete published content.
|
|
815
|
+
*
|
|
816
|
+
* @example
|
|
817
|
+
* # Dry run (default): report the pages that would change
|
|
818
|
+
* npx doc-tools generate migrate-rpcn-descriptions
|
|
819
|
+
*
|
|
820
|
+
* # Apply the migration
|
|
821
|
+
* npx doc-tools generate migrate-rpcn-descriptions --write
|
|
822
|
+
*
|
|
823
|
+
* # Rewire page headers only, leaving the body prose alone
|
|
824
|
+
* npx doc-tools generate migrate-rpcn-descriptions --write --skip-body
|
|
825
|
+
*/
|
|
826
|
+
automation
|
|
827
|
+
.command('migrate-rpcn-descriptions')
|
|
828
|
+
.description('One-time migration of frozen connector summaries and description prose onto the regenerated description partial. Dry run unless --write is given.')
|
|
829
|
+
.option('--write', 'Apply changes (default is a dry run that only reports)')
|
|
830
|
+
.option('--skip-body', 'Rewire page headers only, leaving the frozen intro prose in the page')
|
|
831
|
+
.action((options) => {
|
|
832
|
+
const { migrateDescriptionsToPartials } = require('../tools/redpanda-connect/migrate-descriptions-to-partials.js')
|
|
833
|
+
migrateDescriptionsToPartials({ write: Boolean(options.write), skipBody: Boolean(options.skipBody) })
|
|
834
|
+
})
|
|
835
|
+
|
|
797
836
|
/**
|
|
798
837
|
* generate migrate-rpcn-metadata
|
|
799
838
|
*
|
package/package.json
CHANGED
|
@@ -126,9 +126,19 @@ function parseJSDocComments(sourceFile) {
|
|
|
126
126
|
const content = fs.readFileSync(sourceFile, 'utf8');
|
|
127
127
|
const comments = {};
|
|
128
128
|
|
|
129
|
-
// Regex to match JSDoc comments followed by command definitions
|
|
130
|
-
// Matches both top-level commands and automation.command()
|
|
131
|
-
|
|
129
|
+
// Regex to match JSDoc comments followed by command definitions.
|
|
130
|
+
// Matches both top-level commands and automation.command().
|
|
131
|
+
//
|
|
132
|
+
// The comment body is `(?:(?!\*\/)[\s\S])*?`, a tempered match that cannot
|
|
133
|
+
// cross a closing `*\/`, rather than a plain `[\s\S]*?`. With the plain
|
|
134
|
+
// version two ADJACENT JSDoc blocks merged into one match: the lazy body ran
|
|
135
|
+
// past the first block's terminator, because that terminator is followed by
|
|
136
|
+
// `/**` rather than by `.command(`, and kept going to the second. The
|
|
137
|
+
// extractor then took the FIRST @description it found, so the second command
|
|
138
|
+
// was documented with the first command's prose. That is exactly what
|
|
139
|
+
// happened to `generate migrate-rpcn-descriptions`, which shipped a
|
|
140
|
+
// regenerated section describing migrate-rpcn-metadata.
|
|
141
|
+
const pattern = /\/\*\*\s*((?:(?!\*\/)[\s\S])*?)\s*\*\/\s*(?:programCli|automation|validation)\s*\.command\(['"]([^'"]+)['"]\)/g;
|
|
132
142
|
|
|
133
143
|
let match;
|
|
134
144
|
while ((match = pattern.exec(content)) !== null) {
|
|
@@ -306,7 +316,8 @@ IMPORTANT: This documentation is auto-generated. Do not edit manually. Run \`npm
|
|
|
306
316
|
'crd-spec',
|
|
307
317
|
'bundle-openapi',
|
|
308
318
|
'update-connect-version',
|
|
309
|
-
'migrate-rpcn-metadata'
|
|
319
|
+
'migrate-rpcn-metadata',
|
|
320
|
+
'migrate-rpcn-descriptions'
|
|
310
321
|
];
|
|
311
322
|
|
|
312
323
|
generateSubcommands.forEach(subcmd => {
|
|
@@ -5,7 +5,9 @@ const path = require('path');
|
|
|
5
5
|
const handlebars = require('handlebars');
|
|
6
6
|
const yaml = require('yaml');
|
|
7
7
|
const helpers = require('./helpers');
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
lostMetadataSections, typeDirFor, flattenToAttributeValue, CONNECTOR_TYPE_DIRS, resolvePageTypeDir,
|
|
10
|
+
} = require('./metadata-utils.js');
|
|
9
11
|
|
|
10
12
|
// Register each helper under handlebars, verifying that it’s a function
|
|
11
13
|
Object.entries(helpers).forEach(([name, fn]) => {
|
|
@@ -22,6 +24,12 @@ const DEFAULT_TEMPLATE = path.resolve(__dirname, './templates/connector.hbs');
|
|
|
22
24
|
// Default template for the regenerated per-connector metadata partial.
|
|
23
25
|
const DEFAULT_METADATA_TEMPLATE = path.resolve(__dirname, './templates/metadata-partials.hbs');
|
|
24
26
|
|
|
27
|
+
// Default template for the regenerated per-connector description partial. Like
|
|
28
|
+
// the metadata partial, this is rewritten on every run so summary/description
|
|
29
|
+
// changes upstream flow into already-published pages that include it.
|
|
30
|
+
const DEFAULT_DESCRIPTION_TEMPLATE = path.resolve(__dirname, './templates/descriptions-partials.hbs');
|
|
31
|
+
const { hasStructuralHeadings, hasMarkdownHeadings, firstHeadingDepth, escapePlaceholderBraces, LONG_HEADINGLESS_THRESHOLD } = require('./helpers/renderConnectDescription.js');
|
|
32
|
+
|
|
25
33
|
// Banner-only content written to a metadata partial when its `== Metadata`
|
|
26
34
|
// section is removed upstream. Kept as an AsciiDoc comment so the file renders
|
|
27
35
|
// nothing while any lingering include directive still resolves cleanly.
|
|
@@ -30,6 +38,44 @@ const STALE_METADATA_PARTIAL =
|
|
|
30
38
|
'The connector description no longer contains a `== Metadata` section, so this partial is intentionally empty. ' +
|
|
31
39
|
'You can remove the include directive that points at this file from the connector page.\n';
|
|
32
40
|
|
|
41
|
+
// Banner-only content written to a description partial when the connector's
|
|
42
|
+
// description is removed upstream. Same rationale as STALE_METADATA_PARTIAL:
|
|
43
|
+
// deleting the file would break any published page that includes it, so blank
|
|
44
|
+
// it instead and log so a human removes the include line.
|
|
45
|
+
//
|
|
46
|
+
// The empty attrs/body regions are load-bearing. Pages include this file with
|
|
47
|
+
// `[tag=attrs]` and `[tag=body]`, and Asciidoctor logs `tag 'attrs' not found
|
|
48
|
+
// in include file` for every tag a target does not define -- so a banner
|
|
49
|
+
// without the regions turns one blanked partial into two build warnings on
|
|
50
|
+
// every page that includes it, on every build, which is a build failure
|
|
51
|
+
// wherever Asciidoctor warnings are treated as errors. An empty region
|
|
52
|
+
// resolves to nothing and says nothing.
|
|
53
|
+
const STALE_DESCRIPTION_PARTIAL =
|
|
54
|
+
'// This content is autogenerated. Do not edit manually. ' +
|
|
55
|
+
'The connector no longer has a description, so this partial is intentionally empty. ' +
|
|
56
|
+
'You can remove the include directive that points at this file from the connector page.\n' +
|
|
57
|
+
'\n// tag::attrs[]\n// end::attrs[]\n' +
|
|
58
|
+
'\n// tag::body[]\n// end::body[]\n';
|
|
59
|
+
|
|
60
|
+
// Component families with their own reference pages, keyed by the canonical
|
|
61
|
+
// type directory from typeDirFor(). Other data keys -- `config`, the bloblang
|
|
62
|
+
// functions and methods -- have no per-component page that could include a
|
|
63
|
+
// partial, so none is emitted for them.
|
|
64
|
+
const CONNECTOR_PAGE_TYPE_DIRS = new Set(CONNECTOR_TYPE_DIRS);
|
|
65
|
+
|
|
66
|
+
// Blast-radius guard for the post-generation orphan sweep. The sweep blanks
|
|
67
|
+
// every description partial the current dataset did not claim, so an
|
|
68
|
+
// incomplete dataset (a truncated fetch, a half-succeeded --force-fresh, a
|
|
69
|
+
// hand-made subset file) does not look different from "hundreds of connectors
|
|
70
|
+
// were deleted upstream" -- and the partials are published content. Blanking
|
|
71
|
+
// up to ORPHAN_SWEEP_ALWAYS_ALLOWED files is always fine (that is the size of
|
|
72
|
+
// a plausible upstream deletion); above that the sweep also has to stay under
|
|
73
|
+
// ORPHAN_SWEEP_MAX_FRACTION of the tree, or it refuses to run and reports
|
|
74
|
+
// instead. `pruneOrphanedDescriptions` is the explicit opt-in that overrides
|
|
75
|
+
// the refusal once a human has confirmed the dataset is complete.
|
|
76
|
+
const ORPHAN_SWEEP_ALWAYS_ALLOWED = 10;
|
|
77
|
+
const ORPHAN_SWEEP_MAX_FRACTION = 0.1;
|
|
78
|
+
|
|
33
79
|
/**
|
|
34
80
|
* Reads a file at `filePath` and registers it as a Handlebars partial called `name`.
|
|
35
81
|
* Throws if the file cannot be read.
|
|
@@ -216,6 +262,7 @@ function resolveReferences(obj, root) {
|
|
|
216
262
|
* @param {string} [options.templateFields] - Path to the fields partial template.
|
|
217
263
|
* @param {string} [options.templateExamples] - Path to the examples partial template.
|
|
218
264
|
* @param {boolean} options.writeFullDrafts - If true, generates full draft documentation; otherwise, generates partials.
|
|
265
|
+
* @param {boolean} [options.pruneOrphanedDescriptions=false] - Allow the post-generation sweep to blank description partials absent from the dataset even when they exceed the safety threshold. Only pass this for a dataset confirmed complete.
|
|
219
266
|
* @returns {Promise<Object>} An object summarizing the number and paths of generated partials and drafts.
|
|
220
267
|
*
|
|
221
268
|
* @throws {Error} If reading or parsing input files fails, if template rendering fails for a component, or if $ref references cannot be resolved.
|
|
@@ -257,8 +304,12 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
257
304
|
templateFields,
|
|
258
305
|
templateExamples,
|
|
259
306
|
templateMetadata,
|
|
307
|
+
templateDescription,
|
|
260
308
|
templateBloblang,
|
|
261
309
|
writeFullDrafts,
|
|
310
|
+
// Explicit opt-in for the destructive description-partial orphan sweep.
|
|
311
|
+
// Off by default: see ORPHAN_SWEEP_MAX_FRACTION.
|
|
312
|
+
pruneOrphanedDescriptions = false,
|
|
262
313
|
cgoOnly = [], // Array of cgo-only connectors from cgo binary inspection
|
|
263
314
|
cloudOnly = [], // Array of cloud-only connectors from cloud binary inspection
|
|
264
315
|
csvMetadata = [] // Array of CSV metadata with support levels
|
|
@@ -358,6 +409,12 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
358
409
|
const metadataTemplatePath = templateMetadata || DEFAULT_METADATA_TEMPLATE;
|
|
359
410
|
registerPartial('metadata', metadataTemplatePath);
|
|
360
411
|
|
|
412
|
+
// Always register the description partial. Like metadata, it is regenerated
|
|
413
|
+
// on every run so summary/description edits upstream flow into any page that
|
|
414
|
+
// includes it, instead of being frozen into the page body at first draft.
|
|
415
|
+
const descriptionTemplatePath = templateDescription || DEFAULT_DESCRIPTION_TEMPLATE;
|
|
416
|
+
registerPartial('description', descriptionTemplatePath);
|
|
417
|
+
|
|
361
418
|
// In draft mode, also register the intro partial
|
|
362
419
|
if (writeFullDrafts && templateIntro) {
|
|
363
420
|
registerPartial('intro', templateIntro);
|
|
@@ -367,6 +424,7 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
367
424
|
const fieldsOutRoot = path.join(outputRoot, 'fields');
|
|
368
425
|
const examplesOutRoot = path.join(outputRoot, 'examples');
|
|
369
426
|
const metadataOutRoot = path.join(outputRoot, 'metadata');
|
|
427
|
+
const descriptionOutRoot = path.join(outputRoot, 'descriptions');
|
|
370
428
|
// Drafts go to pages/, not partials/
|
|
371
429
|
const componentsRoot = writeFullDrafts
|
|
372
430
|
? path.resolve(process.cwd(), 'modules/components/pages')
|
|
@@ -377,10 +435,22 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
377
435
|
fs.mkdirSync(fieldsOutRoot, { recursive: true });
|
|
378
436
|
fs.mkdirSync(examplesOutRoot, { recursive: true });
|
|
379
437
|
fs.mkdirSync(metadataOutRoot, { recursive: true });
|
|
438
|
+
fs.mkdirSync(descriptionOutRoot, { recursive: true });
|
|
380
439
|
}
|
|
381
440
|
|
|
441
|
+
// Compile the one-line partial wrappers once, not once per component: they
|
|
442
|
+
// are constants, and the loop runs for every entry in every data key.
|
|
443
|
+
const renderFieldsPartial = handlebars.compile('{{> fields children=config.children}}');
|
|
444
|
+
const renderExamplesPartial = handlebars.compile('{{> examples examples=examples}}');
|
|
445
|
+
const renderMetadataPartial = handlebars.compile('{{> metadata description=description type=type typeDir=typeDir name=name}}');
|
|
446
|
+
const renderDescriptionPartial = handlebars.compile('{{> description summary=summary version=version description=description type=type typeDir=typeDir name=name}}');
|
|
447
|
+
|
|
382
448
|
let partialsWritten = 0;
|
|
449
|
+
const descriptionReports = [];
|
|
383
450
|
const lostSectionWarnings = [];
|
|
451
|
+
// typeDir/name keys handled this run; used after the loop to blank
|
|
452
|
+
// description partials orphaned by a connector deleted upstream.
|
|
453
|
+
const visitedDescriptionPartials = new Set();
|
|
384
454
|
let draftsWritten = 0;
|
|
385
455
|
const partialFiles = [];
|
|
386
456
|
const draftFiles = [];
|
|
@@ -395,17 +465,23 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
395
465
|
// Compute typeDir once for this item (used in templates and file paths)
|
|
396
466
|
const typeDir = type.endsWith('s') ? type : `${type}s`;
|
|
397
467
|
item.typeDir = typeDir;
|
|
468
|
+
// Canonical directory for the partials that pages address by include
|
|
469
|
+
// line. `typeDir` keeps the raw data-key spelling because the fields
|
|
470
|
+
// and examples partials are written under it and published pages
|
|
471
|
+
// already include them by that path; the metadata and description
|
|
472
|
+
// partials are addressed through typeDirFor(), so writing them under
|
|
473
|
+
// anything else leaves the include unresolved. Rate limits are the
|
|
474
|
+
// live case: data key `rate-limits`, include path `rate_limits`.
|
|
475
|
+
const partialTypeDir = typeDirFor(item);
|
|
398
476
|
|
|
399
477
|
// Always generate field and example partials (needed for both standalone and draft modes)
|
|
400
478
|
// Render fields using the registered "fields" partial
|
|
401
|
-
const fieldsOut =
|
|
402
|
-
.compile('{{> fields children=config.children}}')(item);
|
|
479
|
+
const fieldsOut = renderFieldsPartial(item);
|
|
403
480
|
|
|
404
481
|
// Render examples only if an examples template was provided
|
|
405
482
|
let examplesOut = '';
|
|
406
483
|
if (examplesTemplatePath) {
|
|
407
|
-
examplesOut =
|
|
408
|
-
.compile('{{> examples examples=examples}}')(item);
|
|
484
|
+
examplesOut = renderExamplesPartial(item);
|
|
409
485
|
}
|
|
410
486
|
|
|
411
487
|
if (fieldsOut.trim()) {
|
|
@@ -418,7 +494,11 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
418
494
|
}
|
|
419
495
|
}
|
|
420
496
|
|
|
421
|
-
|
|
497
|
+
// Same allowlist as the metadata and description partials below: one
|
|
498
|
+
// gate, one list. The old denylist named only the two bloblang keys, so
|
|
499
|
+
// any other non-page data key (config today, anything added upstream
|
|
500
|
+
// tomorrow) leaked partials no page can include.
|
|
501
|
+
if (examplesOut.trim() && CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
422
502
|
const ePath = path.join(examplesOutRoot, type, `${name}.adoc`);
|
|
423
503
|
fs.mkdirSync(path.dirname(ePath), { recursive: true });
|
|
424
504
|
fs.writeFileSync(ePath, examplesOut);
|
|
@@ -428,15 +508,17 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
428
508
|
}
|
|
429
509
|
}
|
|
430
510
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
//
|
|
439
|
-
|
|
511
|
+
if (CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
512
|
+
// Render the metadata partial from the connector's description. Only
|
|
513
|
+
// written when the description contains a `== Metadata` section.
|
|
514
|
+
// Rendered inside the guard, not before it: the loop covers every
|
|
515
|
+
// data key, so rendering first meant computing and discarding a
|
|
516
|
+
// partial for every bloblang function, method and config entry.
|
|
517
|
+
const metadataOut = renderMetadataPartial(item);
|
|
518
|
+
// Write under the canonical type directory (not the raw data key) so
|
|
519
|
+
// the file path always matches the include directive built by
|
|
520
|
+
// metadataIncludeLine().
|
|
521
|
+
const mPath = path.join(metadataOutRoot, partialTypeDir, `${name}.adoc`);
|
|
440
522
|
if (metadataOut.trim()) {
|
|
441
523
|
fs.mkdirSync(path.dirname(mPath), { recursive: true });
|
|
442
524
|
if (fs.existsSync(mPath)) {
|
|
@@ -478,6 +560,106 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
478
560
|
}
|
|
479
561
|
}
|
|
480
562
|
|
|
563
|
+
// Render the description partial (summary + version + description, with
|
|
564
|
+
// the `== Metadata` block replaced by its own partial include). Emitted
|
|
565
|
+
// for every connector type that has reference pages so a page that
|
|
566
|
+
// includes it always shows the current summary/description instead of
|
|
567
|
+
// first-draft text. The summary is brace-escaped here, the same way
|
|
568
|
+
// renderConnectDescription escapes the description body: the template
|
|
569
|
+
// emits it raw in both the body and the :description: attribute, where
|
|
570
|
+
// an unescaped {placeholder} is silently consumed as a missing
|
|
571
|
+
// attribute reference.
|
|
572
|
+
if (CONNECTOR_PAGE_TYPE_DIRS.has(partialTypeDir)) {
|
|
573
|
+
const descriptionItem = typeof item.summary === 'string'
|
|
574
|
+
? { ...item, summary: escapePlaceholderBraces(item.summary) }
|
|
575
|
+
: item;
|
|
576
|
+
const descriptionOut = renderDescriptionPartial(descriptionItem);
|
|
577
|
+
visitedDescriptionPartials.add(`${partialTypeDir}/${name}`);
|
|
578
|
+
const dPath = path.join(descriptionOutRoot, partialTypeDir, `${name}.adoc`);
|
|
579
|
+
if (descriptionOut.trim()) {
|
|
580
|
+
fs.mkdirSync(path.dirname(dPath), { recursive: true });
|
|
581
|
+
if (fs.existsSync(dPath)) {
|
|
582
|
+
// Same content-loss check the metadata partial ten lines up has
|
|
583
|
+
// had since #236, for the same reason: regeneration is
|
|
584
|
+
// authoritative, but a section disappearing from a published
|
|
585
|
+
// partial is content loss, not cleanup. Now that pages include
|
|
586
|
+
// these files the risk is real -- a section can only vanish
|
|
587
|
+
// because it left the upstream description or because a human
|
|
588
|
+
// added it here -- so warn before overwriting instead of
|
|
589
|
+
// dropping it without a trace.
|
|
590
|
+
const lost = lostMetadataSections(fs.readFileSync(dPath, 'utf8'), descriptionOut);
|
|
591
|
+
if (lost.length) {
|
|
592
|
+
lostSectionWarnings.push({ partial: path.relative(process.cwd(), dPath), sections: lost });
|
|
593
|
+
console.warn(
|
|
594
|
+
`Warning: regenerated description partial ${path.relative(process.cwd(), dPath)} ` +
|
|
595
|
+
`drops previously published section(s): ${lost.map((h) => `"${h}"`).join(', ')}. ` +
|
|
596
|
+
'If this content is still wanted, move it to the connector page or restore it ' +
|
|
597
|
+
'in the upstream description.'
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
fs.writeFileSync(dPath, descriptionOut);
|
|
602
|
+
// The partial now exists for this connector, so a drafted page can
|
|
603
|
+
// include its body instead of freezing the summary and description
|
|
604
|
+
// into the page. Gated on the file actually being written:
|
|
605
|
+
// component families outside CONNECTOR_PAGE_TYPE_DIRS, and
|
|
606
|
+
// connectors with neither summary nor description, keep the inline
|
|
607
|
+
// text rather than getting an include that cannot resolve.
|
|
608
|
+
item.hasDescriptionPartial = true;
|
|
609
|
+
// The attrs region only sets :description: when the connector has a
|
|
610
|
+
// summary, so a page may only replace its header attribute with the
|
|
611
|
+
// include when there is one. Otherwise the page would lose its meta
|
|
612
|
+
// description entirely and keep the inline attribute instead.
|
|
613
|
+
if (typeof item.summary === 'string' && item.summary.trim()) {
|
|
614
|
+
item.hasDescriptionAttrs = true;
|
|
615
|
+
}
|
|
616
|
+
if (!writeFullDrafts) {
|
|
617
|
+
partialsWritten++;
|
|
618
|
+
partialFiles.push(path.relative(process.cwd(), dPath));
|
|
619
|
+
}
|
|
620
|
+
// Long heading-less descriptions render as a wall of text. The fix
|
|
621
|
+
// belongs upstream (add == sections in the Connect source), so
|
|
622
|
+
// surface them here instead of hiding them in the docs build.
|
|
623
|
+
if (
|
|
624
|
+
typeof item.description === 'string' &&
|
|
625
|
+
item.description.length > LONG_HEADINGLESS_THRESHOLD &&
|
|
626
|
+
!hasStructuralHeadings(item.description)
|
|
627
|
+
) {
|
|
628
|
+
const md = hasMarkdownHeadings(item.description);
|
|
629
|
+
const msg = md
|
|
630
|
+
? `Markdown-style headings: ${partialTypeDir}/${name} structures its description with ## headings, ` +
|
|
631
|
+
'which render unreliably. Convert them to == sections upstream in the Connect source.'
|
|
632
|
+
: `Long heading-less description: ${partialTypeDir}/${name} ` +
|
|
633
|
+
`(${item.description.length} chars). Consider adding == sections upstream in the Connect source.`;
|
|
634
|
+
descriptionReports.push({ connector: `${partialTypeDir}/${name}`, message: msg });
|
|
635
|
+
console.warn(msg);
|
|
636
|
+
}
|
|
637
|
+
// Headings that start deeper than level one render out of
|
|
638
|
+
// sequence on the page. Upstream fix, so report it.
|
|
639
|
+
const headingDepth = typeof item.description === 'string' ? firstHeadingDepth(item.description) : null;
|
|
640
|
+
if (headingDepth !== null && headingDepth > 2) {
|
|
641
|
+
const seqMsg =
|
|
642
|
+
`Out-of-sequence headings: ${partialTypeDir}/${name} starts at level ${headingDepth - 1} ` +
|
|
643
|
+
'(=== or ### with no parent section). Promote the top headings upstream in the Connect source.';
|
|
644
|
+
descriptionReports.push({ connector: `${partialTypeDir}/${name}`, message: seqMsg });
|
|
645
|
+
console.warn(seqMsg);
|
|
646
|
+
}
|
|
647
|
+
} else if (fs.existsSync(dPath)) {
|
|
648
|
+
// The upstream description disappeared, but a previously generated
|
|
649
|
+
// partial is still on disk and an already published page may include
|
|
650
|
+
// it. Deleting the file would break that include, so blank it to a
|
|
651
|
+
// banner-only file (the include still resolves and renders nothing)
|
|
652
|
+
// and log it so a human knows to remove the include line. Without
|
|
653
|
+
// this, the stale first-draft text would live on disk forever --
|
|
654
|
+
// exactly the drift these partials exist to prevent.
|
|
655
|
+
fs.writeFileSync(dPath, STALE_DESCRIPTION_PARTIAL);
|
|
656
|
+
console.log(
|
|
657
|
+
`Description removed upstream; emptied stale partial ${path.relative(process.cwd(), dPath)} ` +
|
|
658
|
+
`(remove its include directive from the ${type}/${name} page)`
|
|
659
|
+
);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
481
663
|
if (writeFullDrafts) {
|
|
482
664
|
if (String(item.status || '').toLowerCase() === 'deprecated') {
|
|
483
665
|
console.log(`Skipping draft for deprecated component: ${type}/${name}`);
|
|
@@ -677,6 +859,71 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
677
859
|
}
|
|
678
860
|
}
|
|
679
861
|
|
|
862
|
+
// A connector deleted upstream leaves no dataset entry at all, so the
|
|
863
|
+
// in-loop stale-partial blanking above never sees it and its description
|
|
864
|
+
// partial would keep serving deleted-connector text forever, silently.
|
|
865
|
+
// Sweep the description output root for partials no connector claimed this
|
|
866
|
+
// run and blank them the same way. Skipped in draft mode: drafts run on a
|
|
867
|
+
// filtered dataset (missing connectors only), so "not visited" there means
|
|
868
|
+
// "already documented", not "deleted upstream".
|
|
869
|
+
// The sweep is destructive against published content, so collect the
|
|
870
|
+
// candidates first and check the blast radius before writing anything.
|
|
871
|
+
let orphanSweepSkipped = null;
|
|
872
|
+
if (!writeFullDrafts && fs.existsSync(descriptionOutRoot)) {
|
|
873
|
+
const orphans = [];
|
|
874
|
+
let partialsOnDisk = 0;
|
|
875
|
+
for (const typeDirName of fs.readdirSync(descriptionOutRoot)) {
|
|
876
|
+
const typeDirPath = path.join(descriptionOutRoot, typeDirName);
|
|
877
|
+
if (!fs.statSync(typeDirPath).isDirectory()) continue;
|
|
878
|
+
for (const file of fs.readdirSync(typeDirPath)) {
|
|
879
|
+
if (!file.endsWith('.adoc')) continue;
|
|
880
|
+
partialsOnDisk++;
|
|
881
|
+
const key = `${typeDirName}/${file.slice(0, -'.adoc'.length)}`;
|
|
882
|
+
if (visitedDescriptionPartials.has(key)) continue;
|
|
883
|
+
const orphanPath = path.join(typeDirPath, file);
|
|
884
|
+
// Already blanked on an earlier run: leave it alone so it is not
|
|
885
|
+
// re-reported forever.
|
|
886
|
+
if (fs.readFileSync(orphanPath, 'utf8') === STALE_DESCRIPTION_PARTIAL) continue;
|
|
887
|
+
orphans.push({ key, orphanPath });
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// Over the threshold the far likelier explanation is an incomplete
|
|
892
|
+
// dataset, not a mass upstream deletion, so refuse and report.
|
|
893
|
+
const overBlastRadius =
|
|
894
|
+
orphans.length > ORPHAN_SWEEP_ALWAYS_ALLOWED &&
|
|
895
|
+
orphans.length > partialsOnDisk * ORPHAN_SWEEP_MAX_FRACTION;
|
|
896
|
+
|
|
897
|
+
if (orphans.length && overBlastRadius && !pruneOrphanedDescriptions) {
|
|
898
|
+
const percent = Math.round((orphans.length / partialsOnDisk) * 100);
|
|
899
|
+
const msg =
|
|
900
|
+
`Orphan sweep refused: ${orphans.length} of ${partialsOnDisk} description partial(s) (${percent}%) ` +
|
|
901
|
+
`are absent from this run's dataset, over the ${Math.round(ORPHAN_SWEEP_MAX_FRACTION * 100)}% safety ` +
|
|
902
|
+
'threshold. That normally means the dataset is truncated or filtered, not that this many connectors ' +
|
|
903
|
+
'were deleted upstream, and blanking them would wipe published content. Nothing was blanked. ' +
|
|
904
|
+
'Confirm the dataset is complete, then re-run with --prune-orphaned-descriptions to sweep. ' +
|
|
905
|
+
`Affected: ${orphans.slice(0, 10).map((o) => o.key).join(', ')}` +
|
|
906
|
+
(orphans.length > 10 ? `, and ${orphans.length - 10} more` : '');
|
|
907
|
+
orphanSweepSkipped = {
|
|
908
|
+
orphans: orphans.length,
|
|
909
|
+
partialsOnDisk,
|
|
910
|
+
connectors: orphans.map((o) => o.key),
|
|
911
|
+
};
|
|
912
|
+
descriptionReports.push({ connector: 'descriptions (sweep)', message: msg });
|
|
913
|
+
console.error(msg);
|
|
914
|
+
} else {
|
|
915
|
+
for (const { key, orphanPath } of orphans) {
|
|
916
|
+
fs.writeFileSync(orphanPath, STALE_DESCRIPTION_PARTIAL);
|
|
917
|
+
const msg =
|
|
918
|
+
`Connector removed upstream: emptied orphaned description partial ` +
|
|
919
|
+
`${path.relative(process.cwd(), orphanPath)} ` +
|
|
920
|
+
`(remove its include directive and retire the ${key} page)`;
|
|
921
|
+
descriptionReports.push({ connector: key, message: msg });
|
|
922
|
+
console.log(msg);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
680
927
|
// Self-heal published page headers: pages are one-time drafts, so pages
|
|
681
928
|
// created before the template emitted :description: never got one.
|
|
682
929
|
let descriptionBackfill = { backfilled: [], skippedNoSummary: [] };
|
|
@@ -689,12 +936,13 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
689
936
|
draftsWritten,
|
|
690
937
|
partialFiles,
|
|
691
938
|
draftFiles,
|
|
939
|
+
descriptionReports,
|
|
692
940
|
lostSectionWarnings,
|
|
941
|
+
orphanSweepSkipped,
|
|
693
942
|
descriptionBackfill
|
|
694
943
|
};
|
|
695
944
|
}
|
|
696
945
|
|
|
697
|
-
|
|
698
946
|
// Connector page families that have per-component pages under
|
|
699
947
|
// modules/components/pages/. Other data keys (config, bloblang-functions,
|
|
700
948
|
// bloblang-methods) have no per-component page to backfill.
|
|
@@ -740,25 +988,19 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
|
|
|
740
988
|
const lines = content.split('\n');
|
|
741
989
|
const headerEnd = lines.findIndex((l) => l.trim() === '');
|
|
742
990
|
const header = lines.slice(0, headerEnd === -1 ? lines.length : headerEnd);
|
|
743
|
-
|
|
991
|
+
// A page migrated to the description partial carries the attrs include
|
|
992
|
+
// instead of a literal :description:. Without this arm the backfill
|
|
993
|
+
// splices a competing meta block into every migrated page, so the page
|
|
994
|
+
// ends up with a dead include plus a hand-inserted attribute that
|
|
995
|
+
// silently wins.
|
|
996
|
+
if (header.some((l) => l.startsWith(':description:') || /partial\$descriptions\//.test(l))) continue;
|
|
744
997
|
|
|
745
998
|
// Meta descriptions are read as plain text by search results, link
|
|
746
|
-
// previews, and assistants, so
|
|
747
|
-
//
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
// Bare URL macros (no xref:/link: prefix) and internal xref shorthand
|
|
752
|
-
// also appear in source summaries and render literally if left in place.
|
|
753
|
-
.replace(/https?:\/\/[^\s\[\]]+\[([^\]]*)\]/g, '$1')
|
|
754
|
-
.replace(/<<[^,>]+,([^>]+)>>/g, '$1')
|
|
755
|
-
.replace(/<<([^>]+)>>/g, '$1')
|
|
756
|
-
// Strip the "open in new tab" caret that AsciiDoc URL macros carry
|
|
757
|
-
// inside the link text, e.g. [Open Telemetry collector^].
|
|
758
|
-
.replace(/\^/g, '')
|
|
759
|
-
.replace(/`([^`]*)`/g, '$1')
|
|
760
|
-
.replace(/\s+/g, ' ')
|
|
761
|
-
.trim();
|
|
999
|
+
// previews, and assistants, so the markup comes out mechanically. Same
|
|
1000
|
+
// flattener the description partial's attrs region uses, so a page
|
|
1001
|
+
// cannot publish one meta description before migrating to the partial
|
|
1002
|
+
// and a different one after.
|
|
1003
|
+
const summary = flattenToAttributeValue(item.summary);
|
|
762
1004
|
if (!summary) {
|
|
763
1005
|
results.skippedNoSummary.push(`${typeDir}/${item.name}`);
|
|
764
1006
|
continue;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { descriptionIncludeLine } = require('../metadata-utils.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handlebars helper: emit the include directive for a connector's description
|
|
7
|
+
* partial, optionally for one of its tag regions.
|
|
8
|
+
*
|
|
9
|
+
* {{{descriptionInclude this 'attrs'}}}
|
|
10
|
+
* {{{descriptionInclude this 'body'}}}
|
|
11
|
+
*
|
|
12
|
+
* Drafted pages use this instead of freezing the summary and description into
|
|
13
|
+
* the page, so an upstream wording change reaches the published page on the
|
|
14
|
+
* next generator run. Delegates to descriptionIncludeLine so the path a page
|
|
15
|
+
* carries is built by the same function that decides where the generator
|
|
16
|
+
* writes the file.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} item connector data with `type`/`typeDir` and `name`
|
|
19
|
+
* @param {string} [tag] `attrs` or `body`
|
|
20
|
+
* @returns {string}
|
|
21
|
+
*/
|
|
22
|
+
module.exports = function descriptionInclude (item, tag) {
|
|
23
|
+
return descriptionIncludeLine(item, typeof tag === 'string' ? tag : undefined);
|
|
24
|
+
};
|
|
@@ -12,7 +12,10 @@ module.exports = {
|
|
|
12
12
|
renderConnectFields: require('./renderConnectFields.js'),
|
|
13
13
|
renderConnectExamples: require('./renderConnectExamples.js'),
|
|
14
14
|
renderConnectMetadata: require('./renderConnectMetadata.js'),
|
|
15
|
+
renderConnectDescription: require('./renderConnectDescription.js'),
|
|
16
|
+
summaryAttribute: require('./summaryAttribute.js'),
|
|
15
17
|
descriptionWithMetadataInclude: require('./descriptionWithMetadataInclude.js'),
|
|
18
|
+
descriptionInclude: require('./descriptionInclude.js'),
|
|
16
19
|
renderLeafField: require('./renderLeafField.js'),
|
|
17
20
|
renderObjectField: require('./renderObjectField.js'),
|
|
18
21
|
buildConfigYaml: require('./buildConfigYaml.js'),
|