@antora/assembler 1.0.0-rc.1 → 1.0.0-rc.11

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.
@@ -11,22 +11,29 @@ const {
11
11
  rewriteStyleAttribute,
12
12
  } = require('./util/rewriter')
13
13
  const sanitize = require('./util/sanitize')
14
+ const toHash = require('./util/to-hash')
14
15
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
15
16
 
16
17
  const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
17
18
  const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
18
19
  const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
19
20
  const DISCARD_ATTRIBUTE_NAMES = [
20
- 'doctype',
21
- 'leveloffset',
22
- 'preface-title',
21
+ 'assembly-docname',
22
+ 'assembly-doctitle',
23
+ 'assembly-doctype',
23
24
  'assembly-header-attributes',
25
+ 'assembly-overview-title',
24
26
  'assembly-navtitle',
25
- 'assembly-slug',
27
+ 'assembly-root-title',
26
28
  'assembly-style',
29
+ 'doctype',
30
+ 'leveloffset',
31
+ 'page-aliases',
32
+ 'preface-title',
27
33
  'underscore',
28
34
  ]
29
35
  const { NAMED_ID_ATTR_RX } = require('./util/rx')
36
+ const ASSEMBLY_STYLE_SUPPORTS_SECTIONS = ['abstract', 'appendix', 'part', 'preface', 'section']
30
37
 
31
38
  function produceAssemblyFile (
32
39
  loadAsciiDoc,
@@ -39,35 +46,39 @@ function produceAssemblyFile (
39
46
  assemblyModel
40
47
  ) {
41
48
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
42
- if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
43
49
  const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
50
+ if (outline.urlType === 'internal' && !pagesByUrl.has(outline.pathname) && !outline.items?.length) return
51
+ const { name: component, version } = componentVersion
44
52
  const { rootLevel, xmlIds } = assemblyModel
53
+ const scratchDoc = loadAsciiDoc(
54
+ {
55
+ contents: Buffer.alloc(0),
56
+ src: { component, version, module: 'ROOT', family: 'page', relative: '_.adoc', path: '_.adoc' },
57
+ },
58
+ undefined,
59
+ Object.assign({}, asciidocConfig, { antoraResourceRefs: false, extensions: [] })
60
+ )
45
61
  const idSeparators = {
46
62
  prefix:
47
63
  'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
48
64
  scope: xmlIds ? '---' : ':::',
49
65
  coordinate: xmlIds ? '----' : ':',
50
- generateIdFromTitle: generateIdFromTitle.bind(
51
- loadAsciiDoc(
52
- { contents: Buffer.alloc(0), src: { family: 'page', relative: 'generate-id-from-title.adoc' } },
53
- undefined,
54
- asciidocConfig
55
- )
56
- ),
66
+ generateIdFromTitle: generateIdFromTitle.bind(scratchDoc),
57
67
  }
58
- const { name: component, version } = componentVersion
59
68
  asciidocConfig = prepareAsciiDocConfig(
60
69
  contentCatalog,
61
70
  { component, version },
62
71
  pagesInOutline,
63
72
  asciidocConfig,
64
73
  assemblyModel,
65
- idSeparators
74
+ idSeparators,
75
+ scratchDoc
66
76
  )
67
- const buffer = mergeAsciiDoc(
77
+ const builder = buildAssemblyHeader(componentVersion, outline.content, assemblyModel)
78
+ mergeAsciiDoc(
68
79
  loadAsciiDoc,
69
80
  contentCatalog,
70
- buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
81
+ builder,
71
82
  componentVersion,
72
83
  outline,
73
84
  files,
@@ -78,22 +89,24 @@ function produceAssemblyFile (
78
89
  assemblyModel
79
90
  )
80
91
  const stem =
81
- (buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)) ||
92
+ (builder.docname
93
+ ? sanitizeDocname(builder.docname)
94
+ : generateDocname(rootLevel === 0 ? 'index' : builder.baseDoctitle)) ||
82
95
  'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
83
96
  const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
84
97
  const file = contentCatalog.createFile({
85
98
  asciidoc: asciidocConfig,
86
99
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
87
- contents: Buffer.from(buffer.join('\n') + '\n'),
100
+ contents: builder.serialize(),
88
101
  src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
89
102
  pub: false,
90
103
  })
91
- file.path = file.out.path // use out path as file path so assets can easily be published to same hierarchy
104
+ file.path = file.out.path // use out path as path so assets can be published to same hierarchy
92
105
  delete file.out
93
106
  return file
94
107
  }
95
108
 
96
- function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
109
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators, doc) {
97
110
  let attributesModified
98
111
  const configShared = asciidocConfig.$shared
99
112
  const sharedAttributes = asciidocConfig.attributes
@@ -101,7 +114,7 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
101
114
  if (configShared == null) {
102
115
  const assets = pagesInOutline.assembled.assets
103
116
  for (const [name, val] of Object.entries(sharedAttributes)) {
104
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
117
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
105
118
  let newVal
106
119
  if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
107
120
  if (~(newVal = val).indexOf('image:')) {
@@ -112,9 +125,19 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
112
125
  }
113
126
  }
114
127
  for (const [name, val] of Object.entries(sharedAttributes)) {
115
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
128
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
116
129
  if (~val.indexOf('xref:')) {
117
- const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
130
+ const newVal = rewriteXrefs(
131
+ val,
132
+ contentCatalog,
133
+ assemblyModel,
134
+ ctx,
135
+ false,
136
+ pagesInOutline,
137
+ idSeparators,
138
+ null,
139
+ doc
140
+ )
118
141
  if (newVal !== val) (attributesModified ??= {})[name] = newVal
119
142
  }
120
143
  }
@@ -123,43 +146,74 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
123
146
  return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
124
147
  }
125
148
 
126
- function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
127
- const doctype = assemblyModel.doctype ?? 'book'
128
- const navtitlePlain = sanitize(navtitle)
149
+ function buildAssemblyHeader (componentVersion, navtitle, assemblyModel) {
150
+ const { name: componentName, version, displayVersion, title } = componentVersion
129
151
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
130
- const doctitle = (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
131
- const version = componentVersion.version === 'master' ? '' : componentVersion.version
132
- const displayVersion = componentVersion.displayVersion
133
- const buffer = [
134
- `= ${doctitle}`,
135
- ...(version ? [`:revnumber: ${displayVersion}`] : []),
136
- ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
137
- ':underscore: _',
138
- // Q: should we pass these via the CLI so they cannot be modified?
139
- `:page-component-name: ${componentVersion.name}`,
140
- `:page-component-version:${version ? ' ' + version : ''}`,
141
- ':page-version: {page-component-version}',
142
- `:page-component-display-version: ${displayVersion}`,
143
- `:page-component-title: ${componentVersion.title}`,
144
- ]
145
- return Object.assign(buffer, { navtitle })
152
+ const navtitlePlain = sanitize(navtitle)
153
+ const doctitleQualifier = assemblyModel.rootLevel === 0 ? undefined : title
154
+ const versioned = !!(version && version !== 'master')
155
+ return {
156
+ header: [
157
+ `= ${doctitleQualifier && doctitleQualifier !== navtitlePlain ? doctitleQualifier + ': ' : ''}${navtitleAsciiDoc}`,
158
+ ...(versioned ? [`:revnumber: ${displayVersion}`] : []),
159
+ `:doctype: ${(assemblyModel.doctype ??= 'book')}`,
160
+ ':underscore: _',
161
+ `:page-component-name: ${componentName}`,
162
+ `:page-component-version:${versioned ? ' ' + version : ''}`,
163
+ ':page-version: {page-component-version}',
164
+ `:page-component-display-version: ${displayVersion}`,
165
+ `:page-component-title: ${title}`,
166
+ ],
167
+ currentComponentVersion: componentVersion,
168
+ doctitleQualifier,
169
+ baseDoctitle: navtitle,
170
+ baseDoctitlePlain: navtitlePlain,
171
+ setAuthor (author) {
172
+ this.header.splice(1, 0, author)
173
+ },
174
+ updateDoctitle (doctitle, doctitleAsciiDoc, doctitlePlain, useQualifier = true) {
175
+ this.baseDoctitle = doctitle
176
+ this.baseDoctitlePlain = doctitlePlain
177
+ const qualifyDoctitle = useQualifier && !!this.doctitleQualifier && this.doctitleQualifier !== doctitlePlain
178
+ this.header[0] = `= ${qualifyDoctitle ? this.doctitleQualifier + ': ' : ''}${doctitleAsciiDoc}`
179
+ },
180
+ matchesDoctitle (candidate) {
181
+ return candidate === this.baseDoctitlePlain
182
+ },
183
+ setDoctype (doctype) {
184
+ this.header[this.header.findIndex((line) => line.startsWith(':doctype: '))] = `:doctype: ${doctype}`
185
+ },
186
+ setDocid (docid) {
187
+ this.header.unshift(`[#${docid}]`)
188
+ },
189
+ serialize () {
190
+ const normalizedHeader = this.header.filter((it) => it !== ':doctype: article')
191
+ return this.body
192
+ ? Buffer.from(normalizedHeader.join('\n') + '\n\n' + this.body.join('\n') + '\n')
193
+ : Buffer.from(normalizedHeader.join('\n') + '\n')
194
+ },
195
+ }
146
196
  }
147
197
 
148
198
  function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
149
199
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
150
- const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
151
- if (page) {
152
- accum.set(createResourceKey(page.src), page)
153
- accum.set(outlineEntry.url, page)
200
+ const { urlType, url, hash, unresolved, items = [] } = outlineEntry
201
+ if (urlType === 'internal' && !unresolved) {
202
+ const pathname = (outlineEntry.pathname ??= hash ? url.substring(0, url.length - hash.length) : url)
203
+ let page
204
+ if (!accum.has(pathname) && (page = pagesByUrl.get(pathname))) {
205
+ accum.set(createResourceKey(page.src), page)
206
+ accum.set(pathname, page)
207
+ }
154
208
  }
155
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
209
+ for (const item of items) selectPagesInOutline(item, pagesByUrl, accum)
156
210
  return accum
157
211
  }
158
212
 
159
213
  function mergeAsciiDoc (
160
214
  loadAsciiDoc,
161
215
  contentCatalog,
162
- buffer,
216
+ builder,
163
217
  componentVersion,
164
218
  outlineEntry,
165
219
  files,
@@ -168,133 +222,115 @@ function mergeAsciiDoc (
168
222
  asciidocConfig,
169
223
  mutableAttributes,
170
224
  assemblyModel,
171
- lastComponentVersion = componentVersion,
172
225
  level = 0,
173
226
  supportsParts = false
174
227
  ) {
228
+ const { items = [], roles = [], unresolved, urlType, url, pathname, hash } = outlineEntry
175
229
  // TODO: we could try to be smart about it and make sure the page with fragment is included at least once
176
- if (outlineEntry.hash) {
177
- buffer.inBody ??= false
178
- return buffer
230
+ if (roles.includes('site-only') || (urlType === 'external' && !URL.canParse(url))) {
231
+ builder.body ??= []
232
+ return builder
179
233
  }
180
- let navtitle = outlineEntry.navtitle ?? outlineEntry.content
181
- let navtitlePlain = sanitize(navtitle)
182
- let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
183
- const { items = [], roles = [], unresolved, urlType, url } = outlineEntry
184
- const { filetype, linkReferenceStyle, pubRoot, siteRoot, logger, rootLevel } = assemblyModel
185
234
  const assembled = pagesInOutline.assembled
186
235
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
187
- const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
188
- const atDocumentRoot = !buffer.inBody
189
- const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
236
+ const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(pathname) : undefined
237
+ let navtitle = outlineEntry.navtitle ?? (page && hash ? page.asciidoc.navtitle : outlineEntry.content)
238
+ let navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
239
+ let navtitlePlain = sanitize(navtitle)
240
+ const { doctype, filetype, linkReferenceStyle, pubRoot, siteRoot, logger, rootLevel, rootPageStyle } = assemblyModel
241
+ let sectionMergeStrategy = assemblyModel.sectionMergeStrategy
242
+ const atStartOfBody = !builder.body?.length
243
+ const isRootEntry = atStartOfBody && !level
244
+ let atBookRoot = isRootEntry && doctype === 'book' && (supportsParts = true)
190
245
  const hasItems = items.length > 0
191
246
  if (page && !assembled.pages.has(page)) {
192
247
  const contents = page.src.contents
193
248
  if (contents == null) {
194
- buffer.inBody ??= false
195
- return buffer
249
+ builder.body ??= []
250
+ return builder
196
251
  }
197
- const isRootPage = atDocumentRoot && !level
198
- const { component, version, module: module_, relative, origin, mediaType } = page.src
199
252
  const pageAsAsciiDoc = new page.constructor(
200
- Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
253
+ Object.assign({}, page, { contents: Buffer.from(contents.toString().trimEnd()), mediaType: page.src.mediaType })
201
254
  )
202
255
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
203
- doc.logger = logger
204
- ? Object.assign(doc.getLogger().$dup(), {
205
- delegate: {
206
- warn () {
207
- return logger.warn.apply(logger, arguments)
208
- },
209
- },
210
- })
211
- : doc.getLogger()
212
- doc.source_header_attributes ??= doc.parent.$to_h()
213
- if (isRootPage && doc.hasAttribute('assembly-slug')) buffer.slug = doc.getAttribute('assembly-slug')
256
+ if (typeof doc.applyLogger === 'function') doc.applyLogger()
257
+ if (logger) doc.getLogger().delegate = logger
258
+ doc.catalog.syntheticIds = {}
259
+ doc.source_header_attributes ??= toHash()
260
+ const isRootPage = isRootEntry
261
+ const { component, version, module: module_, relative, origin } = page.src
214
262
  let hasAssemblyNavtitleAttr
215
263
  if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
216
- navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
217
- navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
218
- if (buffer.inBody == null) {
219
- buffer.navtitle = navtitle
220
- if (rootLevel) {
221
- const doctitle =
222
- (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
223
- buffer[0] = `= ${doctitle}`
224
- }
264
+ navtitleAsciiDoc = unconvertInlineAsciiDoc(doc.getAttribute('assembly-navtitle'))
265
+ navtitlePlain = sanitize((navtitle = doc.$sub_attributes(doc.$apply_reftext_subs(navtitleAsciiDoc))))
266
+ }
267
+ if (isRootPage) {
268
+ const doctypeOverride = doc.getAttribute('assembly-doctype')
269
+ if (doctypeOverride && doctypeOverride !== doctype) {
270
+ builder.setDoctype(doctypeOverride)
271
+ if (doctypeOverride !== 'book') atBookRoot = supportsParts = false
272
+ }
273
+ if (doc.hasAttribute('assembly-docname')) builder.docname = doc.getAttribute('assembly-docname')
274
+ const doctitleOverride = doc.getAttribute('assembly-doctitle')
275
+ if (doctitleOverride) {
276
+ let doctitle
277
+ const doctitleAsciiDoc = unconvertInlineAsciiDoc(doctitleOverride)
278
+ const doctitlePlain = sanitize((doctitle = doc.$sub_attributes(doc.$apply_reftext_subs(doctitleAsciiDoc))))
279
+ builder.updateDoctitle(doctitle, doctitleAsciiDoc, doctitlePlain, false)
280
+ } else if (hasAssemblyNavtitleAttr && rootLevel) {
281
+ builder.updateDoctitle(navtitle, navtitleAsciiDoc, navtitlePlain)
225
282
  }
226
283
  }
284
+ const overviewTitle = doc.getAttribute('assembly-overview-title') || 'Overview'
285
+ let overviewTitleAsciiDoc
227
286
  // NOTE: in Antora, docname is relative src path from module without file extension
228
- const docname = doc.getAttribute('docname')
287
+ const pageDocname = doc.getAttribute('docname')
229
288
  const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
230
289
  const pageIdLeader = pageId + idSeparators.scope
231
- let pageFragment = ''
290
+ let pageFragment = `#${pageId}`
232
291
  let pageRoles = []
233
292
  let pageStyle = doc.getAttribute('assembly-style', '')
234
- if (
235
- !pageStyle &&
236
- atBookRoot &&
237
- rootLevel === 0 &&
238
- assemblyModel.sectionMergeStrategy === 'fuse' &&
239
- !doc.source_header_attributes['$key?']('assembly-style')
240
- ) {
241
- pageStyle = 'preface'
293
+ if (!pageStyle && isRootPage && hasItems && !doc.source_header_attributes['$key?']('assembly-style')) {
294
+ pageStyle = rootPageStyle
242
295
  }
243
296
  let part
244
- if ((part = pageStyle === 'part')) {
245
- pageStyle = ''
246
- if (!supportsParts) part = undefined
247
- } else if ((part = pageStyle.endsWith('-part'))) {
248
- pageStyle = pageStyle.substring(0, pageStyle.length - 5)
249
- if (!supportsParts) part = undefined
297
+ if (pageStyle) {
298
+ if (
299
+ pageStyle === 'part' ||
300
+ (pageStyle.endsWith('-part') && (pageStyle = pageStyle.substring(0, pageStyle.length - 5)))
301
+ ) {
302
+ part = supportsParts ? true : (pageStyle = undefined)
303
+ }
304
+ } else {
305
+ pageStyle = undefined
250
306
  }
251
- let nextSectionLevel = 1
307
+ const hasSections = doc.hasSections()
308
+ const supportsSections = pageStyle == null ? true : ASSEMBLY_STYLE_SUPPORTS_SECTIONS.includes(pageStyle)
309
+ const encloseSections = sectionMergeStrategy === 'enclose' && hasItems && hasSections
310
+ let nextSectionLevel = atBookRoot ? undefined : 1 // undefined means allow part or chapter at top level
252
311
  const lines = doc.getSourceLines()
253
312
  const ignoreLines = []
254
- if (!buffer.inBody) {
255
- buffer.inBody = true
256
- buffer.endHeaderIdx = buffer.length - 1
257
- }
258
- buffer.push('')
259
- buffer.push(`:docname: ${docname}`)
260
- if (component !== lastComponentVersion.name) {
261
- const thisComponentVersion =
262
- component === componentVersion.name && version === componentVersion.version
263
- ? componentVersion
264
- : contentCatalog.getComponentVersion(component, version)
265
- if (thisComponentVersion) {
266
- buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
267
- buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
268
- buffer.push(':page-version: {page-component-version}')
269
- buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
270
- buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
271
- lastComponentVersion = thisComponentVersion
272
- }
273
- }
274
- buffer.push(`:page-module: ${module_}`)
275
- buffer.push(`:page-relative-src-path: ${relative}`)
276
- buffer.push(`:page-origin-url: ${origin.url}`)
277
- buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
278
- buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
279
- buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
280
- buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
313
+ const buffer = (builder.body ??= [])
314
+ if (buffer.length) buffer.push('')
281
315
  let headerHasBlockAttrs
282
316
  if (doc.hasHeader()) {
283
- for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
317
+ for (const entry of processPageHeader(doc, lines, ignoreLines, isRootPage)) {
284
318
  if (entry.type === 'author_line') {
285
- buffer.splice(1, 0, entry.lines[0])
286
- buffer.endHeaderIdx += 1
319
+ builder.setAuthor(entry.lines[0])
287
320
  } else if (entry.type === 'attribute_entry') {
288
321
  const name = entry.name
289
- if (!entry.negated && !doc.isAttributeLocked(name)) {
322
+ if (entry.negated && name === 'sectnums') {
323
+ // catch sectnums missed by reducer; only allow turned off, not turning on
324
+ doc.source_header_attributes['$[]=']('sectnums', null)
325
+ } else if (!entry.negated && !doc.isAttributeLocked(name)) {
290
326
  let val, newVal
291
327
  if (
292
328
  name.endsWith('-image') &&
293
329
  (val = doc.getAttribute(name)) &&
294
- (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
330
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets)) != null
295
331
  ) {
296
- if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
297
- } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
332
+ if (newVal !== val) entry.value = newVal
333
+ } else if (~(val = entry.value).indexOf(':')) {
298
334
  newVal = val
299
335
  if (~newVal.indexOf('image:')) {
300
336
  newVal = rewriteInlineImages(
@@ -321,15 +357,10 @@ function mergeAsciiDoc (
321
357
  { file: relative, lineno: entry.lineno }
322
358
  )
323
359
  }
324
- if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
360
+ if (newVal !== val) entry.value = newVal
325
361
  }
326
362
  }
327
- if (entry.promote) {
328
- buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
329
- buffer.endHeaderIdx += entry.lines.length
330
- } else {
331
- buffer.push(...entry.lines)
332
- }
363
+ ;(entry.promote ? builder.header : buffer).push(...entry.lines)
333
364
  } else {
334
365
  if (entry.type === 'attrlist') headerHasBlockAttrs = true
335
366
  buffer.push(...entry.lines)
@@ -338,102 +369,144 @@ function mergeAsciiDoc (
338
369
  pageRoles = doc.getRoles()
339
370
  }
340
371
  if (roles.length) pageRoles = pageRoles.concat(roles[0] === 'page' ? roles.slice(1) : roles)
372
+ buffer.push(`:page-docname: ${pageDocname}`)
373
+ if (component !== builder.currentComponentVersion.name) {
374
+ const thisComponentVersion =
375
+ component === componentVersion.name && version === componentVersion.version
376
+ ? componentVersion
377
+ : contentCatalog.getComponentVersion(component, version)
378
+ if (thisComponentVersion) {
379
+ buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
380
+ buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
381
+ buffer.push(':page-version: {page-component-version}')
382
+ buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
383
+ buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
384
+ builder.currentComponentVersion = thisComponentVersion
385
+ }
386
+ }
387
+ buffer.push(`:page-module: ${module_}`)
388
+ buffer.push(`:page-relative-src-path: ${relative}`)
389
+ buffer.push(`:page-origin-url: ${origin.url}`)
390
+ buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
391
+ buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
392
+ buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
393
+ buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
341
394
  let heading
342
- if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
343
- let htitleAsciiDoc = navtitleAsciiDoc
344
- let notitle
395
+ if (pageStyle != null && isRootPage) {
396
+ let htitleAsciiDoc, htitleOverride
345
397
  if (pageStyle === 'preface') {
346
398
  const hasPrefaceTitleAttr =
347
399
  doc.isAttribute('preface-title') ||
348
400
  doc.source_header_attributes['$key?']('preface-title') ||
349
401
  doc.isAttributeLocked('preface-title')
350
- const htitleOverride = hasPrefaceTitleAttr
351
- ? doc.getAttribute('preface-title', '')
352
- : atDocumentRoot && (rootLevel ? true : !hasAssemblyNavtitleAttr)
353
- ? ''
354
- : undefined
355
- if (htitleOverride != null) {
356
- if (htitleOverride === '') {
357
- if (headerHasBlockAttrs || doc.hasSections()) {
358
- htitleAsciiDoc = '{empty}'
359
- notitle = true
360
- } else {
361
- if (hasPrefaceTitleAttr) buffer.splice(++buffer.endHeaderIdx, 0, ':preface-title:')
402
+ if (hasPrefaceTitleAttr) {
403
+ htitleOverride = doc.getAttribute('preface-title', '')
404
+ } else if (!(htitleOverride = doc.getAttribute('assembly-root-title'))) {
405
+ if (encloseSections) {
406
+ htitleOverride = overviewTitle
407
+ } else if (builder.matchesDoctitle(navtitlePlain)) {
408
+ htitleOverride = ''
409
+ } else {
410
+ htitleOverride = undefined // means use navtitleAsciiDoc
411
+ }
412
+ }
413
+ if (htitleOverride === '') {
414
+ nextSectionLevel = 2
415
+ if (headerHasBlockAttrs) {
416
+ htitleAsciiDoc = ''
417
+ } else {
418
+ sectionMergeStrategy = 'discrete'
419
+ if (atBookRoot && hasPrefaceTitleAttr) {
420
+ builder.header.push(':preface-title:')
421
+ }
422
+ }
423
+ } else {
424
+ htitleAsciiDoc = htitleOverride == null ? navtitleAsciiDoc : unconvertInlineAsciiDoc(htitleOverride)
425
+ if (htitleOverride === overviewTitle) overviewTitleAsciiDoc = htitleAsciiDoc // optimization
426
+ if (atBookRoot && hasPrefaceTitleAttr) {
427
+ if (!headerHasBlockAttrs && (sectionMergeStrategy === 'discrete' || !hasSections)) {
428
+ builder.header.push(`:preface-title: ${htitleAsciiDoc}`)
362
429
  htitleAsciiDoc = undefined
363
430
  }
364
- } else if (hasPrefaceTitleAttr && !(headerHasBlockAttrs || doc.hasSections())) {
365
- buffer.splice(++buffer.endHeaderIdx, 0, `:preface-title: ${unconvertInlineAsciiDoc(htitleOverride)}`)
366
- htitleAsciiDoc = undefined
367
- } else {
368
- htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
369
431
  }
370
432
  }
433
+ } else if ((htitleOverride = doc.getAttribute('assembly-root-title', ''))) {
434
+ htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
435
+ } else if (encloseSections) {
436
+ htitleAsciiDoc = overviewTitleAsciiDoc = unconvertInlineAsciiDoc(overviewTitle)
437
+ } else {
438
+ htitleAsciiDoc = navtitleAsciiDoc
371
439
  }
372
440
  if (htitleAsciiDoc != null) {
373
- if (atDocumentRoot) {
374
- // NOTE use pageStyle as fallback because this section must have a unique ID
375
- pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
376
- buffer.unshift(`[#${pageId}]`)
441
+ heading = { title: htitleAsciiDoc, hlevel: part ? 1 : 2 }
442
+ if (pageStyle === 'part') {
443
+ nextSectionLevel = 1
377
444
  } else {
378
- pageFragment = `#${pageId}`
445
+ nextSectionLevel = 2 // next section level is 2, even for special part
446
+ // enable next two lines if we always want to promote ID of root page to docid
447
+ //builder.setDocid(pageId)
448
+ //pageFragment = '#' + idSeparators.generateIdFromTitle(htitleAsciiDoc, idSeparators, pageIdLeader)
449
+ if (pageStyle === 'preface' && doctype !== 'book') {
450
+ heading.style = ''
451
+ if (doc.hasAttribute('sectnums')) {
452
+ doc.source_header_attributes['$[]=']('sectnums', null)
453
+ buffer.push(':!sectnums:')
454
+ }
455
+ }
379
456
  }
380
- heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
381
- if (notitle) heading.notitle = true
382
- nextSectionLevel = 2 // next section level is 2, even for special part
383
457
  }
458
+ } else if (part) {
459
+ heading = { title: navtitleAsciiDoc, hlevel: level-- } // NOTE: level will always come in at 1
460
+ nextSectionLevel = pageStyle === 'part' ? 1 : 2
384
461
  } else if (level) {
385
- if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title && !hasAssemblyNavtitleAttr) {
462
+ if (atStartOfBody && !pageStyle && builder.matchesDoctitle(navtitlePlain)) {
386
463
  level--
464
+ if (headerHasBlockAttrs) heading = { title: '', hlevel: ++level + 1 }
387
465
  } else {
388
- pageFragment = `#${pageId}`
389
- if (part && level === 1) level--
390
- if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
391
- Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
392
- }
466
+ heading = { title: navtitleAsciiDoc, hlevel: level + 1 }
393
467
  }
394
- } else if (atDocumentRoot && rootLevel === 0 && hasAssemblyNavtitleAttr) {
395
- pageFragment = `#${pageId}`
396
- heading = { title: navtitleAsciiDoc, level: (nextSectionLevel = part ? 1 : 2) }
397
- } else if (atBookRoot) {
398
- nextSectionLevel = undefined
399
468
  }
400
- let enclosed
401
469
  if (heading) {
470
+ if (heading.hlevel > 6) Object.assign(heading, { hlevel: 6, style: `discrete.h${heading.hlevel}` })
402
471
  const rolesAttr = pageRoles.reduce((str, it) => str + '.' + it, '')
403
- const notitleAttrs = heading.notitle ? '%notitle,toclevels=0,outlinelevels=0' : ''
404
- buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${rolesAttr}${notitleAttrs}]`)
405
- buffer.push(`${'='.repeat(heading.level)} ${heading.title}`)
406
- } else {
407
- if (atDocumentRoot) buffer.unshift(`[#${pageId}]`)
408
- if (assemblyModel.sectionMergeStrategy === 'enclose' && hasItems && doc.hasSections()) {
409
- enclosed = true
410
- // TODO: make overview section title configurable
411
- //let overviewTitle = doc.getDocumentTitle()
412
- //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
413
- const overviewTitle = doc.getAttribute('overview-title', 'Overview')
414
- const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
415
- let hlevel = level + 2
416
- if (hlevel > 6) {
417
- const blockStyle = `discrete.h${hlevel}`
418
- hlevel = 6
419
- buffer.push(`[${blockStyle}#${syntheticId}]`)
420
- } else {
421
- buffer.push(`[#${syntheticId}]`)
422
- }
423
- buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
472
+ const notitle = (heading.title ||= '{empty}') === '{empty}'
473
+ // remove conditional if we always want to promote ID of root page to docid
474
+ if (notitle && atStartOfBody) {
475
+ builder.setDocid(pageId)
476
+ pageFragment = '#' + idSeparators.generateIdFromTitle('', idSeparators, pageIdLeader)
424
477
  }
478
+ const style = heading.style ?? (pageStyle === 'part' || pageStyle === 'section' ? '' : (pageStyle ?? ''))
479
+ buffer.push(`[${style}${pageFragment}${rolesAttr}${notitle ? '%notitle' : ''}]`)
480
+ buffer.push(`${'='.repeat(heading.hlevel)} ${heading.title}`)
481
+ if (notitle || !supportsSections) sectionMergeStrategy = 'discrete'
482
+ } else if (atStartOfBody) {
483
+ builder.setDocid(pageId)
425
484
  }
485
+ if (isRootPage || (atStartOfBody && !heading)) assembled.pages.rootPageFragment = pageFragment
426
486
  assembled.pages.set(page, pageFragment)
427
- if (doc.hasSections()) fixSectionLevels(doc.getSections(), nextSectionLevel)
487
+ if (encloseSections && !(atStartOfBody && heading)) {
488
+ overviewTitleAsciiDoc ??= unconvertInlineAsciiDoc(overviewTitle)
489
+ const sectionFragment = '#' + idSeparators.generateIdFromTitle(overviewTitleAsciiDoc, idSeparators, pageIdLeader)
490
+ if (heading && buffer.length) buffer.push('')
491
+ let hlevel = level + (nextSectionLevel = 2)
492
+ if (hlevel > 6) {
493
+ buffer.push(`[discrete.h${hlevel}${sectionFragment}]`)
494
+ hlevel = 6
495
+ } else {
496
+ buffer.push(`[${sectionFragment}]`)
497
+ }
498
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitleAsciiDoc}`)
499
+ }
500
+ if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
428
501
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
429
502
  it.getContext() === 'document'
430
503
  ? it.getDocument().isNested()
431
504
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
432
505
  )
433
506
  if (doc.getDoctype() === 'manpage') {
434
- const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
507
+ const firstSectionIdx = hasSections ? doc.getSections()[0].getLineNumber() - 1 : lines.length
435
508
  for (let idx = 0; idx < firstSectionIdx; idx++) {
436
- if (~ignoreLines.indexOf(idx)) continue
509
+ if (ignoreLines.includes(idx)) continue
437
510
  const line = lines[idx]
438
511
  if (line.startsWith('== ') && line.length > 3) {
439
512
  allBlocks.unshift({
@@ -477,10 +550,10 @@ function mergeAsciiDoc (
477
550
  })
478
551
  let skipping
479
552
  for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
480
- if (~ignoreLines.indexOf(idx)) continue
553
+ if (ignoreLines.includes(idx)) continue
481
554
  let line = lines[idx]
482
555
  if (line.startsWith('//')) {
483
- if (line[2] !== '/') continue
556
+ if (line.charAt(2) !== '/') continue
484
557
  if (line.length > 3 && line === '/'.repeat(line.length)) {
485
558
  if (skipping) {
486
559
  if (line === skipping) skipping = undefined
@@ -547,9 +620,9 @@ function mergeAsciiDoc (
547
620
  let idx = lineno - 1
548
621
  if (context === 'section' && !block.getDocument().isNested()) {
549
622
  if (block.getSectionName() === 'header') return
550
- let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
623
+ let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
551
624
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
552
- let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
625
+ let targetMarkerLength = block.level + 1 + level
553
626
  if (targetMarkerLength > 6) {
554
627
  blockStyle = `discrete.h${targetMarkerLength}`
555
628
  targetMarkerLength = 6
@@ -593,10 +666,13 @@ function mergeAsciiDoc (
593
666
  buffer,
594
667
  lines.filter((it) => it !== undefined)
595
668
  )
596
- const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
669
+ const attributeEntries = doc.source_header_attributes.$entries()
597
670
  if (attributeEntries.length) {
598
671
  const resolvedAttributeEntries = attributeEntries.reduce(
599
672
  (accum, [name, val]) => {
673
+ if (val) {
674
+ if (val['$nil?']()) val = null
675
+ }
600
676
  // Q: couldn't we just check if attribute is locked?
601
677
  if (name in mutableAttributes) {
602
678
  const initialVal = mutableAttributes[name]
@@ -614,19 +690,19 @@ function mergeAsciiDoc (
614
690
  )
615
691
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
616
692
  }
617
- } else if (level) {
618
- if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
619
- buffer.inBody ??= false
693
+ if (typeof doc.restoreLogger === 'function') doc.restoreLogger()
694
+ } else if (level && !hash) {
695
+ const buffer = (builder.body ??= [])
696
+ if (atStartOfBody && builder.matchesDoctitle(navtitlePlain)) {
620
697
  level--
621
698
  } else {
622
- buffer.inBody = true
623
- buffer.push('')
699
+ if (buffer.length) buffer.push('')
624
700
  const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
625
701
  let sectionTitle = navtitleAsciiDoc
626
702
  if (urlType === 'external') {
627
703
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
628
704
  } else if (urlType === 'internal' && !unresolved) {
629
- const resource = files.find((it) => it.pub.url === url)
705
+ const resource = files.find((it) => it.pub.url === pathname)
630
706
  if (resource) {
631
707
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
632
708
  const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
@@ -636,41 +712,42 @@ function mergeAsciiDoc (
636
712
  }
637
713
  }
638
714
  }
715
+ if (supportsParts && level === 1 && roles.includes('part')) {
716
+ roles.splice(roles.indexOf('part', 1))
717
+ level--
718
+ }
639
719
  const hlevel = level > 5 ? 6 : level + 1
640
720
  const hroles = urlType === 'internal' ? roles.slice(1) : roles
641
721
  buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
642
722
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
643
723
  }
644
724
  }
645
- if (hasItems) {
646
- const nextLevel = level + 1
647
- // NOTE: drop first child if same as parent; should we keep if content is different?
648
- ;(urlType === 'internal' && urlType === items[0].urlType && url === items[0].url && !items[0].items
649
- ? items.slice(1)
650
- : items
651
- ).forEach((item) => {
652
- mergeAsciiDoc(
653
- loadAsciiDoc,
654
- contentCatalog,
655
- buffer,
656
- componentVersion,
657
- item,
658
- files,
659
- pagesInOutline,
660
- idSeparators,
661
- asciidocConfig,
662
- mutableAttributes,
663
- assemblyModel,
664
- lastComponentVersion,
665
- nextLevel,
666
- atBookRoot
667
- )
668
- })
669
- }
670
- return buffer
725
+ if (!hasItems) return
726
+ const nextLevel = level + 1
727
+ // NOTE: drop first child if same as parent; should we keep if content is different?
728
+ ;(urlType === 'internal' && items[0].urlType === 'internal' && pathname === items[0].url && !items[0].items?.length
729
+ ? items.slice(1)
730
+ : items
731
+ ).forEach((item) => {
732
+ mergeAsciiDoc(
733
+ loadAsciiDoc,
734
+ contentCatalog,
735
+ builder,
736
+ componentVersion,
737
+ item,
738
+ files,
739
+ pagesInOutline,
740
+ idSeparators,
741
+ asciidocConfig,
742
+ mutableAttributes,
743
+ assemblyModel,
744
+ nextLevel,
745
+ atBookRoot
746
+ )
747
+ })
671
748
  }
672
749
 
673
- function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
750
+ function processPageHeader (doc, lines, ignoreLines, isRootPage) {
674
751
  const entries = []
675
752
  let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
676
753
  assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
@@ -690,8 +767,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
690
767
  }
691
768
  assemblyHeaderAttributes.delete('%authors')
692
769
  }
693
- const headerAttributes = doc.source_header_attributes
694
- for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
770
+ for (const name of assemblyHeaderAttributes) doc.source_header_attributes.$delete(name)
695
771
  }
696
772
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
697
773
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
@@ -706,7 +782,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
706
782
  }
707
783
  const line = lines[idx]
708
784
  if (open === ':' || open === '-:') {
709
- if (open === ':') current.lines.push(line)
785
+ if (open === ':') current.appendLine(line)
710
786
  if (!line?.endsWith(' \\')) current = open = undefined
711
787
  } else if (line) {
712
788
  let attributeEntryMatch
@@ -714,13 +790,14 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
714
790
  if (chr0 === '/' && line.charAt(1) === '/') {
715
791
  if (line.startsWith('////') && line === '/'.repeat(line.length)) {
716
792
  if (open) {
717
- current.lines.push(line)
793
+ current.appendLine(line)
718
794
  if (open === line) current = open = undefined
719
795
  } else {
720
796
  entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
797
+ current.appendLine = Array.prototype.push.bind(current.lines)
721
798
  }
722
799
  } else if (open) {
723
- current.lines.push(line)
800
+ current.appendLine(line)
724
801
  } else if (belowDoctitle && line.charAt(2) === '/') {
725
802
  break
726
803
  } else {
@@ -728,7 +805,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
728
805
  current = undefined
729
806
  }
730
807
  } else if (open) {
731
- current.lines.push(line)
808
+ current.appendLine(line)
732
809
  } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
733
810
  let name = attributeEntryMatch[1]
734
811
  const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
@@ -737,7 +814,17 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
737
814
  if (line.endsWith(' \\')) open = '-:' // disallow value continuation
738
815
  } else {
739
816
  if (line.endsWith(' \\')) open = ':'
740
- entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
817
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [], lineno: idx + 1 }))
818
+ Object.defineProperty(current, 'value', {
819
+ get () {
820
+ return this.lines.join('\n').substring(this.name.length + 3)
821
+ },
822
+ set (newValue) {
823
+ this.lines = `:${this.name}: ${newValue}`.split('\n')
824
+ },
825
+ })
826
+ current.lines.push(negated ? `:!${name}:` : line)
827
+ current.appendLine = Array.prototype.push.bind(negated ? [] : current.lines)
741
828
  if (assemblyHeaderAttributes.has(name)) current.promote = true
742
829
  }
743
830
  } else if (belowDoctitle) {
@@ -761,9 +848,9 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
761
848
  return entries
762
849
  }
763
850
 
764
- function generateSlug (string) {
851
+ function generateDocname (string) {
765
852
  if (string === 'index') return string
766
- return sanitizeSlug(
853
+ return sanitizeDocname(
767
854
  string
768
855
  .toLowerCase()
769
856
  .replace(/<[^>]+>/g, '')
@@ -776,8 +863,8 @@ function generateSlug (string) {
776
863
  )
777
864
  }
778
865
 
779
- function sanitizeSlug (slug) {
780
- return slug.replace(/[^\p{Alpha}0-9_.-]/gu, '-').replace(/^-+|-+$|(-)-+/g, '$1')
866
+ function sanitizeDocname (docname) {
867
+ return docname.replace(/[^\p{Alpha}0-9_.-]/gu, '-').replace(/^-+|-+$|(-)-+/g, '$1')
781
868
  }
782
869
 
783
870
  function fixSectionLevels (sections, expectedLevel) {
@@ -788,33 +875,29 @@ function fixSectionLevels (sections, expectedLevel) {
788
875
  })
789
876
  }
790
877
 
791
- // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
792
- // IMPORTANT: this must not leave behind lines the parser will drop!
793
- // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
794
- function trimAsciiDoc (buffer) {
795
- return Buffer.from(
796
- buffer
797
- .toString()
798
- .replace(/^(?:[ \t]*\r\n?|[ \t]*\n)+/, '')
799
- .trimRight()
800
- )
801
- }
802
-
803
878
  function safePush (onto, entries) {
804
879
  try {
805
880
  onto.push(...entries)
806
- } catch (err) {
807
- /* istanbul ignore if */
808
- if (!(err instanceof RangeError)) throw err
881
+ } catch {
809
882
  for (const e of entries) onto.push(e)
810
883
  }
811
884
  }
812
885
 
813
- function generateIdFromTitle (titleAsciiDoc, idSeparators) {
814
- const Section = this.$class().$const_get('::Asciidoctor::Section')
815
- const baseId = Section.$generate_id(titleAsciiDoc, this)
816
- this.getCatalog().refs['$[]='](baseId, true)
817
- return `_${idSeparators.coordinate}${baseId}`
886
+ function generateIdFromTitle (titleAsciiDoc, idSeparators, idLeader) {
887
+ const Section = this.$class().$$base_module.Section
888
+ // NOTE: refs in catalog will always be empty
889
+ const baseId = titleAsciiDoc ? Section.$generate_id(titleAsciiDoc, this) : '_'
890
+ let id = `_${idSeparators.coordinate}${idLeader ?? ''}${baseId}`
891
+ const syntheticIds = (this.catalog.syntheticIds ??= {})
892
+ if (id in syntheticIds) {
893
+ const sep = (this.getAttribute('idseparator') ?? '_').charAt()
894
+ let cnt = this.$class().$$base_module.Compliance.unique_id_start_index
895
+ let candidate
896
+ while ((candidate = `${id}${sep}${cnt}`) in syntheticIds) cnt++
897
+ id = candidate
898
+ }
899
+ syntheticIds[id] = true
900
+ return id
818
901
  }
819
902
 
820
903
  module.exports = produceAssemblyFile