@redpanda-data/docs-extensions-and-macros 3.2.5 → 3.2.7

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 CHANGED
@@ -180,18 +180,9 @@ antora:
180
180
  - require: '@redpanda-data/docs-extensions-and-macros/extensions/add-global-attributes'
181
181
  ```
182
182
 
183
- === Modify redirects file
183
+ === Produce redirects (customization of core Antora)
184
184
 
185
- This extension removes redundant redirects from the Netlify redirects file. The need for this extension arises from an issue where the use of the `indexify` feature in Antora, combined with page aliases, can inadvertently create redirect loops or redundant redirects where the source and target URLs are identical.
186
-
187
- The issue is https://antora.zulipchat.com/#narrow/stream/282400-users/topic/Redirect.20Loop.20Issue.20with.20Page.20Renaming.20and.20Indexify/near/433691700[recognized as a bug] within Antora's redirect producer, which does not currently
188
- check if the source and target URLs are the same before creating a redirect.
189
-
190
- The purpose of this script is to scan the `_redirects` file and remove any entries that point
191
- a URL to itself, which not only prevents redirect loops but also optimizes the redirect process
192
- by eliminating unnecessary entries. This cleanup helps ensure that the redirects file only contains valid and useful redirection rules.
193
-
194
- By integrating this script into the Antora pipeline, we ensure that each build's output is optimized and free from potential issues related to improper redirects, enhancing both site performance and user experience.
185
+ This extension replaces the default https://gitlab.com/antora/antora/-/tree/v3.1.x/packages/redirect-producer[`produceRedirects()` function] in Antora to handle redirect loops caused by https://docs.antora.org/antora/latest/page/page-aliases/[page aliases]. Normally, page aliases in Antora are used to resolve outdated links without causing issues. However, with https://docs.antora.org/antora/latest/playbook/urls-html-extension-style/#html-extension-style-key[`indexify`], the same URL may inadvertently be used for both the source and target of a redirect, leading to loops. This problem is https://antora.zulipchat.com/#narrow/stream/282400-users/topic/Redirect.20Loop.20Issue.20with.20Page.20Renaming.20and.20Indexify/near/433691700[recognized as a bug] in core Antora. For example, creating a page alias for `modules/manage/security/authorization.adoc` to point to `modules/manage/security/authorization/index.adoc' can lead to a redirect loop where `manage/security/authorization/` points to `manage/security/authorization/`. Furthermore, omitting the alias would lead to `xref not found` errors because Antora relies on the alias to resolve the old xrefs. This extension is necessary until such behaviors are natively supported or fixed in Antora core.
195
186
 
196
187
  ==== Registration example
197
188
 
@@ -0,0 +1,173 @@
1
+ 'use strict';
2
+
3
+ const File = require('vinyl')
4
+ const { posix: path } = require('path')
5
+
6
+ const ENCODED_SPACE_RX = /%20/g
7
+
8
+ module.exports.register = function () {
9
+ this.once('contextStarted', () => {
10
+ const { publishFiles: produceRedirectsDelegate } = this.getFunctions()
11
+ this.replaceFunctions({
12
+ produceRedirects (playbook, aliases) {
13
+ if ('findBy' in aliases) aliases = aliases.findBy({ family: 'alias' }) // @deprecated remove in Antora 4
14
+ if (!aliases.length) return []
15
+ let siteUrl = playbook.site.url
16
+ if (siteUrl) siteUrl = stripTrailingSlash(siteUrl, '')
17
+ const directoryRedirects = (playbook.urls.htmlExtensionStyle || 'default') !== 'default'
18
+ switch (playbook.urls.redirectFacility) {
19
+ case 'gitlab':
20
+ return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects, false)
21
+ case 'httpd':
22
+ return createHttpdHtaccess(aliases, extractUrlPath(siteUrl), directoryRedirects)
23
+ case 'netlify':
24
+ return createNetlifyRedirects(aliases, extractUrlPath(siteUrl), !directoryRedirects)
25
+ case 'nginx':
26
+ return createNginxRewriteConf(aliases, extractUrlPath(siteUrl))
27
+ case 'static':
28
+ return populateStaticRedirectFiles(
29
+ aliases.filter((it) => it.out),
30
+ siteUrl
31
+ )
32
+ default:
33
+ return unpublish(aliases)
34
+ }
35
+ return produceRedirectsDelegate.call(this, playbook, aliases)
36
+ }
37
+ })
38
+ })
39
+ }
40
+
41
+ function createStaticRedirectContents (file, siteUrl) {
42
+ const targetUrl = file.rel.pub.url
43
+ let linkTag
44
+ let to = targetUrl.charAt() === '/' ? computeRelativeUrlPath(file.pub.url, targetUrl) : undefined
45
+ let toText = to
46
+ if (to) {
47
+ if (siteUrl && siteUrl.charAt() !== '/') {
48
+ linkTag = `<link rel="canonical" href="${(toText = siteUrl + targetUrl)}">\n`
49
+ }
50
+ } else {
51
+ linkTag = `<link rel="canonical" href="${(toText = to = targetUrl)}">\n`
52
+ }
53
+ return `<!DOCTYPE html>
54
+ <meta charset="utf-8">
55
+ ${linkTag || ''}<script>location="${to}"</script>
56
+ <meta http-equiv="refresh" content="0; url=${to}">
57
+ <meta name="robots" content="noindex">
58
+ <title>Redirect Notice</title>
59
+ <h1>Redirect Notice</h1>
60
+ <p>The page you requested has been relocated to <a href="${to}">${toText}</a>.</p>`
61
+ }
62
+
63
+ function extractUrlPath (url) {
64
+ if (url) {
65
+ if (url.charAt() === '/') return url
66
+ const urlPath = new URL(url).pathname
67
+ return urlPath === '/' ? '' : urlPath
68
+ } else {
69
+ return ''
70
+ }
71
+ }
72
+
73
+ function createHttpdHtaccess (files, urlPath, directoryRedirects = false) {
74
+ const rules = files.reduce((accum, file) => {
75
+ if (isRedirectLoop(file)) return accum
76
+ delete file.out
77
+ let fromUrl = file.pub.url
78
+ fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
79
+ let toUrl = file.rel.pub.url
80
+ toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
81
+ // see https://httpd.apache.org/docs/current/en/mod/mod_alias.html#redirect
82
+ // NOTE: redirect rule for directory prefix does not require trailing slash
83
+ if (file.pub.splat) {
84
+ accum.push(`Redirect 302 ${fromUrl} ${stripTrailingSlash(toUrl)}`)
85
+ } else if (directoryRedirects) {
86
+ accum.push(`RedirectMatch 301 ^${regexpEscape(fromUrl)}$ ${stripTrailingSlash(toUrl)}`)
87
+ } else {
88
+ accum.push(`Redirect 301 ${fromUrl} ${toUrl}`)
89
+ }
90
+ return accum
91
+ }, [])
92
+ return [new File({ contents: Buffer.from(rules.join('\n') + '\n'), out: { path: '.htaccess' } })]
93
+ }
94
+
95
+ // NOTE: a trailing slash on the pathname will be ignored
96
+ // see https://docs.netlify.com/routing/redirects/redirect-options/#trailing-slash
97
+ // however, we keep it when generating the rules for clarity
98
+ function createNetlifyRedirects (files, urlPath, addDirectoryRedirects = false, useForceFlag = true) {
99
+ const rules = files.reduce((accum, file) => {
100
+ if (isRedirectLoop(file)) return accum
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
+ if (isRedirectLoop(file)) return ''
121
+ delete file.out
122
+ let fromUrl = file.pub.url
123
+ fromUrl = ~fromUrl.indexOf('%20') ? `'${urlPath}${fromUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + fromUrl
124
+ let toUrl = file.rel.pub.url
125
+ toUrl = ~toUrl.indexOf('%20') ? `'${urlPath}${toUrl.replace(ENCODED_SPACE_RX, ' ')}'` : urlPath + toUrl
126
+ if (file.pub.splat) {
127
+ const toUrlWithTrailingSlash = ensureTrailingSlash(toUrl)
128
+ return `location ^~ ${fromUrl}/ { rewrite ^${regexpEscape(fromUrl)}/(.*)$ ${toUrlWithTrailingSlash}$1 redirect; }`
129
+ } else {
130
+ return `location = ${fromUrl} { return 301 ${toUrl}; }`
131
+ }
132
+ })
133
+ return [new File({ contents: Buffer.from(rules.join('\n').trim() + '\n'), out: { path: '.etc/nginx/rewrite.conf' } })]
134
+ }
135
+
136
+ function populateStaticRedirectFiles(files, siteUrl) {
137
+ for (const file of files) {
138
+ if (isRedirectLoop(file)) continue
139
+ const content = createStaticRedirectContents(file, siteUrl) + '\n';
140
+ file.contents = Buffer.from(content);
141
+ }
142
+ return []
143
+ }
144
+
145
+ function unpublish (files) {
146
+ files.forEach((file) => delete file.out)
147
+ return []
148
+ }
149
+
150
+ function computeRelativeUrlPath (from, to) {
151
+ if (to === from) return to.charAt(to.length - 1) === '/' ? './' : path.basename(to)
152
+ return (path.relative(path.dirname(from + '.'), to) || '.') + (to.charAt(to.length - 1) === '/' ? '/' : '')
153
+ }
154
+
155
+ function ensureTrailingSlash (str) {
156
+ return str.charAt(str.length - 1) === '/' ? str : str + '/'
157
+ }
158
+
159
+ function stripTrailingSlash (str, root = '/') {
160
+ if (str === '/') return root
161
+ const lastIdx = str.length - 1
162
+ return str.charAt(lastIdx) === '/' ? str.substr(0, lastIdx) : str
163
+ }
164
+
165
+ function regexpEscape (str) {
166
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // don't escape "-" since it's meaningless in a literal
167
+ }
168
+
169
+ function isRedirectLoop (file) {
170
+ if (file.pub.url === file.rel.pub.url) return true
171
+ return false
172
+ }
173
+
@@ -91,9 +91,10 @@ module.exports.register = function (registry, config = {}) {
91
91
  self.named('glossterm')
92
92
  //Specifying the regexp allows spaces in the term.
93
93
  self.$option('regexp', /glossterm:([^[]+)\[(|.*?[^\\])\]/)
94
- self.positionalAttributes(['definition'])
94
+ self.positionalAttributes(['definition', 'customText']);
95
95
  self.process(function (parent, target, attributes) {
96
96
  const term = attributes.term || target
97
+ const customText = attributes.customText || term;
97
98
  const document = parent.document
98
99
  const context = vfs.getContext()
99
100
  const customLinkCandidate = context.gloss.find(candidate => 'link' in candidate && candidate.term === term);
@@ -136,10 +137,10 @@ module.exports.register = function (registry, config = {}) {
136
137
  const termExistsInContext = context.gloss.some((candidate) => candidate.term === term);
137
138
  if ((termExistsInContext && links) || (links && customLink)) {
138
139
  inline = customLink
139
- ? self.createInline(parent, 'anchor', target, { type: 'link', target: customLink, attributes: { ...attrs, window: '_blank', rel: 'noopener noreferrer' } })
140
- : self.createInline(parent, 'anchor', target, { type: 'xref', target: `${glossaryPage}#${termId(term)}`, reftext: target, attributes: attrs })
140
+ ? self.createInline(parent, 'anchor', customText, { type: 'link', target: customLink, attributes: { ...attrs, window: '_blank', rel: 'noopener noreferrer' } })
141
+ : self.createInline(parent, 'anchor', customText, { type: 'xref', target: `${glossaryPage}#${termId(term)}`, reftext: customText, attributes: attrs })
141
142
  } else {
142
- inline = self.createInline(parent, 'quoted', target, { attributes: attrs })
143
+ inline = self.createInline(parent, 'quoted', customText, { attributes: attrs })
143
144
  }
144
145
  if (tooltip) {
145
146
  const a = inline.convert()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@redpanda-data/docs-extensions-and-macros",
3
- "version": "3.2.5",
3
+ "version": "3.2.7",
4
4
  "description": "Antora extensions and macros developed for Redpanda documentation.",
5
5
  "keywords": [
6
6
  "antora",
@@ -35,7 +35,7 @@
35
35
  "./extensions/validate-attributes": "./extensions/validate-attributes.js",
36
36
  "./extensions/find-related-docs": "./extensions/find-related-docs.js",
37
37
  "./extensions/find-related-labs": "./extensions/find-related-labs.js",
38
- "./extensions/modify-redirects": "./extensions/modify-redirects.js",
38
+ "./extensions/modify-redirects": "./extensions/produce-redirects.js",
39
39
  "./extensions/algolia-indexer/index": "./extensions/algolia-indexer/index.js",
40
40
  "./extensions/aggregate-terms": "./extensions/aggregate-terms.js",
41
41
  "./macros/glossary": "./macros/glossary.js",
@@ -1,36 +0,0 @@
1
- 'use strict';
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
-
6
- function redirectModifier(files, outputDir, logger) {
7
- files.forEach((file) => {
8
- const filePath = path.join(outputDir, file);
9
- if (!fs.existsSync(filePath)) return
10
- let content = fs.readFileSync(filePath, 'utf8');
11
- const lines = content.split('\n');
12
-
13
- // Filter out redirects that point to themselves
14
- const modifiedLines = lines.filter((line) => {
15
- const parts = line.split(' ');
16
- if (parts[0] == parts[1]) logger.info(`Removed redirect that points to itself: ${line}`)
17
- return parts[0] !== parts[1]; // Ensure the source and target are not the same
18
- });
19
-
20
- // Join the array back into a string and write it back to the file
21
- const modifiedContent = modifiedLines.join('\n');
22
- fs.writeFileSync(filePath, modifiedContent, 'utf8');
23
- logger.info(`Processed and updated redirects in ${filePath}`);
24
- })
25
- }
26
-
27
- module.exports.register = function ({ config }) {
28
- const logger = this.getLogger('redirects-produced');
29
- this.on('sitePublished', async ({ publications }) => {
30
- publications.forEach(publication => {
31
- const outputDir = publication.resolvedPath;
32
- const redirectFile = ['_redirects'];
33
- redirectModifier(redirectFile, outputDir, logger);
34
- });
35
- });
36
- };