@antora/assembler 1.0.0-alpha.8 → 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,146 @@
1
+ 'use strict'
2
+
3
+ const createAsciiDocFile = require('./util/create-asciidoc-file')
4
+ const filterComponentVersions = require('./filter-component-versions')
5
+ const produceAssemblyFile = require('./produce-assembly-file')
6
+ const selectMutableAttributes = require('./select-mutable-attributes')
7
+
8
+ const IMAGE_MACRO_RX = /^image::?(.+?)\[(.*?)\]$/
9
+
10
+ function produceAssemblyFiles (loadAsciiDoc, contentCatalog, assemblerConfig, resolveAssemblyModel) {
11
+ const { asciidoc: assemblerAsciiDocConfig, assembly: assemblyConfig } = assemblerConfig
12
+ resolveAssemblyModel ??= (componentVersion) => ({
13
+ doctype: assemblyConfig.doctype,
14
+ insertStartPage: assemblyConfig.insertStartPage,
15
+ rootLevel: assemblyConfig.rootLevel,
16
+ sectionMergeStrategy: assemblyConfig.sectionMergeStrategy,
17
+ navigation: componentVersion.navigation,
18
+ xmlIds: assemblyConfig.xmlIds,
19
+ })
20
+ const assemblerAsciiDocAttributes = Object.assign({}, assemblerAsciiDocConfig.attributes)
21
+ const { revdate, 'source-highlighter': sourceHighlighter } = assemblerAsciiDocAttributes
22
+ delete assemblerAsciiDocAttributes.revdate
23
+ delete assemblerAsciiDocAttributes['source-highlighter']
24
+ const publishableFiles = contentCatalog.getFiles().filter((file) => file.out)
25
+ return filterComponentVersions(contentCatalog.getComponents(), assemblerConfig.componentVersionFilter.names).reduce(
26
+ (accum, componentVersion) => {
27
+ const assemblyModel = resolveAssemblyModel(componentVersion)
28
+ if (!assemblyModel.navigation) return accum
29
+ const { name: componentName, version, title } = componentVersion
30
+ const componentVersionAsciiDocConfig = getAsciiDocConfigWithAsciidoctorReducerExtension(componentVersion)
31
+ const mergedAsciiDocConfig = Object.assign({}, componentVersionAsciiDocConfig, {
32
+ attributes: Object.assign({ revdate }, componentVersionAsciiDocConfig.attributes, assemblerAsciiDocAttributes),
33
+ })
34
+ const mergedAsciiDocAttributes = mergedAsciiDocConfig.attributes
35
+ const auxiliaryImages = new Set()
36
+ Object.entries(mergedAsciiDocAttributes).forEach(([name, val]) => {
37
+ const match = name.endsWith('-image') && val.startsWith('image:') && IMAGE_MACRO_RX.exec(val)
38
+ if (!(match && isResourceRef(match[1]))) return
39
+ // Q should we allow image to be resolved relative to component version?
40
+ const image = contentCatalog.resolveResource(match[1], undefined, 'image', ['image'])
41
+ if (!image?.out) return
42
+ mergedAsciiDocAttributes[name] = `image:${image.out.path}[${match[2]}]`
43
+ auxiliaryImages.add(image)
44
+ })
45
+ const rootEntry = { content: title }
46
+ const { navigation, rootLevel } = assemblyModel
47
+ let startPage =
48
+ 'startPage' in componentVersion
49
+ ? componentVersion.startPage
50
+ : contentCatalog.resolvePage('index.adoc', { component: componentName, version })
51
+ if (startPage && startPage.src.component === componentName && startPage.src.version === version) {
52
+ if (assemblyModel.insertStartPage) {
53
+ const navtitle = startPage.asciidoc?.navtitle || rootEntry.content
54
+ Object.assign(rootEntry, { navtitle, url: startPage.pub.url, urlType: 'internal' })
55
+ }
56
+ } else {
57
+ // Q: should we always use a reference page as startPage for computing mutableAttributes?
58
+ startPage = createAsciiDocFile(contentCatalog, {
59
+ src: {
60
+ component: componentVersion.name,
61
+ version: componentVersion.version,
62
+ module: 'ROOT',
63
+ family: 'page',
64
+ relative: '.start-page.adoc',
65
+ origin: (componentVersion.origins || [])[0],
66
+ },
67
+ })
68
+ }
69
+ const mutableAttributes = selectMutableAttributes(loadAsciiDoc, contentCatalog, startPage, mergedAsciiDocConfig)
70
+ delete mutableAttributes.doctype // Q: should this be in selectMutableAttributes?
71
+ prepareOutlines(navigation, rootEntry, rootLevel).reduce((any, outline) => {
72
+ const assemblyFile = produceAssemblyFile(
73
+ loadAsciiDoc,
74
+ contentCatalog,
75
+ componentVersion,
76
+ outline,
77
+ publishableFiles,
78
+ mergedAsciiDocConfig,
79
+ mutableAttributes,
80
+ assemblyModel
81
+ )
82
+ if (!assemblyFile) return any
83
+ if (!any && auxiliaryImages.size) {
84
+ const assembledAssets = assemblyFile.assembler.assembled.assets
85
+ auxiliaryImages.forEach((asset) => assembledAssets.add(asset))
86
+ }
87
+ accum.push(assemblyFile)
88
+ return true
89
+ }, false)
90
+ mergedAsciiDocAttributes.doctype = assemblyModel.doctype
91
+ sourceHighlighter
92
+ ? (mergedAsciiDocAttributes['source-highlighter'] = sourceHighlighter)
93
+ : delete mergedAsciiDocAttributes['source-highlighter']
94
+ return accum
95
+ },
96
+ []
97
+ )
98
+ }
99
+
100
+ function getAsciiDocConfigWithAsciidoctorReducerExtension (componentVersion) {
101
+ const asciidoctorReducerExtension = require('@asciidoctor/reducer') // NOTE: must be required lazily
102
+ const asciidocConfig = componentVersion.asciidoc
103
+ const extensions = [asciidoctorReducerExtension]
104
+ const configuredExtensions = asciidocConfig.extensions || []
105
+ if (!configuredExtensions.length) return Object.assign({}, asciidocConfig, { extensions, sourcemap: true })
106
+ return Object.assign({}, asciidocConfig, {
107
+ extensions: configuredExtensions.reduce((accum, candidate) => {
108
+ if (candidate !== asciidoctorReducerExtension) accum.push(candidate)
109
+ return accum
110
+ }, extensions),
111
+ sourcemap: true,
112
+ })
113
+ }
114
+
115
+ function includedInNav (items, url) {
116
+ return items.find((it) => it.url === url || includedInNav(it.items || [], url))
117
+ }
118
+
119
+ function isResourceRef (target) {
120
+ return ~target.indexOf(':') && !(~target.indexOf('://') || (target.startsWith('data:') && ~target.indexOf(',')))
121
+ }
122
+
123
+ function prepareOutlines (navigation, rootEntry, rootLevel) {
124
+ const singleEntry = navigation.length === 1
125
+ const items = navigation.reduce((accum, it) => {
126
+ if (!it.content || (singleEntry && it.url ? it.url === rootEntry.url : it.content === rootEntry.content)) {
127
+ accum.push(...it.items)
128
+ } else {
129
+ accum.push(it)
130
+ }
131
+ return accum
132
+ }, [])
133
+ if (rootLevel === 0) {
134
+ if (rootEntry.url && includedInNav(items, rootEntry.url)) {
135
+ for (const p of ['url', 'urlType']) delete rootEntry[p]
136
+ }
137
+ return [Object.assign(rootEntry, { index: true, items })]
138
+ }
139
+ if (rootEntry.url && !includedInNav(items, rootEntry.url)) {
140
+ rootEntry.content = singleEntry && rootEntry.url === navigation[0].url ? navigation[0].content : rootEntry.navtitle
141
+ items.unshift(rootEntry)
142
+ }
143
+ return items
144
+ }
145
+
146
+ module.exports = produceAssemblyFiles
@@ -0,0 +1,57 @@
1
+ 'use strict'
2
+
3
+ const { posix: path } = require('node:path')
4
+
5
+ function computeOut (src) {
6
+ const { component, version, module: module_, family, relative } = src
7
+ const outRelative = family === 'page' ? relative.replace(/\.adoc$/, '.html') : relative
8
+ const { dir: dirname, base: basename, ext: extname, name: stem } = path.parse(outRelative)
9
+ const componentVersion = this.getComponentVersion(component, version)
10
+ const versionSegment =
11
+ 'activeVersionSegment' in componentVersion
12
+ ? componentVersion.activeVersionSegment
13
+ : resolveActiveVersionSegment.call(this, component, version)
14
+ const outDirSegments = []
15
+ const moduleRootPathSegments = []
16
+ if (component !== 'ROOT') outDirSegments.push(component)
17
+ if (versionSegment) outDirSegments.push(versionSegment)
18
+ if (module_ !== 'ROOT') outDirSegments.push(module_)
19
+ const outModuleDirSegments = outDirSegments.slice()
20
+ if (family !== 'page') {
21
+ outDirSegments.push(`_${family}s`)
22
+ moduleRootPathSegments.push('..')
23
+ }
24
+ if (dirname) {
25
+ outDirSegments.push(dirname)
26
+ for (const _ of dirname.split('/')) moduleRootPathSegments.push('..')
27
+ }
28
+ const rootPathSegments = moduleRootPathSegments.slice()
29
+ for (const _ of outModuleDirSegments) rootPathSegments.push('..')
30
+ const outDirname = outDirSegments.join('/')
31
+ const result = {
32
+ dirname: outDirname,
33
+ basename,
34
+ path: outDirname + '/' + basename,
35
+ moduleRootPath: moduleRootPathSegments.length ? moduleRootPathSegments.join('/') : '.',
36
+ rootPath: rootPathSegments.length ? rootPathSegments.join('/') : '.',
37
+ }
38
+ return result
39
+ }
40
+
41
+ function resolveActiveVersionSegment (component, version) {
42
+ let startPage = this.resolvePage('index.adoc', { component, version })
43
+ let startPageSrc
44
+ if (startPage) {
45
+ startPageSrc = startPage.src
46
+ } else {
47
+ startPageSrc = { component, version, module: 'ROOT', family: 'page', relative: 'index.adoc' }
48
+ this.removeFile((startPage = this.addFile({ src: startPageSrc })))
49
+ }
50
+ const outPathSegments = startPage.out.path.split('/')
51
+ for (const depth of startPage.out.moduleRootPath.split('/')) outPathSegments.pop()
52
+ if (startPageSrc.module !== 'ROOT') outPathSegments.pop()
53
+ if (startPageSrc.component !== 'ROOT') outPathSegments.shift()
54
+ return outPathSegments.length ? outPathSegments[0] : ''
55
+ }
56
+
57
+ module.exports = computeOut
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ const computeOut = require('./compute-out')
4
+ const { posix: path } = require('node:path')
5
+
6
+ function createAsciiDocFile (contentCatalog, file) {
7
+ file.mediaType = 'text/asciidoc'
8
+ const src = file.src
9
+ const out = computeOut.call(contentCatalog, src)
10
+ if (src.family === 'export') {
11
+ contentCatalog.removeFile((file = contentCatalog.addFile(Object.assign(file, { path: out.path, out: null }))))
12
+ return file
13
+ }
14
+ const pub = { url: '/' + out.path, moduleRootPath: out.moduleRootPath, rootPath: out.rootPath }
15
+ return { contents: src.contents ?? Buffer.alloc(0), src, out, pub }
16
+ }
17
+
18
+ module.exports = createAsciiDocFile
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@antora/assembler",
3
- "version": "1.0.0-alpha.8",
4
- "description": "An extension library for Antora that assembles content from multiple pages into a single AsciiDoc file to converted and publish.",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "A JavaScript library that merges AsciiDoc content from multiple pages in an Antora site into assembly files and delegates to an exporter to convert those files to another format, such as PDF.",
5
5
  "license": "MPL-2.0",
6
6
  "author": "OpenDevise Inc. (https://opendevise.com)",
7
7
  "contributors": [
@@ -9,34 +9,42 @@
9
9
  "Sarah White <sarah@opendevise.com>"
10
10
  ],
11
11
  "homepage": "https://antora.org",
12
- "repository": "gitlab:antora/antora-assembler",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://gitlab.com/antora/antora-assembler.git",
15
+ "directory": "packages/assembler"
16
+ },
13
17
  "bugs": {
14
18
  "url": "https://gitlab.com/antora/antora-assembler/issues"
15
19
  },
16
20
  "scripts": {
17
- "test": "_mocha test",
21
+ "test": "node --test test/*-test.js",
18
22
  "prepublishOnly": "npx -y downdoc --prepublish",
19
23
  "postpublish": "npx -y downdoc --postpublish"
20
24
  },
21
25
  "main": "lib/index.js",
22
26
  "exports": {
23
27
  ".": "./lib/index.js",
24
- "./asciidoctor/reducer-extension": "./lib/asciidoctor/reducer-extension.js",
25
28
  "./filter-component-versions": "./lib/filter-component-versions.js",
26
29
  "./load-config": "./lib/load-config.js",
27
- "./produce-aggregate-document": "./lib/produce-aggregate-document.js",
28
- "./produce-aggregate-documents": "./lib/produce-aggregate-documents.js",
30
+ "./produce-assembly-file": "./lib/produce-assembly-file.js",
31
+ "./produce-assembly-files": "./lib/produce-assembly-files.js",
29
32
  "./select-mutable-attributes": "./lib/select-mutable-attributes.js"
30
33
  },
34
+ "imports": {
35
+ "#run-command": "@antora/run-command-helper",
36
+ "#unconvert-inline-asciidoc": "./lib/util/unconvert-inline-asciidoc.js"
37
+ },
31
38
  "dependencies": {
32
- "@antora/expand-path-helper": "~2.0",
39
+ "@asciidoctor/reducer": "~1.1",
40
+ "@antora/expand-path-helper": "~3.0",
33
41
  "braces": "~3.0",
34
42
  "picomatch": "~3.0",
35
- "vinyl": "~2.2",
36
43
  "js-yaml": "~4.1"
37
44
  },
38
45
  "devDependencies": {
39
46
  "@antora/asciidoc-loader": "~3.1",
47
+ "@antora/navigation-builder": "~3.1",
40
48
  "@antora/site-publisher": "~3.1"
41
49
  },
42
50
  "engines": {
@@ -50,8 +58,5 @@
50
58
  "antora-extension",
51
59
  "asciidoc",
52
60
  "documentation"
53
- ],
54
- "publishConfig": {
55
- "access": "public"
56
- }
61
+ ]
57
62
  }
@@ -1,235 +0,0 @@
1
- 'use strict'
2
-
3
- const Opal = global.Opal
4
- const Asciidoctor = Opal.Asciidoctor
5
- const $Reducer = function $Reducer () {}
6
-
7
- const DocumentExt = (() => {
8
- const parentScope = Opal.module(Asciidoctor, 'Reducer', $Reducer)
9
- const scope = Opal.module(parentScope, 'DocumentExt', function $DocumentExt () {})
10
-
11
- Opal.defn(scope, '$save_attributes', function saveAttributes () {
12
- this.attributes_defined_in_header = this.attributes_modified.$to_a().reduce((accum, name) => {
13
- accum[name] = this.getAttribute(name)
14
- return accum
15
- }, {})
16
- return Opal.send(this, Opal.find_super_dispatcher(this, 'save_attributes', saveAttributes), [])
17
- })
18
-
19
- return scope
20
- })()
21
-
22
- const ConditionalDirectiveTracker = (() => {
23
- const parentScope = Opal.module(Asciidoctor, 'Reducer', $Reducer)
24
- const scope = Opal.module(parentScope, 'ConditionalDirectiveTracker', function $ConditionalDirectiveTracker () {})
25
-
26
- Opal.defn(
27
- scope,
28
- '$preprocess_conditional_directive',
29
- function preprocessConditionalDirective (keyword, target, delimiter, text) {
30
- const skipActive = this.skipping
31
- const depth = this.conditional_stack.length
32
- let directiveLineno = this.lineno
33
- const result = Opal.send(
34
- this,
35
- Opal.find_super_dispatcher(this, 'preprocess_conditional_directive', preprocessConditionalDirective),
36
- [keyword, target, delimiter, text]
37
- )
38
- if (this.skipping && skipActive) return result
39
- const currIncReplacement = this.includeReplacements.$current()
40
- const drop = currIncReplacement.drop || (currIncReplacement.drop = [])
41
- directiveLineno -= currIncReplacement.offset || 0
42
- const depthChange = this.conditional_stack.length - depth
43
- if (depthChange < 0) {
44
- if (skipActive) {
45
- for (let n = drop.pop(); n <= directiveLineno; n++) drop.push(n)
46
- } else {
47
- drop.push(directiveLineno)
48
- }
49
- } else if (depthChange > 0 || directiveLineno === this.lineno) {
50
- drop.push(directiveLineno)
51
- } else {
52
- drop.push([directiveLineno, text])
53
- }
54
- return result
55
- }
56
- )
57
-
58
- return scope
59
- })()
60
-
61
- const CurrentPosition = (() => {
62
- const parentScope = Opal.module(Opal.Asciidoctor, 'Reducer', $Reducer)
63
- const scope = Opal.module(parentScope, 'CurrentPosition', function $CurrentPosition () {})
64
-
65
- Opal.defn(Opal.get_singleton_class(scope), '$extended', function $extended (instance) {
66
- instance.$to_end()
67
- })
68
-
69
- Opal.defn(scope, '$current', function $current () {
70
- return this[this.pointer]
71
- })
72
-
73
- Opal.defn(scope, '$to_end', function $toEnd () {
74
- this.pointer = this.length - 1
75
- })
76
-
77
- Opal.defn(scope, '$up', function $up () {
78
- this.pointer = this.$current().into
79
- })
80
-
81
- return scope
82
- })()
83
-
84
- const IncludeDirectiveTracker = (() => {
85
- const parentScope = Opal.module(Opal.Asciidoctor, 'Reducer', $Reducer)
86
- const scope = Opal.module(parentScope, 'IncludeDirectiveTracker', function $IncludeDirectiveTracker () {})
87
-
88
- Opal.defn(Opal.get_singleton_class(scope), '$extended', function $extended (instance) {
89
- instance.includeReplacements = [{}].$extend(CurrentPosition)
90
- instance.$$reducer = {}
91
- })
92
-
93
- Opal.defn(scope, '$preprocess_include_directive', function preprocessIncludeDirective (target, attrlist) {
94
- this.$$reducer.includeDirectiveLine = `include::${target}[${attrlist}]`
95
- this.$$reducer.includePushed = false
96
- const directiveLineno = this.lineno // we're currently on the include line, which is 1-based
97
- const result = Opal.send(
98
- this,
99
- Opal.find_super_dispatcher(this, 'preprocess_include_directive', preprocessIncludeDirective),
100
- [target, attrlist]
101
- )
102
- if (!this.$$reducer.includePushed) {
103
- let ln = this.peekLine(true)
104
- let unresolved
105
- if (
106
- ln &&
107
- ln.charAt(ln.length - 1) === ']' &&
108
- !(unresolved = ln.startsWith('Unresolved directive in ')) &&
109
- directiveLineno === this.lineno &&
110
- (unresolved = ln.startsWith('link:'))
111
- ) {
112
- ln = `${ln.slice(0, ln.length - 1)}role=include]`
113
- }
114
- pushIncludeReplacement.call(this, directiveLineno, unresolved ? [ln] : [], 0, unresolved)
115
- }
116
- this.$$reducer = {}
117
- return result
118
- })
119
-
120
- Opal.defn(scope, '$push_include', function pushInclude (data, file, path, lineno, attrs) {
121
- this.$$reducer.includePushed = true
122
- const directiveLineno = this.lineno - 1 // we're below the include line, which is 1-based
123
- const prevIncDepth = this.include_stack.length
124
- let offset = lineno > 1 ? lineno - 1 : 0
125
- const result = Opal.send(this, Opal.find_super_dispatcher(this, 'push_include', pushInclude), [
126
- data,
127
- file,
128
- path,
129
- lineno,
130
- attrs,
131
- ])
132
- let incLines = []
133
- if (this.include_stack.length > prevIncDepth) {
134
- incLines = this.$lines()
135
- if (attrs['$key?']('leveloffset') && incLines[0].startsWith(':leveloffset: ') && incLines[1] === '') offset -= 2
136
- }
137
- pushIncludeReplacement.call(this, directiveLineno, incLines, offset)
138
- return result
139
- })
140
-
141
- Opal.defn(scope, '$pop_include', function popInclude () {
142
- if (!this.$$reducer.includePushed) this.includeReplacements.$up()
143
- return Opal.send(this, Opal.find_super_dispatcher(this, 'pop_include', popInclude), [])
144
- })
145
-
146
- function pushIncludeReplacement (lineno, lines, offset, unresolved) {
147
- const incReplacements = this.includeReplacements
148
- const into = incReplacements.pointer
149
- const line = this.$$reducer.includeDirectiveLine
150
- incReplacements.push({ into, lineno: lineno - (incReplacements.$current().offset || 0), line, lines, offset })
151
- if (!unresolved && lines.length) incReplacements.$to_end()
152
- }
153
-
154
- return scope
155
- })()
156
-
157
- function preprocessor () {
158
- this.process((doc, reader) =>
159
- doc.getOptions().preserve_conditionals
160
- ? reader.$extend(IncludeDirectiveTracker)
161
- : reader.$extend(ConditionalDirectiveTracker, IncludeDirectiveTracker)
162
- )
163
- }
164
-
165
- function treeProcessor () {
166
- this.process((doc) => {
167
- const incReplacements = doc.reader.includeReplacements
168
- if (incReplacements.length > 1 || (incReplacements[0].drop || []).length) {
169
- const sourceLines = doc.getSourceLines()
170
- incReplacements[0].lines = sourceLines.slice()
171
- incReplacements
172
- .slice()
173
- .reverse()
174
- .forEach(({ into, lineno, lines, line, drop }) => {
175
- let targetLines, idx
176
- if (into != null) {
177
- targetLines = incReplacements[into].lines
178
- // adds extra assurance that the program is replacing the correct line
179
- if (targetLines[(idx = lineno - 1)] !== line) {
180
- const msg = `include directive to reduce not found; expected: "${line}"; got: "${targetLines[idx]}"`
181
- doc.getLogger().error(msg)
182
- return
183
- }
184
- }
185
- if ((drop || []).length) {
186
- drop
187
- .slice()
188
- .reverse()
189
- .forEach((dropIt) => {
190
- Array.isArray(dropIt) ? (lines[dropIt[0] - 1] = dropIt[1]) : lines.splice(dropIt - 1, 1)
191
- })
192
- }
193
- if (targetLines) targetLines[idx] = lines
194
- })
195
- const reducedSourceLines = flattenDeep(incReplacements[0].lines)
196
- if (doc.getSourcemap()) {
197
- const logger = Asciidoctor.LoggerManager.getLogger()
198
- const opts = Object.assign(doc.getOptions(), { logger: undefined, parse: false, reduced: true })
199
- if (opts.extension_registry) {
200
- opts.extension_registry = Asciidoctor.Extensions.Registry.$new(opts.extension_registry.groups)
201
- }
202
- const includes = doc.getCatalog().includes
203
- doc = Asciidoctor.load(reducedSourceLines, opts)
204
- doc.catalog.$send('[]=', 'includes', includes)
205
- doc.parse()
206
- Asciidoctor.LoggerManager.setLogger(logger)
207
- } else {
208
- while (reducedSourceLines[reducedSourceLines.length - 1] === '') reducedSourceLines.pop()
209
- sourceLines.splice(0, sourceLines.length, ...reducedSourceLines)
210
- }
211
- }
212
- return doc
213
- })
214
- }
215
-
216
- function flattenDeep (array, accum = []) {
217
- const len = array.length
218
- for (let i = 0, it; i < len; i++) Array.isArray((it = array[i])) ? flattenDeep(it, accum) : accum.push(it)
219
- return accum
220
- }
221
-
222
- function toProc (fn) {
223
- return Object.defineProperty(fn, '$$arity', { value: fn.length })
224
- }
225
-
226
- module.exports.register = (registry) => {
227
- const extGroup = toProc(function () {
228
- const doc = this.document
229
- doc.$extend(DocumentExt)
230
- if (doc.getOptions().reduced) return
231
- this.preprocessor(preprocessor)
232
- this.treeProcessor(treeProcessor)
233
- })
234
- registry.groups.$send('[]=', 'reducer', extGroup)
235
- }