@redpanda-data/docs-extensions-and-macros 5.54.0 → 5.56.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools-mcp.js +4 -5
- package/bin/doc-tools.js +354 -9
- package/bin/mcp-tools/generated-docs-review.js +26 -9
- package/bin/mcp-tools/overrides-audit.js +1 -1
- package/bin/mcp-tools/rpcn-docs.js +4 -14
- package/cli-utils/docs-links-to-xrefs.js +174 -0
- package/cli-utils/format-helm-spec.js +4 -3
- package/cli-utils/generated-marker.js +43 -0
- package/cli-utils/helm-commented-values.js +125 -21
- package/cli-utils/self-managed-docs-branch.js +36 -30
- package/extension-utils/html-utils.js +27 -0
- package/extension-utils/llms-utils.js +6 -2
- package/extension-utils/metadata-candidates.js +304 -0
- package/extension-utils/metadata-check.js +174 -0
- package/extension-utils/metadata-suggest.js +573 -0
- package/extension-utils/taxonomy-categories.js +106 -0
- package/extension-utils/taxonomy.js +566 -0
- package/extensions/REFERENCE.adoc +110 -8
- package/extensions/add-git-dates.js +13 -1
- package/extensions/aggregate-terms.js +34 -20
- package/extensions/algolia-indexer/generate-index.js +13 -1
- package/extensions/algolia-indexer/index.js +4 -2
- package/extensions/convert-sitemap-to-markdown.js +47 -12
- package/extensions/convert-to-markdown.js +176 -0
- package/extensions/generate-fields-only-pages.js +14 -0
- package/extensions/modify-connect-tag-playbook.js +181 -3
- package/extensions/resolve-xrefs-in-attachments.js +1 -12
- package/extensions/url-to-xref.js +92 -22
- package/extensions/util/connect-docs-asset.js +6 -5
- package/extensions/validate-attributes.js +10 -1
- package/extensions/version-fetcher/fetch-latest-docker-tag.js +35 -13
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +64 -40
- package/extensions/version-fetcher/list-connect-releases.js +3 -3
- package/extensions/version-fetcher/release-lines.js +64 -0
- package/extensions/version-fetcher/set-latest-version.js +73 -3
- package/macros/README.adoc +1 -0
- package/macros/REFERENCE.adoc +99 -0
- package/macros/badge.js +5 -8
- package/macros/enterprise.js +2 -5
- package/macros/glossary.js +4 -14
- package/macros/iceberg-explorer.js +242 -0
- package/macros/prop.js +3 -13
- package/mcp/USER_GUIDE.adoc +25 -135
- package/package.json +5 -2
- package/tools/check-metadata/index.js +523 -0
- package/tools/cloud-regions/cloud-regions-table-adoc.hbs +1 -2
- package/tools/cloud-regions/cloud-regions-table-md.hbs +2 -2
- package/tools/cloud-regions/render-cloud-regions.js +3 -0
- package/tools/dispatch-graph/check-dispatch-graph.js +790 -0
- package/tools/dispatch-graph/classify-runs.js +128 -0
- package/tools/dispatch-graph/parse-workflows.js +499 -0
- package/tools/doc-impact-recording/index.js +190 -0
- package/tools/enterprise-features/detect-gating-changes.js +393 -0
- package/tools/enterprise-features/verify.js +2 -1
- package/tools/gen-rpk-ascii.py +7 -1
- package/tools/lint-strings/fixtures/connect/lint_connect.go +8 -0
- package/tools/lint-strings/fixtures/crd/lint_types.go +4 -1
- package/tools/lint-strings/fixtures/rpk-pr461/delete.base.go +42 -0
- package/tools/lint-strings/fixtures/rpk-pr461/delete.head.go +45 -0
- package/tools/lint-strings/fixtures/rpk-pr461/reviewer-comments.json +1 -0
- package/tools/lint-strings/index.js +133 -3
- package/tools/lint-strings/rules/verbatim-asciidoc.js +166 -1
- package/tools/lint-strings/surfaces/connect.js +72 -12
- package/tools/lint-strings/surfaces/crd.js +10 -6
- package/tools/metrics/metrics.py +11 -0
- package/tools/overrides-audit/README.adoc +1 -2
- package/tools/overrides-audit/adapters/properties.js +1 -1
- package/tools/overrides-audit/index.js +3 -4
- package/tools/property-extractor/generate-handlebars-docs.js +9 -1
- package/tools/property-extractor/helpers/applyPropertyLinks.js +35 -0
- package/tools/redpanda-connect/AUTOMATION.md +3 -36
- package/tools/redpanda-connect/README.adoc +5 -33
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +32 -11
- package/tools/redpanda-connect/pr-summary-formatter.js +6 -4
- package/tools/redpanda-connect/report-delta.js +6 -3
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +42 -25
- package/tools/redpanda-connect/templates/bloblang-function.hbs +1 -1
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/examples-partials.hbs +1 -1
- package/tools/redpanda-connect/templates/fields-partials.hbs +2 -2
- package/tools/redpanda-connect/templates/metadata-partials.hbs +1 -1
- package/tools/redpanda-release-notes/generate-release-notes.js +116 -1
- package/tools/rpk-docs/generate-rpk-docs.js +2 -0
- package/tools/rpk-docs/generate-x-env-partial.js +7 -4
- package/tools/rpk-docs/helpers/index.js +4 -1
- package/tools/rpk-docs/templates/command.hbs +2 -2
- package/tools/overrides-audit/adapters/connect.js +0 -144
|
@@ -8,6 +8,7 @@ module.exports.register = function ({ config }) {
|
|
|
8
8
|
const GetLatestDockerTag = require('./fetch-latest-docker-tag');
|
|
9
9
|
const GetLatestHelmChartVersionFromOperator = require('./get-latest-redpanda-helm-version-from-operator');
|
|
10
10
|
const GetLatestConnectVersion = require('./get-latest-connect');
|
|
11
|
+
const { latestStablePerLine, releaseLineOf, isOlderReleaseLine } = require('./release-lines');
|
|
11
12
|
const logger = this.getLogger('set-latest-version-extension');
|
|
12
13
|
|
|
13
14
|
const { getGitHubApiToken } = require('../../cli-utils/github-token');
|
|
@@ -55,7 +56,14 @@ module.exports.register = function ({ config }) {
|
|
|
55
56
|
// pre-freeze version forever.
|
|
56
57
|
GetLatestRedpandaVersion(github, owner, 'streaming-enterprise', logger),
|
|
57
58
|
GetLatestDockerTag(dockerNamespace, 'console', logger),
|
|
58
|
-
|
|
59
|
+
// Three pages (300 tags) so older docs versions can still find the
|
|
60
|
+
// newest patch of their own operator line. Docker Hub lists recent
|
|
61
|
+
// pushes first, so the first page holds every line that still gets
|
|
62
|
+
// patches. Today the second and third pages add only legacy lines
|
|
63
|
+
// (2.x, 22.3, 23.x) that no built docs version pins to. They are
|
|
64
|
+
// headroom for a line that stops getting patches and drops off the
|
|
65
|
+
// first page as newer tags are pushed.
|
|
66
|
+
GetLatestDockerTag(dockerNamespace, 'redpanda-operator', logger, { maxPages: 3 }),
|
|
59
67
|
GetLatestConnectVersion(github, owner, 'connect', logger),
|
|
60
68
|
]);
|
|
61
69
|
|
|
@@ -97,6 +105,33 @@ module.exports.register = function ({ config }) {
|
|
|
97
105
|
};
|
|
98
106
|
|
|
99
107
|
const components = await contentCatalog.getComponents();
|
|
108
|
+
|
|
109
|
+
// An operator or Helm chart release supports only its own Redpanda line
|
|
110
|
+
// and the lines next to it, so a docs version for an older line must
|
|
111
|
+
// install that line's release, not the newest one. Resolve the newest
|
|
112
|
+
// operator tag of each line from the fetched tags, and that line's chart
|
|
113
|
+
// version from its release branch, once per line.
|
|
114
|
+
const operatorLines = latestStablePerLine(latestVersions.operator?.stableReleases);
|
|
115
|
+
const lineVersions = new Map();
|
|
116
|
+
for (const component of components) {
|
|
117
|
+
for (const { version } of component.versions) {
|
|
118
|
+
if (!isOlderReleaseLine(version, component.latest?.version)) continue;
|
|
119
|
+
const line = releaseLineOf(version);
|
|
120
|
+
if (lineVersions.has(line)) continue;
|
|
121
|
+
const operatorTag = operatorLines.get(line) || null;
|
|
122
|
+
lineVersions.set(line, { operator: operatorTag, helmChart: null });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
await Promise.all([...lineVersions].map(async ([line, resolved]) => {
|
|
126
|
+
if (!resolved.operator) return;
|
|
127
|
+
try {
|
|
128
|
+
const chart = await GetLatestHelmChartVersionFromOperator(github, owner, 'redpanda-operator', resolved.operator, null, logger);
|
|
129
|
+
resolved.helmChart = chart?.latestStableRelease || null;
|
|
130
|
+
} catch (error) {
|
|
131
|
+
logger.warn(`Helm chart lookup for the ${line} line failed: ${error.message || error}`);
|
|
132
|
+
}
|
|
133
|
+
}));
|
|
134
|
+
|
|
100
135
|
components.forEach(component => {
|
|
101
136
|
const prerelease = component.latestPrerelease;
|
|
102
137
|
|
|
@@ -105,13 +140,45 @@ module.exports.register = function ({ config }) {
|
|
|
105
140
|
asciidoc.attributes['page-component-version-is-prerelease'] = 'true';
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
// Older docs versions get the newest operator and chart release of
|
|
144
|
+
// their own line. The latest version, prereleases, and unversioned
|
|
145
|
+
// components get the newest release overall. When an older line has
|
|
146
|
+
// no release to pin to, the newest release would not support it, so
|
|
147
|
+
// the version keeps the value its antora.yml sets, and gets the
|
|
148
|
+
// newest release only when antora.yml sets nothing.
|
|
149
|
+
let operatorVersion = latestVersions.operator?.latestStableRelease;
|
|
150
|
+
let helmChartVersion = latestVersions.helmChart?.latestStableRelease;
|
|
151
|
+
if (isOlderReleaseLine(version, component.latest?.version)) {
|
|
152
|
+
const line = releaseLineOf(version);
|
|
153
|
+
const resolved = lineVersions.get(line) || {};
|
|
154
|
+
const keepOrNewest = (key, newest, what) => {
|
|
155
|
+
const own = asciidoc.attributes[key];
|
|
156
|
+
if (own) {
|
|
157
|
+
logger.warn(`No ${what} release found for the ${line} line, so ${name} ${version} keeps ${key} from its antora.yml (${own}).`);
|
|
158
|
+
return own;
|
|
159
|
+
}
|
|
160
|
+
logger.warn(`No ${what} release found for the ${line} line and its antora.yml sets no ${key}, so ${name} ${version} uses the newest release (${newest || 'unknown'}).`);
|
|
161
|
+
return newest;
|
|
162
|
+
};
|
|
163
|
+
if (resolved.operator) {
|
|
164
|
+
operatorVersion = resolved.operator;
|
|
165
|
+
} else {
|
|
166
|
+
operatorVersion = keepOrNewest('latest-operator-version', operatorVersion, 'stable Redpanda Operator');
|
|
167
|
+
}
|
|
168
|
+
if (resolved.helmChart) {
|
|
169
|
+
helmChartVersion = resolved.helmChart;
|
|
170
|
+
} else {
|
|
171
|
+
helmChartVersion = keepOrNewest('latest-redpanda-helm-chart-version', helmChartVersion, 'Redpanda Helm chart');
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
108
175
|
// Set operator and helm chart attributes via helper function. These keep
|
|
109
176
|
// their raw fetched form, including the "v" prefix on
|
|
110
177
|
// latest-operator-version, because docs pages pass that value straight
|
|
111
178
|
// to `helm --version`. Only the short sibling is derived.
|
|
112
179
|
updateAttributes(asciidoc, [
|
|
113
|
-
{ condition: latestVersions.operator, key: 'latest-operator-version', value:
|
|
114
|
-
{ condition: latestVersions.helmChart, key: 'latest-redpanda-helm-chart-version', value:
|
|
180
|
+
{ condition: latestVersions.operator, key: 'latest-operator-version', value: operatorVersion },
|
|
181
|
+
{ condition: latestVersions.helmChart, key: 'latest-redpanda-helm-chart-version', value: helmChartVersion }
|
|
115
182
|
]);
|
|
116
183
|
|
|
117
184
|
// Set attributes for console and connect versions
|
|
@@ -190,6 +257,9 @@ module.exports.register = function ({ config }) {
|
|
|
190
257
|
summary.forEach(([label, stableVersion, betaVersion]) => {
|
|
191
258
|
logger.info(`- ${label}: ${stableVersion || 'unknown'}${betaVersion ? ', beta: ' + betaVersion : ''}`);
|
|
192
259
|
});
|
|
260
|
+
[...lineVersions].sort(([a], [b]) => semver.rcompare(`${a}.0`, `${b}.0`)).forEach(([line, resolved]) => {
|
|
261
|
+
logger.info(`- ${line} docs: operator ${resolved.operator || 'antora.yml value (no release in line)'}, Helm chart ${resolved.helmChart || 'antora.yml value (no release in line)'}`);
|
|
262
|
+
});
|
|
193
263
|
} catch (error) {
|
|
194
264
|
logger.error(`Error updating versions: ${error}`);
|
|
195
265
|
}
|
package/macros/README.adoc
CHANGED
|
@@ -64,6 +64,7 @@ IMPORTANT: Macros must be registered under the `asciidoc.extensions` key in your
|
|
|
64
64
|
|
|
65
65
|
=== Block macros
|
|
66
66
|
|
|
67
|
+
* **iceberg-explorer** - Emit a mount point that docs-ui hydrates into the interactive Iceberg Mode Explorer
|
|
67
68
|
* **components_by_category** - Display Redpanda Connect components grouped by category
|
|
68
69
|
* **component_table** - Generate searchable component tables
|
|
69
70
|
* **component_type_dropdown** - Create dropdown for component types
|
package/macros/REFERENCE.adoc
CHANGED
|
@@ -430,6 +430,105 @@ asciidoc:
|
|
|
430
430
|
- '@redpanda-data/docs-extensions-and-macros/macros/helm-ref'
|
|
431
431
|
----
|
|
432
432
|
|
|
433
|
+
== iceberg-explorer
|
|
434
|
+
|
|
435
|
+
This macro registers an `[iceberg-explorer]` block that emits a version-aware mount point for the interactive Iceberg Mode Explorer:
|
|
436
|
+
|
|
437
|
+
[,html]
|
|
438
|
+
----
|
|
439
|
+
<div class="iceberg-explorer-mount" data-iceberg-explorer="1" data-version="26.2">
|
|
440
|
+
<p class="iceberg-explorer-fallback">Interactive Iceberg Mode Explorer. If this text is still here...</p>
|
|
441
|
+
</div>
|
|
442
|
+
----
|
|
443
|
+
|
|
444
|
+
The block renders the container only. The explorer UI and its translation engine live in the docs-ui bundle, which hydrates the mount point, so the block is inert unless the site uses a docs-ui build that includes the explorer. This is the same "macro emits container, docs-ui hydrates it" handshake used by the Redpanda Connect component metadata block.
|
|
445
|
+
|
|
446
|
+
Until the explorer module replaces the container's contents, the fallback paragraph is visible. It is ordinary markup rather than `<noscript>` on purpose: the failure readers are most likely to hit is JavaScript running normally on a site whose UI bundle has no explorer module, and a browser hides `<noscript>` in that case, leaving a zero-height blank space with no explanation.
|
|
447
|
+
|
|
448
|
+
The page's component version (`page-version`, falling back to `page-component-version`) is stamped onto the mount point as `data-version` so the UI can load the engine build that matches the doc version. If neither attribute is set, `data-version` is omitted.
|
|
449
|
+
|
|
450
|
+
Antora derives both attributes from the component version and locks them, so a page header cannot set them: an unversioned component (`version: ~`, which is what this repo's preview site uses) emits no `data-version`, and docs-ui falls back to its own version detection.
|
|
451
|
+
|
|
452
|
+
=== Mount contract
|
|
453
|
+
|
|
454
|
+
docs-ui must hydrate on the `data-iceberg-explorer` attribute (`[data-iceberg-explorer]`), never on a class name that matches the block name.
|
|
455
|
+
|
|
456
|
+
Asciidoctor turns an unrecognized block style into a CSS class. A site that uses `[iceberg-explorer]` but whose playbook does not register this macro still publishes `<div class="openblock iceberg-explorer">`, with the author's JSON body dumped onto the page as a code listing. If the explorer module hydrated on `.iceberg-explorer`, that accident would render as a working-looking explorer filled with built-in sample data, the author's defaults silently discarded, and no error anywhere to trace it back to the missing playbook entry.
|
|
457
|
+
|
|
458
|
+
Only this macro can emit a data attribute, so:
|
|
459
|
+
|
|
460
|
+
* `data-iceberg-explorer` is the mount contract, and its value is the contract version (`1` today). Bump it only if the emitted attributes change shape.
|
|
461
|
+
* `iceberg-explorer-mount` is a styling hook, not a mount signal.
|
|
462
|
+
|
|
463
|
+
The constants are exported (`MOUNT_ATTRIBUTE`, `MOUNT_CONTRACT_VERSION`, `MOUNT_CLASS`, `MOUNT_SELECTOR`) so docs-ui and the tests can assert against one definition.
|
|
464
|
+
|
|
465
|
+
=== Usage
|
|
466
|
+
|
|
467
|
+
```asciidoc
|
|
468
|
+
[iceberg-explorer]
|
|
469
|
+
--
|
|
470
|
+
--
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Set the initial configuration with the `config` attribute:
|
|
474
|
+
|
|
475
|
+
```asciidoc
|
|
476
|
+
[iceberg-explorer,config="key:mode=binary;value:mode=binary"]
|
|
477
|
+
--
|
|
478
|
+
--
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
Or pass a JSON object as the block body to supply author-defined defaults:
|
|
482
|
+
|
|
483
|
+
```asciidoc
|
|
484
|
+
[iceberg-explorer]
|
|
485
|
+
----
|
|
486
|
+
{
|
|
487
|
+
"config": "value:mode=schema_latest"
|
|
488
|
+
}
|
|
489
|
+
----
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
The explorer module reads only the `config` key and renders its own sample schema and record. Other keys are forwarded verbatim in `data-defaults` and ignored, so do not document author-supplied schemas or records as supported until docs-ui reads them.
|
|
493
|
+
|
|
494
|
+
The JSON is attribute-escaped and emitted as `data-defaults`. The body must be a JSON *object*: `JSON.parse()` also accepts scalars and arrays, and docs-ui reads keys off the parsed value, so a bare `123` is as unusable as a syntax error. If the body is not a JSON object, the macro logs a warning that names the page, omits `data-defaults`, and still renders the mount point.
|
|
495
|
+
|
|
496
|
+
The block accepts an open (`--`), listing (`----`), literal, or pass body. It is not registered for the paragraph context, because the processor replaces the block with the mount point: registering paragraphs would delete the prose under a stray `[iceberg-explorer]` line from the published page.
|
|
497
|
+
|
|
498
|
+
|===
|
|
499
|
+
| Attribute | Description
|
|
500
|
+
|
|
501
|
+
| `config`
|
|
502
|
+
| Initial configuration string. Emitted as `data-config`. Falls back to the `config` key of the JSON body.
|
|
503
|
+
|
|
504
|
+
| `height`
|
|
505
|
+
| Minimum height for the container, emitted as an inline `min-height` style. A bare number is treated as pixels (`height=400` becomes `min-height:400px`). Otherwise the value must be a CSS length in `px`, `rem`, `em`, `vh`, `vw`, or `%`; any other value is rejected with a warning rather than interpolated into the style attribute.
|
|
506
|
+
|
|
507
|
+
| `engine-base`
|
|
508
|
+
| Optional base location for the translation engine build, emitted as `data-engine-base`. It must be a root-relative path on the docs site, such as `/_/engine/26.2`, because docs-ui loads a script from it. Any other value (an absolute or protocol-relative URL, a relative path, backslashes, or control characters) is ignored with a build warning. docs-ui falls back to its own default when this is absent, so set it only to pin a specific engine build.
|
|
509
|
+
|===
|
|
510
|
+
|
|
511
|
+
=== Registration
|
|
512
|
+
|
|
513
|
+
[,yaml]
|
|
514
|
+
----
|
|
515
|
+
asciidoc:
|
|
516
|
+
extensions:
|
|
517
|
+
- '@redpanda-data/docs-extensions-and-macros/macros/iceberg-explorer'
|
|
518
|
+
----
|
|
519
|
+
|
|
520
|
+
=== docs-ui dependency
|
|
521
|
+
|
|
522
|
+
This block is half of a two-repo handshake and does nothing on its own. The explorer UI and its translation engine ship in docs-ui, so the block requires a docs-ui release that includes the explorer module. On a site built with a bundle that lacks the module, the block renders the container and its fallback paragraph, nothing more.
|
|
523
|
+
|
|
524
|
+
Order of operations before an author uses the block on a published page:
|
|
525
|
+
|
|
526
|
+
. Merge and publish this package, so consumers can resolve the macro.
|
|
527
|
+
. Release a docs-ui bundle that includes the explorer module, with hydration keyed on `[data-iceberg-explorer]` (see <<Mount contract>>).
|
|
528
|
+
. Add the macro to each consuming playbook's `asciidoc.extensions` (`redpanda-data/docs`, `cloud-docs`). Registering it in this repo's `local-antora-playbook.yml` covers only this repo's preview site.
|
|
529
|
+
|
|
530
|
+
Until step 2 lands, a page that uses the block shows the fallback paragraph rather than an explorer. That is deliberate: it is visible, so a missing or outdated UI bundle looks broken rather than looking like a page with nothing on it.
|
|
531
|
+
|
|
433
532
|
== components_by_category
|
|
434
533
|
|
|
435
534
|
This macro generates a tabbed interface to display Redpanda Connect components by category.
|
package/macros/badge.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { escapeHtml } = require('../extension-utils/html-utils');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Build the HTML for a badge.
|
|
5
7
|
*
|
|
@@ -34,12 +36,7 @@ function buildBadgeHtml ({ label, size, tooltip } = {}) {
|
|
|
34
36
|
// handlers). The label needs it as much as the tooltip -- it lands in the
|
|
35
37
|
// class attribute and in the text content, and escaping only the tooltip left
|
|
36
38
|
// `badge:x[label=x" onmouseover="...]` free to break out of the class.
|
|
37
|
-
const
|
|
38
|
-
.replace(/&/g, '&')
|
|
39
|
-
.replace(/"/g, '"')
|
|
40
|
-
.replace(/</g, '<')
|
|
41
|
-
.replace(/>/g, '>');
|
|
42
|
-
const className = `badge--${escapeAttr(text.toLowerCase().replace(/\s+/g, '-'))}`;
|
|
39
|
+
const className = `badge--${escapeHtml(text.toLowerCase().replace(/\s+/g, '-'))}`;
|
|
43
40
|
// hasOwnProperty, not a bare index: `label=constructor` and `label=__proto__`
|
|
44
41
|
// are already lowercase, so a bare lookup shipped
|
|
45
42
|
// `function Object() { [native code] }` to readers as hover text. prop.js
|
|
@@ -49,12 +46,12 @@ function buildBadgeHtml ({ label, size, tooltip } = {}) {
|
|
|
49
46
|
? DEFAULT_TOOLTIPS[labelKey]
|
|
50
47
|
: undefined;
|
|
51
48
|
const hoverText = tooltip || defaultTooltip;
|
|
52
|
-
const tooltipAttr = hoverText ? ` data-tooltip="${
|
|
49
|
+
const tooltipAttr = hoverText ? ` data-tooltip="${escapeHtml(hoverText)}"` : '';
|
|
53
50
|
|
|
54
51
|
// Add brackets if not large. The brackets matter beyond styling: they are
|
|
55
52
|
// real text content, so the label survives an HTML-to-Markdown conversion as
|
|
56
53
|
// "(beta)" rather than vanishing with the CSS class.
|
|
57
|
-
const renderedLabel = isLarge ?
|
|
54
|
+
const renderedLabel = isLarge ? escapeHtml(text) : `(${escapeHtml(text)})`;
|
|
58
55
|
|
|
59
56
|
return `<span class="badge ${className} ${sizeClass}"${tooltipAttr}>${renderedLabel}</span>`;
|
|
60
57
|
}
|
package/macros/enterprise.js
CHANGED
|
@@ -69,6 +69,7 @@
|
|
|
69
69
|
*/
|
|
70
70
|
|
|
71
71
|
const yaml = require('js-yaml')
|
|
72
|
+
const { escapeHtml } = require('../extension-utils/html-utils')
|
|
72
73
|
const semver = require('semver')
|
|
73
74
|
const { buildBadgeHtml, DEFAULT_TOOLTIPS } = require('./badge')
|
|
74
75
|
const { isAttributeSet } = require('./prop')
|
|
@@ -545,11 +546,7 @@ function buildEnterpriseContent ({ feature, text, xref, url, tooltip, licensingP
|
|
|
545
546
|
// consumer that re-parses the page then reads as markup -- the docs UI
|
|
546
547
|
// promotes this attribute into a tooltip, the Markdown converter and the
|
|
547
548
|
// search indexer both re-read it.
|
|
548
|
-
const escapedTooltip = tooltipText
|
|
549
|
-
.replace(/&/g, '&')
|
|
550
|
-
.replace(/"/g, '"')
|
|
551
|
-
.replace(/</g, '<')
|
|
552
|
-
.replace(/>/g, '>')
|
|
549
|
+
const escapedTooltip = escapeHtml(tooltipText)
|
|
553
550
|
const tooltipHtml = tooltipAttr ? ` ${tooltipAttr}="${escapedTooltip}"` : ''
|
|
554
551
|
let inner = display
|
|
555
552
|
if (links) {
|
package/macros/glossary.js
CHANGED
|
@@ -3,22 +3,12 @@
|
|
|
3
3
|
const $glossaryContexts = Symbol('$glossaryContexts')
|
|
4
4
|
const { posix: path } = require('path')
|
|
5
5
|
const logger = require('@antora/logger')('glossary-macro')
|
|
6
|
+
const { escapeHtml } = require('../extension-utils/html-utils')
|
|
6
7
|
|
|
7
8
|
// Backtick-monospace in hover-text. The unconstrained (double-backtick) form is
|
|
8
9
|
// matched first, otherwise it leaves a stray delimiter on each side.
|
|
9
10
|
const MONOSPACE_RX = /``([^`]+)``|`([^`]+)`/g
|
|
10
11
|
|
|
11
|
-
// Escape for interpolation into a double-quoted HTML attribute. Same four
|
|
12
|
-
// replacements, in the same order, as badge.js and enterprise.js; `&` has to go
|
|
13
|
-
// first or it re-escapes the entities the later passes introduce.
|
|
14
|
-
function escapeAttr (value) {
|
|
15
|
-
return String(value)
|
|
16
|
-
.replace(/&/g, '&')
|
|
17
|
-
.replace(/"/g, '"')
|
|
18
|
-
.replace(/</g, '<')
|
|
19
|
-
.replace(/>/g, '>')
|
|
20
|
-
}
|
|
21
|
-
|
|
22
12
|
// Only a data-* tooltip is read by a JS tooltip library that renders it as HTML;
|
|
23
13
|
// the native title attribute is always plain text. Shared with the
|
|
24
14
|
// glossary-tooltip validation below so the two cannot drift apart.
|
|
@@ -45,14 +35,14 @@ function tooltipRendersHtml (tooltipAttr) {
|
|
|
45
35
|
// file's raw :hover-text: value -- so it is escaped once rather than twice and
|
|
46
36
|
// its markup survives to the reader instead of being shown as literal tags.
|
|
47
37
|
function formatTooltipDefinition (text, tooltipAttr, definitionIsHtml) {
|
|
48
|
-
if (!tooltipRendersHtml(tooltipAttr)) return
|
|
38
|
+
if (!tooltipRendersHtml(tooltipAttr)) return escapeHtml(text)
|
|
49
39
|
if (definitionIsHtml) {
|
|
50
40
|
// Already HTML, so one pass is right: the attribute decode hands the markup
|
|
51
41
|
// back intact for innerHTML to render, and a quote in the text still cannot
|
|
52
42
|
// terminate the attribute early.
|
|
53
|
-
return
|
|
43
|
+
return escapeHtml(text)
|
|
54
44
|
}
|
|
55
|
-
return
|
|
45
|
+
return escapeHtml(escapeHtml(text)).replace(
|
|
56
46
|
MONOSPACE_RX,
|
|
57
47
|
(match, unconstrained, constrained) =>
|
|
58
48
|
`<code>${unconstrained !== undefined ? unconstrained : constrained}</code>`
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { escapeHtml } = require('../extension-utils/html-utils');
|
|
4
|
+
// Same logger as the other macros (#283): warnings land in Antora's log with
|
|
5
|
+
// a level Antora can fail on, instead of bypassing it through console.warn.
|
|
6
|
+
const logger = require('@antora/logger')('iceberg-explorer-macro');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Iceberg Mode Explorer block.
|
|
10
|
+
*
|
|
11
|
+
* Registers an `[iceberg-explorer]` AsciiDoc block that emits a lightweight,
|
|
12
|
+
* version-aware mount point. The interactive tool itself (Ace editors,
|
|
13
|
+
* controls, rendering) and its translation engine live in the docs-ui bundle;
|
|
14
|
+
* a docs-ui JS module hydrates every element carrying the mount attribute
|
|
15
|
+
* (`MOUNT_SELECTOR` below).
|
|
16
|
+
*
|
|
17
|
+
* This mirrors the "macro emits container -> docs-ui JS hydrates" handshake
|
|
18
|
+
* used by the component metadata block in `rp-connect-components.js` +
|
|
19
|
+
* `docs-ui/src/js/24-move-connector-metadata.js`.
|
|
20
|
+
*
|
|
21
|
+
* MOUNT CONTRACT. docs-ui must hydrate on the `data-iceberg-explorer`
|
|
22
|
+
* attribute, never on a class name that matches the block name. Asciidoctor
|
|
23
|
+
* turns an unrecognized block style into a CSS class, so a page that uses
|
|
24
|
+
* `[iceberg-explorer]` in a site whose playbook forgot to register this macro
|
|
25
|
+
* still renders `<div class="openblock iceberg-explorer">`. Hydrating on
|
|
26
|
+
* `.iceberg-explorer` would dress that accident up as a working explorer
|
|
27
|
+
* showing built-in sample data while the author's JSON body was discarded, with
|
|
28
|
+
* no error anywhere. Only this macro can emit a data attribute, so the
|
|
29
|
+
* attribute is the contract and the class is for styling only.
|
|
30
|
+
*
|
|
31
|
+
* Authoring:
|
|
32
|
+
*
|
|
33
|
+
* [iceberg-explorer]
|
|
34
|
+
* --
|
|
35
|
+
* --
|
|
36
|
+
*
|
|
37
|
+
* or with author-supplied defaults (JSON) as the block body:
|
|
38
|
+
*
|
|
39
|
+
* [iceberg-explorer]
|
|
40
|
+
* ----
|
|
41
|
+
* {
|
|
42
|
+
* "config": "key:mode=binary;value:mode=schema_id_prefix,layout=flat;headers:value_type=binary"
|
|
43
|
+
* }
|
|
44
|
+
* ----
|
|
45
|
+
*
|
|
46
|
+
* The explorer module reads only the "config" key (docs-ui
|
|
47
|
+
* src/js/27-iceberg-explorer.js), rendering its own sample schema and record.
|
|
48
|
+
* Any other key is forwarded verbatim in `data-defaults` and ignored today, so
|
|
49
|
+
* do not advertise author-supplied schemas or records as supported.
|
|
50
|
+
*
|
|
51
|
+
* The block reads the page's component version and stamps it onto the mount
|
|
52
|
+
* point so the UI can fetch the engine build that matches the doc version.
|
|
53
|
+
*
|
|
54
|
+
* @param {Registry} registry - The Antora/Asciidoctor extension registry.
|
|
55
|
+
* @param {Object} context - The Antora context (unused today; kept for parity
|
|
56
|
+
* with the other macros and for future content-catalog resolution).
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
// Mount contract shared with docs-ui. Exported so both sides and the tests
|
|
60
|
+
// reference one definition instead of a hard-coded string in three places.
|
|
61
|
+
const MOUNT_ATTRIBUTE = 'data-iceberg-explorer';
|
|
62
|
+
// Bumped only if the emitted attributes change shape, so a docs-ui build can
|
|
63
|
+
// refuse a mount point it does not understand.
|
|
64
|
+
const MOUNT_CONTRACT_VERSION = '1';
|
|
65
|
+
// Styling hook only. Deliberately NOT the block name: see MOUNT CONTRACT above.
|
|
66
|
+
const MOUNT_CLASS = 'iceberg-explorer-mount';
|
|
67
|
+
const MOUNT_SELECTOR = `[${MOUNT_ATTRIBUTE}]`;
|
|
68
|
+
|
|
69
|
+
module.exports.register = function (registry, context) {
|
|
70
|
+
// Support both calling conventions:
|
|
71
|
+
// - Antora passes a registry as the first argument; it exposes `.block()`
|
|
72
|
+
// directly, so register on it.
|
|
73
|
+
// - Some preview harnesses (e.g. docs-ui's build-preview-pages) call
|
|
74
|
+
// `register.call(Asciidoctor.Extensions)` with the Extensions module as
|
|
75
|
+
// `this` and no arguments. That module exposes `.register(fn)` (not
|
|
76
|
+
// `.block()`); inside `fn`, `this` is a registry with `.block()`.
|
|
77
|
+
const target = registry || this;
|
|
78
|
+
if (typeof target.block === 'function') {
|
|
79
|
+
defineBlock(target);
|
|
80
|
+
} else if (typeof target.register === 'function') {
|
|
81
|
+
target.register(function () { defineBlock(this); });
|
|
82
|
+
} else {
|
|
83
|
+
throw new Error('iceberg-explorer: no usable Asciidoctor registry provided');
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function defineBlock (registry) {
|
|
88
|
+
registry.block(function () {
|
|
89
|
+
const self = this;
|
|
90
|
+
self.named('iceberg-explorer');
|
|
91
|
+
// Accept an empty open block (`--`) or a listing/literal/pass block whose
|
|
92
|
+
// body carries author-supplied JSON defaults. `paragraph` is deliberately
|
|
93
|
+
// absent: the processor discards the body, so registering the paragraph
|
|
94
|
+
// context made `[iceberg-explorer]` above ordinary prose delete that
|
|
95
|
+
// paragraph from the published page.
|
|
96
|
+
self.onContext(['open', 'listing', 'literal', 'pass']);
|
|
97
|
+
self.process((parent, reader, attrs) => {
|
|
98
|
+
const attributes = parent.getDocument().getAttributes();
|
|
99
|
+
// Name the page in every warning. Antora sets page-relative-src-path
|
|
100
|
+
// (the convention rp-connect-components.js follows); plain Asciidoctor
|
|
101
|
+
// runs only have docfile.
|
|
102
|
+
const page =
|
|
103
|
+
attributes['page-relative-src-path'] ||
|
|
104
|
+
attributes['docfile'] ||
|
|
105
|
+
'unknown page';
|
|
106
|
+
const warn = (message) => {
|
|
107
|
+
logger.warn(`${page}: ${message}`);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Author-supplied defaults (optional). Validate that the body is a JSON
|
|
111
|
+
// object so a typo surfaces at build time rather than silently shipping
|
|
112
|
+
// bad data. docs-ui reads keys off the parsed value, so a scalar or an
|
|
113
|
+
// array is as unusable as a syntax error, and JSON.parse accepts both.
|
|
114
|
+
let defaults = null;
|
|
115
|
+
const body = reader.getLines().join('\n').trim();
|
|
116
|
+
if (body) {
|
|
117
|
+
let parsed;
|
|
118
|
+
let valid = false;
|
|
119
|
+
try {
|
|
120
|
+
parsed = JSON.parse(body);
|
|
121
|
+
valid = true;
|
|
122
|
+
} catch (err) {
|
|
123
|
+
warn(
|
|
124
|
+
`block body is not valid JSON (${err.message}). ` +
|
|
125
|
+
'Rendering the explorer with built-in defaults instead.'
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (valid) {
|
|
129
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
130
|
+
warn(
|
|
131
|
+
`block body must be a JSON object with a "config" key, not ${describeJson(parsed)}. ` +
|
|
132
|
+
'Rendering the explorer with built-in defaults instead.'
|
|
133
|
+
);
|
|
134
|
+
} else {
|
|
135
|
+
defaults = parsed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Doc version drives which engine build the UI loads. `page-version` is
|
|
141
|
+
// the short component version (for example, "26.2"); fall back to the
|
|
142
|
+
// component version attribute if present.
|
|
143
|
+
const version =
|
|
144
|
+
attributes['page-version'] ||
|
|
145
|
+
attributes['page-component-version'] ||
|
|
146
|
+
'';
|
|
147
|
+
|
|
148
|
+
// Optional named attributes let an author pin behavior without a body.
|
|
149
|
+
// `config` sets the initial DSL string; `height` overrides the min
|
|
150
|
+
// height; `engine-base` overrides where docs-ui fetches the engine from.
|
|
151
|
+
const initialConfig = attrs.config || (defaults && defaults.config) || '';
|
|
152
|
+
const height = cssLength(attrs.height, warn);
|
|
153
|
+
const engineBase = rootRelativePath(attrs['engine-base'], warn);
|
|
154
|
+
|
|
155
|
+
const dataDefaults = defaults
|
|
156
|
+
? escapeHtml(JSON.stringify(defaults))
|
|
157
|
+
: '';
|
|
158
|
+
const dataConfig = initialConfig ? escapeHtml(initialConfig) : '';
|
|
159
|
+
const styleAttr = height ? ` style="min-height:${height}"` : '';
|
|
160
|
+
const versionAttr = version ? ` data-version="${escapeHtml(version)}"` : '';
|
|
161
|
+
const engineAttr = engineBase ? ` data-engine-base="${escapeHtml(engineBase)}"` : '';
|
|
162
|
+
|
|
163
|
+
// The container is a mount point only; docs-ui replaces its contents on
|
|
164
|
+
// hydration. Until then the fallback paragraph is visible, so the two
|
|
165
|
+
// ways this can fail (JS disabled, or a docs-ui build without the
|
|
166
|
+
// explorer module) both show a message instead of a 0px-tall void. It is
|
|
167
|
+
// plain markup, not <noscript>, precisely because the common failure is
|
|
168
|
+
// JS running fine with the module missing, which <noscript> hides.
|
|
169
|
+
const html = `
|
|
170
|
+
<div class="${MOUNT_CLASS}" ${MOUNT_ATTRIBUTE}="${MOUNT_CONTRACT_VERSION}"${versionAttr}${engineAttr}${dataConfig ? ` data-config="${dataConfig}"` : ''}${dataDefaults ? ` data-defaults="${dataDefaults}"` : ''}${styleAttr}>
|
|
171
|
+
<p class="iceberg-explorer-fallback">Interactive Iceberg Mode Explorer. If this text is still here, the explorer could not load: it needs JavaScript and a docs-ui build that includes the explorer module. See the Iceberg topic configuration reference for the equivalent settings.</p>
|
|
172
|
+
</div>`;
|
|
173
|
+
|
|
174
|
+
return self.createBlock(parent, 'pass', html);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Describe a parsed-but-unusable JSON body for the build warning.
|
|
181
|
+
* @param {*} value
|
|
182
|
+
* @returns {string}
|
|
183
|
+
*/
|
|
184
|
+
function describeJson (value) {
|
|
185
|
+
if (Array.isArray(value)) return 'an array';
|
|
186
|
+
if (value === null) return 'null';
|
|
187
|
+
return `a ${typeof value}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Validate the `height` attribute as a CSS length. A bare number is the natural
|
|
192
|
+
* thing to write and produces `min-height:400`, which browsers drop, so accept
|
|
193
|
+
* it and add `px`. Anything else is rejected rather than interpolated: the value
|
|
194
|
+
* lands inside a style attribute, where `1px;position:fixed;inset:0` would turn
|
|
195
|
+
* a doc page into a full-viewport overlay.
|
|
196
|
+
* @param {string} raw
|
|
197
|
+
* @param {function(string): void} warn
|
|
198
|
+
* @returns {string} A safe CSS length, or '' to omit the style attribute.
|
|
199
|
+
*/
|
|
200
|
+
function cssLength (raw, warn) {
|
|
201
|
+
if (!raw) return '';
|
|
202
|
+
const value = String(raw).trim();
|
|
203
|
+
if (/^\d+$/.test(value)) return `${value}px`;
|
|
204
|
+
if (/^\d+(\.\d+)?(px|rem|em|vh|vw|%)$/.test(value)) return value;
|
|
205
|
+
warn(
|
|
206
|
+
`height="${value}" is not a CSS length (for example, 400, 400px, 30rem, 60vh). Ignoring it.`
|
|
207
|
+
);
|
|
208
|
+
return '';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Validate the `engine-base` attribute as a same-origin, root-relative path.
|
|
213
|
+
* docs-ui loads `<engine-base>/iceberg-engine-<version>.js` as a script, so any
|
|
214
|
+
* other value would let page source run another origin's JavaScript on the docs
|
|
215
|
+
* site. Escaping stops HTML injection but does not make a URL trustworthy, so
|
|
216
|
+
* reject schemes, protocol-relative paths (`//host`), backslashes (which
|
|
217
|
+
* browsers treat as `/`), whitespace, and raw or percent-encoded control
|
|
218
|
+
* characters.
|
|
219
|
+
* @param {string} raw
|
|
220
|
+
* @param {function(string): void} warn
|
|
221
|
+
* @returns {string} A safe root-relative path, or '' to use the docs-ui default.
|
|
222
|
+
*/
|
|
223
|
+
function rootRelativePath (raw, warn) {
|
|
224
|
+
if (!raw) return '';
|
|
225
|
+
const value = String(raw);
|
|
226
|
+
const safe =
|
|
227
|
+
/^\/(?![\/\\])/.test(value) &&
|
|
228
|
+
!/[\\\s\x00-\x1f\x7f]/.test(value) &&
|
|
229
|
+
!/%(?:[01][0-9a-f]|7f|5c)/i.test(value);
|
|
230
|
+
if (safe) return value;
|
|
231
|
+
warn(
|
|
232
|
+
`engine-base="${value}" is not a root-relative path on this site (for example, /_/engine/26.2). ` +
|
|
233
|
+
'Ignoring it, so docs-ui loads the engine from its default location.'
|
|
234
|
+
);
|
|
235
|
+
return '';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports.MOUNT_ATTRIBUTE = MOUNT_ATTRIBUTE;
|
|
239
|
+
module.exports.MOUNT_CONTRACT_VERSION = MOUNT_CONTRACT_VERSION;
|
|
240
|
+
module.exports.MOUNT_CLASS = MOUNT_CLASS;
|
|
241
|
+
module.exports.MOUNT_SELECTOR = MOUNT_SELECTOR;
|
|
242
|
+
module.exports.rootRelativePath = rootRelativePath;
|
package/macros/prop.js
CHANGED
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
*/
|
|
92
92
|
|
|
93
93
|
const semver = require('semver')
|
|
94
|
+
const { escapeHtml } = require('../extension-utils/html-utils')
|
|
94
95
|
const logger = require('@antora/logger')('prop-macro')
|
|
95
96
|
|
|
96
97
|
const $propertyRegistry = Symbol('$propertyRegistry')
|
|
@@ -867,19 +868,8 @@ function buildPropContent ({ name, text, link, page, scope, role, helmPath = fal
|
|
|
867
868
|
// attribute, docUrl from a page's published URL, and name from a dataset
|
|
868
869
|
// key: an unescaped quote in any of them ends the attribute early and the
|
|
869
870
|
// remainder becomes stray attributes on every marked property in the build.
|
|
870
|
-
const docAttr = docUrl ? ` data-doc-url="${
|
|
871
|
-
return `<code class="${
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
/**
|
|
875
|
-
* Escape a value for use inside a double-quoted HTML attribute.
|
|
876
|
-
*/
|
|
877
|
-
function escapeAttribute (value) {
|
|
878
|
-
return String(value === undefined || value === null ? '' : value)
|
|
879
|
-
.replace(/&/g, '&')
|
|
880
|
-
.replace(/"/g, '"')
|
|
881
|
-
.replace(/</g, '<')
|
|
882
|
-
.replace(/>/g, '>')
|
|
871
|
+
const docAttr = docUrl ? ` data-doc-url="${escapeHtml(docUrl)}"` : ''
|
|
872
|
+
return `<code class="${escapeHtml(role)}" data-property-name="${escapeHtml(name)}"${docAttr}>${inner}</code>`
|
|
883
873
|
}
|
|
884
874
|
|
|
885
875
|
function propInlineMacro (config) {
|