@redpanda-data/docs-extensions-and-macros 5.12.4 → 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/cli-utils/big-int-json.js +85 -0
- package/extensions/render-property-descriptions.js +339 -0
- package/extensions/resolve-xrefs-in-attachments.js +24 -1
- package/package.json +7 -2
- package/tools/generate-cli-docs.js +15 -4
- package/tools/property-extractor/compare-properties.js +16 -5
- package/tools/property-extractor/generate-handlebars-docs.js +6 -1
- 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 +46 -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
|
@@ -2,6 +2,7 @@ const AUTOGEN_NOTICE = '// This content is autogenerated. Do not edit manually.
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const fs = require('fs');
|
|
5
|
+
const bigIntJson = require('../../cli-utils/big-int-json');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const handlebars = require('handlebars');
|
|
7
8
|
const helpers = require('./helpers');
|
|
@@ -425,7 +426,11 @@ function convertConfigRefsToProp(text) {
|
|
|
425
426
|
* Main generator
|
|
426
427
|
*/
|
|
427
428
|
function generateAllDocs(inputFile, outputDir) {
|
|
428
|
-
|
|
429
|
+
// bigIntJson, not JSON.parse: the uint64 and int64 limits carried as property
|
|
430
|
+
// maxima are beyond what a JS number holds, so a plain parse rounded them and
|
|
431
|
+
// the generated partials -- and therefore the published reference pages --
|
|
432
|
+
// showed 18446744073709552000 where the data says 18446744073709551615.
|
|
433
|
+
const data = bigIntJson.parse(fs.readFileSync(inputFile, 'utf8'));
|
|
429
434
|
// Support both 'properties' (from property_extractor.py) and 'topic_properties' (from topic_property_extractor.py)
|
|
430
435
|
const properties = data.properties || data.topic_properties || {};
|
|
431
436
|
|
|
@@ -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'),
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// One annotator for every scanner over connector prose, including the ones in
|
|
4
|
+
// metadata-utils that run before these. See annotateVerbatimLines there for
|
|
5
|
+
// why there is exactly one.
|
|
6
|
+
const { descriptionWithMetadataInclude, annotateVerbatimLines } = require('../metadata-utils.js');
|
|
7
|
+
|
|
8
|
+
// Length (characters) above which a heading-less description is reported as a
|
|
9
|
+
// candidate for upstream structure. Roughly a screen of prose.
|
|
10
|
+
const LONG_HEADINGLESS_THRESHOLD = 1200;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* True when the description contains at least one structural heading that sits
|
|
14
|
+
* outside a `----` listing block or a markdown ```/~~~ fence.
|
|
15
|
+
*
|
|
16
|
+
* Used to report long heading-less descriptions during generation. Embedded
|
|
17
|
+
* headings are otherwise passed through unchanged: they render as top-level
|
|
18
|
+
* page sections, which matches every published connector page today. Demoting
|
|
19
|
+
* them is not an option because the description renders before the page's
|
|
20
|
+
* first `==` section, so demoted headings produce "section title out of
|
|
21
|
+
* sequence" errors. Collapsible wrapping was evaluated and rejected: it hides
|
|
22
|
+
* primary content behind unreliable cross-browser find and deep-link behavior,
|
|
23
|
+
* and bodies containing their own `====` delimiters cannot be wrapped at all.
|
|
24
|
+
*/
|
|
25
|
+
function hasStructuralHeadings (body) {
|
|
26
|
+
// AsciiDoc headings only. Markdown-style ## headings deliberately do NOT
|
|
27
|
+
// count: the descriptions that render worst are exactly the ones whose
|
|
28
|
+
// only structure is markdown headings, and counting them here exempted
|
|
29
|
+
// those from the long-description report (they get their own report).
|
|
30
|
+
return annotateVerbatimLines(body)
|
|
31
|
+
.some(({ line, verbatim }) => !verbatim && /^={2,}\s+\S/.test(line));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* True when the description contains a markdown-style heading (`##`) outside
|
|
36
|
+
* listing blocks and fences. These are reported for upstream conversion to
|
|
37
|
+
* `==` (a `#` line inside a fence is a comment in an example, not a heading).
|
|
38
|
+
*/
|
|
39
|
+
function hasMarkdownHeadings (body) {
|
|
40
|
+
return annotateVerbatimLines(body)
|
|
41
|
+
.some(({ line, verbatim }) => !verbatim && /^#{2,}\s+\S/.test(line));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handlebars helper: render a connector's description for the regenerated
|
|
46
|
+
* description partial.
|
|
47
|
+
*
|
|
48
|
+
* The `== Metadata` block is replaced by an include of the metadata partial
|
|
49
|
+
* (de-duplicating it, since metadata is emitted as its own partial).
|
|
50
|
+
* Everything else passes through unchanged: the description is the page's
|
|
51
|
+
* primary content, and structure belongs upstream in the connector source.
|
|
52
|
+
* The generator reports long heading-less descriptions so they get headings
|
|
53
|
+
* added there instead of being hidden by the docs build.
|
|
54
|
+
*
|
|
55
|
+
* @param {object} item connector data (needs `description`, `type`/`typeDir`, `name`)
|
|
56
|
+
* @returns {string}
|
|
57
|
+
*/
|
|
58
|
+
module.exports = function renderConnectDescription (item) {
|
|
59
|
+
const body = descriptionWithMetadataInclude(item);
|
|
60
|
+
if (!body || !body.trim()) return '';
|
|
61
|
+
return escapePlaceholderBraces(ensureHeadingSeparation(body.trim()));
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Insert the blank line Asciidoctor requires before a section title. A
|
|
66
|
+
* heading glued to the paragraph above it renders as literal text and
|
|
67
|
+
* orphans its subsections into "section title out of sequence" warnings
|
|
68
|
+
* (live case: the protobuf processor's "== Operators"). Listing blocks and
|
|
69
|
+
* fences are untouched — a glued `#` comment inside a fenced example must
|
|
70
|
+
* not have a blank line pushed into the example.
|
|
71
|
+
*/
|
|
72
|
+
function ensureHeadingSeparation (body) {
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const { line, verbatim } of annotateVerbatimLines(body)) {
|
|
75
|
+
if (
|
|
76
|
+
!verbatim &&
|
|
77
|
+
/^(?:={2,6}|#{2,6})\s+\S/.test(line) &&
|
|
78
|
+
out.length > 0 &&
|
|
79
|
+
out[out.length - 1].trim() !== ''
|
|
80
|
+
) {
|
|
81
|
+
out.push('');
|
|
82
|
+
}
|
|
83
|
+
out.push(line);
|
|
84
|
+
}
|
|
85
|
+
return out.join('\n');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Escape template placeholders like {endpoint} so Asciidoctor keeps them
|
|
90
|
+
* instead of consuming them as attribute references (which substitutes to
|
|
91
|
+
* nothing and logs "skipping reference to missing attribute" on the
|
|
92
|
+
* published page — live today on salesforce_graphql and otlp_http).
|
|
93
|
+
* Single-backtick spans still apply attribute substitution, so spans are
|
|
94
|
+
* escaped too; `----` listing blocks and ```/~~~ fences are verbatim and stay
|
|
95
|
+
* untouched (a `\{` inside a fence would render as a literal backslash).
|
|
96
|
+
*/
|
|
97
|
+
function escapePlaceholderBraces (body) {
|
|
98
|
+
return annotateVerbatimLines(body).map(({ line, verbatim }) => (
|
|
99
|
+
verbatim ? line : line.replace(/(?<![\\{])\{([a-z][\w.-]{1,30})\}/g, '\\{$1}')
|
|
100
|
+
)).join('\n');
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Depth of the first structural heading (AsciiDoc `=` or markdown-compat
|
|
105
|
+
* `#`) outside listing blocks and fences, or null when the body has none.
|
|
106
|
+
* Descriptions whose first heading is deeper than level one (`===`, `###`)
|
|
107
|
+
* render "section title out of sequence" on the page, so the generator
|
|
108
|
+
* reports them as upstream fixes (seen on aws_dynamodb_cdc, iceberg,
|
|
109
|
+
* protobuf).
|
|
110
|
+
*/
|
|
111
|
+
function firstHeadingDepth (body) {
|
|
112
|
+
for (const { line, verbatim } of annotateVerbatimLines(body)) {
|
|
113
|
+
if (verbatim) continue;
|
|
114
|
+
const m = line.match(/^(={2,6}|#{2,6})\s+\S/);
|
|
115
|
+
if (m) return m[1].length;
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports.hasStructuralHeadings = hasStructuralHeadings;
|
|
121
|
+
module.exports.hasMarkdownHeadings = hasMarkdownHeadings;
|
|
122
|
+
module.exports.escapePlaceholderBraces = escapePlaceholderBraces;
|
|
123
|
+
module.exports.ensureHeadingSeparation = ensureHeadingSeparation;
|
|
124
|
+
module.exports.firstHeadingDepth = firstHeadingDepth;
|
|
125
|
+
module.exports.LONG_HEADINGLESS_THRESHOLD = LONG_HEADINGLESS_THRESHOLD;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { flattenToAttributeValue } = require('../metadata-utils.js');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handlebars helper: flatten a connector summary into a single-line, plain-text
|
|
7
|
+
* value suitable for the `:description:` page attribute (and therefore for the
|
|
8
|
+
* page's `<meta name="description">`).
|
|
9
|
+
*
|
|
10
|
+
* Delegates to the shared flattener so the description partial's attrs region
|
|
11
|
+
* and `backfillPageDescriptions` publish the same text for the same summary.
|
|
12
|
+
* Collapsing whitespace alone is not enough: summaries carry xrefs, link
|
|
13
|
+
* macros and inline code, and an unflattened value ships raw
|
|
14
|
+
* `xref:guides:bloblang/about.adoc[Bloblang mapping]` markup into search
|
|
15
|
+
* snippets and link previews (verified in a real Antora build).
|
|
16
|
+
*
|
|
17
|
+
* @param {string} summary
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
module.exports = function summaryAttribute (summary) {
|
|
21
|
+
return flattenToAttributeValue(summary);
|
|
22
|
+
};
|