@antora/assembler 1.0.0-beta.9 → 1.0.0-rc.10

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.
@@ -1,15 +1,38 @@
1
1
  'use strict'
2
2
 
3
- const createAsciiDocFile = require('./util/create-asciidoc-file')
4
- const path = require('node:path/posix')
3
+ const createResourceKey = require('./util/create-resource-key')
4
+ const generateScopedId = require('./util/generate-scoped-id')
5
+ const { resolveLinkTarget } = require('./util/resolver')
6
+ const {
7
+ rewriteXrefs,
8
+ rewriteImageAttr,
9
+ rewriteImageRef,
10
+ rewriteInlineImages,
11
+ rewriteStyleAttribute,
12
+ } = require('./util/rewriter')
5
13
  const sanitize = require('./util/sanitize')
14
+ const toHash = require('./util/to-hash')
6
15
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
7
16
 
8
- const AttributeEntryRx = /^:([^:-][^:]*):(?: .*)?$/
9
- const BuiltInNamedEntities = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
10
- const CharRefRx = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
11
- const DiscardAttributes = 'doctype leveloffset assembly-navtitle assembly-style underscore'.split(' ')
12
- const ReservedIdNames = 'content header footnotes footer footer-text premable toc toctitle'.split(' ')
17
+ const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
18
+ const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
19
+ const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
20
+ const DISCARD_ATTRIBUTE_NAMES = [
21
+ 'assembly-doctype',
22
+ 'assembly-header-attributes',
23
+ 'assembly-overview-title',
24
+ 'assembly-navtitle',
25
+ 'assembly-root-title',
26
+ 'assembly-slug',
27
+ 'assembly-style',
28
+ 'doctype',
29
+ 'leveloffset',
30
+ 'page-aliases',
31
+ 'preface-title',
32
+ 'underscore',
33
+ ]
34
+ const { NAMED_ID_ATTR_RX } = require('./util/rx')
35
+ const ASSEMBLY_STYLE_SUPPORTS_SECTIONS = ['abstract', 'appendix', 'part', 'preface', 'section']
13
36
 
14
37
  function produceAssemblyFile (
15
38
  loadAsciiDoc,
@@ -22,185 +45,318 @@ function produceAssemblyFile (
22
45
  assemblyModel
23
46
  ) {
24
47
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
- if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
- const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
27
- const buffer = mergeAsciiDoc(
48
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
49
+ if (outline.urlType === 'internal' && !pagesByUrl.has(outline.pathname) && !outline.items?.length) return
50
+ const { name: component, version } = componentVersion
51
+ const { rootLevel, xmlIds } = assemblyModel
52
+ const scratchDoc = loadAsciiDoc(
53
+ {
54
+ contents: Buffer.alloc(0),
55
+ src: { component, version, module: 'ROOT', family: 'page', relative: '_.adoc', path: '_.adoc' },
56
+ },
57
+ undefined,
58
+ Object.assign({}, asciidocConfig, { antoraResourceRefs: false, extensions: [] })
59
+ )
60
+ const idSeparators = {
61
+ prefix:
62
+ 'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
63
+ scope: xmlIds ? '---' : ':::',
64
+ coordinate: xmlIds ? '----' : ':',
65
+ generateIdFromTitle: generateIdFromTitle.bind(scratchDoc),
66
+ }
67
+ asciidocConfig = prepareAsciiDocConfig(
68
+ contentCatalog,
69
+ { component, version },
70
+ pagesInOutline,
71
+ asciidocConfig,
72
+ assemblyModel,
73
+ idSeparators,
74
+ scratchDoc
75
+ )
76
+ const builder = buildAssemblyHeader(componentVersion, outline.content, assemblyModel)
77
+ mergeAsciiDoc(
28
78
  loadAsciiDoc,
29
79
  contentCatalog,
30
- buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
80
+ builder,
31
81
  componentVersion,
32
82
  outline,
33
83
  files,
34
84
  pagesInOutline,
85
+ idSeparators,
35
86
  asciidocConfig,
36
87
  mutableAttributes,
37
88
  assemblyModel
38
89
  )
39
- const rootLevel = assemblyModel.rootLevel
40
- const stem = rootLevel === 0 ? 'index' : generateSlug(buffer.navtitle)
41
- const downloadStem = [componentVersion.name, componentVersion.version, rootLevel === 0 ? '' : stem]
42
- .filter((it) => it)
43
- .join('-')
44
- return createAsciiDocFile(contentCatalog, {
90
+ const stem =
91
+ (builder.slug ? sanitizeSlug(builder.slug) : generateSlug(rootLevel === 0 ? 'index' : builder.baseDoctitle)) ||
92
+ 'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
93
+ const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
94
+ const file = contentCatalog.createFile({
45
95
  asciidoc: asciidocConfig,
46
96
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
47
- contents: Buffer.from(buffer.join('\n') + '\n'),
48
- src: {
49
- component: componentVersion.name,
50
- version: componentVersion.version,
51
- module: 'ROOT',
52
- family: 'export',
53
- relative: stem + '.adoc',
54
- },
97
+ contents: builder.serialize(),
98
+ src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
99
+ pub: false,
55
100
  })
101
+ file.path = file.out.path // use out path as path so assets can be published to same hierarchy
102
+ delete file.out
103
+ return file
56
104
  }
57
105
 
58
- function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
59
- const doctype = assemblyModel.doctype ?? 'book'
60
- const navtitlePlain = sanitize(navtitle)
106
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators, doc) {
107
+ let attributesModified
108
+ const configShared = asciidocConfig.$shared
109
+ const sharedAttributes = asciidocConfig.attributes
110
+ if (!configShared) {
111
+ if (configShared == null) {
112
+ const assets = pagesInOutline.assembled.assets
113
+ for (const [name, val] of Object.entries(sharedAttributes)) {
114
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
115
+ let newVal
116
+ if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
117
+ if (~(newVal = val).indexOf('image:')) {
118
+ newVal = rewriteInlineImages(newVal, contentCatalog, assemblyModel, ctx, assets)
119
+ }
120
+ }
121
+ if (newVal !== val) sharedAttributes[name] = newVal
122
+ }
123
+ }
124
+ for (const [name, val] of Object.entries(sharedAttributes)) {
125
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
126
+ if (~val.indexOf('xref:')) {
127
+ const newVal = rewriteXrefs(
128
+ val,
129
+ contentCatalog,
130
+ assemblyModel,
131
+ ctx,
132
+ false,
133
+ pagesInOutline,
134
+ idSeparators,
135
+ null,
136
+ doc
137
+ )
138
+ if (newVal !== val) (attributesModified ??= {})[name] = newVal
139
+ }
140
+ }
141
+ if (configShared == null) Object.defineProperty(asciidocConfig, '$shared', { value: attributesModified == null })
142
+ }
143
+ return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
144
+ }
145
+
146
+ function buildAssemblyHeader (componentVersion, navtitle, assemblyModel) {
147
+ const { name: componentName, version, displayVersion, title } = componentVersion
61
148
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
62
- let doctitle = navtitleAsciiDoc
63
- if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
64
- const version = componentVersion.version === 'master' ? '' : componentVersion.version
65
- const buffer = [
66
- `= ${doctitle}`,
67
- ...(version ? [`:revnumber: ${version}`] : []),
68
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
69
- ':underscore: _',
70
- // Q: should we pass these via the CLI so they cannot be modified?
71
- `:page-component-name: ${componentVersion.name}`,
72
- `:page-component-version:${version ? ' ' + version : ''}`,
73
- ':page-version: {page-component-version}',
74
- `:page-component-display-version: ${componentVersion.displayVersion}`,
75
- `:page-component-title: ${componentVersion.title}`,
76
- ]
77
- return Object.assign(buffer, { navtitle })
149
+ const navtitlePlain = sanitize(navtitle)
150
+ const doctitleQualifier = assemblyModel.rootLevel === 0 ? '' : title
151
+ const versioned = !!(version && version !== 'master')
152
+ return {
153
+ header: [
154
+ `= ${doctitleQualifier && doctitleQualifier !== navtitlePlain ? doctitleQualifier + ': ' : ''}${navtitleAsciiDoc}`,
155
+ ...(versioned ? [`:revnumber: ${displayVersion}`] : []),
156
+ `:doctype: ${(assemblyModel.doctype ??= 'book')}`,
157
+ ':underscore: _',
158
+ `:page-component-name: ${componentName}`,
159
+ `:page-component-version:${versioned ? ' ' + version : ''}`,
160
+ ':page-version: {page-component-version}',
161
+ `:page-component-display-version: ${displayVersion}`,
162
+ `:page-component-title: ${title}`,
163
+ ],
164
+ currentComponentVersion: componentVersion,
165
+ doctitleQualifier,
166
+ baseDoctitle: navtitle,
167
+ baseDoctitlePlain: navtitlePlain,
168
+ setAuthor (author) {
169
+ this.header.splice(1, 0, author)
170
+ },
171
+ setDoctitle (doctitle, doctitleAsciiDoc, doctitlePlain) {
172
+ this.baseDoctitle = doctitle
173
+ this.baseDoctitlePlain = doctitlePlain
174
+ const qualifyDoctitle = this.doctitleQualifier ? this.doctitleQualifier !== doctitlePlain : false
175
+ this.header[0] = `= ${qualifyDoctitle ? this.doctitleQualifier + ': ' : ''}${doctitleAsciiDoc}`
176
+ },
177
+ matchesDoctitle (candidate) {
178
+ return candidate === this.baseDoctitlePlain
179
+ },
180
+ setDoctype (doctype) {
181
+ this.header[this.header.findIndex((line) => line.startsWith(':doctype: '))] = `:doctype: ${doctype}`
182
+ },
183
+ setDocid (docid) {
184
+ this.header.unshift(`[#${docid}]`)
185
+ },
186
+ serialize () {
187
+ const normalizedHeader = this.header.filter((it) => it !== ':doctype: article')
188
+ return this.body
189
+ ? Buffer.from(normalizedHeader.join('\n') + '\n\n' + this.body.join('\n') + '\n')
190
+ : Buffer.from(normalizedHeader.join('\n') + '\n')
191
+ },
192
+ }
78
193
  }
79
194
 
80
- function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
195
+ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
81
196
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
82
- const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
83
- if (page) {
84
- if (page.src.component === componentVersion.name && page.src.version === componentVersion.version) {
85
- accum.set(`${page.src.module === 'ROOT' ? '' : page.src.module + ':'}${page.src.relative}`, page)
197
+ const { urlType, url, hash, unresolved, items = [] } = outlineEntry
198
+ if (urlType === 'internal' && !unresolved) {
199
+ const pathname = (outlineEntry.pathname ??= hash ? url.substring(0, url.length - hash.length) : url)
200
+ let page
201
+ if (!accum.has(pathname) && (page = pagesByUrl.get(pathname))) {
202
+ accum.set(createResourceKey(page.src), page)
203
+ accum.set(pathname, page)
86
204
  }
87
- accum.set(page.pub.url, page)
88
205
  }
89
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
206
+ for (const item of items) selectPagesInOutline(item, pagesByUrl, accum)
90
207
  return accum
91
208
  }
92
209
 
93
210
  function mergeAsciiDoc (
94
211
  loadAsciiDoc,
95
212
  contentCatalog,
96
- buffer,
213
+ builder,
97
214
  componentVersion,
98
215
  outlineEntry,
99
216
  files,
100
217
  pagesInOutline,
218
+ idSeparators,
101
219
  asciidocConfig,
102
220
  mutableAttributes,
103
221
  assemblyModel,
104
- lastComponentVersion = componentVersion,
105
222
  level = 0,
106
223
  supportsParts = false
107
224
  ) {
225
+ const { items = [], roles = [], unresolved, urlType, url, pathname, hash } = outlineEntry
108
226
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
109
- if (outlineEntry.hash) {
110
- buffer.inBody ??= false
111
- return buffer
227
+ if (roles.includes('site-only') || (urlType === 'external' && !URL.canParse(url))) {
228
+ builder.body ??= []
229
+ return builder
112
230
  }
113
- let navtitle = outlineEntry.content
114
- let navtitlePlain = sanitize(navtitle)
115
- let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
116
- const { items = [], unresolved, urlType, url } = outlineEntry
117
- const {
118
- doctype,
119
- filetype,
120
- embedReferenceStyle: embedRefStyle,
121
- linkReferenceStyle: linkRefStyle,
122
- outDirname,
123
- siteRoot,
124
- xmlIds,
125
- } = assemblyModel
231
+ const assembled = pagesInOutline.assembled
126
232
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
127
- const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
128
- const atDocumentRoot = !buffer.inBody
129
- const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
233
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(pathname) : undefined
234
+ let navtitle = outlineEntry.navtitle ?? (page && hash ? page.asciidoc.navtitle : outlineEntry.content)
235
+ let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
236
+ let navtitlePlain = sanitize(navtitle)
237
+ const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, logger, rootPageStyle } = assemblyModel
238
+ let sectionMergeStrategy = assemblyModel.sectionMergeStrategy
239
+ const atStartOfBody = !builder.body?.length
240
+ const isRootEntry = atStartOfBody && !level
241
+ let atBookRoot = isRootEntry && doctype === 'book' && (supportsParts = true)
130
242
  const hasItems = items.length > 0
131
- const pubRoot = outDirname ? '/' + outDirname : ''
132
- const idSeparator = xmlIds ? '-' : ':'
133
- const idScopeSeparator = idSeparator.repeat(3)
134
- const idCoordinateSeparator = idSeparator === '-' ? '----' : idSeparator
135
- if (page && !pagesInOutline.assembled.pages.has(page)) {
243
+ if (page && !assembled.pages.has(page)) {
136
244
  const contents = page.src.contents
137
245
  if (contents == null) {
138
- buffer.inBody ??= false
139
- return buffer
246
+ builder.body ??= []
247
+ return builder
140
248
  }
141
- const { component, version, module: module_, relative, origin, mediaType } = page.src
142
- const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
143
249
  const pageAsAsciiDoc = new page.constructor(
144
- Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
250
+ Object.assign({}, page, { contents: Buffer.from(contents.toString().trimEnd()), mediaType: page.src.mediaType })
145
251
  )
146
252
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
147
- if (doc.hasAttribute('assembly-navtitle')) {
253
+ if (typeof doc.applyLogger === 'function') doc.applyLogger()
254
+ if (logger) doc.getLogger().delegate = logger
255
+ doc.catalog.syntheticIds = {}
256
+ doc.source_header_attributes ??= toHash()
257
+ const isRootPage = isRootEntry
258
+ const { component, version, module: module_, relative, origin } = page.src
259
+ const doctypeOverride = doc.getAttribute('assembly-doctype')
260
+ if (doctypeOverride) {
261
+ if (doctypeOverride !== doctype) builder.setDoctype(doctypeOverride)
262
+ if (doctypeOverride !== 'book') atBookRoot = supportsParts = false
263
+ }
264
+ if (isRootPage && doc.hasAttribute('assembly-slug')) builder.slug = doc.getAttribute('assembly-slug')
265
+ let hasAssemblyNavtitleAttr
266
+ if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
148
267
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
149
268
  navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
150
- if (buffer.inBody == null) {
151
- buffer.navtitle = navtitle
152
- // Q do we need to assert !level here?
153
- if (assemblyModel.rootLevel) {
154
- let doctitle = navtitleAsciiDoc
155
- if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
156
- buffer[0] = `= ${doctitle}`
157
- }
158
- }
159
- }
160
- if (atDocumentRoot) {
161
- const authors = doc.getAuthors()
162
- if (authors.length) {
163
- const authorLine = authors
164
- .map((author) => {
165
- const email = author.getEmail()
166
- return email ? `${author.getName()} <${author.getEmail()}>` : author.getName()
167
- })
168
- .join('; ')
169
- buffer.splice(1, 0, authorLine)
170
- }
269
+ if (isRootPage) builder.setDoctitle(navtitle, navtitleAsciiDoc, navtitlePlain)
171
270
  }
271
+ const overviewTitle = doc.getAttribute('assembly-overview-title', 'Overview')
272
+ let overviewTitleAsciiDoc
172
273
  // NOTE: in Antora, docname is relative src path from module without file extension
173
274
  const docname = doc.getAttribute('docname')
174
- const docnameForId = docname.replace(/[/.]/g, '-')
175
- const qualifyId = component !== componentVersion.name
176
- let idScope = docnameForId
177
- let idPrefix
178
- if (qualifyId) {
179
- idScope = [component, module_ === 'ROOT' ? '' : module_, idScope].join(idCoordinateSeparator)
180
- } else if (module_ !== 'ROOT') {
181
- idScope = module_ + idCoordinateSeparator + idScope
182
- } else if (ReservedIdNames.includes(docnameForId)) {
183
- idScope = idPrefix = idScope + idScopeSeparator
184
- }
185
- idPrefix ??= idScope + idScopeSeparator
186
- let pageFragment = ''
187
- let pageRoles = ''
275
+ const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
276
+ const pageIdLeader = pageId + idSeparators.scope
277
+ let pageFragment = `#${pageId}`
278
+ let pageRoles = []
188
279
  let pageStyle = doc.getAttribute('assembly-style', '')
280
+ if (!pageStyle && isRootPage && hasItems && !doc.source_header_attributes['$key?']('assembly-style')) {
281
+ pageStyle = rootPageStyle
282
+ }
189
283
  let part
190
- if ((part = pageStyle === 'part')) {
191
- pageStyle = ''
192
- if (!supportsParts) part = undefined
193
- } else if ((part = pageStyle.endsWith('-part'))) {
194
- pageStyle = pageStyle.slice(0, -5)
195
- if (!supportsParts) part = undefined
284
+ if (pageStyle) {
285
+ if (
286
+ pageStyle === 'part' ||
287
+ (pageStyle.endsWith('-part') && (pageStyle = pageStyle.substring(0, pageStyle.length - 5)))
288
+ ) {
289
+ part = supportsParts ? true : (pageStyle = undefined)
290
+ }
291
+ } else {
292
+ pageStyle = undefined
196
293
  }
197
- let nextSectionLevel = 1
294
+ const hasSections = doc.hasSections()
295
+ const supportsSections = pageStyle == null ? true : ASSEMBLY_STYLE_SUPPORTS_SECTIONS.includes(pageStyle)
296
+ const encloseSections = sectionMergeStrategy === 'enclose' && hasItems && hasSections
297
+ let nextSectionLevel = atBookRoot ? undefined : 1 // undefined means allow part or chapter at top level
198
298
  const lines = doc.getSourceLines()
199
299
  const ignoreLines = []
200
- buffer.inBody = true
201
- buffer.push('')
202
- buffer.push(`:docname: ${docname}`)
203
- if (component !== lastComponentVersion.name) {
300
+ const buffer = (builder.body ??= [])
301
+ if (buffer.length) buffer.push('')
302
+ if (doc.hasHeader()) {
303
+ for (const entry of processPageHeader(doc, lines, ignoreLines, isRootPage)) {
304
+ if (entry.type === 'author_line') {
305
+ builder.setAuthor(entry.lines[0])
306
+ } else if (entry.type === 'attribute_entry') {
307
+ const name = entry.name
308
+ if (entry.negated && name === 'sectnums') {
309
+ // catch sectnums missed by reducer; only allow turned off, not turning on
310
+ doc.source_header_attributes['$[]=']('sectnums', null)
311
+ } else if (!entry.negated && !doc.isAttributeLocked(name)) {
312
+ let val, newVal
313
+ if (
314
+ name.endsWith('-image') &&
315
+ (val = doc.getAttribute(name)) &&
316
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets)) != null
317
+ ) {
318
+ if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
319
+ } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
320
+ newVal = val
321
+ if (~newVal.indexOf('image:')) {
322
+ newVal = rewriteInlineImages(
323
+ newVal,
324
+ contentCatalog,
325
+ assemblyModel,
326
+ page.src,
327
+ assembled.assets,
328
+ false,
329
+ doc
330
+ )
331
+ }
332
+ if (~newVal.indexOf('xref:')) {
333
+ newVal = rewriteXrefs(
334
+ newVal,
335
+ contentCatalog,
336
+ assemblyModel,
337
+ page.src,
338
+ false,
339
+ pagesInOutline,
340
+ idSeparators,
341
+ pageIdLeader,
342
+ doc,
343
+ { file: relative, lineno: entry.lineno }
344
+ )
345
+ }
346
+ if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
347
+ }
348
+ }
349
+ ;(entry.promote ? builder.header : buffer).push(...entry.lines)
350
+ } else {
351
+ if (entry.type === 'attrlist') builder.headerHasBlockAttrs = true
352
+ buffer.push(...entry.lines)
353
+ }
354
+ }
355
+ pageRoles = doc.getRoles()
356
+ }
357
+ if (roles.length) pageRoles = pageRoles.concat(roles[0] === 'page' ? roles.slice(1) : roles)
358
+ buffer.push(`:page-docname: ${docname}`)
359
+ if (component !== builder.currentComponentVersion.name) {
204
360
  const thisComponentVersion =
205
361
  component === componentVersion.name && version === componentVersion.version
206
362
  ? componentVersion
@@ -211,7 +367,7 @@ function mergeAsciiDoc (
211
367
  buffer.push(':page-version: {page-component-version}')
212
368
  buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
213
369
  buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
214
- lastComponentVersion = thisComponentVersion
370
+ builder.currentComponentVersion = thisComponentVersion
215
371
  }
216
372
  }
217
373
  buffer.push(`:page-module: ${module_}`)
@@ -221,90 +377,120 @@ function mergeAsciiDoc (
221
377
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
222
378
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
223
379
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
224
- if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
225
380
  let heading
226
- if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
227
- let htitleAsciiDoc = navtitleAsciiDoc
228
- let htitlePlain = navtitlePlain
229
- let htitleOverride
230
- if (
231
- (htitleOverride = pageStyle === 'preface' ? doc.getAttribute('preface-title') : undefined) ||
232
- (atDocumentRoot && (htitleOverride = outlineEntry.navtitle))
233
- ) {
381
+ if (pageStyle != null && isRootPage) {
382
+ let htitleAsciiDoc, htitleOverride
383
+ if (pageStyle === 'preface') {
384
+ const hasPrefaceTitleAttr =
385
+ doc.isAttribute('preface-title') ||
386
+ doc.source_header_attributes['$key?']('preface-title') ||
387
+ doc.isAttributeLocked('preface-title')
388
+ if (hasPrefaceTitleAttr) {
389
+ htitleOverride = doc.getAttribute('preface-title', '')
390
+ } else if (!(htitleOverride = doc.getAttribute('assembly-root-title'))) {
391
+ if (encloseSections) {
392
+ htitleOverride = overviewTitle
393
+ } else if (builder.matchesDoctitle(navtitlePlain)) {
394
+ htitleOverride = ''
395
+ } else {
396
+ htitleOverride = undefined // means use navtitleAsciiDoc
397
+ }
398
+ }
399
+ if (htitleOverride === '') {
400
+ nextSectionLevel = 2
401
+ if (builder.headerHasBlockAttrs) {
402
+ htitleAsciiDoc = ''
403
+ } else {
404
+ sectionMergeStrategy = 'discrete'
405
+ if (atBookRoot && hasPrefaceTitleAttr) {
406
+ builder.header.push(':preface-title:')
407
+ }
408
+ }
409
+ } else {
410
+ htitleAsciiDoc = htitleOverride == null ? navtitleAsciiDoc : unconvertInlineAsciiDoc(htitleOverride)
411
+ if (htitleOverride === overviewTitle) overviewTitleAsciiDoc = htitleAsciiDoc // optimization
412
+ if (atBookRoot && hasPrefaceTitleAttr) {
413
+ if (!builder.headerHasBlockAttrs && (sectionMergeStrategy === 'discrete' || !hasSections)) {
414
+ builder.header.push(`:preface-title: ${htitleAsciiDoc}`)
415
+ htitleAsciiDoc = undefined
416
+ }
417
+ }
418
+ }
419
+ } else if ((htitleOverride = doc.getAttribute('assembly-root-title', ''))) {
234
420
  htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
235
- htitlePlain = sanitize(htitleOverride)
236
- }
237
- if (atDocumentRoot && pageStyle === 'preface' && htitlePlain === componentVersion.title) {
238
- assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
421
+ } else if (encloseSections) {
422
+ htitleAsciiDoc = overviewTitleAsciiDoc = unconvertInlineAsciiDoc(overviewTitle)
239
423
  } else {
240
- if (atDocumentRoot) {
241
- pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
242
- buffer.unshift(`[#${idScope}]`)
424
+ htitleAsciiDoc = navtitleAsciiDoc
425
+ }
426
+ if (htitleAsciiDoc != null) {
427
+ heading = { title: htitleAsciiDoc, hlevel: part ? 1 : 2 }
428
+ if (pageStyle === 'part') {
429
+ nextSectionLevel = 1
243
430
  } else {
244
- pageFragment = `#${idScope}`
431
+ nextSectionLevel = 2 // next section level is 2, even for special part
432
+ // enable next two lines if we always want to promote ID of root page to docid
433
+ //builder.setDocid(pageId)
434
+ //pageFragment = '#' + idSeparators.generateIdFromTitle(htitleAsciiDoc, idSeparators, pageIdLeader)
435
+ if (pageStyle === 'preface' && doctype !== 'book') {
436
+ heading.style = ''
437
+ if (doc.hasAttribute('sectnums')) {
438
+ doc.source_header_attributes['$[]=']('sectnums', null)
439
+ buffer.push(':!sectnums:')
440
+ }
441
+ }
245
442
  }
246
- heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
247
- nextSectionLevel++
248
443
  }
444
+ } else if (part) {
445
+ heading = { title: navtitleAsciiDoc, hlevel: level-- } // NOTE: level will always come in at 1
446
+ nextSectionLevel = pageStyle === 'part' ? 1 : 2
249
447
  } else if (level) {
250
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
448
+ if (atStartOfBody && !pageStyle && builder.matchesDoctitle(navtitlePlain)) {
251
449
  level--
252
- } else {
253
- pageFragment = `#${idScope}`
254
- if (part && level === 1) level--
255
- if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
256
- Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
257
- }
450
+ } else if ((heading = { title: navtitleAsciiDoc, hlevel: level + 1 }).hlevel > 6) {
451
+ Object.assign(heading, { hlevel: 6, style: `discrete.h${heading.hlevel}` })
258
452
  }
259
453
  }
260
454
  if (heading) {
261
- buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
262
- buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
263
- } else if (atDocumentRoot) {
264
- buffer.unshift(`[#${idScope}]`)
265
- }
266
- let enclosed
267
- if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
268
- enclosed = true
269
- // TODO: make overview section title configurable
270
- //let overviewTitle = doc.getDocumentTitle()
271
- //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
272
- const overviewTitle = doc.getAttribute('overview-title', 'Overview')
273
- buffer.push('')
274
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
275
- let toggleSectids, syntheticId
276
- if (doc.isAttribute('sectids')) {
277
- if (doc.isAttributeLocked('sectids')) {
278
- syntheticId = `__object-id-${getObjectId(outlineEntry)}`
279
- } else {
280
- buffer.push(':!sectids:')
281
- toggleSectids = true
282
- }
455
+ const rolesAttr = pageRoles.reduce((str, it) => str + '.' + it, '')
456
+ const notitle = (heading.title ||= '{empty}') === '{empty}'
457
+ // remove conditional if we always want to promote ID of root page to docid
458
+ if (notitle) {
459
+ builder.setDocid(pageId)
460
+ pageFragment = '#' + idSeparators.generateIdFromTitle('', idSeparators, pageIdLeader)
283
461
  }
284
- let hlevel = level + 2
462
+ const style = heading.style ?? (pageStyle === 'part' || pageStyle === 'section' ? '' : (pageStyle ?? ''))
463
+ buffer.push(`[${style}${pageFragment}${rolesAttr}${notitle ? '%notitle' : ''}]`)
464
+ buffer.push(`${'='.repeat(heading.hlevel)} ${heading.title}`)
465
+ if (notitle || !supportsSections) sectionMergeStrategy = 'discrete'
466
+ } else if (atStartOfBody) {
467
+ builder.setDocid(pageId)
468
+ }
469
+ if (isRootPage || (atStartOfBody && !heading)) assembled.pages.rootPageFragment = pageFragment
470
+ assembled.pages.set(page, pageFragment)
471
+ if (encloseSections && !(atStartOfBody && heading)) {
472
+ overviewTitleAsciiDoc ??= unconvertInlineAsciiDoc(overviewTitle)
473
+ const sectionFragment = '#' + idSeparators.generateIdFromTitle(overviewTitleAsciiDoc, idSeparators, pageIdLeader)
474
+ if (heading && buffer.length) buffer.push('')
475
+ let hlevel = level + (nextSectionLevel = 2)
285
476
  if (hlevel > 6) {
286
- const blockStyle = `discrete.h${hlevel}`
477
+ buffer.push(`[discrete.h${hlevel}${sectionFragment}]`)
287
478
  hlevel = 6
288
- buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
289
- } else if (syntheticId) {
290
- buffer.push(`[#${syntheticId}]`)
479
+ } else {
480
+ buffer.push(`[${sectionFragment}]`)
291
481
  }
292
- buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
293
- if (toggleSectids) buffer.push(':sectids:')
294
- }
295
- pagesInOutline.assembled.pages.set(page, pageFragment)
296
- if (doc.hasSections()) {
297
- fixSectionLevels(doc.getSections(), atBookRoot && !pageStyle ? undefined : nextSectionLevel)
482
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitleAsciiDoc}`)
298
483
  }
484
+ if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
299
485
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
300
486
  it.getContext() === 'document'
301
487
  ? it.getDocument().isNested()
302
488
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
303
489
  )
304
490
  if (doc.getDoctype() === 'manpage') {
305
- const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
491
+ const firstSectionIdx = hasSections ? doc.getSections()[0].getLineNumber() - 1 : lines.length
306
492
  for (let idx = 0; idx < firstSectionIdx; idx++) {
307
- if (~ignoreLines.indexOf(idx)) continue
493
+ if (ignoreLines.includes(idx)) continue
308
494
  const line = lines[idx]
309
495
  if (line.startsWith('== ') && line.length > 3) {
310
496
  allBlocks.unshift({
@@ -348,10 +534,10 @@ function mergeAsciiDoc (
348
534
  })
349
535
  let skipping
350
536
  for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
351
- if (~ignoreLines.indexOf(idx)) continue
537
+ if (ignoreLines.includes(idx)) continue
352
538
  let line = lines[idx]
353
539
  if (line.startsWith('//')) {
354
- if (line[2] !== '/') continue
540
+ if (line.charAt(2) !== '/') continue
355
541
  if (line.length > 3 && line === '/'.repeat(line.length)) {
356
542
  if (skipping) {
357
543
  if (line === skipping) skipping = undefined
@@ -366,7 +552,7 @@ function mergeAsciiDoc (
366
552
  if (
367
553
  line.charAt() === ':' &&
368
554
  ~line.indexOf(':', 2) &&
369
- (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
555
+ (line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
370
556
  ) {
371
557
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
372
558
  lines[idx] = undefined
@@ -378,111 +564,31 @@ function mergeAsciiDoc (
378
564
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
379
565
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
380
566
  }
381
- return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
567
+ return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
382
568
  })
383
569
  }
384
570
  // NOTE: the next check takes care of inline and block anchors
385
571
  if (~line.indexOf('[[')) {
386
- line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, `[[${idPrefix}$1$2]]`)
387
- }
388
- if (~line.indexOf('xref:')) {
389
- // Q: should we allow : as first character of target?
390
- line = line.replace(/(?<![\\+])xref:((?:\.\/)?[\p{Alpha}0-9_/.{#].*?)\[(|.*?[^\\])\]/gu, (m, target, text) => {
391
- let relativePart, fragment, resource, isPage
392
- const hashIdx = target.indexOf('#')
393
- const dollarIdx = target.indexOf('$')
394
- if (~hashIdx) {
395
- relativePart = target.slice(0, hashIdx)
396
- fragment = target.slice(hashIdx + 1)
397
- } else if (~dollarIdx || target.endsWith('.adoc')) {
398
- relativePart = target
399
- fragment = ''
400
- } else {
401
- fragment = target
402
- }
403
- // Q: should we validate the internal ID here?
404
- if (!relativePart) return `xref:${idPrefix}${fragment}[${text}]`
405
- if (~dollarIdx) {
406
- if (relativePart.slice(dollarIdx).startsWith('$./')) {
407
- relativePart = relativePart.slice(0, dollarIdx + 1) + topicPrefix + relativePart.slice(dollarIdx + 3)
408
- }
409
- if ((isPage = /\bpage\$/.test(relativePart))) relativePart = relativePart.replace('page$', '')
410
- } else {
411
- isPage = true
412
- if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2)
413
- if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc'
414
- }
415
- if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) {
416
- if (siteRoot && (resource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
417
- text ||= resource.asciidoc?.xreftext || target
418
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
419
- }
420
- // TODO: handle unresolved resource better
421
- return m
422
- }
423
- let targetModule
424
- const colonIdx = relativePart.indexOf(':')
425
- if (~colonIdx) {
426
- targetModule = relativePart.slice(0, colonIdx)
427
- relativePart = relativePart.slice(colonIdx + 1)
428
- if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2)
429
- } else {
430
- targetModule = module_
431
- }
432
- const pageResourceRef = targetModule === 'ROOT' ? relativePart : `${targetModule}:${relativePart}`
433
- if (!(resource = pagesInOutline.get(pageResourceRef))) {
434
- if (siteRoot && (resource = contentCatalog.resolvePage(pageResourceRef, page.src)) && resource.out) {
435
- text ||= resource.asciidoc?.xreftext || target
436
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
437
- }
438
- // TODO: handle unresolved page better
439
- return m
440
- }
441
- if (targetModule !== 'ROOT') relativePart = `${targetModule}${idCoordinateSeparator}${relativePart}`
442
- relativePart = relativePart.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
443
- if (fragment === resource.asciidoc.id) fragment = ''
444
- const refid = fragment
445
- ? `${relativePart}${idScopeSeparator}${fragment}`
446
- : relativePart + (ReservedIdNames.includes(relativePart) ? idScopeSeparator : '')
447
- if (
448
- text &&
449
- (assemblyModel.dropExplicitXrefText === 'always' ||
450
- (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
451
- ) {
452
- text = ''
453
- }
454
- return `xref:${refid}[${text}]`
572
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
573
+ return `[[${pageIdLeader}${refid}${text}]]`
455
574
  })
456
575
  }
457
- if (~line.indexOf('link:{attachmentsdir}/')) {
458
- line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
459
- const attachment =
460
- siteRoot &&
461
- contentCatalog.getById({
462
- component: componentVersion.name,
463
- version: componentVersion.version,
464
- module: module_,
465
- family: 'attachment',
466
- relative,
467
- })
468
- // TODO: handle unresolved attachment page
469
- return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
470
- })
576
+ if (~line.indexOf('xref:')) {
577
+ line = rewriteXrefs(
578
+ line,
579
+ contentCatalog,
580
+ assemblyModel,
581
+ page.src,
582
+ true,
583
+ pagesInOutline,
584
+ idSeparators,
585
+ pageIdLeader,
586
+ doc,
587
+ { file: relative, lineno: idx + 1 }
588
+ )
471
589
  }
472
590
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
473
- line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
474
- if (isResourceSpec(target)) {
475
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
476
- // TODO: handle (or report) unresolved image better
477
- if (image?.out && (filetype !== 'html' || siteRoot)) {
478
- pagesInOutline.assembled.assets.add(image)
479
- return filetype === 'html'
480
- ? `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
481
- : `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
482
- }
483
- }
484
- return m
485
- })
591
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
486
592
  }
487
593
  lines[idx] = line
488
594
  }
@@ -498,9 +604,9 @@ function mergeAsciiDoc (
498
604
  let idx = lineno - 1
499
605
  if (context === 'section' && !block.getDocument().isNested()) {
500
606
  if (block.getSectionName() === 'header') return
501
- let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
607
+ let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
502
608
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
503
- let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
609
+ let targetMarkerLength = block.level + 1 + level
504
610
  if (targetMarkerLength > 6) {
505
611
  blockStyle = `discrete.h${targetMarkerLength}`
506
612
  targetMarkerLength = 6
@@ -508,56 +614,49 @@ function mergeAsciiDoc (
508
614
  return '='.repeat(targetMarkerLength) + ' ' + rest
509
615
  })
510
616
  // NOTE: ID will be undefined if sectids are turned off
511
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
617
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
512
618
  } else {
513
619
  if (context === 'image') {
514
620
  let line = lines[idx] || ''
515
621
  let prefix = ''
516
622
  // Q: can we use startsWith('image::') in certain cases?
517
623
  let imageMacroOffset = (
518
- lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
624
+ lastImageMacroAt?.[0] === idx ? line.substring(0, lastImageMacroAt[1]) : line
519
625
  ).lastIndexOf('image::')
520
626
  if (imageMacroOffset > 0) {
521
627
  if (
522
628
  block.getDocument().isNested() &&
523
- (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
629
+ (prefix = line.substring(0, imageMacroOffset)).trimRight().endsWith('|')
524
630
  ) {
525
- line = line.slice(prefix.length)
631
+ line = line.substring(prefix.length)
526
632
  } else {
527
633
  imageMacroOffset = -1
528
634
  }
529
635
  }
530
- if (imageMacroOffset >= 0) {
636
+ if (~imageMacroOffset) {
531
637
  const target = block.getAttribute('target')
532
- if (isResourceSpec(target)) {
533
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
534
- // FIXME: handle (or report) case when image is not resolved
535
- if (image?.out && (filetype !== 'html' || siteRoot)) {
536
- const attrlist = line.slice(line.indexOf('[') + 1, -1)
537
- pagesInOutline.assembled.assets.add(image)
538
- lines[idx] =
539
- filetype === 'html'
540
- ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
541
- : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
542
- }
543
- }
638
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
639
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.substring(line.indexOf('['))}`
544
640
  lastImageMacroAt = [idx, imageMacroOffset]
545
641
  }
546
642
  } else if (context === 'document' && block.hasHeader()) {
547
643
  // nested document
548
644
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
549
645
  }
550
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
646
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
551
647
  }
552
648
  })
553
649
  safePush(
554
650
  buffer,
555
651
  lines.filter((it) => it !== undefined)
556
652
  )
557
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
653
+ const attributeEntries = doc.source_header_attributes.$entries()
558
654
  if (attributeEntries.length) {
559
655
  const resolvedAttributeEntries = attributeEntries.reduce(
560
656
  (accum, [name, val]) => {
657
+ if (val) {
658
+ if (val['$nil?']()) val = null
659
+ }
561
660
  // Q: couldn't we just check if attribute is locked?
562
661
  if (name in mutableAttributes) {
563
662
  const initialVal = mutableAttributes[name]
@@ -566,7 +665,7 @@ function mergeAsciiDoc (
566
665
  } else if (val !== initialVal) {
567
666
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
568
667
  }
569
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
668
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
570
669
  accum.push(`:!${name}:`)
571
670
  }
572
671
  return accum
@@ -575,87 +674,88 @@ function mergeAsciiDoc (
575
674
  )
576
675
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
577
676
  }
578
- } else if (level) {
579
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
580
- buffer.inBody ??= false
677
+ if (typeof doc.restoreLogger === 'function') doc.restoreLogger()
678
+ } else if (level && !hash) {
679
+ const buffer = (builder.body ??= [])
680
+ if (atStartOfBody && builder.matchesDoctitle(navtitlePlain)) {
581
681
  level--
582
682
  } else {
583
- buffer.inBody = true
584
- buffer.push('')
585
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
586
- // Q: should we unset docname, page-module, etc?
587
- let toggleSectids, syntheticId
588
- if (!('sectids' in asciidocConfig.attributes)) {
589
- buffer.push(':!sectids:')
590
- toggleSectids = true
591
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
592
- if ('sectids' in mutableAttributes) {
593
- buffer.push(':!sectids:')
594
- toggleSectids = true
595
- } else {
596
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
597
- }
598
- }
683
+ if (buffer.length) buffer.push('')
684
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
599
685
  let sectionTitle = navtitleAsciiDoc
600
686
  if (urlType === 'external') {
601
687
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
602
688
  } else if (urlType === 'internal' && !unresolved) {
603
- const resource = files.find((it) => it.pub.url === url)
689
+ const resource = files.find((it) => it.pub.url === pathname)
604
690
  if (resource) {
605
691
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
606
- let refid = resource.src.relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
607
- if (refid.startsWith('./')) refid = topicPrefix + refid.slice(2)
608
- if (resource.src.module !== 'ROOT') refid = `${resource.src.module}${idCoordinateSeparator}${refid}`
692
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
609
693
  sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
610
694
  } else if (siteRoot) {
611
- sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
695
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
612
696
  }
613
697
  }
614
698
  }
615
- let hlevel = level + 1
616
- if (hlevel > 6) {
617
- hlevel = 6
618
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
619
- } else if (syntheticId) {
620
- buffer.push(`[#${syntheticId}]`)
699
+ if (supportsParts && level === 1 && roles.includes('part')) {
700
+ roles.splice(roles.indexOf('part', 1))
701
+ level--
621
702
  }
703
+ const hlevel = level > 5 ? 6 : level + 1
704
+ const hroles = urlType === 'internal' ? roles.slice(1) : roles
705
+ buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
622
706
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
623
- if (toggleSectids) buffer.push(':sectids:')
624
707
  }
625
708
  }
626
-
627
- if (hasItems) {
628
- const nextLevel = level + 1
629
- // NOTE: drop first child if same as parent; should we keep if content is different?
630
- ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
631
- ? items.slice(1)
632
- : items
633
- ).forEach((item) => {
634
- mergeAsciiDoc(
635
- loadAsciiDoc,
636
- contentCatalog,
637
- buffer,
638
- componentVersion,
639
- item,
640
- files,
641
- pagesInOutline,
642
- asciidocConfig,
643
- mutableAttributes,
644
- assemblyModel,
645
- lastComponentVersion,
646
- nextLevel,
647
- atBookRoot
648
- )
649
- })
650
- }
651
- return buffer
709
+ if (!hasItems) return
710
+ const nextLevel = level + 1
711
+ // NOTE: drop first child if same as parent; should we keep if content is different?
712
+ ;(urlType === 'internal' && items[0].urlType === 'internal' && pathname === items[0].url && !items[0].items?.length
713
+ ? items.slice(1)
714
+ : items
715
+ ).forEach((item) => {
716
+ mergeAsciiDoc(
717
+ loadAsciiDoc,
718
+ contentCatalog,
719
+ builder,
720
+ componentVersion,
721
+ item,
722
+ files,
723
+ pagesInOutline,
724
+ idSeparators,
725
+ asciidocConfig,
726
+ mutableAttributes,
727
+ assemblyModel,
728
+ nextLevel,
729
+ atBookRoot
730
+ )
731
+ })
652
732
  }
653
733
 
654
- function processDocumentHeader (doc, lines, buffer, ignoreLines) {
734
+ function processPageHeader (doc, lines, ignoreLines, isRootPage) {
735
+ const entries = []
736
+ let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
737
+ assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
738
+ if (assemblyHeaderAttributes.size) {
739
+ if (assemblyHeaderAttributes.has('%authors')) {
740
+ const authors = doc.getAuthors()
741
+ if (authors.length) {
742
+ entries.push({
743
+ type: 'author_line',
744
+ promote: true,
745
+ lines: [
746
+ authors
747
+ .map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
748
+ .join('; '),
749
+ ],
750
+ })
751
+ }
752
+ assemblyHeaderAttributes.delete('%authors')
753
+ }
754
+ for (const name of assemblyHeaderAttributes) doc.source_header_attributes.$delete(name)
755
+ }
655
756
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
656
757
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
657
- let belowDoctitle
658
- let open
758
+ let belowDoctitle, current, open
659
759
  const implicitLines = []
660
760
  for (let idx = 0; idx < end; idx++) {
661
761
  if (idx === doctitleIdx) {
@@ -666,41 +766,54 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
666
766
  }
667
767
  const line = lines[idx]
668
768
  if (open === ':' || open === '-:') {
669
- if (line) {
670
- if (open === ':') buffer.push(line)
671
- if (!line.endsWith(' \\')) open = undefined
672
- } else {
673
- open = undefined
674
- }
769
+ if (open === ':') current.appendLine(line)
770
+ if (!line?.endsWith(' \\')) current = open = undefined
675
771
  } else if (line) {
676
- const chr0 = line.charAt()
677
772
  let attributeEntryMatch
773
+ const chr0 = line.charAt()
678
774
  if (chr0 === '/' && line.charAt(1) === '/') {
679
- if (line.startsWith('////')) {
680
- open = open ? (open === line ? undefined : open) : line
681
- } else if (belowDoctitle && !open && line.charAt(2) === '/') {
775
+ if (line.startsWith('////') && line === '/'.repeat(line.length)) {
776
+ if (open) {
777
+ current.appendLine(line)
778
+ if (open === line) current = open = undefined
779
+ } else {
780
+ entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
781
+ current.appendLine = Array.prototype.push.bind(current.lines)
782
+ }
783
+ } else if (open) {
784
+ current.appendLine(line)
785
+ } else if (belowDoctitle && line.charAt(2) === '/') {
682
786
  break
787
+ } else {
788
+ entries.push({ type: 'line_comment', lines: [line] })
789
+ current = undefined
683
790
  }
684
- buffer.push(line)
685
791
  } else if (open) {
686
- buffer.push(line)
687
- } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
688
- const attributeName = attributeEntryMatch[1].replace('!', '')
689
- if (DiscardAttributes.includes(attributeName)) {
690
- if (line.endsWith(' \\')) open = '-:'
792
+ current.appendLine(line)
793
+ } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
794
+ let name = attributeEntryMatch[1]
795
+ const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
796
+ if (negated) name = name.replace('!', '')
797
+ if (DISCARD_ATTRIBUTE_NAMES.includes(name) && !assemblyHeaderAttributes.has(name)) {
798
+ if (line.endsWith(' \\')) open = '-:' // disallow value continuation
691
799
  } else {
692
800
  if (line.endsWith(' \\')) open = ':'
693
- buffer.push(line)
801
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [], lineno: idx + 1 }))
802
+ current.lines.push(negated ? `:!${name}:` : line)
803
+ current.appendLine = Array.prototype.push.bind(negated ? [] : current.lines)
804
+ if (assemblyHeaderAttributes.has(name)) current.promote = true
694
805
  }
695
806
  } else if (belowDoctitle) {
696
807
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
697
808
  implicitLines.push(line)
698
809
  } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
810
+ current = undefined
699
811
  const attrlist = line
700
- .slice(1, -1)
812
+ .substring(1, line.length - 1)
701
813
  .trim()
702
- .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
703
- if (attrlist) buffer.push('[' + attrlist + ']')
814
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
815
+ .replace(NAMED_ID_ATTR_RX, '')
816
+ if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
704
817
  }
705
818
  } else if (belowDoctitle) {
706
819
  break
@@ -708,23 +821,26 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
708
821
  lines[idx] = undefined
709
822
  ignoreLines.push(idx)
710
823
  }
711
- return doc
712
- .getRoles()
713
- .map((role) => '.' + role)
714
- .join('')
824
+ return entries
715
825
  }
716
826
 
717
- function generateSlug (title) {
718
- return title
719
- .toLowerCase()
720
- .replace(/<[^>]+>/g, '')
721
- .replace(CharRefRx, (_, name, dec, hex) => {
722
- if (name) return BuiltInNamedEntities[name] ?? '?'
723
- return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
724
- })
725
- .replace(/[\x27\u2019]/g, '')
726
- .replace(/[^\p{Alpha}0-9-]/gu, '-')
727
- .replace(/^-+|-+$|(-)-+/g, '$1')
827
+ function generateSlug (string) {
828
+ if (string === 'index') return string
829
+ return sanitizeSlug(
830
+ string
831
+ .toLowerCase()
832
+ .replace(/<[^>]+>/g, '')
833
+ .replace(CHAR_REF_RX, (_, name, dec, hex) => {
834
+ if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
835
+ return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
836
+ })
837
+ .replace(/[\x27\u2019]/g, '')
838
+ .replace(/[_.]/, '-')
839
+ )
840
+ }
841
+
842
+ function sanitizeSlug (slug) {
843
+ return slug.replace(/[^\p{Alpha}0-9_.-]/gu, '-').replace(/^-+|-+$|(-)-+/g, '$1')
728
844
  }
729
845
 
730
846
  function fixSectionLevels (sections, expectedLevel) {
@@ -735,115 +851,29 @@ function fixSectionLevels (sections, expectedLevel) {
735
851
  })
736
852
  }
737
853
 
738
- function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') {
739
- let prevLine = lines[idx - 1]
740
- const char0 = prevLine?.charAt()
741
- if (char0) {
742
- if (
743
- (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
744
- (char0 === '[' &&
745
- prevLine.charAt(1) === '[' &&
746
- /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
747
- ) {
748
- return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle)
749
- }
750
- }
751
- let cellSpec
752
- if (
753
- char0 &&
754
- (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
755
- prevLine.charAt(prevLine.length - 1) === ']'
756
- ) {
757
- if (cellSpec) {
758
- prevLine = cellSpec[2]
759
- cellSpec = cellSpec[1]
760
- }
761
- let rawStyle
762
- const commaIdx = prevLine.indexOf(',')
763
- if (~commaIdx) {
764
- rawStyle = prevLine.slice(1, commaIdx)
765
- if (~rawStyle.indexOf('=')) rawStyle = undefined
766
- } else if (!~prevLine.indexOf('=')) {
767
- rawStyle = prevLine.slice(1, prevLine.length - 1)
768
- }
769
- if (rawStyle) {
770
- if (~rawStyle.indexOf('#')) {
771
- prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`)
772
- if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
773
- } else {
774
- prevLine = `[${
775
- replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
776
- }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
777
- }
778
- } else {
779
- prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
780
- }
781
- if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
782
- lines[idx - 1] = prevLine
783
- } else {
784
- lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`)
785
- }
786
- }
787
-
788
- function isResourceSpec (str) {
789
- return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
790
- }
791
-
792
- function getObjectId (obj) {
793
- return global.Opal.id(obj)
794
- }
795
-
796
- // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
797
- // IMPORTANT: this must not leave behind lines the parser will drop!
798
- // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
799
- function trimAsciiDoc (buffer) {
800
- return Buffer.from(
801
- buffer
802
- .toString()
803
- .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
804
- .trimRight()
805
- )
806
- }
807
-
808
854
  function safePush (onto, entries) {
809
855
  try {
810
856
  onto.push(...entries)
811
- } catch (err) {
812
- /* istanbul ignore if */
813
- if (!(err instanceof RangeError)) throw err
857
+ } catch {
814
858
  for (const e of entries) onto.push(e)
815
859
  }
816
860
  }
817
861
 
818
- function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
819
- const target =
820
- referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
821
- return escapeForInline ? target.replace(/_/g, '{underscore}') : target
822
- }
823
-
824
- function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
825
- let target
826
- if (resource.site?.url) {
827
- target = ['', resource.pub.url]
828
- } else {
829
- switch (referenceStyle) {
830
- case 'absolute':
831
- target = ['', siteRoot.url + resource.pub.url]
832
- break
833
- case 'root-relative':
834
- target = ['link:', siteRoot.path + resource.pub.url]
835
- break
836
- default:
837
- target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
838
- }
862
+ function generateIdFromTitle (titleAsciiDoc, idSeparators, idLeader) {
863
+ const Section = this.$class().$$base_module.Section
864
+ // NOTE: refs in catalog will always be empty
865
+ const baseId = titleAsciiDoc ? Section.$generate_id(titleAsciiDoc, this) : '_'
866
+ let id = `_${idSeparators.coordinate}${idLeader ?? ''}${baseId}`
867
+ const syntheticIds = (this.catalog.syntheticIds ??= {})
868
+ if (id in syntheticIds) {
869
+ const sep = (this.getAttribute('idseparator') ?? '_').charAt()
870
+ let cnt = this.$class().$$base_module.Compliance.unique_id_start_index
871
+ let candidate
872
+ while ((candidate = `${id}${sep}${cnt}`) in syntheticIds) cnt++
873
+ id = candidate
839
874
  }
840
- if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
841
- return target.join('')
842
- }
843
-
844
- function computeRelativeUrl (from, to) {
845
- const rel = path.relative(from, to)
846
- return to.charAt(to.length - 1) === '/' ? rel + '/' : rel
875
+ syntheticIds[id] = true
876
+ return id
847
877
  }
848
878
 
849
879
  module.exports = produceAssemblyFile