@redpanda-data/docs-extensions-and-macros 5.13.0 → 5.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/doc-tools.js +39 -0
- package/package.json +1 -1
- package/tools/generate-cli-docs.js +15 -4
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +275 -33
- package/tools/redpanda-connect/helpers/descriptionInclude.js +24 -0
- package/tools/redpanda-connect/helpers/index.js +3 -0
- package/tools/redpanda-connect/helpers/renderConnectDescription.js +125 -0
- package/tools/redpanda-connect/helpers/summaryAttribute.js +22 -0
- package/tools/redpanda-connect/metadata-utils.js +183 -35
- package/tools/redpanda-connect/migrate-descriptions-to-partials.js +275 -0
- package/tools/redpanda-connect/normalize-metadata.js +6 -4
- package/tools/redpanda-connect/pr-summary-formatter.js +51 -0
- package/tools/redpanda-connect/rpcn-connector-docs-handler.js +37 -7
- package/tools/redpanda-connect/templates/connector.hbs +17 -4
- package/tools/redpanda-connect/templates/descriptions-partials.hbs +21 -0
- package/tools/redpanda-connect/templates/intro.hbs +1 -1
|
@@ -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
|
+
};
|
|
@@ -18,8 +18,18 @@ const path = require('path');
|
|
|
18
18
|
|
|
19
19
|
const METADATA_HEADING = /^==\s+Metadata\s*$/;
|
|
20
20
|
// AsciiDoc listing/literal block delimiter (`----`, possibly longer). Lines
|
|
21
|
-
// inside such blocks must not be treated as headings.
|
|
21
|
+
// inside such blocks must not be treated as headings. Asciidoctor only reads a
|
|
22
|
+
// delimiter at column 0 -- an indented `----` is a literal paragraph -- so the
|
|
23
|
+
// test is against the raw line, never a trimmed one.
|
|
22
24
|
const BLOCK_DELIMITER = /^-{4,}$/;
|
|
25
|
+
// AsciiDoc literal block delimiter (`....`, possibly longer) -- as
|
|
26
|
+
// substitution-free as a ---- listing block, and just as liable to contain a
|
|
27
|
+
// brace or heading-shaped line that must not be touched.
|
|
28
|
+
const LITERAL_DELIMITER = /^\.{4,}$/;
|
|
29
|
+
// Markdown-style fence delimiter (``` or ~~~, possibly with a language tag).
|
|
30
|
+
// Descriptions and metadata blocks carry fenced examples alongside AsciiDoc
|
|
31
|
+
// ---- blocks.
|
|
32
|
+
const FENCE_DELIMITER = /^(`{3,}|~{3,})/;
|
|
23
33
|
// The metadata block ends at the next structural element. Besides the next
|
|
24
34
|
// level-2 heading, this also covers page-level constructs that follow the
|
|
25
35
|
// section when locateMetadata runs against a full reference page (not just a
|
|
@@ -29,6 +39,54 @@ const BLOCK_DELIMITER = /^-{4,}$/;
|
|
|
29
39
|
// swallow the trailing `include::...partial$fields[]` and `// end::single-source[]`.
|
|
30
40
|
const SECTION_END = /^(?:==\s+\S|include::|\/\/\s*(?:tag|end)::)/;
|
|
31
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Split `text` into lines annotated with whether each line sits inside (or
|
|
44
|
+
* delimits) a verbatim region: an AsciiDoc `----` listing block or a markdown
|
|
45
|
+
* ```/~~~ fence. Every scanner over connector prose walks the text through
|
|
46
|
+
* this, so fence interiors are treated exactly like listing-block interiors:
|
|
47
|
+
* content, never headings, never escapable prose, never section terminators.
|
|
48
|
+
*
|
|
49
|
+
* Layered state: while one delimiter kind is open, only its own closer
|
|
50
|
+
* matters. A `----` line inside a fence (or a fence line inside a `----`
|
|
51
|
+
* block) is content -- treating it as a delimiter leaks the state and
|
|
52
|
+
* swallows everything after it.
|
|
53
|
+
*
|
|
54
|
+
* This is the only such state machine in the tree on purpose. There used to
|
|
55
|
+
* be three, and they had already drifted: two tested the trimmed line and one
|
|
56
|
+
* the raw line, so an indented `----` opened a verbatim region for one
|
|
57
|
+
* scanner and was plain content for another, on the same description in the
|
|
58
|
+
* same pipeline. Asciidoctor itself only honours a delimiter at column 0, so
|
|
59
|
+
* the raw-line test is the correct one and the only one here.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} text
|
|
62
|
+
* @returns {Array<{line: string, verbatim: boolean}>}
|
|
63
|
+
*/
|
|
64
|
+
function annotateVerbatimLines (text) {
|
|
65
|
+
let inBlock = false;
|
|
66
|
+
let inLiteral = false;
|
|
67
|
+
let fence = null;
|
|
68
|
+
return String(text == null ? '' : text).split('\n').map((line) => {
|
|
69
|
+
if (inBlock) {
|
|
70
|
+
if (BLOCK_DELIMITER.test(line)) inBlock = false;
|
|
71
|
+
return { line, verbatim: true };
|
|
72
|
+
}
|
|
73
|
+
if (inLiteral) {
|
|
74
|
+
if (LITERAL_DELIMITER.test(line)) inLiteral = false;
|
|
75
|
+
return { line, verbatim: true };
|
|
76
|
+
}
|
|
77
|
+
if (fence) {
|
|
78
|
+
const closer = line.match(FENCE_DELIMITER);
|
|
79
|
+
if (closer && closer[1][0] === fence) fence = null;
|
|
80
|
+
return { line, verbatim: true };
|
|
81
|
+
}
|
|
82
|
+
const fenceMatch = line.match(FENCE_DELIMITER);
|
|
83
|
+
if (fenceMatch) { fence = fenceMatch[1][0]; return { line, verbatim: true }; }
|
|
84
|
+
if (BLOCK_DELIMITER.test(line)) { inBlock = true; return { line, verbatim: true }; }
|
|
85
|
+
if (LITERAL_DELIMITER.test(line)) { inLiteral = true; return { line, verbatim: true }; }
|
|
86
|
+
return { line, verbatim: false };
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
32
90
|
/**
|
|
33
91
|
* Locate the `== Metadata` section within a description.
|
|
34
92
|
* @param {string} description
|
|
@@ -39,22 +97,19 @@ const SECTION_END = /^(?:==\s+\S|include::|\/\/\s*(?:tag|end)::)/;
|
|
|
39
97
|
function locateMetadata (description) {
|
|
40
98
|
if (!description || typeof description !== 'string') return null;
|
|
41
99
|
|
|
42
|
-
const
|
|
100
|
+
const annotated = annotateVerbatimLines(description);
|
|
101
|
+
const lines = annotated.map((a) => a.line);
|
|
43
102
|
let headingLine = -1;
|
|
44
|
-
let
|
|
45
|
-
|
|
46
|
-
if (BLOCK_DELIMITER.test(lines[i])) { inBlock = !inBlock; continue; }
|
|
47
|
-
if (!inBlock && METADATA_HEADING.test(lines[i])) { headingLine = i; break; }
|
|
103
|
+
for (let i = 0; i < annotated.length; i++) {
|
|
104
|
+
if (!annotated[i].verbatim && METADATA_HEADING.test(lines[i])) { headingLine = i; break; }
|
|
48
105
|
}
|
|
49
106
|
if (headingLine === -1) return null;
|
|
50
107
|
|
|
51
108
|
// Find the terminating element after the metadata heading: the next level-2
|
|
52
109
|
// heading, an Antora include directive, or a single-source tag comment.
|
|
53
110
|
let endLine = lines.length;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (BLOCK_DELIMITER.test(lines[i])) { inBlock = !inBlock; continue; }
|
|
57
|
-
if (!inBlock && SECTION_END.test(lines[i])) { endLine = i; break; }
|
|
111
|
+
for (let i = headingLine + 1; i < annotated.length; i++) {
|
|
112
|
+
if (!annotated[i].verbatim && SECTION_END.test(lines[i])) { endLine = i; break; }
|
|
58
113
|
}
|
|
59
114
|
|
|
60
115
|
// Trim trailing blank lines inside the section so the block ends cleanly.
|
|
@@ -78,16 +133,64 @@ function extractMetadata (description) {
|
|
|
78
133
|
return found ? found.block : '';
|
|
79
134
|
}
|
|
80
135
|
|
|
136
|
+
// Rate limits are the one component family the upstream data and the docs
|
|
137
|
+
// repo spell differently: the dataset key is `rate-limits`, `item.type` is
|
|
138
|
+
// `rate_limit`, and the pages directory is `rate_limits`. Every spelling has
|
|
139
|
+
// to collapse to one partial directory or the write path and the include path
|
|
140
|
+
// disagree and the include does not resolve.
|
|
141
|
+
const TYPE_DIR_ALIASES = new Map([
|
|
142
|
+
['rate-limit', 'rate_limits'],
|
|
143
|
+
['rate-limits', 'rate_limits'],
|
|
144
|
+
['rate_limit', 'rate_limits'],
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The component families that have their own reference pages under
|
|
149
|
+
* `modules/components/pages/<typeDir>/`, in canonical typeDir spelling.
|
|
150
|
+
*
|
|
151
|
+
* This list existed in eight places and the copies disagreed three ways:
|
|
152
|
+
* `config` was in two of them, `rate_limits` was missing from one, and two
|
|
153
|
+
* carried both rate-limit spellings. A component family added upstream that
|
|
154
|
+
* is missed by one copy silently gets no partial, no page backfill or no
|
|
155
|
+
* cloud-docs check, depending on which copy forgot it.
|
|
156
|
+
* @type {string[]}
|
|
157
|
+
*/
|
|
158
|
+
const CONNECTOR_TYPE_DIRS = [
|
|
159
|
+
'inputs', 'outputs', 'processors', 'caches', 'rate_limits',
|
|
160
|
+
'buffers', 'metrics', 'scanners', 'tracers',
|
|
161
|
+
];
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Every top-level key of the connector dataset that holds component objects:
|
|
165
|
+
* the page-backed families plus `config`, which has entries but no per-entry
|
|
166
|
+
* page. Callers that walk the dataset (augmenting, diffing, stripping) want
|
|
167
|
+
* this; callers that write per-page artifacts want CONNECTOR_TYPE_DIRS.
|
|
168
|
+
* @type {string[]}
|
|
169
|
+
*/
|
|
170
|
+
const CONNECTOR_DATA_KEYS = [...CONNECTOR_TYPE_DIRS, 'config'];
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Canonical partial directory for a plural type directory. Applied to every
|
|
174
|
+
* derivation of a type directory so `typeDirFor` is the single source of truth
|
|
175
|
+
* for both the file the generator writes and the include line a page carries.
|
|
176
|
+
* @param {string} typeDir
|
|
177
|
+
* @returns {string}
|
|
178
|
+
*/
|
|
179
|
+
function normalizeTypeDir (typeDir) {
|
|
180
|
+
return TYPE_DIR_ALIASES.get(typeDir) || typeDir;
|
|
181
|
+
}
|
|
182
|
+
|
|
81
183
|
/**
|
|
82
|
-
* Derive the plural type directory (for example `input` ->
|
|
184
|
+
* Derive the plural, canonical type directory (for example `input` ->
|
|
185
|
+
* `inputs`, `rate-limits` -> `rate_limits`).
|
|
83
186
|
* @param {object} item connector data with `type` and/or `typeDir`
|
|
84
187
|
* @returns {string}
|
|
85
188
|
*/
|
|
86
189
|
function typeDirFor (item) {
|
|
87
|
-
if (item && item.typeDir) return item.typeDir;
|
|
190
|
+
if (item && item.typeDir) return normalizeTypeDir(item.typeDir);
|
|
88
191
|
const type = item && item.type;
|
|
89
192
|
if (!type) return '';
|
|
90
|
-
return type.endsWith('s') ? type : `${type}s
|
|
193
|
+
return normalizeTypeDir(type.endsWith('s') ? type : `${type}s`);
|
|
91
194
|
}
|
|
92
195
|
|
|
93
196
|
/**
|
|
@@ -139,9 +242,22 @@ function descriptionWithMetadataInclude (item) {
|
|
|
139
242
|
return description.slice(0, found.start) + metadataIncludeLine(item) + description.slice(found.end);
|
|
140
243
|
}
|
|
141
244
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Build the Antora include directive for a connector's description partial.
|
|
247
|
+
* Mirrors {@link metadataIncludeLine} so a drafted page can pull its
|
|
248
|
+
* auto-generated summary and description from a regenerated partial instead of
|
|
249
|
+
* freezing them into the page body at first draft.
|
|
250
|
+
*
|
|
251
|
+
* The partial defines two tag regions, so pages address it twice: `attrs` in
|
|
252
|
+
* the page header (sets `:description:`) and `body` where the prose renders.
|
|
253
|
+
* @param {object} item connector data with `type`/`typeDir` and `name`
|
|
254
|
+
* @param {string} [tag] tag region to include (`attrs` or `body`); omit for the whole file
|
|
255
|
+
* @returns {string}
|
|
256
|
+
*/
|
|
257
|
+
function descriptionIncludeLine (item, tag) {
|
|
258
|
+
const attrs = tag ? `tag=${tag}` : '';
|
|
259
|
+
return `include::connect:components:partial$descriptions/${typeDirFor(item)}/${item.name}.adoc[${attrs}]`;
|
|
260
|
+
}
|
|
145
261
|
|
|
146
262
|
/**
|
|
147
263
|
* Collect the section heading titles in an AsciiDoc block, ignoring lines
|
|
@@ -155,25 +271,8 @@ const FENCE_DELIMITER = /^(`{3,}|~{3,})/;
|
|
|
155
271
|
function sectionHeadings (text) {
|
|
156
272
|
if (!text || typeof text !== 'string') return [];
|
|
157
273
|
const headings = [];
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
for (const line of text.split('\n')) {
|
|
161
|
-
// Layered state: while inside one delimiter kind, the only thing that
|
|
162
|
-
// matters is its own closer. A fence-like line inside a ---- literal
|
|
163
|
-
// block (or a ---- line inside a fence) is content, not a delimiter —
|
|
164
|
-
// treating it as one leaks the state and swallows every later heading.
|
|
165
|
-
if (inBlock) {
|
|
166
|
-
if (BLOCK_DELIMITER.test(line)) inBlock = false;
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (fence) {
|
|
170
|
-
const closer = line.match(FENCE_DELIMITER);
|
|
171
|
-
if (closer && closer[1][0] === fence) fence = null;
|
|
172
|
-
continue;
|
|
173
|
-
}
|
|
174
|
-
const fenceMatch = line.match(FENCE_DELIMITER);
|
|
175
|
-
if (fenceMatch) { fence = fenceMatch[1][0]; continue; }
|
|
176
|
-
if (BLOCK_DELIMITER.test(line)) { inBlock = true; continue; }
|
|
274
|
+
for (const { line, verbatim } of annotateVerbatimLines(text)) {
|
|
275
|
+
if (verbatim) continue;
|
|
177
276
|
const m = line.match(/^=+\s+(\S.*)$/);
|
|
178
277
|
if (m) headings.push(m[1].trim());
|
|
179
278
|
}
|
|
@@ -196,13 +295,62 @@ function lostMetadataSections (oldContent, newContent) {
|
|
|
196
295
|
return sectionHeadings(oldContent).filter((h) => !newHeadings.has(h));
|
|
197
296
|
}
|
|
198
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Flatten AsciiDoc prose into a value usable as a `:description:` attribute
|
|
300
|
+
* and, downstream, as the page's `<meta name="description">`.
|
|
301
|
+
*
|
|
302
|
+
* Meta descriptions are read as plain text by search results, link previews
|
|
303
|
+
* and assistants, so markup has to come out mechanically: xref and link
|
|
304
|
+
* macros become their labels, bare URL macros become their labels, `<<>>`
|
|
305
|
+
* shorthand becomes its text, the "open in new tab" caret goes, inline code
|
|
306
|
+
* loses its backticks, and whitespace (including hard line breaks, which an
|
|
307
|
+
* attribute value cannot carry) collapses to single spaces. No prose is
|
|
308
|
+
* edited.
|
|
309
|
+
*
|
|
310
|
+
* This is the one implementation: `summaryAttribute` (the description
|
|
311
|
+
* partial's attrs region) and `backfillPageDescriptions` (the page-header
|
|
312
|
+
* self-heal) both call it, so the two paths cannot drift into publishing
|
|
313
|
+
* different meta descriptions for the same summary.
|
|
314
|
+
*
|
|
315
|
+
* @param {string} text
|
|
316
|
+
* @returns {string}
|
|
317
|
+
*/
|
|
318
|
+
function flattenToAttributeValue (text) {
|
|
319
|
+
if (!text) return '';
|
|
320
|
+
return String(text)
|
|
321
|
+
.replace(/(?:xref|link):[^\[\]]+\[([^\]]*)\]/g, '$1')
|
|
322
|
+
// Bare URL macros (no xref:/link: prefix) and internal xref shorthand
|
|
323
|
+
// also appear in source summaries and render literally if left in place.
|
|
324
|
+
.replace(/https?:\/\/[^\s\[\]]+\[([^\]]*)\]/g, '$1')
|
|
325
|
+
// image:: and inline image: macros have no textual value in a meta
|
|
326
|
+
// description; drop them rather than leaking the target path.
|
|
327
|
+
.replace(/image:{1,2}[^\[\]\s]*\[[^\]]*\]/g, '')
|
|
328
|
+
.replace(/<<[^,>]+,([^>]+)>>/g, '$1')
|
|
329
|
+
.replace(/<<([^>]+)>>/g, '$1')
|
|
330
|
+
// Strip the "open in new tab" caret that AsciiDoc URL macros carry
|
|
331
|
+
// inside the link text, e.g. [Open Telemetry collector^].
|
|
332
|
+
.replace(/\^/g, '')
|
|
333
|
+
.replace(/`([^`]*)`/g, '$1')
|
|
334
|
+
.replace(/\s+/g, ' ')
|
|
335
|
+
.trim();
|
|
336
|
+
}
|
|
337
|
+
|
|
199
338
|
module.exports = {
|
|
200
339
|
locateMetadata,
|
|
340
|
+
flattenToAttributeValue,
|
|
341
|
+
CONNECTOR_TYPE_DIRS,
|
|
342
|
+
CONNECTOR_DATA_KEYS,
|
|
343
|
+
normalizeTypeDir,
|
|
201
344
|
resolvePageTypeDir,
|
|
202
345
|
extractMetadata,
|
|
203
346
|
typeDirFor,
|
|
204
347
|
metadataIncludeLine,
|
|
205
348
|
descriptionWithMetadataInclude,
|
|
349
|
+
descriptionIncludeLine,
|
|
206
350
|
sectionHeadings,
|
|
207
351
|
lostMetadataSections,
|
|
352
|
+
annotateVerbatimLines,
|
|
353
|
+
BLOCK_DELIMITER,
|
|
354
|
+
LITERAL_DELIMITER,
|
|
355
|
+
FENCE_DELIMITER,
|
|
208
356
|
};
|