@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.
@@ -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, converter, { siteCatalog, configSource }) {
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 (!converter) return aggregateDocuments
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 converter so it doesn't have to compute internal stuff
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) => () => converter.call(this, doc, buildConfig, runCommand)))
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 path_ = `${templateFile.out.path}.adoc`
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 [navtitlePlain, navtitleAsciiDoc] = sanitize(navtitle)
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 [navtitlePlain, navtitleAsciiDoc, navtitlePass] = sanitize(navtitle)
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' && items && doc.hasSections()) {
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 && attachment.out
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 && image.out) {
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 && image.out) {
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}[${navtitlePass ? navtitleAsciiDoc : navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
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?.out) {
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 (items) {
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
- const navBranch =
134
- navigation.length === 1
135
- ? navigation[0]
136
- : { items: navigation.reduce((navTree, it) => navTree.concat(it.content ? it : it.items), []) }
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])
@@ -5,11 +5,9 @@ const XML_SPECIAL_CHARS = { '&lt;': '<', '&gt;': '>', '&amp;': '&' }
5
5
  const XML_SPECIAL_CHARS_RX = /&(?:[lg]t|amp);/g
6
6
 
7
7
  function sanitize (str) {
8
- return ~str.indexOf('<')
9
- ? [str.replace(XML_TAG_RX, '').replace(XML_SPECIAL_CHARS_RX, (m) => XML_SPECIAL_CHARS[m]), `+++${str}+++`, true]
10
- : ~str.indexOf('&')
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.8",
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",