@antora/assembler 1.0.0-alpha.8 → 1.0.0-alpha.9
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/lib/assemble-content.js
CHANGED
|
@@ -5,7 +5,7 @@ const produceAggregateDocuments = require('./produce-aggregate-documents')
|
|
|
5
5
|
const PromiseQueue = require('./util/promise-queue')
|
|
6
6
|
const runCommand = require('./util/run-command')
|
|
7
7
|
|
|
8
|
-
async function assembleContent (playbook, contentCatalog,
|
|
8
|
+
async function assembleContent (playbook, contentCatalog, convertDocument, { siteCatalog, configSource }) {
|
|
9
9
|
// Q: could we get ContentCatalog#getComponentVersionStartPage() in Antora core?
|
|
10
10
|
if (typeof contentCatalog.getComponentVersionStartPage !== 'function') {
|
|
11
11
|
contentCatalog.getComponentVersionStartPage = function (component, version) {
|
|
@@ -17,14 +17,14 @@ async function assembleContent (playbook, contentCatalog, converter, { siteCatal
|
|
|
17
17
|
const generatorFunctions = this ? this.getFunctions() : {}
|
|
18
18
|
const { loadAsciiDoc = require('@antora/asciidoc-loader') } = generatorFunctions
|
|
19
19
|
const aggregateDocuments = produceAggregateDocuments(loadAsciiDoc, contentCatalog, assemblerConfig)
|
|
20
|
-
if (!
|
|
20
|
+
if (!convertDocument) return aggregateDocuments
|
|
21
21
|
const { publishSite: publishFiles = require('@antora/site-publisher') } = generatorFunctions
|
|
22
22
|
const buildConfig = assemblerConfig.build
|
|
23
23
|
await prepareWorkspace(publishFiles, aggregateDocuments, contentCatalog, buildConfig)
|
|
24
|
-
// TODO: pass more information to
|
|
24
|
+
// TODO: pass more information to convertDocument so it doesn't have to compute internal stuff
|
|
25
25
|
// Q: don't we need to pass in the combined/resolved AsciiDoc attributes per file or component version?
|
|
26
26
|
return new PromiseQueue({ concurrency: buildConfig.processLimit })
|
|
27
|
-
.add(aggregateDocuments.map((doc) => () =>
|
|
27
|
+
.add(aggregateDocuments.map((doc) => () => convertDocument.call(this, doc, buildConfig, runCommand)))
|
|
28
28
|
.toPromise()
|
|
29
29
|
.then((files) => {
|
|
30
30
|
if (buildConfig.publish && siteCatalog) siteCatalog.addFiles(files)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const File = require('vinyl')
|
|
4
4
|
const path = require('node:path/posix')
|
|
5
5
|
const sanitize = require('./util/sanitize')
|
|
6
|
+
const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
|
|
6
7
|
|
|
7
8
|
function produceAggregateDocument (
|
|
8
9
|
loadAsciiDoc,
|
|
@@ -26,7 +27,8 @@ function produceAggregateDocument (
|
|
|
26
27
|
relative: generateSlug(navtitle),
|
|
27
28
|
},
|
|
28
29
|
})
|
|
29
|
-
const
|
|
30
|
+
const { dir: outDir, name: outName } = path.parse(templateFile.out.path)
|
|
31
|
+
const path_ = outName === templateFile.src.relative ? path.join(outDir, outName + '.adoc') : outDir + '.adoc'
|
|
30
32
|
contentCatalog.removeFile(templateFile)
|
|
31
33
|
const header = buildAsciiDocHeader(componentVersion, navtitle, doctype)
|
|
32
34
|
const body = aggregateAsciiDoc(
|
|
@@ -57,7 +59,8 @@ function produceAggregateDocument (
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
function buildAsciiDocHeader (componentVersion, navtitle, doctype = 'book') {
|
|
60
|
-
const
|
|
62
|
+
const navtitlePlain = sanitize(navtitle)
|
|
63
|
+
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
61
64
|
let doctitle = navtitleAsciiDoc
|
|
62
65
|
if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
|
|
63
66
|
const version = componentVersion.version === 'master' ? '' : componentVersion.version
|
|
@@ -104,8 +107,10 @@ function aggregateAsciiDoc (
|
|
|
104
107
|
const buffer = []
|
|
105
108
|
// TODO: we could try to be smart about it and make sure the page with fragment is included at least once
|
|
106
109
|
if (outlineEntry.hash) return buffer
|
|
107
|
-
const { content: navtitle, items, unresolved, urlType, url } = outlineEntry
|
|
108
|
-
const
|
|
110
|
+
const { content: navtitle, items = [], unresolved, urlType, url } = outlineEntry
|
|
111
|
+
const hasItems = items.length > 0
|
|
112
|
+
const navtitlePlain = sanitize(navtitle)
|
|
113
|
+
const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
|
|
109
114
|
const siteUrl = ((val) => {
|
|
110
115
|
if (!val || val === '/') return ''
|
|
111
116
|
return val.charAt(val.length - 1) === '/' ? val.slice(0, val.length - 1) : val
|
|
@@ -181,7 +186,7 @@ function aggregateAsciiDoc (
|
|
|
181
186
|
} else {
|
|
182
187
|
header.unshift(`[#${idprefix}]`)
|
|
183
188
|
}
|
|
184
|
-
if (sectionMergeStrategy === 'enclose' &&
|
|
189
|
+
if (sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
|
|
185
190
|
enclosed = true
|
|
186
191
|
// TODO: make overview section title configurable
|
|
187
192
|
//let overviewTitle = doc.getDocumentTitle()
|
|
@@ -340,9 +345,7 @@ function aggregateAsciiDoc (
|
|
|
340
345
|
relative,
|
|
341
346
|
})
|
|
342
347
|
// TODO: handle unresolved attachment page
|
|
343
|
-
return attachment
|
|
344
|
-
? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]`
|
|
345
|
-
: m
|
|
348
|
+
return attachment?.out ? `${siteUrl}${attachment.pub.url.replace(/_/g, '{underscore}')}[${text}]` : m
|
|
346
349
|
})
|
|
347
350
|
}
|
|
348
351
|
if (~line.indexOf('image:') && !line.startsWith('image::')) {
|
|
@@ -350,7 +353,7 @@ function aggregateAsciiDoc (
|
|
|
350
353
|
if (isResourceSpec(target)) {
|
|
351
354
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
352
355
|
// TODO: handle (or report) unresolved image better
|
|
353
|
-
if (image
|
|
356
|
+
if (image?.out) {
|
|
354
357
|
image.out.assembled = true
|
|
355
358
|
return `image:${image.out.path.replace(/_/g, '{underscore}')}[${attrlist}]`
|
|
356
359
|
}
|
|
@@ -409,7 +412,7 @@ function aggregateAsciiDoc (
|
|
|
409
412
|
if (isResourceSpec(target)) {
|
|
410
413
|
const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
|
|
411
414
|
// FIXME: handle (or report) case when image is not resolved
|
|
412
|
-
if (image
|
|
415
|
+
if (image?.out) {
|
|
413
416
|
const boxedAttrlist = line.slice(line.indexOf('['))
|
|
414
417
|
image.out.assembled = true
|
|
415
418
|
lines[idx] = `${prefix}image::${image.out.path}${boxedAttrlist}`
|
|
@@ -475,13 +478,10 @@ function aggregateAsciiDoc (
|
|
|
475
478
|
}
|
|
476
479
|
let sectionTitle = navtitleAsciiDoc
|
|
477
480
|
if (urlType === 'external') {
|
|
478
|
-
sectionTitle = `${url}[${
|
|
481
|
+
sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
479
482
|
} else if (urlType === 'internal' && !unresolved && siteUrl) {
|
|
480
|
-
const resource = contentCatalog.getFiles().find((it) => it.pub.url === url)
|
|
481
|
-
if (resource
|
|
482
|
-
sectionTitle = navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')
|
|
483
|
-
sectionTitle = `${siteUrl}${resource.pub.url}[${sectionTitle}]`
|
|
484
|
-
}
|
|
483
|
+
const resource = contentCatalog.getFiles().find((it) => it.out && it.pub.url === url)
|
|
484
|
+
if (resource) sectionTitle = `${siteUrl}${resource.pub.url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
|
|
485
485
|
}
|
|
486
486
|
let hlevel = level + 1
|
|
487
487
|
if (hlevel > 6) {
|
|
@@ -496,7 +496,7 @@ function aggregateAsciiDoc (
|
|
|
496
496
|
}
|
|
497
497
|
|
|
498
498
|
const nextLevel = level + 1
|
|
499
|
-
if (
|
|
499
|
+
if (hasItems) {
|
|
500
500
|
// NOTE: drop first child if same as parent; should we keep if content is different?
|
|
501
501
|
;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
|
|
502
502
|
? items.slice(1)
|
|
@@ -130,10 +130,13 @@ function isResourceRef (target) {
|
|
|
130
130
|
// in this case, if there's only a single navigation menu with no title, promote each top-level item to a menu
|
|
131
131
|
function prepareOutlines (navigation, rootEntry, rootLevel) {
|
|
132
132
|
if (rootLevel === 0 || navigation.length === 1) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
133
|
+
let navBranch
|
|
134
|
+
if (navigation.length === 1) {
|
|
135
|
+
navBranch = navigation[0]
|
|
136
|
+
} else {
|
|
137
|
+
const items = navigation.reduce((accum, it) => accum.concat(it.content ? it : it.items), [])
|
|
138
|
+
navBranch = items.length ? { items } : {}
|
|
139
|
+
}
|
|
137
140
|
return rootLevel === 0 || navBranch.content ? [Object.assign(rootEntry, navBranch)] : navBranch.items
|
|
138
141
|
}
|
|
139
142
|
return navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), [rootEntry])
|
package/lib/util/sanitize.js
CHANGED
|
@@ -5,11 +5,9 @@ const XML_SPECIAL_CHARS = { '<': '<', '>': '>', '&': '&' }
|
|
|
5
5
|
const XML_SPECIAL_CHARS_RX = /&(?:[lg]t|amp);/g
|
|
6
6
|
|
|
7
7
|
function sanitize (str) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
? Array(2).fill(str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]))
|
|
12
|
-
: [str, str]
|
|
8
|
+
if (~str.indexOf('<')) str = str.replace(XML_TAG_RX, '')
|
|
9
|
+
if (~str.indexOf('&')) str = str.replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m])
|
|
10
|
+
return str
|
|
13
11
|
}
|
|
14
12
|
|
|
15
13
|
module.exports = sanitize
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const ATTRIBUTE_REFERENCE_RX = /\{[a-z0-9_][a-z0-9_-]*\}/g
|
|
4
|
+
const STRICT_WORD_CHAR_RX = /[\p{L}\d_]/u
|
|
5
|
+
const WORD_CHAR_RX = /[\p{L}\d_;}:<>]/u
|
|
6
|
+
|
|
7
|
+
const MARK_FOR_TAG = { code: '`', em: '_', mark: '#', span: '#', strong: '*' }
|
|
8
|
+
const SKIP_SPAN = { icon: '<i ', image: '<img ' }
|
|
9
|
+
|
|
10
|
+
module.exports = (str) => {
|
|
11
|
+
if (!str) return str
|
|
12
|
+
let matchIndex = str.indexOf('<')
|
|
13
|
+
if (!~matchIndex) return ~str.indexOf('{') ? str.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : str
|
|
14
|
+
let current = { contents: '' }
|
|
15
|
+
const stack = [current]
|
|
16
|
+
let lastIndex = 0
|
|
17
|
+
do {
|
|
18
|
+
if (matchIndex > lastIndex) {
|
|
19
|
+
const matched = str.slice(lastIndex, matchIndex)
|
|
20
|
+
current.contents += ~matched.indexOf('{') ? matched.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : matched
|
|
21
|
+
}
|
|
22
|
+
const isCloseTag = str[++matchIndex] === '/' ? ++matchIndex : false
|
|
23
|
+
let tagName = str.slice(matchIndex, (lastIndex = str.indexOf('>', matchIndex) + 1) - 1)
|
|
24
|
+
if (isCloseTag) {
|
|
25
|
+
const parent = current // TODO expect tagName to equal current.tagName
|
|
26
|
+
stack.pop()
|
|
27
|
+
current = stack[stack.length - 1]
|
|
28
|
+
if (parent.mark) {
|
|
29
|
+
let { contents, mark, id, role } = parent
|
|
30
|
+
const attrlist = (id ? '#' + id : '') + (role ? '.' + role.replace(/ /g, '.') : '')
|
|
31
|
+
if (
|
|
32
|
+
current.mark === mark ||
|
|
33
|
+
current.mark === '_' ||
|
|
34
|
+
isWordChar(str.charAt(lastIndex), true) ||
|
|
35
|
+
isWordChar(current.contents[current.contents.length - 1] || current.mark)
|
|
36
|
+
) {
|
|
37
|
+
mark = mark.repeat(2)
|
|
38
|
+
}
|
|
39
|
+
current.contents += (attrlist ? '[' + attrlist + ']' : '') + mark + contents + mark
|
|
40
|
+
} else {
|
|
41
|
+
current.contents += parent.contents
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
let attrs, attrlistIndex, role
|
|
45
|
+
if (~(attrlistIndex = tagName.indexOf(' '))) {
|
|
46
|
+
role = (attrs = parseAttrlist(tagName.slice(attrlistIndex))).class
|
|
47
|
+
tagName = tagName.slice(0, attrlistIndex)
|
|
48
|
+
}
|
|
49
|
+
if (tagName === 'img') {
|
|
50
|
+
current.contents += 'image:' + attrs.src + '[' + attrs.alt + ']'
|
|
51
|
+
} else if (tagName === 'i' && current.tagName === 'span' && current.role === 'icon') {
|
|
52
|
+
current.contents += 'icon:' + role.slice(6) + '[]'
|
|
53
|
+
lastIndex += 4
|
|
54
|
+
} else {
|
|
55
|
+
let check, mark
|
|
56
|
+
const id = attrs?.id
|
|
57
|
+
if (tagName !== 'span' || id || (role && !((check = SKIP_SPAN[role]) && str.startsWith(check, lastIndex)))) {
|
|
58
|
+
mark = MARK_FOR_TAG[tagName]
|
|
59
|
+
}
|
|
60
|
+
stack.push((current = { tagName, role, id, mark, contents: '' }))
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
} while (~(matchIndex = str.indexOf('<', lastIndex)))
|
|
64
|
+
const rest = str.slice(lastIndex)
|
|
65
|
+
if (rest) current.contents += ~rest.indexOf('{') ? rest.replace(ATTRIBUTE_REFERENCE_RX, '\\$&') : rest
|
|
66
|
+
return current.contents
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseAttrlist (str) {
|
|
70
|
+
let lastIndex = 0
|
|
71
|
+
const attrs = {}
|
|
72
|
+
while (str.charAt(lastIndex++) === ' ') {
|
|
73
|
+
const spaceIndex = str.indexOf(' ', lastIndex)
|
|
74
|
+
const equalsIndex = str.indexOf('=', lastIndex)
|
|
75
|
+
if (~spaceIndex && spaceIndex < equalsIndex) {
|
|
76
|
+
attrs[str.slice(lastIndex, (lastIndex = spaceIndex))] = true
|
|
77
|
+
} else if (~equalsIndex) {
|
|
78
|
+
const name = str.slice(lastIndex, equalsIndex)
|
|
79
|
+
const valueIndex = equalsIndex + 1
|
|
80
|
+
attrs[name] =
|
|
81
|
+
str.charAt(valueIndex) === '"'
|
|
82
|
+
? str.slice(valueIndex + 1, (lastIndex = str.indexOf('"', valueIndex + 1) + 1) - 1)
|
|
83
|
+
: str.slice(valueIndex, (lastIndex = ~spaceIndex ? spaceIndex : str.length))
|
|
84
|
+
} else {
|
|
85
|
+
attrs[str.slice(lastIndex)] = true
|
|
86
|
+
break
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return attrs
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isWordChar (str, strict) {
|
|
93
|
+
if (!str) return false
|
|
94
|
+
return (strict ? STRICT_WORD_CHAR_RX : WORD_CHAR_RX).test(str)
|
|
95
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antora/assembler",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.9",
|
|
4
4
|
"description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "OpenDevise Inc. (https://opendevise.com)",
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
"./produce-aggregate-documents": "./lib/produce-aggregate-documents.js",
|
|
29
29
|
"./select-mutable-attributes": "./lib/select-mutable-attributes.js"
|
|
30
30
|
},
|
|
31
|
+
"imports": {
|
|
32
|
+
"#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
|
|
33
|
+
},
|
|
31
34
|
"dependencies": {
|
|
32
35
|
"@antora/expand-path-helper": "~2.0",
|
|
33
36
|
"braces": "~3.0",
|