@redpanda-data/docs-extensions-and-macros 2.5.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/README.adoc +422 -0
- package/extensions/add-global-attributes.js +77 -0
- package/extensions/algolia-indexer/generate-index.js +209 -0
- package/extensions/algolia-indexer/index.js +83 -0
- package/extensions/algolia-indexer/lazy-readable.js +18 -0
- package/extensions/algolia-indexer/multi-file-read-stream.js +24 -0
- package/extensions/algolia-indexer/template.js +3 -0
- package/extensions/replace-attributes-in-attachments.js +29 -0
- package/extensions/unlisted-pages.js +38 -0
- package/extensions/version-fetcher/getLatestConsoleVersion.js +33 -0
- package/extensions/version-fetcher/getLatestRedpandaVersion.js +46 -0
- package/extensions/version-fetcher/set-latest-version.js +60 -0
- package/macros/config-ref.js +43 -0
- package/macros/glossary.js +180 -0
- package/macros/helm-ref.js +34 -0
- package/package.json +32 -0
package/README.adoc
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
= Antora Extensions and Macros for Redpanda Docs
|
|
2
|
+
:url-project: https://github.com/JakeSCahill/antora-extensions-and-macros
|
|
3
|
+
:url-git: https://git-scm.com
|
|
4
|
+
:url-git-dl: {url-git}/downloads
|
|
5
|
+
:url-nodejs: https://nodejs.org
|
|
6
|
+
:url-nodejs-releases: https://github.com/nodejs/Release#release-schedule
|
|
7
|
+
:url-nvm-install: {url-nvm}#installation
|
|
8
|
+
:idprefix:
|
|
9
|
+
:idseparator: -
|
|
10
|
+
ifdef::env-github[]
|
|
11
|
+
:important-caption: :exclamation:
|
|
12
|
+
:note-caption: :paperclip:
|
|
13
|
+
endif::[]
|
|
14
|
+
:toc:
|
|
15
|
+
:toc-title: Contents
|
|
16
|
+
|
|
17
|
+
toc::[]
|
|
18
|
+
|
|
19
|
+
This library provides https://docs.antora.org/antora/latest/extend/extensions/[Antora extensions] and https://docs.asciidoctor.org/asciidoctor.js/latest/extend/extensions/register/[Asciidoc macros] developed for Redpanda documentation.
|
|
20
|
+
|
|
21
|
+
== Prerequisites
|
|
22
|
+
|
|
23
|
+
To use this library, you must have {url-nodejs}[Node.js] 16 or higher installed on your machine.
|
|
24
|
+
|
|
25
|
+
[,bash]
|
|
26
|
+
----
|
|
27
|
+
node --version
|
|
28
|
+
----
|
|
29
|
+
|
|
30
|
+
If this command fails with an error, you don't have Node.js installed.
|
|
31
|
+
|
|
32
|
+
When you have Node.js installed, use the following command to install the `antora-extensions-and-macros` package in your project:
|
|
33
|
+
|
|
34
|
+
[,bash]
|
|
35
|
+
----
|
|
36
|
+
npm i antora-extensions-and-macros
|
|
37
|
+
----
|
|
38
|
+
|
|
39
|
+
To use the development version instead, refer to the <<development-quickstart,Development Quickstart>>.
|
|
40
|
+
|
|
41
|
+
== Extensions
|
|
42
|
+
|
|
43
|
+
This section documents the Antora extensions that are provided by this library and how to configure them.
|
|
44
|
+
|
|
45
|
+
IMPORTANT: Be sure to register each extension under the `antora.extensions` key in the playbook, not the `asciidoc.extensions` key.
|
|
46
|
+
|
|
47
|
+
=== Algolia indexer
|
|
48
|
+
|
|
49
|
+
This extension generates an Algolia index for each version of each component. The index entries are then saved to Algolia using the `saveObjects()` method, and also saved as JSON files in the site catalog. JSON files are published to the site root using the following template: `algolia-<component>-<version>.json`.
|
|
50
|
+
|
|
51
|
+
NOTE: Only pages that include an `<article>` element with the `doc` class are indexed. Pages marked as "noindex" for "robots" are skipped.
|
|
52
|
+
|
|
53
|
+
==== Environment variables
|
|
54
|
+
|
|
55
|
+
- `ALGOLIA_ADMIN_API_KEY` (required)
|
|
56
|
+
- `ALGOLIA_APP_ID` (required)
|
|
57
|
+
- `ALGOLIA_INDEX_NAME` (required)
|
|
58
|
+
|
|
59
|
+
==== Configuration options
|
|
60
|
+
|
|
61
|
+
The extension accepts the following configuration options:
|
|
62
|
+
|
|
63
|
+
excludes (optional)::
|
|
64
|
+
Any elements, classes, or IDs that you want to exclude from the index.
|
|
65
|
+
index-latest-only (optional)::
|
|
66
|
+
Whether to index all versions or just the latest version of a component.
|
|
67
|
+
|
|
68
|
+
==== Registration example
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
antora:
|
|
72
|
+
extensions:
|
|
73
|
+
- require: 'node_modules/antora-extensions-and-macros/extensions/algolia-indexer/index.js'
|
|
74
|
+
excludes: ['.thumbs','script', '.page-versions','.feedback-section','.banner-container']
|
|
75
|
+
index-latest-only: true
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
=== Version fetcher
|
|
79
|
+
|
|
80
|
+
This extension fetches the latest release tag and latest release commit hash from http://github.com/redpanda-data/redpanda. These values are then assigned to the `full-version` and `latest-release-commit` attributes of the latest version of the Redpanda documentation, respectively.
|
|
81
|
+
|
|
82
|
+
It also fetches the latest version of Redpanda Console and assigns it to the `latest-console-version` attribute in the playbook so that all components have access to it.
|
|
83
|
+
|
|
84
|
+
==== Environment variables
|
|
85
|
+
|
|
86
|
+
- `REDPANDA_GITHUB_TOKEN` (optional): A Personal access token (PAT) that has `repo` permissions for the `redpanda-data` GitHub organization.
|
|
87
|
+
|
|
88
|
+
NOTE: If you don't set the environment variable, the latest versions may not be made available to your build. When the environment variable is not set, the extension sends unauthenticated requests to GitHub. Unauthenticated requests may result in hitting the API rate limit and cause GitHub to reject the request.
|
|
89
|
+
|
|
90
|
+
==== Registration example
|
|
91
|
+
|
|
92
|
+
```yaml
|
|
93
|
+
antora:
|
|
94
|
+
extensions:
|
|
95
|
+
- 'node_modules/antora-extensions-and-macros/extensions/version-fetcher/set-latest-version.js'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
=== Global attributes
|
|
99
|
+
|
|
100
|
+
This extension fetches the content of all YAML files in a GitHub directory and merges the contents with the `asciidoc.attributes` object in the Antora playbook. This allows you to define Asciidoc attributes in an external repository and automatically include them in your documentation.
|
|
101
|
+
|
|
102
|
+
[IMPORTANT]
|
|
103
|
+
====
|
|
104
|
+
- The GitHub directory that contains the global attributes must be named `global-attributes`.
|
|
105
|
+
- Only YAML files are supported. Other types of files are ignored.
|
|
106
|
+
- If a key is present in both the global attributes and the playbook's `asciidoc.attributes`, the value in the playbook takes precedence.
|
|
107
|
+
====
|
|
108
|
+
|
|
109
|
+
==== Environment variables
|
|
110
|
+
|
|
111
|
+
- `GLOBAL_ATTRIBUTES_GITHUB_TOKEN` (optional): A Personal access token (PAT) that has `repo` permissions for the GitHub organization defined in `org`.
|
|
112
|
+
|
|
113
|
+
NOTE: If you don't set the environment variable, global attributes may not be made available to your build. When the environment variable is not set, the extension sends unauthenticated requests to GitHub. Unauthenticated requests may result in hitting the API rate limit and cause GitHub to reject the request.
|
|
114
|
+
|
|
115
|
+
==== Configuration options
|
|
116
|
+
|
|
117
|
+
The extension accepts the following configuration options:
|
|
118
|
+
|
|
119
|
+
org (required)::
|
|
120
|
+
The GitHub organization that owns the repository.
|
|
121
|
+
|
|
122
|
+
repo (required)::
|
|
123
|
+
The name of the repository.
|
|
124
|
+
|
|
125
|
+
branch (required)::
|
|
126
|
+
The branch in the repository where the global attributes are located.
|
|
127
|
+
|
|
128
|
+
==== Registration example
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
antora:
|
|
132
|
+
extensions:
|
|
133
|
+
- require: 'node_modules/antora-extensions-and-macros/extensions/add-global-attributes.js'
|
|
134
|
+
org: example
|
|
135
|
+
repo: test
|
|
136
|
+
branch: main
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
=== Replace attributes in attachments
|
|
140
|
+
|
|
141
|
+
This extension replaces AsciiDoc attribute placeholders with their respective values in attachment files, such as CSS, HTML, and YAML.
|
|
142
|
+
|
|
143
|
+
[IMPORTANT]
|
|
144
|
+
====
|
|
145
|
+
- By default, this extension processes attachments for the `ROOT` (redpanda) component only. This behavior is hardcoded and cannot be changed in the configuration.
|
|
146
|
+
- The `@` character is removed from attribute values to prevent potential issues with CSS or HTML syntax.
|
|
147
|
+
- If the same attribute placeholder is used multiple times within a file, all instances will be replaced with the attribute's value.
|
|
148
|
+
====
|
|
149
|
+
|
|
150
|
+
==== Registration example
|
|
151
|
+
|
|
152
|
+
```yaml
|
|
153
|
+
antora:
|
|
154
|
+
extensions:
|
|
155
|
+
- 'node_modules/antora-extensions-and-macros/extensions/replace-attributes-in-attachments.js'
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
=== Unlisted pages
|
|
159
|
+
|
|
160
|
+
This extension identifies and logs any pages that aren't listed in the navigation (nav) file of each version of each component. It then optionally adds these unlisted pages to the end of the navigation tree under a configurable heading.
|
|
161
|
+
|
|
162
|
+
IMPORTANT: By default, this extension excludes components named 'api'. This behavior is hardcoded and cannot be changed in the configuration.
|
|
163
|
+
|
|
164
|
+
==== Configuration options
|
|
165
|
+
|
|
166
|
+
This extension accepts the following configuration options:
|
|
167
|
+
|
|
168
|
+
addToNavigation (optional)::
|
|
169
|
+
Whether to add unlisted pages to the navigation. The default is `false` (unlisted pages are not added).
|
|
170
|
+
|
|
171
|
+
unlistedPagesHeading (optional)::
|
|
172
|
+
The heading under which to list the unlisted pages in the navigation. The default is 'Unlisted Pages'.
|
|
173
|
+
|
|
174
|
+
==== Registration example
|
|
175
|
+
|
|
176
|
+
```yaml
|
|
177
|
+
antora:
|
|
178
|
+
extensions:
|
|
179
|
+
- require: 'node_modules/antora-extensions-and-macros/extensions/unlisted-pages.js'
|
|
180
|
+
addToNavigation: true
|
|
181
|
+
unlistedPagesHeading: 'Additional Resources'
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
== Macros
|
|
185
|
+
|
|
186
|
+
This section documents the Asciidoc macros that are provided by this library and how to configure them.
|
|
187
|
+
|
|
188
|
+
IMPORTANT: Be sure to register each extension under the `asciidoc.extensions` key in the playbook, not the `antora.extensions` key.
|
|
189
|
+
|
|
190
|
+
=== config_ref
|
|
191
|
+
|
|
192
|
+
This inline macro is used to generate a reference to a configuration value in the Redpanda documentation. The macro's parameters allow for control over the generated reference's format and the type of output produced.
|
|
193
|
+
|
|
194
|
+
==== Usage
|
|
195
|
+
|
|
196
|
+
The `config_ref` macro is used in an AsciiDoc document as follows:
|
|
197
|
+
|
|
198
|
+
[,asciidoc]
|
|
199
|
+
----
|
|
200
|
+
config_ref:configRef,isLink,path[]
|
|
201
|
+
----
|
|
202
|
+
|
|
203
|
+
The `config_ref` macro takes three parameters:
|
|
204
|
+
|
|
205
|
+
configRef::
|
|
206
|
+
This is the configuration reference, which is also used to generate the anchor link if `isLink` is `true`.
|
|
207
|
+
|
|
208
|
+
isLink::
|
|
209
|
+
Whether the output should be a link. If `isLink` is set to `true`, the output will be a cross-reference (xref) to the relevant configuration value.
|
|
210
|
+
|
|
211
|
+
path::
|
|
212
|
+
This is the path to the document where the configuration value is defined. This parameter is used to to generate the link if `isLink` is `true`.
|
|
213
|
+
|
|
214
|
+
IMPORTANT: The path must be the name of a document at the root of the `reference` module.
|
|
215
|
+
|
|
216
|
+
NOTE: The `config_ref` macro is environment-aware. It checks if the document it is being used in is part of a Kubernetes environment by checking if the `env-kubernetes` attribute is set in the document's attributes. Depending on this check, it either prepends `storage.tieredConfig.` to the `configRef` or just uses the `configRef` as is.
|
|
217
|
+
|
|
218
|
+
For example:
|
|
219
|
+
|
|
220
|
+
[,asciidoc]
|
|
221
|
+
----
|
|
222
|
+
config_ref:example_config,true,tunable-properties[]
|
|
223
|
+
----
|
|
224
|
+
|
|
225
|
+
==== Registration example
|
|
226
|
+
|
|
227
|
+
[,yaml]
|
|
228
|
+
----
|
|
229
|
+
asciidoc:
|
|
230
|
+
extensions:
|
|
231
|
+
- 'node_modules/antora-extensions-and-macros/macros/config-ref.js'
|
|
232
|
+
----
|
|
233
|
+
|
|
234
|
+
=== glossary and glossterm
|
|
235
|
+
|
|
236
|
+
The glossary module provides a way to define and reference glossary terms in your AsciiDoc documents.
|
|
237
|
+
|
|
238
|
+
This module consists of two parts: a block macro (`glossary`) and an inline macro (`glossterm`).
|
|
239
|
+
|
|
240
|
+
NOTE: This macro is a customized version of https://gitlab.com/djencks/asciidoctor-glossary[`asciidoctor-glossary`]. We added the ability to define terms globally in <<global-attributes, global attributes>> as well as link to external URLs.
|
|
241
|
+
|
|
242
|
+
==== Usage
|
|
243
|
+
|
|
244
|
+
To insert a glossary dlist, use the glossary block macro.
|
|
245
|
+
|
|
246
|
+
[,asciidoc]
|
|
247
|
+
----
|
|
248
|
+
glossary::[]
|
|
249
|
+
----
|
|
250
|
+
|
|
251
|
+
Glossary terms defined in the `glossterm` inline macro before the `glossary` macro is used appear as a definition list, sorted by term.
|
|
252
|
+
|
|
253
|
+
The `glossterm` inline macro is used to reference a term within the text of the document:
|
|
254
|
+
|
|
255
|
+
[,asciidoc]
|
|
256
|
+
----
|
|
257
|
+
glossterm:myTerm[myDefinition]
|
|
258
|
+
----
|
|
259
|
+
|
|
260
|
+
It takes two parameters:
|
|
261
|
+
|
|
262
|
+
term::
|
|
263
|
+
The term to be defined.
|
|
264
|
+
|
|
265
|
+
definition (optional)::
|
|
266
|
+
The definition of the term. If the term is defined in the <<global-attributes, global attributes>>, you can omit the definition as it will always be replaced by the definition in the global attributes.
|
|
267
|
+
|
|
268
|
+
==== Configuration options
|
|
269
|
+
|
|
270
|
+
glossary-log-terms (optional)::
|
|
271
|
+
Whether to log a textual representation of a definition list item to the console.
|
|
272
|
+
|
|
273
|
+
glossary-term-role (optional)::
|
|
274
|
+
Role to assign each term. By default, glossary terms are assigned the `glossary-term` role, which gives them the class `glossary-term` in generated html.
|
|
275
|
+
|
|
276
|
+
glossary-links (optional)::
|
|
277
|
+
Whether to generate links to glossary entries.
|
|
278
|
+
By default, links to the glossary entries are generated from the glossary terms. To avoid this, set the attribute to `false` as either asciidoctor configuration or a header attribute.
|
|
279
|
+
|
|
280
|
+
glossary-page (optional)::
|
|
281
|
+
Target page for glossary links. By default, links are generated to the same page as the glossary term. To specify the target page, set this attribute to the resource ID of a page where the `glossary` block macro is used.
|
|
282
|
+
|
|
283
|
+
glossary-tooltip (optional)::
|
|
284
|
+
Whether to enable tooltips for the defined terms. Valid values are:
|
|
285
|
+
- title: This uses the browser built-in `title` attribute to display the definition.
|
|
286
|
+
|
|
287
|
+
- true: This inserts the definition as the value of the attribute `data-glossary-tooltip`.
|
|
288
|
+
|
|
289
|
+
- data-<attribute-name>: This inserts the definition as the value of the supplied attribute name, which must start with `data`.
|
|
290
|
+
|
|
291
|
+
The last two options are intended to support js/css tooltip solutions such as tippy.js.
|
|
292
|
+
|
|
293
|
+
[IMPORTANT]
|
|
294
|
+
.Multi-page use
|
|
295
|
+
====
|
|
296
|
+
In Antora, a glossary is constructed for each component-version.
|
|
297
|
+
When the `glossary` block macro is evaluated, only terms known as of the rendering can be included.
|
|
298
|
+
Therefore, it is necessary that the page containing this macro in a component-version be rendered last.
|
|
299
|
+
It may be possible to arrange this by naming the page starting with a lot of 'z’s, such as `zzzzzz-glossary.adoc`.
|
|
300
|
+
====
|
|
301
|
+
|
|
302
|
+
==== Registration example
|
|
303
|
+
|
|
304
|
+
[,yaml]
|
|
305
|
+
----
|
|
306
|
+
asciidoc:
|
|
307
|
+
extensions:
|
|
308
|
+
- 'node_modules/antora-extensions-and-macros/macros/glossary.js'
|
|
309
|
+
----
|
|
310
|
+
|
|
311
|
+
=== helm_ref
|
|
312
|
+
|
|
313
|
+
This is an inline macro to create links to a Helm `values.yaml` file on ArtifactHub.
|
|
314
|
+
|
|
315
|
+
==== Usage
|
|
316
|
+
|
|
317
|
+
In an AsciiDoc document, use the `helm_ref` macro as follows:
|
|
318
|
+
|
|
319
|
+
[,asciidoc]
|
|
320
|
+
----
|
|
321
|
+
helm_ref:<helmRef>[]
|
|
322
|
+
----
|
|
323
|
+
|
|
324
|
+
Where `<helmRef>` is the Helm configuration value you want to reference in the `values.yaml` file.
|
|
325
|
+
|
|
326
|
+
For example:
|
|
327
|
+
|
|
328
|
+
Given a Helm reference value of `myConfigValue`, you would use the macro like this:
|
|
329
|
+
|
|
330
|
+
[,asciidoc]
|
|
331
|
+
----
|
|
332
|
+
helm_ref:myConfigValue[]
|
|
333
|
+
----
|
|
334
|
+
|
|
335
|
+
This will generate the following output:
|
|
336
|
+
|
|
337
|
+
[,asciidoc]
|
|
338
|
+
----
|
|
339
|
+
For default values and documentation for configuration options, see the https://artifacthub.io/packages/helm/redpanda-data/redpanda?modal=values&path=myConfigValue[values.yaml] file.
|
|
340
|
+
----
|
|
341
|
+
|
|
342
|
+
If you do not specify a Helm reference value, the macro generates a link without specifying a path.
|
|
343
|
+
|
|
344
|
+
==== Registration example
|
|
345
|
+
|
|
346
|
+
[,yaml]
|
|
347
|
+
----
|
|
348
|
+
asciidoc:
|
|
349
|
+
extensions:
|
|
350
|
+
- 'node_modules/antora-extensions-and-macros/macros/helm-ref.js'
|
|
351
|
+
----
|
|
352
|
+
|
|
353
|
+
== Development quickstart
|
|
354
|
+
|
|
355
|
+
This section provides information on how to develop this project.
|
|
356
|
+
|
|
357
|
+
=== Prerequisites
|
|
358
|
+
|
|
359
|
+
To build this project, you need the following software installed on your computer:
|
|
360
|
+
|
|
361
|
+
* {url-git}[git] (command: `git`)
|
|
362
|
+
* {url-nodejs}[Node.js] (commands: `node`, `npm`, and `npx`)
|
|
363
|
+
|
|
364
|
+
==== git
|
|
365
|
+
|
|
366
|
+
Make sure you have git installed.
|
|
367
|
+
|
|
368
|
+
[,bash]
|
|
369
|
+
----
|
|
370
|
+
git --version
|
|
371
|
+
----
|
|
372
|
+
|
|
373
|
+
If not, {url-git-dl}[download and install] the git package for your system.
|
|
374
|
+
|
|
375
|
+
==== Node.js
|
|
376
|
+
|
|
377
|
+
Make sure that you have Node.js installed (which also provides npm and npx).
|
|
378
|
+
|
|
379
|
+
[,bash]
|
|
380
|
+
----
|
|
381
|
+
node --version
|
|
382
|
+
----
|
|
383
|
+
|
|
384
|
+
If this command fails with an error, you don't have Node.js installed.
|
|
385
|
+
|
|
386
|
+
Now that you have git and Node.js installed, you're ready to start developing on this project.
|
|
387
|
+
|
|
388
|
+
=== Clone the project
|
|
389
|
+
|
|
390
|
+
Clone the project using git:
|
|
391
|
+
|
|
392
|
+
[,bash,subs=attributes+]
|
|
393
|
+
----
|
|
394
|
+
git clone {url-project}
|
|
395
|
+
----
|
|
396
|
+
|
|
397
|
+
Change into the project directory and stay in this directory when running all subsequent commands.
|
|
398
|
+
|
|
399
|
+
=== Install dependencies
|
|
400
|
+
|
|
401
|
+
Use npm to install the project's dependencies inside the project.
|
|
402
|
+
In your terminal, run the following command:
|
|
403
|
+
|
|
404
|
+
[,bash]
|
|
405
|
+
----
|
|
406
|
+
npm ci
|
|
407
|
+
----
|
|
408
|
+
|
|
409
|
+
This command installs the dependencies listed in `package-lock.json` into the `node_modules/` directory inside the project.
|
|
410
|
+
This directory should _not_ be committed to the source control repository.
|
|
411
|
+
|
|
412
|
+
=== Use your local project
|
|
413
|
+
|
|
414
|
+
If you want to use the project locally before it is published, you can specify the path to the extensions in the `local-antora-playbook.yml` file.
|
|
415
|
+
|
|
416
|
+
[,yaml]
|
|
417
|
+
----
|
|
418
|
+
asciidoc:
|
|
419
|
+
attributes:
|
|
420
|
+
extensions:
|
|
421
|
+
- '<path-to-local-project>/antora-extensions-and-macros/extensions/<extension-name>'
|
|
422
|
+
----
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* Example use in the playbook
|
|
2
|
+
* antora:
|
|
3
|
+
extensions:
|
|
4
|
+
* - require: ./extensions/add-global-attributes.js
|
|
5
|
+
org: example
|
|
6
|
+
repo: test
|
|
7
|
+
branch: main
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const yaml = require('js-yaml');
|
|
11
|
+
const { Octokit } = require("@octokit/rest");
|
|
12
|
+
const { retry } = require("@octokit/plugin-retry");
|
|
13
|
+
const OctokitWithRetries = Octokit.plugin(retry);
|
|
14
|
+
const chalk = require('chalk')
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
module.exports.register = function ({ config }) {
|
|
18
|
+
const logger = this.getLogger('global-attributes-extension')
|
|
19
|
+
|
|
20
|
+
this
|
|
21
|
+
.on('playbookBuilt', async ({ playbook }) => {
|
|
22
|
+
|
|
23
|
+
const globalAttributesUrl = `/repos/${config.org}/${config.repo}/contents/global-attributes?ref=${config.branch}`;
|
|
24
|
+
|
|
25
|
+
let githubOptions = {
|
|
26
|
+
userAgent: 'Redpanda Docs',
|
|
27
|
+
baseUrl: 'https://api.github.com',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if(process.env.GLOBAL_ATTRIBUTES_GITHUB_TOKEN){
|
|
31
|
+
githubOptions.auth = process.env.GLOBAL_ATTRIBUTES_GITHUB_TOKEN;
|
|
32
|
+
} else {
|
|
33
|
+
logger.warn('GLOBAL_ATTRIBUTES_GITHUB_TOKEN environment variable not set. Attempting unauthenticated request.')
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const github = new OctokitWithRetries(githubOptions);
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
// Request the contents of the directory from the GitHub API
|
|
40
|
+
const response = await github.request('GET ' + globalAttributesUrl);
|
|
41
|
+
|
|
42
|
+
if (response.status === 200) {
|
|
43
|
+
const directoryContents = response.data;
|
|
44
|
+
|
|
45
|
+
// Filter out only YAML files
|
|
46
|
+
const yamlFiles = directoryContents.filter(file => file.name.endsWith('.yaml') || file.name.endsWith('.yml'));
|
|
47
|
+
|
|
48
|
+
let globalAttributes = {};
|
|
49
|
+
for (let file of yamlFiles) {
|
|
50
|
+
const fileResponse = await github.request('GET ' + file.download_url);
|
|
51
|
+
const fileData = yaml.load(fileResponse.data);
|
|
52
|
+
globalAttributes = {...globalAttributes, ...fileData};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let mergedAttributes = {
|
|
56
|
+
...globalAttributes,
|
|
57
|
+
...playbook.asciidoc.attributes
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
playbook.asciidoc.attributes = mergedAttributes;
|
|
61
|
+
|
|
62
|
+
console.log(chalk.green('Merged global attributes into playbook'));
|
|
63
|
+
|
|
64
|
+
} else {
|
|
65
|
+
logger.warn(`Could not fetch global attributes: ${response.statusText}`);
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
} catch(error) {
|
|
69
|
+
if (error.response.url) {
|
|
70
|
+
logger.warn(error.status + ' Could not get ' + error.response.url)
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
logger.warn(error)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { parse } = require('node-html-parser')
|
|
4
|
+
const { decode } = require('html-entities')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const URL = require('url')
|
|
7
|
+
const chalk = require('chalk')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Generates an Algolia index:
|
|
11
|
+
*
|
|
12
|
+
* Iterates over the specified pages and creates the indexes.
|
|
13
|
+
*
|
|
14
|
+
* @memberof algolia-indexer
|
|
15
|
+
*
|
|
16
|
+
* @param {Object} playbook - The configuration object for Antora.
|
|
17
|
+
* @param {Object} contentCatalog - The Antora content catalog, with pages and metadata.
|
|
18
|
+
* @param {Object} [config={}] - Configuration options
|
|
19
|
+
* @param {Boolean} config.indexLatestOnly - If true, only index the latest version of any given page.
|
|
20
|
+
* @param {Object} config.logger - Logger to use
|
|
21
|
+
* @typedef {Object} SearchIndexData
|
|
22
|
+
* @returns {SearchIndexData} A data object that contains the Algolia index
|
|
23
|
+
*/
|
|
24
|
+
function generateIndex (playbook, contentCatalog, { indexLatestOnly = false, excludes = [], logger } = {}) {
|
|
25
|
+
if (!process.env.ALGOLIA_ADMIN_API_KEY || !process.env.ALGOLIA_APP_ID || !process.env.ALGOLIA_INDEX_NAME) return
|
|
26
|
+
if (!logger) logger = process.env.NODE_ENV === 'test' ? { info: () => undefined } : console
|
|
27
|
+
|
|
28
|
+
const algolia = {}
|
|
29
|
+
|
|
30
|
+
console.log(chalk.cyan('Indexing...'))
|
|
31
|
+
|
|
32
|
+
// Select indexable pages
|
|
33
|
+
const pages = contentCatalog.getPages((page) => {
|
|
34
|
+
if (!page.out || page.asciidoc?.attributes?.noindex != null) return
|
|
35
|
+
return {}
|
|
36
|
+
})
|
|
37
|
+
if (!pages.length) return {}
|
|
38
|
+
|
|
39
|
+
// Handle the site URL
|
|
40
|
+
let siteUrl = playbook.site.url
|
|
41
|
+
if (!siteUrl) {
|
|
42
|
+
siteUrl = ''
|
|
43
|
+
}
|
|
44
|
+
if (siteUrl.charAt(siteUrl.length - 1) === '/') {
|
|
45
|
+
siteUrl = siteUrl.substr(0, siteUrl.length - 1)
|
|
46
|
+
}
|
|
47
|
+
const urlPath = extractUrlPath(siteUrl)
|
|
48
|
+
|
|
49
|
+
const documents = {}
|
|
50
|
+
var algoliaCount = 0
|
|
51
|
+
|
|
52
|
+
for (var i = 0; i < pages.length; i++) {
|
|
53
|
+
const page = pages[i]
|
|
54
|
+
const root = parse(
|
|
55
|
+
page.contents,
|
|
56
|
+
{
|
|
57
|
+
blockTextElements: {
|
|
58
|
+
code: true,
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// skip pages marked as "noindex" for "robots"
|
|
64
|
+
const noindex = root.querySelector('meta[name=robots][content=noindex]')
|
|
65
|
+
if (noindex) {
|
|
66
|
+
continue
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Compute a flag identifying if the current page is in the
|
|
70
|
+
// "current" component version.
|
|
71
|
+
// When indexLatestOnly is set, we only index the current version.
|
|
72
|
+
const component = contentCatalog.getComponent(page.src.component)
|
|
73
|
+
const home = contentCatalog.getComponent('home')
|
|
74
|
+
const thisVersion = contentCatalog.getComponentVersion(component, page.src.version)
|
|
75
|
+
const latestVersion = component.latest
|
|
76
|
+
const isCurrent = thisVersion === latestVersion
|
|
77
|
+
|
|
78
|
+
if (indexLatestOnly && !isCurrent) continue
|
|
79
|
+
|
|
80
|
+
// capture the component name and version
|
|
81
|
+
const cname = component.name
|
|
82
|
+
const version = page.src.version
|
|
83
|
+
|
|
84
|
+
// handle the page keywords
|
|
85
|
+
const kw = root.querySelector('meta[name=keywords]')
|
|
86
|
+
var keywords = []
|
|
87
|
+
if (kw) {
|
|
88
|
+
keywords = kw.getAttribute('content')
|
|
89
|
+
keywords = keywords ? keywords.split(/,\s*/) : []
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// gather page breadcrumbs
|
|
93
|
+
const breadcrumbs = []
|
|
94
|
+
root.querySelectorAll('nav.breadcrumbs > ul > li a')
|
|
95
|
+
.forEach((elem) => {
|
|
96
|
+
var url = path.resolve(
|
|
97
|
+
path.join('/', page.out.dirname),
|
|
98
|
+
elem.getAttribute('href')
|
|
99
|
+
)
|
|
100
|
+
breadcrumbs.push({
|
|
101
|
+
u: url,
|
|
102
|
+
t: elem.text
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const images = {
|
|
107
|
+
'get started': 'get-started-icon.png',
|
|
108
|
+
'develop': 'develop-icon.png',
|
|
109
|
+
'deploy': 'deploy-icon.png',
|
|
110
|
+
'manage': 'manage-icon.png'
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
var image = {}
|
|
114
|
+
|
|
115
|
+
if (breadcrumbs.length > 1) {
|
|
116
|
+
const lowercaseBreadcrumb = breadcrumbs[1].t.toLowerCase()
|
|
117
|
+
for (let key in images) {
|
|
118
|
+
if (lowercaseBreadcrumb.includes(key)) {
|
|
119
|
+
image.src = `${home.url}_images/${images[key]}`
|
|
120
|
+
image.alt = key
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Start handling the article content
|
|
127
|
+
const article = root.querySelector('article.doc')
|
|
128
|
+
if (!article) {
|
|
129
|
+
logger.warn(`Page is not an article...skipping ${page.pub.url}`)
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// handle titles
|
|
134
|
+
const h1 = article.querySelector('h1')
|
|
135
|
+
if (!h1) {
|
|
136
|
+
logger.error(`No H1 in ${page.pub.url}`)
|
|
137
|
+
process.exit(1)
|
|
138
|
+
}
|
|
139
|
+
const documentTitle = h1.text
|
|
140
|
+
h1.remove()
|
|
141
|
+
|
|
142
|
+
const titles = []
|
|
143
|
+
article.querySelectorAll('h2,h3,h4,h5,h6').forEach((title) => {
|
|
144
|
+
var id = title.getAttribute('id')
|
|
145
|
+
if (id) {
|
|
146
|
+
titles.push({
|
|
147
|
+
t: title.text,
|
|
148
|
+
h: id,
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
title.remove()
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// exclude elements within the article that should not be indexed
|
|
155
|
+
excludes.forEach((excl) => {
|
|
156
|
+
if (!excl) return
|
|
157
|
+
article.querySelectorAll(excl).map((e) => e.remove())
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
var intro = article.querySelector('p');
|
|
161
|
+
// decode any HTML entities
|
|
162
|
+
intro = decode(intro.rawText);
|
|
163
|
+
|
|
164
|
+
// establish structure in the Algolia index
|
|
165
|
+
if (!(cname in algolia)) algolia[cname] = {}
|
|
166
|
+
if (!(version in algolia[cname])) algolia[cname][version] = []
|
|
167
|
+
|
|
168
|
+
// Handle the article text
|
|
169
|
+
var text = decode(article.text)
|
|
170
|
+
text = text.replace(/\n/g, ' ')
|
|
171
|
+
.replace(/\r/g, ' ')
|
|
172
|
+
.replace(/\s+/g, ' ')
|
|
173
|
+
.trim()
|
|
174
|
+
if (text.length > 4000) text = text.substr(0, 4000)
|
|
175
|
+
|
|
176
|
+
var tag = `${component.title}-${version}`
|
|
177
|
+
|
|
178
|
+
const indexItem = {
|
|
179
|
+
title: documentTitle,
|
|
180
|
+
product: component.title,
|
|
181
|
+
version: version,
|
|
182
|
+
image: image? image: '',
|
|
183
|
+
text: text,
|
|
184
|
+
breadcrumbs: breadcrumbs,
|
|
185
|
+
intro: intro,
|
|
186
|
+
objectID: urlPath + page.pub.url,
|
|
187
|
+
titles: titles,
|
|
188
|
+
keywords: keywords,
|
|
189
|
+
_tags: [tag]
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
algolia[cname][version].push(indexItem)
|
|
193
|
+
algoliaCount++
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return algolia
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Extract the path from a URL
|
|
200
|
+
function extractUrlPath (url) {
|
|
201
|
+
if (url) {
|
|
202
|
+
if (url.charAt() === '/') return url
|
|
203
|
+
const urlPath = URL.parse(url).pathname
|
|
204
|
+
return urlPath === '/' ? '' : urlPath
|
|
205
|
+
}
|
|
206
|
+
return ''
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = generateIndex
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const generateIndex = require('./generate-index')
|
|
4
|
+
const chalk = require('chalk')
|
|
5
|
+
const algoliasearch = require('algoliasearch');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Algolia indexing for an Antora documentation site.
|
|
11
|
+
*
|
|
12
|
+
* @module antora-algolia-indexer
|
|
13
|
+
*/
|
|
14
|
+
function register ({
|
|
15
|
+
config: {
|
|
16
|
+
indexLatestOnly,
|
|
17
|
+
excludes,
|
|
18
|
+
...unknownOptions
|
|
19
|
+
}
|
|
20
|
+
}) {
|
|
21
|
+
const logger = this.getLogger('algolia-indexer-extension')
|
|
22
|
+
|
|
23
|
+
if (!process.env.ALGOLIA_ADMIN_API_KEY || !process.env.ALGOLIA_APP_ID || !process.env.ALGOLIA_INDEX_NAME) return
|
|
24
|
+
|
|
25
|
+
var client;
|
|
26
|
+
var index;
|
|
27
|
+
|
|
28
|
+
// Connect and authenticate with Algolia
|
|
29
|
+
client = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_API_KEY);
|
|
30
|
+
|
|
31
|
+
// Create a new index and add a record
|
|
32
|
+
index = client.initIndex(process.env.ALGOLIA_INDEX_NAME);
|
|
33
|
+
|
|
34
|
+
if (Object.keys(unknownOptions).length) {
|
|
35
|
+
const keys = Object.keys(unknownOptions)
|
|
36
|
+
throw new Error(`Unrecognized option${keys.length > 1 ? 's' : ''} specified: ${keys.join(', ')}`)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.on('beforePublish', ({ playbook, siteCatalog, contentCatalog }) => {
|
|
40
|
+
const algolia = generateIndex(playbook, contentCatalog, { indexLatestOnly, excludes, logger })
|
|
41
|
+
// write the Algolia indexes
|
|
42
|
+
var algoliaCount = 0
|
|
43
|
+
Object.keys(algolia).forEach((c) => {
|
|
44
|
+
Object.keys(algolia[c]).forEach((v) => {
|
|
45
|
+
algoliaCount += algolia[c][v].length
|
|
46
|
+
// Save all records to the index
|
|
47
|
+
index.saveObjects(algolia[c][v]).wait();
|
|
48
|
+
siteCatalog.addFile({
|
|
49
|
+
mediaType: 'application/json',
|
|
50
|
+
contents: Buffer.from(
|
|
51
|
+
JSON.stringify(algolia[c][v])
|
|
52
|
+
),
|
|
53
|
+
src: { stem: `algolia-${c}` },
|
|
54
|
+
out: { path: `algolia-${c}-${v}.json` },
|
|
55
|
+
pub: { url: `/algolia-${c}-${v}.json`, rootPath: '' },
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
index.setSettings({
|
|
60
|
+
attributesForFaceting: [
|
|
61
|
+
'version',
|
|
62
|
+
'product'
|
|
63
|
+
]
|
|
64
|
+
})
|
|
65
|
+
console.log(`${chalk.bold(algoliaCount)} Algolia index entries created`)
|
|
66
|
+
// Get and print the count of all records in the index
|
|
67
|
+
let recordCount = 0;
|
|
68
|
+
index
|
|
69
|
+
.browseObjects({
|
|
70
|
+
query: '', // for all records
|
|
71
|
+
batch: batch => {
|
|
72
|
+
recordCount += batch.length;
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
.then(() => {
|
|
76
|
+
console.log('Total Records:', recordCount);
|
|
77
|
+
})
|
|
78
|
+
.catch(err => console.log(err));
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { generateIndex, register }
|
|
83
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { PassThrough } = require('stream')
|
|
4
|
+
|
|
5
|
+
// adapted from https://github.com/jpommerening/node-lazystream/blob/master/lib/lazystream.js | license: MIT
|
|
6
|
+
class LazyReadable extends PassThrough {
|
|
7
|
+
constructor (fn, options) {
|
|
8
|
+
super(options)
|
|
9
|
+
this._read = function () {
|
|
10
|
+
delete this._read // restores original method
|
|
11
|
+
fn.call(this, options).on('error', this.emit.bind(this, 'error')).pipe(this)
|
|
12
|
+
return this._read.apply(this, arguments)
|
|
13
|
+
}
|
|
14
|
+
this.emit('readable')
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = LazyReadable
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { PassThrough } = require('stream')
|
|
5
|
+
|
|
6
|
+
class MultiFileReadStream extends PassThrough {
|
|
7
|
+
constructor (paths) {
|
|
8
|
+
super()
|
|
9
|
+
;(this.queue = this.createQueue(paths)).next()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
* createQueue (paths) {
|
|
13
|
+
for (const path of paths) {
|
|
14
|
+
fs.createReadStream(path)
|
|
15
|
+
.once('error', (err) => this.destroy(err))
|
|
16
|
+
.once('end', () => this.queue.next())
|
|
17
|
+
.pipe(this, { end: false })
|
|
18
|
+
yield
|
|
19
|
+
}
|
|
20
|
+
this.push(null)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = MultiFileReadStream
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module.exports.register = function ({ config }) {
|
|
2
|
+
const { family = 'attachment' } = config;
|
|
3
|
+
const logger = this.getLogger('replace-attributes-in-attachments-extension');
|
|
4
|
+
|
|
5
|
+
const sanitizeAttributeValue = (value) => String(value).replace("@", "");
|
|
6
|
+
|
|
7
|
+
this.on('documentsConverted', ({playbook, contentCatalog}) => {
|
|
8
|
+
for (const { versions } of contentCatalog.getComponents()) {
|
|
9
|
+
for (const { name: component, version } of versions) {
|
|
10
|
+
if (component !== 'ROOT') continue;
|
|
11
|
+
const attachments = contentCatalog.findBy({ component, version, family });
|
|
12
|
+
for (const attachment of attachments) {
|
|
13
|
+
let contentString = String.fromCharCode(...attachment['_contents']);
|
|
14
|
+
const attributes = attachment.src.origin.descriptor.asciidoc.attributes;
|
|
15
|
+
const mergedAttributes = {
|
|
16
|
+
...playbook.asciidoc.attributes,
|
|
17
|
+
...attributes
|
|
18
|
+
};
|
|
19
|
+
for (const key in mergedAttributes) {
|
|
20
|
+
const placeholder = "{" + key + "}";
|
|
21
|
+
const sanitizedValue = sanitizeAttributeValue(mergedAttributes[key]);
|
|
22
|
+
contentString = contentString.replace(new RegExp(placeholder, 'g'), sanitizedValue);
|
|
23
|
+
}
|
|
24
|
+
attachment['_contents'] = Buffer.from(contentString, "utf-8");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module.exports.register = function ({ config }) {
|
|
2
|
+
const { addToNavigation, unlistedPagesHeading = 'Unlisted Pages' } = config
|
|
3
|
+
const logger = this.getLogger('unlisted-pages-extension')
|
|
4
|
+
this
|
|
5
|
+
.on('navigationBuilt', ({ contentCatalog }) => {
|
|
6
|
+
contentCatalog.getComponents().forEach(({ versions }) => {
|
|
7
|
+
versions.forEach(({ name: component, version, navigation: nav, url: defaultUrl }) => {
|
|
8
|
+
if (component === 'api') return;
|
|
9
|
+
const navEntriesByUrl = getNavEntriesByUrl(nav)
|
|
10
|
+
const unlistedPages = contentCatalog
|
|
11
|
+
.findBy({ component, version, family: 'page' })
|
|
12
|
+
.filter((page) => page.out)
|
|
13
|
+
.reduce((collector, page) => {
|
|
14
|
+
if ((page.pub.url in navEntriesByUrl) || page.pub.url === defaultUrl) return collector
|
|
15
|
+
logger.warn({ file: page.src, source: page.src.origin }, 'detected unlisted page')
|
|
16
|
+
return collector.concat(page)
|
|
17
|
+
}, [])
|
|
18
|
+
if (unlistedPages.length && addToNavigation) {
|
|
19
|
+
nav.push({
|
|
20
|
+
content: unlistedPagesHeading,
|
|
21
|
+
items: unlistedPages.map((page) => {
|
|
22
|
+
return { content: page.asciidoc.navtitle, url: page.pub.url, urlType: 'internal' }
|
|
23
|
+
}),
|
|
24
|
+
root: true,
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getNavEntriesByUrl (items = [], accum = {}) {
|
|
33
|
+
items.forEach((item) => {
|
|
34
|
+
if (item.urlType === 'internal') accum[item.url.split('#')[0]] = item
|
|
35
|
+
getNavEntriesByUrl(item.items, accum)
|
|
36
|
+
})
|
|
37
|
+
return accum
|
|
38
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Fetch the latest release version from GitHub
|
|
2
|
+
const { Octokit } = require("@octokit/rest");
|
|
3
|
+
const { retry } = require("@octokit/plugin-retry");
|
|
4
|
+
const OctokitWithRetries = Octokit.plugin(retry);
|
|
5
|
+
const owner = 'redpanda-data';
|
|
6
|
+
const repo = 'console';
|
|
7
|
+
|
|
8
|
+
let githubOptions = {
|
|
9
|
+
userAgent: 'Redpanda Docs',
|
|
10
|
+
baseUrl: 'https://api.github.com',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (process.env.REDPANDA_GITHUB_TOKEN) {
|
|
14
|
+
githubOptions.auth = process.env.REDPANDA_GITHUB_TOKEN;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const github = new OctokitWithRetries(githubOptions);
|
|
18
|
+
|
|
19
|
+
var latestConsoleReleaseVersion;
|
|
20
|
+
|
|
21
|
+
module.exports = async () => {
|
|
22
|
+
await github.rest.repos.getLatestRelease({
|
|
23
|
+
owner,
|
|
24
|
+
repo,
|
|
25
|
+
}).then((release => {
|
|
26
|
+
const tag = release.data.tag_name;
|
|
27
|
+
latestConsoleReleaseVersion = tag.replace('v','');
|
|
28
|
+
})).catch((error => {
|
|
29
|
+
console.error(error)
|
|
30
|
+
return null
|
|
31
|
+
}))
|
|
32
|
+
return latestConsoleReleaseVersion;
|
|
33
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Fetch the latest release version from GitHub
|
|
2
|
+
const { Octokit } = require("@octokit/rest");
|
|
3
|
+
const { retry } = require("@octokit/plugin-retry");
|
|
4
|
+
const OctokitWithRetries = Octokit.plugin(retry);
|
|
5
|
+
const owner = 'redpanda-data';
|
|
6
|
+
const repo = 'redpanda';
|
|
7
|
+
|
|
8
|
+
let githubOptions = {
|
|
9
|
+
userAgent: 'Redpanda Docs',
|
|
10
|
+
baseUrl: 'https://api.github.com',
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
if (process.env.REDPANDA_GITHUB_TOKEN) {
|
|
14
|
+
githubOptions.auth = process.env.REDPANDA_GITHUB_TOKEN;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const github = new OctokitWithRetries(githubOptions);
|
|
18
|
+
|
|
19
|
+
var latestRedpandaReleaseVersion;
|
|
20
|
+
var latestRedpandaReleaseCommitHash;
|
|
21
|
+
|
|
22
|
+
module.exports = async () => {
|
|
23
|
+
await github.rest.repos.getLatestRelease({
|
|
24
|
+
owner,
|
|
25
|
+
repo,
|
|
26
|
+
}).then(async function(release) {
|
|
27
|
+
const tag = release.data.tag_name;
|
|
28
|
+
latestRedpandaReleaseVersion = tag.replace('v','');
|
|
29
|
+
await github.rest.git.getRef({
|
|
30
|
+
owner,
|
|
31
|
+
repo,
|
|
32
|
+
ref: `/tags/${tag}`,
|
|
33
|
+
}).then(async function(tagRef) {
|
|
34
|
+
const releaseSha = tagRef.data.object.sha;
|
|
35
|
+
await github.rest.git.getTag({
|
|
36
|
+
owner,
|
|
37
|
+
repo,
|
|
38
|
+
tag_sha: releaseSha,
|
|
39
|
+
}).then((tag => latestRedpandaReleaseCommitHash = tag.data.object.sha.substring(0, 7)))
|
|
40
|
+
})
|
|
41
|
+
}).catch((error => {
|
|
42
|
+
console.error(error)
|
|
43
|
+
return null
|
|
44
|
+
}))
|
|
45
|
+
return [latestRedpandaReleaseVersion, latestRedpandaReleaseCommitHash];
|
|
46
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/* Example:
|
|
2
|
+
antora:
|
|
3
|
+
extensions:
|
|
4
|
+
- require: ./extensions/setLatestVersion.js
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const GetLatestRedpandaVersion = require('./getLatestRedpandaVersion');
|
|
8
|
+
const GetLatestConsoleVersion = require('./getLatestConsoleVersion');
|
|
9
|
+
const chalk = require('chalk')
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
module.exports.register = function ({ config }) {
|
|
13
|
+
const logger = this.getLogger('set-latest-version-extension')
|
|
14
|
+
if (!process.env.REDPANDA_GITHUB_TOKEN) {
|
|
15
|
+
logger.warn('REDPANDA_GITHUB_TOKEN environment variable not set. Attempting unauthenticated request.');
|
|
16
|
+
}
|
|
17
|
+
this
|
|
18
|
+
.on('playbookBuilt', async ({ playbook }) => {
|
|
19
|
+
try {
|
|
20
|
+
const LatestConsoleVersion = await GetLatestConsoleVersion();
|
|
21
|
+
if (!playbook.asciidoc) {
|
|
22
|
+
playbook.asciidoc = {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (!playbook.asciidoc.attributes) {
|
|
26
|
+
playbook.asciidoc.attributes = {};
|
|
27
|
+
}
|
|
28
|
+
playbook.asciidoc.attributes['latest-console-version'] = LatestConsoleVersion
|
|
29
|
+
console.log(`${chalk.green('Set Redpanda Console version to')} ${chalk.bold(LatestConsoleVersion)}`);
|
|
30
|
+
} catch(error) {
|
|
31
|
+
logger.warn(error)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
.on('contentClassified', async ({ contentCatalog }) => {
|
|
35
|
+
try {
|
|
36
|
+
const LatestRedpandaVersion = await GetLatestRedpandaVersion();
|
|
37
|
+
const components = await contentCatalog.getComponents();
|
|
38
|
+
for (let i = 0; i < components.length; i++) {
|
|
39
|
+
let component = components[i];
|
|
40
|
+
if (LatestRedpandaVersion.length !== 2) logger.warn('Could not find the latest Redpanda versions - using defaults');
|
|
41
|
+
if (component.name === 'ROOT') {
|
|
42
|
+
|
|
43
|
+
if (!component.latest.asciidoc) {
|
|
44
|
+
component.latest.asciidoc = {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!component.latest.asciidoc.attributes) {
|
|
48
|
+
component.latest.asciidoc.attributes = {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
component.latest.asciidoc.attributes['full-version'] = `${LatestRedpandaVersion[0]}`;
|
|
52
|
+
component.latest.asciidoc.attributes['latest-release-commit'] = `${LatestRedpandaVersion[1]}`;
|
|
53
|
+
console.log(`${chalk.green('Set Redpanda version to')} ${chalk.bold(LatestRedpandaVersion[0])} ${chalk.bold(LatestRedpandaVersion[1])}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} catch(error) {
|
|
57
|
+
logger.warn(error)
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* Example use in a page
|
|
2
|
+
*
|
|
3
|
+
* config_ref:myConfigValue,true,tunable-properties[]
|
|
4
|
+
*
|
|
5
|
+
* Example use in playbook
|
|
6
|
+
*
|
|
7
|
+
* asciidoc:
|
|
8
|
+
extensions:
|
|
9
|
+
- './macros/config-ref.js'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const buildConfigReference = ({ configRef, isKubernetes, isLink, path }) => {
|
|
13
|
+
let ref = '';
|
|
14
|
+
if (isLink) {
|
|
15
|
+
if (isKubernetes) {
|
|
16
|
+
ref = `xref:reference:${path}.adoc#${configRef}[storage.tieredConfig.${configRef}]`;
|
|
17
|
+
} else {
|
|
18
|
+
ref = `xref:reference:${path}.adoc#${configRef}[${configRef}]`;
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
ref = isKubernetes ? `storage.tieredConfig.${configRef}` : `${configRef}`;
|
|
22
|
+
}
|
|
23
|
+
return ref;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function inlineConfigMacro(context) {
|
|
27
|
+
return function () {
|
|
28
|
+
this.process((parent, target, attrs) => {
|
|
29
|
+
const [configRef, isLink, path] = target.split(',');
|
|
30
|
+
const isKubernetes = parent.getDocument().getAttributes()['env-kubernetes'] !== undefined;
|
|
31
|
+
const content = buildConfigReference({ configRef, isKubernetes, isLink: isLink === 'true', path });
|
|
32
|
+
return this.createInline(parent, 'quoted', content);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function register (registry, context) {
|
|
38
|
+
registry.inlineMacro('config_ref', inlineConfigMacro(context));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports.register = register;
|
|
42
|
+
|
|
43
|
+
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const $glossaryContexts = Symbol('$glossaryContexts')
|
|
4
|
+
const { posix: path } = require('path')
|
|
5
|
+
const chalk = require('chalk')
|
|
6
|
+
|
|
7
|
+
module.exports.register = function (registry, config = {}) {
|
|
8
|
+
|
|
9
|
+
const vfs = adaptVfs()
|
|
10
|
+
|
|
11
|
+
function adaptVfs () {
|
|
12
|
+
function getKey (src) {
|
|
13
|
+
return `${src.version}@${src.component}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const contentCatalog = config.contentCatalog
|
|
17
|
+
if (!contentCatalog[$glossaryContexts]) contentCatalog[$glossaryContexts] = {}
|
|
18
|
+
const glossaryContexts = contentCatalog[$glossaryContexts]
|
|
19
|
+
const key = getKey(config.file.src)
|
|
20
|
+
if (!glossaryContexts[key]) {
|
|
21
|
+
glossaryContexts[key] = {
|
|
22
|
+
gloss: [],
|
|
23
|
+
self: undefined,
|
|
24
|
+
dlist: undefined,
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const context = glossaryContexts[key]
|
|
28
|
+
return {
|
|
29
|
+
getContext: () => context,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//characters to replace by '_' in generated idprefix
|
|
34
|
+
const IDRX = /[/ _.-]+/g
|
|
35
|
+
|
|
36
|
+
function termId (term) {
|
|
37
|
+
return '_glossterm_' + term.toLowerCase().replace(IDRX, '_')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function dlistItem (context, term, def) {
|
|
41
|
+
const id = termId(term)
|
|
42
|
+
term = `anchor:${id}[${term}]${term}`
|
|
43
|
+
const termItem = context.self.createListItem(context.dlist, term)
|
|
44
|
+
const defItem = context.self.createListItem(context.dlist, def)
|
|
45
|
+
return [[termItem], defItem]
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function glossaryBlockMacro () {
|
|
49
|
+
return function () {
|
|
50
|
+
const self = this
|
|
51
|
+
self.named('glossary')
|
|
52
|
+
self.$option('format', 'short') //no target between glossary:: and [params]
|
|
53
|
+
// self.positionalAttributes(['name', 'parameters'])
|
|
54
|
+
self.process(function (parent, target, attributes) {
|
|
55
|
+
const context = vfs.getContext()
|
|
56
|
+
const dlist = self.createList(parent, 'dlist')
|
|
57
|
+
context.self = self
|
|
58
|
+
context.dlist = dlist
|
|
59
|
+
context.gloss
|
|
60
|
+
.forEach(({ term, def }) => {
|
|
61
|
+
dlist.blocks.push(dlistItem(context, term, def))
|
|
62
|
+
})
|
|
63
|
+
return dlist
|
|
64
|
+
})
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const TRX = /(<[a-z]+)([^>]*>.*)/
|
|
69
|
+
|
|
70
|
+
function glossaryInlineMacro () {
|
|
71
|
+
return function () {
|
|
72
|
+
const self = this
|
|
73
|
+
self.named('glossterm')
|
|
74
|
+
//Specifying the regexp allows spaces in the term.
|
|
75
|
+
self.$option('regexp', /glossterm:([^[]+)\[(|.*?[^\\])\]/)
|
|
76
|
+
self.positionalAttributes(['definition'])
|
|
77
|
+
self.process(function (parent, target, attributes) {
|
|
78
|
+
const term = attributes.term || target
|
|
79
|
+
const document = parent.document
|
|
80
|
+
const context = vfs.getContext()
|
|
81
|
+
// See if a predefined list of terms is available
|
|
82
|
+
const globalTerms = document.getAttribute("terms") || [];
|
|
83
|
+
const localTerms = document.getAttribute("local-terms") || [];
|
|
84
|
+
let mergedTerms;
|
|
85
|
+
if(globalTerms.length > 0 && localTerms.length > 0){
|
|
86
|
+
// Convert globalTerms to a Map for fast look up
|
|
87
|
+
let globalTermsMap = new Map(globalTerms.map(i => [i.term, i]));
|
|
88
|
+
|
|
89
|
+
// Create a new array based on localTerms, but replace items if they exist in globalTerms
|
|
90
|
+
mergedTerms = localTerms.map(item => globalTermsMap.has(item.term) ? globalTermsMap.get(item.term) : item);
|
|
91
|
+
|
|
92
|
+
// Update localTerms with the mergedTerms
|
|
93
|
+
document.setAttribute("terms", mergedTerms);
|
|
94
|
+
console.log(chalk.green('Merged global terms with local terms'))
|
|
95
|
+
}
|
|
96
|
+
const termData = (mergedTerms || []).find((t) => t.term === term) || {};
|
|
97
|
+
const customLink = termData.link;
|
|
98
|
+
var tooltip = document.getAttribute('glossary-tooltip')
|
|
99
|
+
if (tooltip === 'true') tooltip = 'data-glossary-tooltip'
|
|
100
|
+
if (tooltip && tooltip !== 'title' && !tooltip.startsWith('data-')) {
|
|
101
|
+
console.log(`glossary-tooltip attribute '${tooltip}' must be 'true', 'title', or start with 'data-`)
|
|
102
|
+
tooltip = undefined
|
|
103
|
+
}
|
|
104
|
+
const logTerms = document.hasAttribute('glossary-log-terms')
|
|
105
|
+
var definition = termData.definition || attributes.definition
|
|
106
|
+
if (definition) {
|
|
107
|
+
logTerms && console.log(`${term}:: ${definition}`)
|
|
108
|
+
addItem(context, term, definition)
|
|
109
|
+
} else if (tooltip) {
|
|
110
|
+
const index = context.gloss.findIndex((candidate) => candidate.term === term)
|
|
111
|
+
definition = ~index ? context.gloss[index].def : `${term} not yet defined`
|
|
112
|
+
}
|
|
113
|
+
const links = document.getAttribute('glossary-links', 'true') === 'true'
|
|
114
|
+
var glossaryPage = document.getAttribute('glossary-page', '')
|
|
115
|
+
if (glossaryPage.endsWith('.adoc')) {
|
|
116
|
+
const page = config.contentCatalog.resolvePage(glossaryPage, config.file.src)
|
|
117
|
+
const relativizedPath = path.relative(path.dirname(config.file.pub.url), page.pub.url)
|
|
118
|
+
const prefix = attributes.prefix
|
|
119
|
+
glossaryPage = prefix ? [prefix, relativizedPath].join('/') : relativizedPath
|
|
120
|
+
}
|
|
121
|
+
const glossaryTermRole = document.getAttribute('glossary-term-role', 'glossary-term')
|
|
122
|
+
const attrs = glossaryTermRole ? { role: glossaryTermRole } : {}
|
|
123
|
+
const inline = links
|
|
124
|
+
? customLink
|
|
125
|
+
? self.createInline(parent, 'anchor', target, { type: 'link', target: customLink, attributes: attrs })
|
|
126
|
+
: self.createInline(parent, 'anchor', target, { type: 'xref', target: `${glossaryPage}#${termId(term)}`, reftext: target, attributes: attrs })
|
|
127
|
+
: self.createInline(parent, 'quoted', target, { attributes: attrs })
|
|
128
|
+
if (tooltip) {
|
|
129
|
+
const a = inline.convert()
|
|
130
|
+
const matches = a.match(TRX)
|
|
131
|
+
if (matches) {
|
|
132
|
+
return self.createInline(parent, 'quoted', `${matches[1]} ${tooltip}="${definition}"${matches[2]}`)
|
|
133
|
+
} else {
|
|
134
|
+
return self.createInline(parent, 'quoted', `<span ${tooltip}="${definition}">${a}</span>`)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return inline
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function addItem (context, term, def) {
|
|
143
|
+
let i = 0
|
|
144
|
+
let comp = -1
|
|
145
|
+
for (; i < context.gloss.length && ((comp = term.localeCompare(context.gloss[i].term)) > 0); i++) {
|
|
146
|
+
}
|
|
147
|
+
if (comp < 0) {
|
|
148
|
+
context.gloss.splice(i, 0, { term, def })
|
|
149
|
+
if (context.self && context.dlist) {
|
|
150
|
+
context.dlist.blocks.splice(i, 0, dlistItem(context, term, def))
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
console.log(`duplicate glossary term ${term}`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function doRegister (registry) {
|
|
158
|
+
if (typeof registry.blockMacro === 'function') {
|
|
159
|
+
registry.blockMacro(glossaryBlockMacro())
|
|
160
|
+
} else {
|
|
161
|
+
console.warn('no \'blockMacro\' method on alleged registry')
|
|
162
|
+
}
|
|
163
|
+
if (typeof registry.inlineMacro === 'function') {
|
|
164
|
+
registry.inlineMacro(glossaryInlineMacro())
|
|
165
|
+
} else {
|
|
166
|
+
console.warn('no \'inlineMacro\' method on alleged registry')
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (typeof registry.register === 'function') {
|
|
171
|
+
registry.register(function () {
|
|
172
|
+
//Capture the global registry so processors can register more extensions.
|
|
173
|
+
registry = this
|
|
174
|
+
doRegister(registry)
|
|
175
|
+
})
|
|
176
|
+
} else {
|
|
177
|
+
doRegister(registry)
|
|
178
|
+
}
|
|
179
|
+
return registry
|
|
180
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* Example use in a page
|
|
2
|
+
*
|
|
3
|
+
* helm_ref:myConfigValue[]
|
|
4
|
+
*
|
|
5
|
+
* Example use in playbook
|
|
6
|
+
*
|
|
7
|
+
* asciidoc:
|
|
8
|
+
extensions:
|
|
9
|
+
- './macros/helm-ref.js'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const buildConfigReference = ({ helmRef }) => {
|
|
13
|
+
let ref = '';
|
|
14
|
+
ref = helmRef ? `For default values and documentation for configuration options, see the https://artifacthub.io/packages/helm/redpanda-data/redpanda?modal=values&path=${helmRef}[values.yaml] file.` : `For default values and documentation for configuration options, see the https://artifacthub.io/packages/helm/redpanda-data/redpanda?modal=values[values.yaml] file.`;
|
|
15
|
+
return ref;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function inlineConfigMacro(context) {
|
|
19
|
+
return function () {
|
|
20
|
+
this.process((parent, target, attrs) => {
|
|
21
|
+
const [helmRef] = target.split(',');
|
|
22
|
+
const content = buildConfigReference({ helmRef });
|
|
23
|
+
return this.createInline(parent, 'quoted', content);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function register (registry, context) {
|
|
29
|
+
registry.inlineMacro('helm_ref', inlineConfigMacro(context));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports.register = register;
|
|
33
|
+
|
|
34
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@redpanda-data/docs-extensions-and-macros",
|
|
3
|
+
"version": "2.5.0",
|
|
4
|
+
"description": "Antora extensions and macros developed for Redpanda documentation.",
|
|
5
|
+
"keywords": ["antora", "extension", "macro", "documentation", "redpanda"],
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Redpanda Docs Team"
|
|
8
|
+
},
|
|
9
|
+
"contributors": [
|
|
10
|
+
{
|
|
11
|
+
"name": "JakeSCahill",
|
|
12
|
+
"email": "jake@redpanda.com"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"license": "ISC",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/redpanda-data/docs-extensions-and-macros"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@antora/site-generator": "3.1.2",
|
|
22
|
+
"@octokit/plugin-retry": "^4.1.3",
|
|
23
|
+
"@octokit/rest": "^19.0.7",
|
|
24
|
+
"algoliasearch": "^4.17.0",
|
|
25
|
+
"chalk": "4.1.2",
|
|
26
|
+
"js-yaml": "^4.1.0",
|
|
27
|
+
"gulp": "^4.0.2",
|
|
28
|
+
"gulp-connect": "^5.7.0",
|
|
29
|
+
"html-entities": "2.3",
|
|
30
|
+
"node-html-parser": "5.4.2-0"
|
|
31
|
+
}
|
|
32
|
+
}
|