@antora/assembler 1.0.0-rc.1 → 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.
@@ -11,22 +11,28 @@ 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-doctype',
23
22
  'assembly-header-attributes',
23
+ 'assembly-overview-title',
24
24
  'assembly-navtitle',
25
+ 'assembly-root-title',
25
26
  'assembly-slug',
26
27
  'assembly-style',
28
+ 'doctype',
29
+ 'leveloffset',
30
+ 'page-aliases',
31
+ 'preface-title',
27
32
  'underscore',
28
33
  ]
29
34
  const { NAMED_ID_ATTR_RX } = require('./util/rx')
35
+ const ASSEMBLY_STYLE_SUPPORTS_SECTIONS = ['abstract', 'appendix', 'part', 'preface', 'section']
30
36
 
31
37
  function produceAssemblyFile (
32
38
  loadAsciiDoc,
@@ -39,35 +45,39 @@ function produceAssemblyFile (
39
45
  assemblyModel
40
46
  ) {
41
47
  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
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
44
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
+ )
45
60
  const idSeparators = {
46
61
  prefix:
47
62
  'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
48
63
  scope: xmlIds ? '---' : ':::',
49
64
  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
- ),
65
+ generateIdFromTitle: generateIdFromTitle.bind(scratchDoc),
57
66
  }
58
- const { name: component, version } = componentVersion
59
67
  asciidocConfig = prepareAsciiDocConfig(
60
68
  contentCatalog,
61
69
  { component, version },
62
70
  pagesInOutline,
63
71
  asciidocConfig,
64
72
  assemblyModel,
65
- idSeparators
73
+ idSeparators,
74
+ scratchDoc
66
75
  )
67
- const buffer = mergeAsciiDoc(
76
+ const builder = buildAssemblyHeader(componentVersion, outline.content, assemblyModel)
77
+ mergeAsciiDoc(
68
78
  loadAsciiDoc,
69
79
  contentCatalog,
70
- buildAsciiDocHeader(componentVersion, outline.content, assemblyModel),
80
+ builder,
71
81
  componentVersion,
72
82
  outline,
73
83
  files,
@@ -78,22 +88,22 @@ function produceAssemblyFile (
78
88
  assemblyModel
79
89
  )
80
90
  const stem =
81
- (buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)) ||
91
+ (builder.slug ? sanitizeSlug(builder.slug) : generateSlug(rootLevel === 0 ? 'index' : builder.baseDoctitle)) ||
82
92
  'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
83
93
  const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
84
94
  const file = contentCatalog.createFile({
85
95
  asciidoc: asciidocConfig,
86
96
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
87
- contents: Buffer.from(buffer.join('\n') + '\n'),
97
+ contents: builder.serialize(),
88
98
  src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
89
99
  pub: false,
90
100
  })
91
- file.path = file.out.path // use out path as file path so assets can easily be published to same hierarchy
101
+ file.path = file.out.path // use out path as path so assets can be published to same hierarchy
92
102
  delete file.out
93
103
  return file
94
104
  }
95
105
 
96
- function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
106
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators, doc) {
97
107
  let attributesModified
98
108
  const configShared = asciidocConfig.$shared
99
109
  const sharedAttributes = asciidocConfig.attributes
@@ -101,7 +111,7 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
101
111
  if (configShared == null) {
102
112
  const assets = pagesInOutline.assembled.assets
103
113
  for (const [name, val] of Object.entries(sharedAttributes)) {
104
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
114
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
105
115
  let newVal
106
116
  if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
107
117
  if (~(newVal = val).indexOf('image:')) {
@@ -112,9 +122,19 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
112
122
  }
113
123
  }
114
124
  for (const [name, val] of Object.entries(sharedAttributes)) {
115
- if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
125
+ if (!(val?.constructor === String && ~val.indexOf(':'))) continue
116
126
  if (~val.indexOf('xref:')) {
117
- const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
127
+ const newVal = rewriteXrefs(
128
+ val,
129
+ contentCatalog,
130
+ assemblyModel,
131
+ ctx,
132
+ false,
133
+ pagesInOutline,
134
+ idSeparators,
135
+ null,
136
+ doc
137
+ )
118
138
  if (newVal !== val) (attributesModified ??= {})[name] = newVal
119
139
  }
120
140
  }
@@ -123,43 +143,74 @@ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocCon
123
143
  return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
124
144
  }
125
145
 
126
- function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
127
- const doctype = assemblyModel.doctype ?? 'book'
128
- const navtitlePlain = sanitize(navtitle)
146
+ function buildAssemblyHeader (componentVersion, navtitle, assemblyModel) {
147
+ const { name: componentName, version, displayVersion, title } = componentVersion
129
148
  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 })
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
+ }
146
193
  }
147
194
 
148
195
  function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
149
196
  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)
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)
204
+ }
154
205
  }
155
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
206
+ for (const item of items) selectPagesInOutline(item, pagesByUrl, accum)
156
207
  return accum
157
208
  }
158
209
 
159
210
  function mergeAsciiDoc (
160
211
  loadAsciiDoc,
161
212
  contentCatalog,
162
- buffer,
213
+ builder,
163
214
  componentVersion,
164
215
  outlineEntry,
165
216
  files,
@@ -168,130 +219,101 @@ function mergeAsciiDoc (
168
219
  asciidocConfig,
169
220
  mutableAttributes,
170
221
  assemblyModel,
171
- lastComponentVersion = componentVersion,
172
222
  level = 0,
173
223
  supportsParts = false
174
224
  ) {
225
+ const { items = [], roles = [], unresolved, urlType, url, pathname, hash } = outlineEntry
175
226
  // 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
227
+ if (roles.includes('site-only') || (urlType === 'external' && !URL.canParse(url))) {
228
+ builder.body ??= []
229
+ return builder
179
230
  }
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
231
  const assembled = pagesInOutline.assembled
186
232
  // 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)
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)
190
242
  const hasItems = items.length > 0
191
243
  if (page && !assembled.pages.has(page)) {
192
244
  const contents = page.src.contents
193
245
  if (contents == null) {
194
- buffer.inBody ??= false
195
- return buffer
246
+ builder.body ??= []
247
+ return builder
196
248
  }
197
- const isRootPage = atDocumentRoot && !level
198
- const { component, version, module: module_, relative, origin, mediaType } = page.src
199
249
  const pageAsAsciiDoc = new page.constructor(
200
- Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
250
+ Object.assign({}, page, { contents: Buffer.from(contents.toString().trimEnd()), mediaType: page.src.mediaType })
201
251
  )
202
252
  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')
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')
214
265
  let hasAssemblyNavtitleAttr
215
266
  if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
216
267
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
217
268
  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
- }
225
- }
269
+ if (isRootPage) builder.setDoctitle(navtitle, navtitleAsciiDoc, navtitlePlain)
226
270
  }
271
+ const overviewTitle = doc.getAttribute('assembly-overview-title', 'Overview')
272
+ let overviewTitleAsciiDoc
227
273
  // NOTE: in Antora, docname is relative src path from module without file extension
228
274
  const docname = doc.getAttribute('docname')
229
275
  const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
230
276
  const pageIdLeader = pageId + idSeparators.scope
231
- let pageFragment = ''
277
+ let pageFragment = `#${pageId}`
232
278
  let pageRoles = []
233
279
  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'
280
+ if (!pageStyle && isRootPage && hasItems && !doc.source_header_attributes['$key?']('assembly-style')) {
281
+ pageStyle = rootPageStyle
242
282
  }
243
283
  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
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
250
293
  }
251
- 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
252
298
  const lines = doc.getSourceLines()
253
299
  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}`)
281
- let headerHasBlockAttrs
300
+ const buffer = (builder.body ??= [])
301
+ if (buffer.length) buffer.push('')
282
302
  if (doc.hasHeader()) {
283
- for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
303
+ for (const entry of processPageHeader(doc, lines, ignoreLines, isRootPage)) {
284
304
  if (entry.type === 'author_line') {
285
- buffer.splice(1, 0, entry.lines[0])
286
- buffer.endHeaderIdx += 1
305
+ builder.setAuthor(entry.lines[0])
287
306
  } else if (entry.type === 'attribute_entry') {
288
307
  const name = entry.name
289
- if (!entry.negated && !doc.isAttributeLocked(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)) {
290
312
  let val, newVal
291
313
  if (
292
314
  name.endsWith('-image') &&
293
315
  (val = doc.getAttribute(name)) &&
294
- (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
316
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets)) != null
295
317
  ) {
296
318
  if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
297
319
  } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
@@ -324,116 +346,151 @@ function mergeAsciiDoc (
324
346
  if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
325
347
  }
326
348
  }
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
- }
349
+ ;(entry.promote ? builder.header : buffer).push(...entry.lines)
333
350
  } else {
334
- if (entry.type === 'attrlist') headerHasBlockAttrs = true
351
+ if (entry.type === 'attrlist') builder.headerHasBlockAttrs = true
335
352
  buffer.push(...entry.lines)
336
353
  }
337
354
  }
338
355
  pageRoles = doc.getRoles()
339
356
  }
340
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) {
360
+ const thisComponentVersion =
361
+ component === componentVersion.name && version === componentVersion.version
362
+ ? componentVersion
363
+ : contentCatalog.getComponentVersion(component, version)
364
+ if (thisComponentVersion) {
365
+ buffer.push(`:page-component-name: ${thisComponentVersion.name}`)
366
+ buffer.push(`:page-component-version:${thisComponentVersion.version ? ' ' + thisComponentVersion.version : ''}`)
367
+ buffer.push(':page-version: {page-component-version}')
368
+ buffer.push(`:page-component-display-version: ${thisComponentVersion.displayVersion}`)
369
+ buffer.push(`:page-component-title: ${thisComponentVersion.title}`)
370
+ builder.currentComponentVersion = thisComponentVersion
371
+ }
372
+ }
373
+ buffer.push(`:page-module: ${module_}`)
374
+ buffer.push(`:page-relative-src-path: ${relative}`)
375
+ buffer.push(`:page-origin-url: ${origin.url}`)
376
+ buffer.push(`:page-origin-start-path:${origin.startPath && ' '}${origin.startPath}`)
377
+ buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
378
+ buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
379
+ buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
341
380
  let heading
342
- if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
343
- let htitleAsciiDoc = navtitleAsciiDoc
344
- let notitle
381
+ if (pageStyle != null && isRootPage) {
382
+ let htitleAsciiDoc, htitleOverride
345
383
  if (pageStyle === 'preface') {
346
384
  const hasPrefaceTitleAttr =
347
385
  doc.isAttribute('preface-title') ||
348
386
  doc.source_header_attributes['$key?']('preface-title') ||
349
387
  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:')
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}`)
362
415
  htitleAsciiDoc = undefined
363
416
  }
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
417
  }
370
418
  }
419
+ } else if ((htitleOverride = doc.getAttribute('assembly-root-title', ''))) {
420
+ htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
421
+ } else if (encloseSections) {
422
+ htitleAsciiDoc = overviewTitleAsciiDoc = unconvertInlineAsciiDoc(overviewTitle)
423
+ } else {
424
+ htitleAsciiDoc = navtitleAsciiDoc
371
425
  }
372
426
  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}]`)
427
+ heading = { title: htitleAsciiDoc, hlevel: part ? 1 : 2 }
428
+ if (pageStyle === 'part') {
429
+ nextSectionLevel = 1
377
430
  } else {
378
- pageFragment = `#${pageId}`
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
+ }
379
442
  }
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
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
384
447
  } else if (level) {
385
- if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title && !hasAssemblyNavtitleAttr) {
448
+ if (atStartOfBody && !pageStyle && builder.matchesDoctitle(navtitlePlain)) {
386
449
  level--
387
- } 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
- }
450
+ } else if ((heading = { title: navtitleAsciiDoc, hlevel: level + 1 }).hlevel > 6) {
451
+ Object.assign(heading, { hlevel: 6, style: `discrete.h${heading.hlevel}` })
393
452
  }
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
453
  }
400
- let enclosed
401
454
  if (heading) {
402
455
  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}`)
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)
424
461
  }
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)
425
468
  }
469
+ if (isRootPage || (atStartOfBody && !heading)) assembled.pages.rootPageFragment = pageFragment
426
470
  assembled.pages.set(page, pageFragment)
427
- if (doc.hasSections()) fixSectionLevels(doc.getSections(), nextSectionLevel)
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)
476
+ if (hlevel > 6) {
477
+ buffer.push(`[discrete.h${hlevel}${sectionFragment}]`)
478
+ hlevel = 6
479
+ } else {
480
+ buffer.push(`[${sectionFragment}]`)
481
+ }
482
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitleAsciiDoc}`)
483
+ }
484
+ if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
428
485
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
429
486
  it.getContext() === 'document'
430
487
  ? it.getDocument().isNested()
431
488
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
432
489
  )
433
490
  if (doc.getDoctype() === 'manpage') {
434
- const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
491
+ const firstSectionIdx = hasSections ? doc.getSections()[0].getLineNumber() - 1 : lines.length
435
492
  for (let idx = 0; idx < firstSectionIdx; idx++) {
436
- if (~ignoreLines.indexOf(idx)) continue
493
+ if (ignoreLines.includes(idx)) continue
437
494
  const line = lines[idx]
438
495
  if (line.startsWith('== ') && line.length > 3) {
439
496
  allBlocks.unshift({
@@ -477,10 +534,10 @@ function mergeAsciiDoc (
477
534
  })
478
535
  let skipping
479
536
  for (let idx = 0, lastIdx = lines.length - 1; idx <= lastIdx; idx++) {
480
- if (~ignoreLines.indexOf(idx)) continue
537
+ if (ignoreLines.includes(idx)) continue
481
538
  let line = lines[idx]
482
539
  if (line.startsWith('//')) {
483
- if (line[2] !== '/') continue
540
+ if (line.charAt(2) !== '/') continue
484
541
  if (line.length > 3 && line === '/'.repeat(line.length)) {
485
542
  if (skipping) {
486
543
  if (line === skipping) skipping = undefined
@@ -547,9 +604,9 @@ function mergeAsciiDoc (
547
604
  let idx = lineno - 1
548
605
  if (context === 'section' && !block.getDocument().isNested()) {
549
606
  if (block.getSectionName() === 'header') return
550
- let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
607
+ let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
551
608
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
552
- let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
609
+ let targetMarkerLength = block.level + 1 + level
553
610
  if (targetMarkerLength > 6) {
554
611
  blockStyle = `discrete.h${targetMarkerLength}`
555
612
  targetMarkerLength = 6
@@ -593,10 +650,13 @@ function mergeAsciiDoc (
593
650
  buffer,
594
651
  lines.filter((it) => it !== undefined)
595
652
  )
596
- const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
653
+ const attributeEntries = doc.source_header_attributes.$entries()
597
654
  if (attributeEntries.length) {
598
655
  const resolvedAttributeEntries = attributeEntries.reduce(
599
656
  (accum, [name, val]) => {
657
+ if (val) {
658
+ if (val['$nil?']()) val = null
659
+ }
600
660
  // Q: couldn't we just check if attribute is locked?
601
661
  if (name in mutableAttributes) {
602
662
  const initialVal = mutableAttributes[name]
@@ -614,19 +674,19 @@ function mergeAsciiDoc (
614
674
  )
615
675
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
616
676
  }
617
- } else if (level) {
618
- if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
619
- 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)) {
620
681
  level--
621
682
  } else {
622
- buffer.inBody = true
623
- buffer.push('')
683
+ if (buffer.length) buffer.push('')
624
684
  const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
625
685
  let sectionTitle = navtitleAsciiDoc
626
686
  if (urlType === 'external') {
627
687
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
628
688
  } else if (urlType === 'internal' && !unresolved) {
629
- const resource = files.find((it) => it.pub.url === url)
689
+ const resource = files.find((it) => it.pub.url === pathname)
630
690
  if (resource) {
631
691
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
632
692
  const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
@@ -636,41 +696,42 @@ function mergeAsciiDoc (
636
696
  }
637
697
  }
638
698
  }
699
+ if (supportsParts && level === 1 && roles.includes('part')) {
700
+ roles.splice(roles.indexOf('part', 1))
701
+ level--
702
+ }
639
703
  const hlevel = level > 5 ? 6 : level + 1
640
704
  const hroles = urlType === 'internal' ? roles.slice(1) : roles
641
705
  buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
642
706
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
643
707
  }
644
708
  }
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
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
+ })
671
732
  }
672
733
 
673
- function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
734
+ function processPageHeader (doc, lines, ignoreLines, isRootPage) {
674
735
  const entries = []
675
736
  let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
676
737
  assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
@@ -690,8 +751,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
690
751
  }
691
752
  assemblyHeaderAttributes.delete('%authors')
692
753
  }
693
- const headerAttributes = doc.source_header_attributes
694
- for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
754
+ for (const name of assemblyHeaderAttributes) doc.source_header_attributes.$delete(name)
695
755
  }
696
756
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
697
757
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
@@ -706,7 +766,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
706
766
  }
707
767
  const line = lines[idx]
708
768
  if (open === ':' || open === '-:') {
709
- if (open === ':') current.lines.push(line)
769
+ if (open === ':') current.appendLine(line)
710
770
  if (!line?.endsWith(' \\')) current = open = undefined
711
771
  } else if (line) {
712
772
  let attributeEntryMatch
@@ -714,13 +774,14 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
714
774
  if (chr0 === '/' && line.charAt(1) === '/') {
715
775
  if (line.startsWith('////') && line === '/'.repeat(line.length)) {
716
776
  if (open) {
717
- current.lines.push(line)
777
+ current.appendLine(line)
718
778
  if (open === line) current = open = undefined
719
779
  } else {
720
780
  entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
781
+ current.appendLine = Array.prototype.push.bind(current.lines)
721
782
  }
722
783
  } else if (open) {
723
- current.lines.push(line)
784
+ current.appendLine(line)
724
785
  } else if (belowDoctitle && line.charAt(2) === '/') {
725
786
  break
726
787
  } else {
@@ -728,7 +789,7 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
728
789
  current = undefined
729
790
  }
730
791
  } else if (open) {
731
- current.lines.push(line)
792
+ current.appendLine(line)
732
793
  } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
733
794
  let name = attributeEntryMatch[1]
734
795
  const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
@@ -737,7 +798,9 @@ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
737
798
  if (line.endsWith(' \\')) open = '-:' // disallow value continuation
738
799
  } else {
739
800
  if (line.endsWith(' \\')) open = ':'
740
- entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
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)
741
804
  if (assemblyHeaderAttributes.has(name)) current.promote = true
742
805
  }
743
806
  } else if (belowDoctitle) {
@@ -788,33 +851,29 @@ function fixSectionLevels (sections, expectedLevel) {
788
851
  })
789
852
  }
790
853
 
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
854
  function safePush (onto, entries) {
804
855
  try {
805
856
  onto.push(...entries)
806
- } catch (err) {
807
- /* istanbul ignore if */
808
- if (!(err instanceof RangeError)) throw err
857
+ } catch {
809
858
  for (const e of entries) onto.push(e)
810
859
  }
811
860
  }
812
861
 
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}`
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
874
+ }
875
+ syntheticIds[id] = true
876
+ return id
818
877
  }
819
878
 
820
879
  module.exports = produceAssemblyFile