@redpanda-data/docs-extensions-and-macros 5.22.1 → 5.22.2
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 +1 -1
- package/extensions/README.adoc +7 -5
- package/extensions/REFERENCE.adoc +1 -17
- package/extensions/USER_GUIDE.adoc +1 -11
- package/macros/glossary.js +66 -3
- package/package.json +1 -3
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +7 -7
- package/tools/redpanda-connect/helpers/descriptionInclude.js +2 -2
- package/tools/redpanda-connect/helpers/summaryAttribute.js +1 -1
- package/tools/redpanda-connect/metadata-utils.js +3 -3
- package/tools/redpanda-connect/migrate-descriptions-to-partials.js +14 -14
- package/tools/redpanda-connect/templates/connector.hbs +3 -3
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +3 -3
- package/extensions/produce-redirects.js +0 -166
package/bin/doc-tools.js
CHANGED
|
@@ -901,7 +901,7 @@ automation
|
|
|
901
901
|
* @description
|
|
902
902
|
* One-time migration that rewires published connector reference pages onto the
|
|
903
903
|
* regenerated description partial: the header `:description:` becomes
|
|
904
|
-
* `include::...[tag=
|
|
904
|
+
* `include::...[tag=meta]` and the frozen intro prose becomes
|
|
905
905
|
* `include::...[tag=body]`. Afterwards `generate rpcn-connector-docs` keeps
|
|
906
906
|
* both fresh on every run.
|
|
907
907
|
*
|
package/extensions/README.adoc
CHANGED
|
@@ -64,7 +64,6 @@ IMPORTANT: Extensions must be registered under the `antora.extensions` key in yo
|
|
|
64
64
|
|
|
65
65
|
=== Navigation
|
|
66
66
|
* **unlisted-pages** - Manage pages that appear in navigation but aren't listed
|
|
67
|
-
* **produce-redirects** - Generate redirect configurations
|
|
68
67
|
* **unpublish-pages** - Mark pages as unpublished
|
|
69
68
|
|
|
70
69
|
=== Integrations
|
|
@@ -125,13 +124,16 @@ antora:
|
|
|
125
124
|
algolia_index_name: YOUR_INDEX
|
|
126
125
|
----
|
|
127
126
|
|
|
128
|
-
===
|
|
127
|
+
=== Redirects
|
|
128
|
+
|
|
129
|
+
No extension required: core Antora (>= 3.1.8) generates redirects for https://docs.antora.org/antora/latest/page/page-aliases/[page aliases] automatically on every build. Define aliases in your `antora.yml`:
|
|
129
130
|
|
|
130
131
|
[,yaml]
|
|
131
132
|
----
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
name: redpanda
|
|
134
|
+
asciidoc:
|
|
135
|
+
attributes:
|
|
136
|
+
page-aliases: old-page.adoc,another-old-page.adoc
|
|
135
137
|
----
|
|
136
138
|
|
|
137
139
|
== Support
|
|
@@ -706,23 +706,7 @@ In this example, the `attributespath` option points to a local YAML file (`./loc
|
|
|
706
706
|
|
|
707
707
|
== Produce redirects (customization of core Antora)
|
|
708
708
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
=== Environment variables
|
|
712
|
-
|
|
713
|
-
This extension does not require any environment variables.
|
|
714
|
-
|
|
715
|
-
=== Configuration options
|
|
716
|
-
|
|
717
|
-
There are no configurable options for this extension.
|
|
718
|
-
|
|
719
|
-
=== Registration
|
|
720
|
-
|
|
721
|
-
```yaml
|
|
722
|
-
antora:
|
|
723
|
-
extensions:
|
|
724
|
-
- '@redpanda-data/docs-extensions-and-macros/extensions/modify-redirects'
|
|
725
|
-
```
|
|
709
|
+
Antora's built-in `@antora/redirect-producer` already produces redirect rules for https://docs.antora.org/antora/latest/page/page-aliases/[page aliases], including the redirect-loop case caused by https://docs.antora.org/antora/latest/playbook/urls-html-extension-style/#html-extension-style-key[`indexify`] that once required a workaround. Antora runs the real module automatically on every build with no extension required. This project previously shipped a hand-copied override of that module to work around the loop, but the override had drifted from the upstream fix and reintroduced a worse bug (a self-referential alias could overwrite the real page it pointed at). The override has been removed; core Antora handles redirects natively.
|
|
726
710
|
|
|
727
711
|
== Replace attributes in attachments
|
|
728
712
|
|
|
@@ -85,7 +85,6 @@ antora:
|
|
|
85
85
|
extensions:
|
|
86
86
|
- '@redpanda-data/docs-extensions-and-macros/extensions/version-fetcher/set-latest-version'
|
|
87
87
|
- '@redpanda-data/docs-extensions-and-macros/extensions/generate-index-data.js'
|
|
88
|
-
- '@redpanda-data/docs-extensions-and-macros/extensions/produce-redirects.js'
|
|
89
88
|
----
|
|
90
89
|
|
|
91
90
|
Extensions run in the order they're listed.
|
|
@@ -122,16 +121,7 @@ antora:
|
|
|
122
121
|
|
|
123
122
|
=== Redirect management
|
|
124
123
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
[,yaml]
|
|
128
|
-
----
|
|
129
|
-
antora:
|
|
130
|
-
extensions:
|
|
131
|
-
- '@redpanda-data/docs-extensions-and-macros/extensions/produce-redirects.js'
|
|
132
|
-
----
|
|
133
|
-
|
|
134
|
-
Define redirects in your `antora.yml`:
|
|
124
|
+
No extension required: core Antora (>= 3.1.8) generates redirects for old URLs automatically. Define redirects in your `antora.yml`:
|
|
135
125
|
|
|
136
126
|
[,yaml]
|
|
137
127
|
----
|
package/macros/glossary.js
CHANGED
|
@@ -4,6 +4,61 @@ const $glossaryContexts = Symbol('$glossaryContexts')
|
|
|
4
4
|
const { posix: path } = require('path')
|
|
5
5
|
const chalk = require('chalk')
|
|
6
6
|
|
|
7
|
+
// Backtick-monospace in hover-text. The unconstrained (double-backtick) form is
|
|
8
|
+
// matched first, otherwise it leaves a stray delimiter on each side.
|
|
9
|
+
const MONOSPACE_RX = /``([^`]+)``|`([^`]+)`/g
|
|
10
|
+
|
|
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
|
+
// Only a data-* tooltip is read by a JS tooltip library that renders it as HTML;
|
|
23
|
+
// the native title attribute is always plain text. Shared with the
|
|
24
|
+
// glossary-tooltip validation below so the two cannot drift apart.
|
|
25
|
+
function tooltipRendersHtml (tooltipAttr) {
|
|
26
|
+
return typeof tooltipAttr === 'string' && tooltipAttr.startsWith('data-')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// hover-text is raw, unconverted source text, so it has to be escaped before it
|
|
30
|
+
// lands in an HTML attribute. How many passes depends on the consumer:
|
|
31
|
+
//
|
|
32
|
+
// title The browser decodes the attribute once and shows the result as
|
|
33
|
+
// text, so one pass is exactly right.
|
|
34
|
+
// data-* docs-ui initializes tippy on these with allowHTML: true
|
|
35
|
+
// (docs-ui/src/js/12-activate-tooltips.js), which means the value is
|
|
36
|
+
// decoded once by the HTML parser and then parsed AGAIN as HTML by
|
|
37
|
+
// innerHTML. A single pass is spent on that first decode and escaped
|
|
38
|
+
// markup comes back to life -- `<img onerror=...>` in hover-text
|
|
39
|
+
// became a live element that fired. Escape twice so one level
|
|
40
|
+
// survives the decode, and add <code> afterwards so it is the only
|
|
41
|
+
// live markup in the value.
|
|
42
|
+
//
|
|
43
|
+
// Set definitionIsHtml for a definition that Asciidoctor already converted --
|
|
44
|
+
// the inline glossterm:term[definition] form arrives as HTML, unlike a term
|
|
45
|
+
// file's raw :hover-text: value -- so it is escaped once rather than twice and
|
|
46
|
+
// its markup survives to the reader instead of being shown as literal tags.
|
|
47
|
+
function formatTooltipDefinition (text, tooltipAttr, definitionIsHtml) {
|
|
48
|
+
if (!tooltipRendersHtml(tooltipAttr)) return escapeAttr(text)
|
|
49
|
+
if (definitionIsHtml) {
|
|
50
|
+
// Already HTML, so one pass is right: the attribute decode hands the markup
|
|
51
|
+
// back intact for innerHTML to render, and a quote in the text still cannot
|
|
52
|
+
// terminate the attribute early.
|
|
53
|
+
return escapeAttr(text)
|
|
54
|
+
}
|
|
55
|
+
return escapeAttr(escapeAttr(text)).replace(
|
|
56
|
+
MONOSPACE_RX,
|
|
57
|
+
(match, unconstrained, constrained) =>
|
|
58
|
+
`<code>${unconstrained !== undefined ? unconstrained : constrained}</code>`
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
7
62
|
module.exports.register = function (registry, config = {}) {
|
|
8
63
|
|
|
9
64
|
const vfs = adaptVfs()
|
|
@@ -113,19 +168,24 @@ module.exports.register = function (registry, config = {}) {
|
|
|
113
168
|
}
|
|
114
169
|
var tooltip = document.getAttribute('glossary-tooltip')
|
|
115
170
|
if (tooltip === 'true') tooltip = 'data-glossary-tooltip'
|
|
116
|
-
if (tooltip && tooltip !== 'title' && !tooltip
|
|
171
|
+
if (tooltip && tooltip !== 'title' && !tooltipRendersHtml(tooltip)) {
|
|
117
172
|
console.log(`glossary-tooltip attribute '${tooltip}' must be 'true', 'title', or start with 'data-`)
|
|
118
173
|
tooltip = undefined
|
|
119
174
|
}
|
|
120
175
|
const logTerms = document.hasAttribute('glossary-log-terms')
|
|
121
176
|
var definition;
|
|
122
177
|
var pageTitle;
|
|
178
|
+
// A term file's :hover-text: is raw source text, but the definition
|
|
179
|
+
// passed inline as glossterm:term[definition] has already been through
|
|
180
|
+
// Asciidoctor's inline substitutions, so the two need different escaping.
|
|
181
|
+
var definitionIsHtml = false;
|
|
123
182
|
const index = context.gloss.findIndex((candidate) => candidate.term === term)
|
|
124
183
|
if (index >= 0) {
|
|
125
184
|
definition = context.gloss[index].def
|
|
126
185
|
pageTitle = context.gloss[index].pageTitle
|
|
127
186
|
} else {
|
|
128
187
|
definition = attributes.definition;
|
|
188
|
+
definitionIsHtml = !!definition;
|
|
129
189
|
}
|
|
130
190
|
if (definition) {
|
|
131
191
|
logTerms && console.log(`${term}:: ${definition}`)
|
|
@@ -154,12 +214,13 @@ module.exports.register = function (registry, config = {}) {
|
|
|
154
214
|
inline = self.createInline(parent, 'quoted', customText, { attributes: attrs })
|
|
155
215
|
}
|
|
156
216
|
if (tooltip) {
|
|
217
|
+
const formattedDefinition = formatTooltipDefinition(definition, tooltip, definitionIsHtml)
|
|
157
218
|
const a = inline.convert()
|
|
158
219
|
const matches = a.match(TRX)
|
|
159
220
|
if (matches) {
|
|
160
|
-
return self.createInline(parent, 'quoted', `${matches[1]} ${tooltip}="${
|
|
221
|
+
return self.createInline(parent, 'quoted', `${matches[1]} ${tooltip}="${formattedDefinition}"${matches[2]}`)
|
|
161
222
|
} else {
|
|
162
|
-
return self.createInline(parent, 'quoted', `<span ${tooltip}="${
|
|
223
|
+
return self.createInline(parent, 'quoted', `<span ${tooltip}="${formattedDefinition}">${a}</span>`)
|
|
163
224
|
}
|
|
164
225
|
}
|
|
165
226
|
return inline
|
|
@@ -186,3 +247,5 @@ module.exports.register = function (registry, config = {}) {
|
|
|
186
247
|
}
|
|
187
248
|
return registry
|
|
188
249
|
}
|
|
250
|
+
|
|
251
|
+
module.exports.formatTooltipDefinition = formatTooltipDefinition
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
-
"version": "5.22.
|
|
3
|
+
"version": "5.22.2",
|
|
4
4
|
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"antora",
|
|
@@ -77,8 +77,6 @@
|
|
|
77
77
|
"./extensions/find-related-labs": "./extensions/find-related-labs.js",
|
|
78
78
|
"./extensions/convert-llms-to-txt": "./extensions/convert-llms-to-txt.js",
|
|
79
79
|
"./extensions/convert-sitemap-to-markdown": "./extensions/convert-sitemap-to-markdown.js",
|
|
80
|
-
"./extensions/modify-redirects": "./extensions/produce-redirects.js",
|
|
81
|
-
"./extensions/produce-redirects": "./extensions/produce-redirects.js",
|
|
82
80
|
"./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
|
|
83
81
|
"./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
|
|
84
82
|
"./extensions/resolve-xrefs-in-attachments": "./extensions/resolve-xrefs-in-attachments.js",
|
|
@@ -43,8 +43,8 @@ const STALE_METADATA_PARTIAL =
|
|
|
43
43
|
// deleting the file would break any published page that includes it, so blank
|
|
44
44
|
// it instead and log so a human removes the include line.
|
|
45
45
|
//
|
|
46
|
-
// The empty
|
|
47
|
-
// `[tag=
|
|
46
|
+
// The empty meta/body regions are load-bearing. Pages include this file with
|
|
47
|
+
// `[tag=meta]` and `[tag=body]`, and Asciidoctor logs `tag 'meta' not found
|
|
48
48
|
// in include file` for every tag a target does not define -- so a banner
|
|
49
49
|
// without the regions turns one blanked partial into two build warnings on
|
|
50
50
|
// every page that includes it, on every build, which is a build failure
|
|
@@ -54,7 +54,7 @@ const STALE_DESCRIPTION_PARTIAL =
|
|
|
54
54
|
'// This content is autogenerated. Do not edit manually. ' +
|
|
55
55
|
'The connector no longer has a description, so this partial is intentionally empty. ' +
|
|
56
56
|
'You can remove the include directive that points at this file from the connector page.\n' +
|
|
57
|
-
'\n// tag::
|
|
57
|
+
'\n// tag::meta[]\n// end::meta[]\n' +
|
|
58
58
|
'\n// tag::body[]\n// end::body[]\n';
|
|
59
59
|
|
|
60
60
|
// Component families with their own reference pages, keyed by the canonical
|
|
@@ -606,12 +606,12 @@ async function generateRpcnConnectorDocs(options) {
|
|
|
606
606
|
// connectors with neither summary nor description, keep the inline
|
|
607
607
|
// text rather than getting an include that cannot resolve.
|
|
608
608
|
item.hasDescriptionPartial = true;
|
|
609
|
-
// The
|
|
609
|
+
// The meta region only sets :description: when the connector has a
|
|
610
610
|
// summary, so a page may only replace its header attribute with the
|
|
611
611
|
// include when there is one. Otherwise the page would lose its meta
|
|
612
612
|
// description entirely and keep the inline attribute instead.
|
|
613
613
|
if (typeof item.summary === 'string' && item.summary.trim()) {
|
|
614
|
-
item.
|
|
614
|
+
item.hasDescriptionMeta = true;
|
|
615
615
|
}
|
|
616
616
|
if (!writeFullDrafts) {
|
|
617
617
|
partialsWritten++;
|
|
@@ -988,7 +988,7 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
|
|
|
988
988
|
const lines = content.split('\n');
|
|
989
989
|
const headerEnd = lines.findIndex((l) => l.trim() === '');
|
|
990
990
|
const header = lines.slice(0, headerEnd === -1 ? lines.length : headerEnd);
|
|
991
|
-
// A page migrated to the description partial carries the
|
|
991
|
+
// A page migrated to the description partial carries the meta include
|
|
992
992
|
// instead of a literal :description:. Without this arm the backfill
|
|
993
993
|
// splices a competing meta block into every migrated page, so the page
|
|
994
994
|
// ends up with a dead include plus a hand-inserted attribute that
|
|
@@ -997,7 +997,7 @@ function backfillPageDescriptions (connectorData, { pagesRoot, dryRun = false }
|
|
|
997
997
|
|
|
998
998
|
// Meta descriptions are read as plain text by search results, link
|
|
999
999
|
// previews, and assistants, so the markup comes out mechanically. Same
|
|
1000
|
-
// flattener the description partial's
|
|
1000
|
+
// flattener the description partial's meta region uses, so a page
|
|
1001
1001
|
// cannot publish one meta description before migrating to the partial
|
|
1002
1002
|
// and a different one after.
|
|
1003
1003
|
const summary = flattenToAttributeValue(item.summary);
|
|
@@ -6,7 +6,7 @@ const { descriptionIncludeLine } = require('../metadata-utils.js');
|
|
|
6
6
|
* Handlebars helper: emit the include directive for a connector's description
|
|
7
7
|
* partial, optionally for one of its tag regions.
|
|
8
8
|
*
|
|
9
|
-
* {{{descriptionInclude this '
|
|
9
|
+
* {{{descriptionInclude this 'meta'}}}
|
|
10
10
|
* {{{descriptionInclude this 'body'}}}
|
|
11
11
|
*
|
|
12
12
|
* Drafted pages use this instead of freezing the summary and description into
|
|
@@ -16,7 +16,7 @@ const { descriptionIncludeLine } = require('../metadata-utils.js');
|
|
|
16
16
|
* writes the file.
|
|
17
17
|
*
|
|
18
18
|
* @param {object} item connector data with `type`/`typeDir` and `name`
|
|
19
|
-
* @param {string} [tag] `
|
|
19
|
+
* @param {string} [tag] `meta` or `body`
|
|
20
20
|
* @returns {string}
|
|
21
21
|
*/
|
|
22
22
|
module.exports = function descriptionInclude (item, tag) {
|
|
@@ -7,7 +7,7 @@ const { flattenToAttributeValue } = require('../metadata-utils.js');
|
|
|
7
7
|
* value suitable for the `:description:` page attribute (and therefore for the
|
|
8
8
|
* page's `<meta name="description">`).
|
|
9
9
|
*
|
|
10
|
-
* Delegates to the shared flattener so the description partial's
|
|
10
|
+
* Delegates to the shared flattener so the description partial's meta region
|
|
11
11
|
* and `backfillPageDescriptions` publish the same text for the same summary.
|
|
12
12
|
* Collapsing whitespace alone is not enough: summaries carry xrefs, link
|
|
13
13
|
* macros and inline code, and an unflattened value ships raw
|
|
@@ -248,10 +248,10 @@ function descriptionWithMetadataInclude (item) {
|
|
|
248
248
|
* auto-generated summary and description from a regenerated partial instead of
|
|
249
249
|
* freezing them into the page body at first draft.
|
|
250
250
|
*
|
|
251
|
-
* The partial defines two tag regions, so pages address it twice: `
|
|
251
|
+
* The partial defines two tag regions, so pages address it twice: `meta` in
|
|
252
252
|
* the page header (sets `:description:`) and `body` where the prose renders.
|
|
253
253
|
* @param {object} item connector data with `type`/`typeDir` and `name`
|
|
254
|
-
* @param {string} [tag] tag region to include (`
|
|
254
|
+
* @param {string} [tag] tag region to include (`meta` or `body`); omit for the whole file
|
|
255
255
|
* @returns {string}
|
|
256
256
|
*/
|
|
257
257
|
function descriptionIncludeLine (item, tag) {
|
|
@@ -308,7 +308,7 @@ function lostMetadataSections (oldContent, newContent) {
|
|
|
308
308
|
* edited.
|
|
309
309
|
*
|
|
310
310
|
* This is the one implementation: `summaryAttribute` (the description
|
|
311
|
-
* partial's
|
|
311
|
+
* partial's meta region) and `backfillPageDescriptions` (the page-header
|
|
312
312
|
* self-heal) both call it, so the two paths cannot drift into publishing
|
|
313
313
|
* different meta descriptions for the same summary.
|
|
314
314
|
*
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* to `modules/components/partials/descriptions/<type>/<name>.adoc` on every
|
|
12
12
|
* run, with two tag regions. This script rewires existing pages to it:
|
|
13
13
|
*
|
|
14
|
-
* header: include::...[tag=
|
|
14
|
+
* header: include::...[tag=meta] inside the page's `// tag::meta[]` region
|
|
15
15
|
* body: include::...[tag=body] where the frozen prose is today
|
|
16
16
|
*
|
|
17
17
|
* The header rewire is mechanical and safe: it swaps one `:description:` line
|
|
@@ -36,7 +36,7 @@ const path = require('path');
|
|
|
36
36
|
const { descriptionIncludeLine, normalizeTypeDir, flattenToAttributeValue } = require('./metadata-utils.js');
|
|
37
37
|
|
|
38
38
|
// The page's meta tag region: the block single-source stubs inherit
|
|
39
|
-
// :description: from, so the
|
|
39
|
+
// :description: from, so the meta include belongs inside it.
|
|
40
40
|
const META_TAG_OPEN = /^\/\/\s*tag::meta\[\]\s*$/;
|
|
41
41
|
const META_TAG_CLOSE = /^\/\/\s*end::meta\[\]\s*$/;
|
|
42
42
|
|
|
@@ -123,34 +123,34 @@ function migrateDescriptionsToPartials ({
|
|
|
123
123
|
continue;
|
|
124
124
|
}
|
|
125
125
|
const partial = fs.readFileSync(partialPath, 'utf8');
|
|
126
|
-
const
|
|
126
|
+
const meta = tagRegion(partial, 'meta');
|
|
127
127
|
const body = tagRegion(partial, 'body');
|
|
128
|
-
if (!
|
|
128
|
+
if (!meta || !body || contentLines(body).length === 0) {
|
|
129
129
|
// A blanked partial (description removed upstream) has both regions
|
|
130
130
|
// but no content. Wiring a page to it would publish an empty page.
|
|
131
131
|
results.skipped.push({ page: key, reason: 'description partial is empty' });
|
|
132
132
|
continue;
|
|
133
133
|
}
|
|
134
134
|
|
|
135
|
-
const
|
|
135
|
+
const includeMeta = descriptionIncludeLine({ typeDir: partialTypeDir, name }, 'meta');
|
|
136
136
|
const includeBody = descriptionIncludeLine({ typeDir: partialTypeDir, name }, 'body');
|
|
137
137
|
|
|
138
138
|
let lines = fs.readFileSync(pagePath, 'utf8').split('\n');
|
|
139
139
|
let changed = false;
|
|
140
140
|
|
|
141
141
|
// --- header ---------------------------------------------------------
|
|
142
|
-
if (!lines.some((l) => l.trim() ===
|
|
142
|
+
if (!lines.some((l) => l.trim() === includeMeta)) {
|
|
143
143
|
const open = lines.findIndex((l) => META_TAG_OPEN.test(l));
|
|
144
144
|
const close = lines.findIndex((l, i) => i > open && META_TAG_CLOSE.test(l));
|
|
145
|
-
const
|
|
145
|
+
const metaValue = contentLines(meta)
|
|
146
146
|
.find((l) => l.startsWith(':description:'));
|
|
147
147
|
const pageAttr = lines.slice(0, introOf(lines)).find((l) => l.startsWith(':description:'));
|
|
148
|
-
if (!
|
|
149
|
-
// The connector has no summary, so the
|
|
148
|
+
if (!metaValue) {
|
|
149
|
+
// The connector has no summary, so the meta region sets nothing.
|
|
150
150
|
// Replacing a hand-written :description: with it would drop the
|
|
151
151
|
// page's meta description.
|
|
152
|
-
results.skipped.push({ page: key, reason: '
|
|
153
|
-
} else if (pageAttr && flattenToAttributeValue(pageAttr) !== flattenToAttributeValue(
|
|
152
|
+
results.skipped.push({ page: key, reason: 'meta region sets no :description:' });
|
|
153
|
+
} else if (pageAttr && flattenToAttributeValue(pageAttr) !== flattenToAttributeValue(metaValue)) {
|
|
154
154
|
// The page's meta description is not the connector summary, so it
|
|
155
155
|
// was authored by hand (often tuned for search) and swapping in the
|
|
156
156
|
// include would silently discard that editorial work. 13 pages in
|
|
@@ -159,7 +159,7 @@ function migrateDescriptionsToPartials ({
|
|
|
159
159
|
// header alone.
|
|
160
160
|
results.skipped.push({ page: key, reason: 'hand-authored :description: differs from the summary (needs a human)' });
|
|
161
161
|
} else if (open !== -1 && close > open) {
|
|
162
|
-
lines.splice(open + 1, close - open - 1,
|
|
162
|
+
lines.splice(open + 1, close - open - 1, includeMeta);
|
|
163
163
|
results.headers.push(key);
|
|
164
164
|
changed = true;
|
|
165
165
|
} else {
|
|
@@ -169,7 +169,7 @@ function migrateDescriptionsToPartials ({
|
|
|
169
169
|
// :description: lines in the first place.
|
|
170
170
|
const attrLine = lines.findIndex((l) => l.startsWith(':description:'));
|
|
171
171
|
if (attrLine !== -1 && attrLine < introOf(lines)) {
|
|
172
|
-
lines.splice(attrLine, 1, '// tag::meta[]',
|
|
172
|
+
lines.splice(attrLine, 1, '// tag::meta[]', includeMeta, '// end::meta[]');
|
|
173
173
|
results.headers.push(key);
|
|
174
174
|
changed = true;
|
|
175
175
|
} else {
|
|
@@ -183,7 +183,7 @@ function migrateDescriptionsToPartials ({
|
|
|
183
183
|
// it could not see and the page ended up with two. AsciiDoc takes the
|
|
184
184
|
// last assignment, so leaving the literal one there would make the
|
|
185
185
|
// include a no-op and the page would keep publishing frozen text.
|
|
186
|
-
if (lines.some((l) => l.trim() ===
|
|
186
|
+
if (lines.some((l) => l.trim() === includeMeta)) {
|
|
187
187
|
for (let i = introOf(lines) - 1; i >= 0; i--) {
|
|
188
188
|
if (lines[i].startsWith(':description:')) {
|
|
189
189
|
lines.splice(i, 1);
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
:type: {{type}}
|
|
4
4
|
{{#if support}}:support: {{support}}{{/if}}
|
|
5
5
|
:categories: [{{join categories ", "}}]
|
|
6
|
-
{{#if
|
|
6
|
+
{{#if hasDescriptionMeta}}
|
|
7
7
|
// tag::meta[]
|
|
8
|
-
{{{descriptionInclude this "
|
|
8
|
+
{{{descriptionInclude this "meta"}}}
|
|
9
9
|
// end::meta[]
|
|
10
10
|
{{else if summary}}
|
|
11
11
|
// tag::meta[]
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
:description: {{{summaryAttribute description}}}
|
|
17
17
|
// end::meta[]
|
|
18
18
|
{{/if}}
|
|
19
|
-
{{!-- The
|
|
19
|
+
{{!-- The meta include stays inside the meta tag region so single-source
|
|
20
20
|
stubs keep inheriting :description: as a header attribute, and the
|
|
21
21
|
value now refreshes with the connector instead of being frozen at
|
|
22
22
|
first draft. Component families with no description partial keep the
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
// This content is autogenerated. Do not edit manually. The summary and description come from the connector's definition in the Connect source. To change them, update the connector upstream, or use the doc-tools CLI with the --overrides option: https://redpandadata.atlassian.net/wiki/spaces/DOC/pages/1247543314/Generate+reference+docs+for+Redpanda+Connect
|
|
3
3
|
//
|
|
4
4
|
// Pages consume this partial in two places so both stay fresh:
|
|
5
|
-
// header: include::...[tag=
|
|
5
|
+
// header: include::...[tag=meta] (sets :description: for search snippets and meta tags)
|
|
6
6
|
// body: include::...[tag=body] (renders the summary and description)
|
|
7
7
|
|
|
8
|
-
// tag::
|
|
8
|
+
// tag::meta[]
|
|
9
9
|
{{#if summary}}:description: {{{summaryAttribute summary}}}
|
|
10
|
-
{{/if}}// end::
|
|
10
|
+
{{/if}}// end::meta[]
|
|
11
11
|
|
|
12
12
|
// tag::body[]
|
|
13
13
|
{{#if summary}}{{{summary}}}
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { raiseListenerLimit } = require('./util/raise-listener-limit')
|
|
4
|
-
|
|
5
|
-
const File = require('vinyl')
|
|
6
|
-
const { posix: path } = require('path')
|
|
7
|
-
|
|
8
|
-
const ENCODED_SPACE_RX = /%20/g
|
|
9
|
-
|
|
10
|
-
module.exports.register = function () {
|
|
11
|
-
raiseListenerLimit(this)
|
|
12
|
-
this.once('contextStarted', () => {
|
|
13
|
-
const { publishFiles: produceRedirectsDelegate } = this.getFunctions()
|
|
14
|
-
this.replaceFunctions({
|
|
15
|
-
produceRedirects (playbook, aliases) {
|
|
16
|
-
if ('findBy' in aliases) aliases = aliases.findBy({ family: 'alias' }) // @deprecated remove in Antora 4
|
|
17
|
-
if (!(aliases = aliases.filter((it) => it.pub.url !== it.rel.pub.url)).length) return []
|
|
18
|
-
let siteUrl = playbook.site.url
|
|
19
|
-
if (siteUrl) siteUrl = stripTrailingSlash(siteUrl, '')
|
|
20
|
-
const directoryRedirects = (playbook.urls.htmlExtensionStyle || 'default') !== 'default'
|
|
21
|
-
switch (playbook.urls.redirectFacility) {
|
|
22
|
-
case 'gitlab':
|
|
23
|
-
return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects, false)
|
|
24
|
-
case 'httpd':
|
|
25
|
-
return createHttpdHtaccess(aliases, extractUrlPath(siteUrl), directoryRedirects)
|
|
26
|
-
case 'netlify':
|
|
27
|
-
return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects)
|
|
28
|
-
case 'nginx':
|
|
29
|
-
return createNginxRewriteConf(aliases, extractUrlPath(siteUrl))
|
|
30
|
-
case 'static':
|
|
31
|
-
return populateStaticRedirectFiles(
|
|
32
|
-
aliases.filter((it) => it.out),
|
|
33
|
-
siteUrl
|
|
34
|
-
)
|
|
35
|
-
default:
|
|
36
|
-
return unpublish(aliases)
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function createStaticRedirectContents (file, siteUrl) {
|
|
44
|
-
const targetUrl = file.rel.pub.url
|
|
45
|
-
let linkTag
|
|
46
|
-
let to = targetUrl.charAt() === '/' ? computeRelativeUrlPath(file.pub.url, targetUrl) : undefined
|
|
47
|
-
let toText = to
|
|
48
|
-
if (to) {
|
|
49
|
-
if (siteUrl && siteUrl.charAt() !== '/') {
|
|
50
|
-
linkTag = `<link rel="canonical" href="${(toText = siteUrl + targetUrl)}">\n`
|
|
51
|
-
}
|
|
52
|
-
} else {
|
|
53
|
-
linkTag = `<link rel="canonical" href="${(toText = to = targetUrl)}">\n`
|
|
54
|
-
}
|
|
55
|
-
return `<!DOCTYPE html>
|
|
56
|
-
<meta charset="utf-8">
|
|
57
|
-
${linkTag || ''}<script>location="${to}"</script>
|
|
58
|
-
<meta http-equiv="refresh" content="0; url=${to}">
|
|
59
|
-
<meta name="robots" content="noindex">
|
|
60
|
-
<title>Redirect Notice</title>
|
|
61
|
-
<h1>Redirect Notice</h1>
|
|
62
|
-
<p>The page you requested has been relocated to <a href="${to}">${toText}</a>.</p>`
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function extractUrlPath (url) {
|
|
66
|
-
if (url) {
|
|
67
|
-
if (url.charAt() === '/') return url
|
|
68
|
-
const urlPath = new URL(url).pathname
|
|
69
|
-
return urlPath === '/' ? '' : urlPath
|
|
70
|
-
} else {
|
|
71
|
-
return ''
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function createHttpdHtaccess (files, urlPath, directoryRedirects = false) {
|
|
76
|
-
const rules = files.reduce((accum, file) => {
|
|
77
|
-
delete file.out
|
|
78
|
-
let fromUrl = file.pub.url
|
|
79
|
-
fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
|
|
80
|
-
let toUrl = file.rel.pub.url
|
|
81
|
-
toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
|
|
82
|
-
// see https://httpd.apache.org/docs/current/en/mod/mod_alias.html#redirect
|
|
83
|
-
// NOTE: redirect rule for directory prefix does not require trailing slash
|
|
84
|
-
if (file.pub.splat) {
|
|
85
|
-
accum.push(`Redirect 302 ${fromUrl} ${stripTrailingSlash(toUrl)}`)
|
|
86
|
-
} else if (directoryRedirects) {
|
|
87
|
-
accum.push(`RedirectMatch 301 ^${regexpEscape(fromUrl)}$ ${stripTrailingSlash(toUrl)}`)
|
|
88
|
-
} else {
|
|
89
|
-
accum.push(`Redirect 301 ${fromUrl} ${toUrl}`)
|
|
90
|
-
}
|
|
91
|
-
return accum
|
|
92
|
-
}, [])
|
|
93
|
-
return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '.htaccess' } })]
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// NOTE: a trailing slash on the pathname will be ignored
|
|
97
|
-
// see https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash
|
|
98
|
-
// however, we keep it when generating the rules for clarity
|
|
99
|
-
function createNetlifyRedirects (files, urlPath, addDirectoryRedirects = false, useForceFlag = true) {
|
|
100
|
-
const rules = files.reduce((accum, file) => {
|
|
101
|
-
delete file.out
|
|
102
|
-
const fromUrl = urlPath + file.pub.url
|
|
103
|
-
const toUrl = urlPath + file.rel.pub.url
|
|
104
|
-
const forceFlag = useForceFlag ? '!' : ''
|
|
105
|
-
if (file.pub.splat) {
|
|
106
|
-
accum.push(`${fromUrl}/* ${ensureTrailingSlash(toUrl)}:splat 302${forceFlag}`)
|
|
107
|
-
} else {
|
|
108
|
-
accum.push(`${fromUrl} ${toUrl} 301${forceFlag}`)
|
|
109
|
-
if (addDirectoryRedirects && fromUrl.endsWith('/index.html')) {
|
|
110
|
-
accum.push(`${fromUrl.substr(0, fromUrl.length - 10)} ${toUrl} 301${forceFlag}`)
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return accum
|
|
114
|
-
}, [])
|
|
115
|
-
return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '_redirects' } })]
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function createNginxRewriteConf (files, urlPath) {
|
|
119
|
-
const rules = files.map((file) => {
|
|
120
|
-
delete file.out
|
|
121
|
-
let fromUrl = file.pub.url
|
|
122
|
-
fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
|
|
123
|
-
let toUrl = file.rel.pub.url
|
|
124
|
-
toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
|
|
125
|
-
if (file.pub.splat) {
|
|
126
|
-
const toUrlWithTrailingSlash = ensureTrailingSlash(toUrl)
|
|
127
|
-
return `location ^~ ${fromUrl}/ { rewrite ^${regexpEscape(fromUrl)}/(.*)$ ${toUrlWithTrailingSlash}$1 redirect; }`
|
|
128
|
-
} else {
|
|
129
|
-
return `location = ${fromUrl} { return 301 ${toUrl}; }`
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
return [new File({ contents: Buffer.from(rules.join('\n').trim() + '\n'), out: { path: '.etc/nginx/rewrite.conf' } })]
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function populateStaticRedirectFiles(files, siteUrl) {
|
|
136
|
-
for (const file of files) {
|
|
137
|
-
const content = createStaticRedirectContents(file, siteUrl) + '\n';
|
|
138
|
-
file.contents = Buffer.from(content);
|
|
139
|
-
}
|
|
140
|
-
return []
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function unpublish (files) {
|
|
144
|
-
files.forEach((file) => delete file.out)
|
|
145
|
-
return []
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function computeRelativeUrlPath (from, to) {
|
|
149
|
-
if (to === from) return to.charAt(to.length - 1) === '/' ? './' : path.basename(to)
|
|
150
|
-
return (path.relative(path.dirname(from + '.'), to) || '.') + (to.charAt(to.length - 1) === '/' ? '/' : '')
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function ensureTrailingSlash (str) {
|
|
154
|
-
return str.charAt(str.length - 1) === '/' ? str : str + '/'
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function stripTrailingSlash (str, root = '/') {
|
|
158
|
-
if (str === '/') return root
|
|
159
|
-
const lastIdx = str.length - 1
|
|
160
|
-
return str.charAt(lastIdx) === '/' ? str.substr(0, lastIdx) : str
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function regexpEscape (str) {
|
|
164
|
-
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // don't escape "-" since it's meaningless in a literal
|
|
165
|
-
}
|
|
166
|
-
|