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

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,32 @@
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')
6
14
  const unconvertInlineAsciiDoc = require('./util/unconvert-inline-asciidoc')
7
15
 
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(' ')
16
+ const ATTR_ENTRY_RX = /^:(!?[\p{Alpha}0-9_][^:]*):(?: |$)/u
17
+ const BUILT_IN_NAMED_ENTITIES = { amp: '&', apos: "'", gt: '>', lt: '<', nbsp: ' ', quot: '"' }
18
+ const CHAR_REF_RX = /&(?:([a-z][a-z]+\d{0,2})|#(?:(\d{2,6})|x([a-z\d]{2,5})));/g
19
+ const DISCARD_ATTRIBUTE_NAMES = [
20
+ 'doctype',
21
+ 'leveloffset',
22
+ 'preface-title',
23
+ 'assembly-header-attributes',
24
+ 'assembly-navtitle',
25
+ 'assembly-slug',
26
+ 'assembly-style',
27
+ 'underscore',
28
+ ]
29
+ const { NAMED_ID_ATTR_RX } = require('./util/rx')
13
30
 
14
31
  function produceAssemblyFile (
15
32
  loadAsciiDoc,
@@ -23,7 +40,30 @@ function produceAssemblyFile (
23
40
  ) {
24
41
  const pagesByUrl = files.reduce((map, it) => (it.src.family === 'page' ? map.set(it.pub.url, it) : map), new Map())
25
42
  if (outline.urlType === 'internal' && !pagesByUrl.get(outline.url) && !(outline.items || []).length) return
26
- const pagesInOutline = selectPagesInOutline(outline, pagesByUrl, componentVersion)
43
+ const pagesInOutline = selectPagesInOutline(outline, pagesByUrl)
44
+ const { rootLevel, xmlIds } = assemblyModel
45
+ const idSeparators = {
46
+ prefix:
47
+ 'assembler-idprefix' in asciidocConfig.attributes ? (asciidocConfig.attributes['assembler-idprefix'] ?? '') : '_',
48
+ scope: xmlIds ? '---' : ':::',
49
+ 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
+ ),
57
+ }
58
+ const { name: component, version } = componentVersion
59
+ asciidocConfig = prepareAsciiDocConfig(
60
+ contentCatalog,
61
+ { component, version },
62
+ pagesInOutline,
63
+ asciidocConfig,
64
+ assemblyModel,
65
+ idSeparators
66
+ )
27
67
  const buffer = mergeAsciiDoc(
28
68
  loadAsciiDoc,
29
69
  contentCatalog,
@@ -32,61 +72,87 @@ function produceAssemblyFile (
32
72
  outline,
33
73
  files,
34
74
  pagesInOutline,
75
+ idSeparators,
35
76
  asciidocConfig,
36
77
  mutableAttributes,
37
78
  assemblyModel
38
79
  )
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, {
80
+ const stem =
81
+ (buffer.slug ? sanitizeSlug(buffer.slug) : generateSlug(rootLevel === 0 ? 'index' : buffer.navtitle)) ||
82
+ 'export-' + (assemblyModel.stemSeq = (assemblyModel.exportSeq ?? 0) + 1)
83
+ const downloadStem = [component, version, stem === 'index' ? '' : stem].filter((it) => it).join('-')
84
+ const file = contentCatalog.createFile({
45
85
  asciidoc: asciidocConfig,
46
86
  assembler: { assembled: pagesInOutline.assembled, downloadStem, rootLevel },
47
87
  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
- },
88
+ src: { component, version, componentVersion, module: 'ROOT', family: 'export', relative: stem + '.adoc' },
89
+ pub: false,
55
90
  })
91
+ file.path = file.out.path // use out path as file path so assets can easily be published to same hierarchy
92
+ delete file.out
93
+ return file
94
+ }
95
+
96
+ function prepareAsciiDocConfig (contentCatalog, ctx, pagesInOutline, asciidocConfig, assemblyModel, idSeparators) {
97
+ let attributesModified
98
+ const configShared = asciidocConfig.$shared
99
+ const sharedAttributes = asciidocConfig.attributes
100
+ if (!configShared) {
101
+ if (configShared == null) {
102
+ const assets = pagesInOutline.assembled.assets
103
+ for (const [name, val] of Object.entries(sharedAttributes)) {
104
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
105
+ let newVal
106
+ if (!name.endsWith('-image') || !(newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, ctx, assets))) {
107
+ if (~(newVal = val).indexOf('image:')) {
108
+ newVal = rewriteInlineImages(newVal, contentCatalog, assemblyModel, ctx, assets)
109
+ }
110
+ }
111
+ if (newVal !== val) sharedAttributes[name] = newVal
112
+ }
113
+ }
114
+ for (const [name, val] of Object.entries(sharedAttributes)) {
115
+ if (!(typeof val === 'string' && ~val.indexOf(':'))) continue
116
+ if (~val.indexOf('xref:')) {
117
+ const newVal = rewriteXrefs(val, contentCatalog, assemblyModel, ctx, false, pagesInOutline, idSeparators)
118
+ if (newVal !== val) (attributesModified ??= {})[name] = newVal
119
+ }
120
+ }
121
+ if (configShared == null) Object.defineProperty(asciidocConfig, '$shared', { value: attributesModified == null })
122
+ }
123
+ return Object.assign({}, asciidocConfig, { attributes: Object.assign({}, sharedAttributes, attributesModified) })
56
124
  }
57
125
 
58
126
  function buildAsciiDocHeader (componentVersion, navtitle, assemblyModel) {
59
127
  const doctype = assemblyModel.doctype ?? 'book'
60
128
  const navtitlePlain = sanitize(navtitle)
61
129
  const navtitleAsciiDoc = unconvertInlineAsciiDoc(navtitle)
62
- let doctitle = navtitleAsciiDoc
63
- if (navtitlePlain !== componentVersion.title) doctitle = `${componentVersion.title}: ${doctitle}`
130
+ const doctitle = (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
64
131
  const version = componentVersion.version === 'master' ? '' : componentVersion.version
132
+ const displayVersion = componentVersion.displayVersion
65
133
  const buffer = [
66
134
  `= ${doctitle}`,
67
- ...(version ? [`:revnumber: ${version}`] : []),
135
+ ...(version ? [`:revnumber: ${displayVersion}`] : []),
68
136
  ...(doctype === 'article' ? [] : [`:doctype: ${doctype ?? 'book'}`]),
69
137
  ':underscore: _',
70
138
  // Q: should we pass these via the CLI so they cannot be modified?
71
139
  `:page-component-name: ${componentVersion.name}`,
72
140
  `:page-component-version:${version ? ' ' + version : ''}`,
73
141
  ':page-version: {page-component-version}',
74
- `:page-component-display-version: ${componentVersion.displayVersion}`,
142
+ `:page-component-display-version: ${displayVersion}`,
75
143
  `:page-component-title: ${componentVersion.title}`,
76
144
  ]
77
145
  return Object.assign(buffer, { navtitle })
78
146
  }
79
147
 
80
- function selectPagesInOutline (outlineEntry, pagesByUrl, componentVersion, accum) {
148
+ function selectPagesInOutline (outlineEntry, pagesByUrl, accum) {
81
149
  accum ??= Object.assign(new Map(), { assembled: { pages: new Map(), assets: new Set() } })
82
150
  const page = outlineEntry.urlType === 'internal' ? pagesByUrl.get(outlineEntry.url) : undefined
83
151
  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)
86
- }
87
- accum.set(page.pub.url, page)
152
+ accum.set(createResourceKey(page.src), page)
153
+ accum.set(outlineEntry.url, page)
88
154
  }
89
- for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, componentVersion, accum)
155
+ for (const item of outlineEntry.items || []) selectPagesInOutline(item, pagesByUrl, accum)
90
156
  return accum
91
157
  }
92
158
 
@@ -98,6 +164,7 @@ function mergeAsciiDoc (
98
164
  outlineEntry,
99
165
  files,
100
166
  pagesInOutline,
167
+ idSeparators,
101
168
  asciidocConfig,
102
169
  mutableAttributes,
103
170
  assemblyModel,
@@ -110,94 +177,79 @@ function mergeAsciiDoc (
110
177
  buffer.inBody ??= false
111
178
  return buffer
112
179
  }
113
- let navtitle = outlineEntry.content
180
+ let navtitle = outlineEntry.navtitle ?? outlineEntry.content
114
181
  let navtitlePlain = sanitize(navtitle)
115
182
  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
183
+ const { items = [], roles = [], unresolved, urlType, url } = outlineEntry
184
+ const { filetype, linkReferenceStyle, pubRoot, sectionMergeStrategy, siteRoot, logger, rootLevel } = assemblyModel
185
+ const assembled = pagesInOutline.assembled
126
186
  // FIXME: ideally, resource ID would be stored in navigation so we can look up the page more efficiently
127
187
  const page = urlType === 'internal' && !unresolved ? pagesInOutline.get(url) : undefined
128
188
  const atDocumentRoot = !buffer.inBody
129
- const atBookRoot = atDocumentRoot && !level && doctype === 'book' && (supportsParts = true)
189
+ const atBookRoot = atDocumentRoot && !level && assemblyModel.doctype === 'book' && (supportsParts = true)
130
190
  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)) {
191
+ if (page && !assembled.pages.has(page)) {
136
192
  const contents = page.src.contents
137
193
  if (contents == null) {
138
194
  buffer.inBody ??= false
139
195
  return buffer
140
196
  }
197
+ const isRootPage = atDocumentRoot && !level
141
198
  const { component, version, module: module_, relative, origin, mediaType } = page.src
142
- const topicPrefix = ~relative.indexOf('/') ? path.dirname(relative) + '/' : ''
143
199
  const pageAsAsciiDoc = new page.constructor(
144
200
  Object.assign({}, page, { contents: trimAsciiDoc(contents), mediaType })
145
201
  )
146
202
  const doc = loadAsciiDoc(pageAsAsciiDoc, contentCatalog, asciidocConfig)
147
- if (doc.hasAttribute('assembly-navtitle')) {
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')
214
+ let hasAssemblyNavtitleAttr
215
+ if ((hasAssemblyNavtitleAttr = !!doc.getAttribute('assembly-navtitle'))) {
148
216
  navtitleAsciiDoc = doc.getAttribute('assembly-navtitle')
149
217
  navtitlePlain = sanitize((navtitle = doc.$apply_reftext_subs(navtitleAsciiDoc)))
150
218
  if (buffer.inBody == null) {
151
219
  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}`
220
+ if (rootLevel) {
221
+ const doctitle =
222
+ (componentVersion.title === navtitlePlain ? '' : componentVersion.title + ': ') + navtitleAsciiDoc
156
223
  buffer[0] = `= ${doctitle}`
157
224
  }
158
225
  }
159
226
  }
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
- }
171
- }
172
227
  // NOTE: in Antora, docname is relative src path from module without file extension
173
228
  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
229
+ const pageId = generateScopedId(page.src, componentVersion, idSeparators, filetype)
230
+ const pageIdLeader = pageId + idSeparators.scope
186
231
  let pageFragment = ''
187
- let pageRoles = ''
232
+ let pageRoles = []
188
233
  let pageStyle = doc.getAttribute('assembly-style', '')
234
+ if (!pageStyle && atBookRoot && rootLevel === 0 && !doc.source_header_attributes['$key?']('assembly-style')) {
235
+ pageStyle = 'preface'
236
+ }
189
237
  let part
190
238
  if ((part = pageStyle === 'part')) {
191
239
  pageStyle = ''
192
240
  if (!supportsParts) part = undefined
193
241
  } else if ((part = pageStyle.endsWith('-part'))) {
194
- pageStyle = pageStyle.slice(0, -5)
242
+ pageStyle = pageStyle.substring(0, pageStyle.length - 5)
195
243
  if (!supportsParts) part = undefined
196
244
  }
245
+ const hasSections = doc.hasSections()
197
246
  let nextSectionLevel = 1
198
247
  const lines = doc.getSourceLines()
199
248
  const ignoreLines = []
200
- buffer.inBody = true
249
+ if (!buffer.inBody) {
250
+ buffer.inBody = true
251
+ buffer.endHeaderIdx = buffer.length - 1
252
+ }
201
253
  buffer.push('')
202
254
  buffer.push(`:docname: ${docname}`)
203
255
  if (component !== lastComponentVersion.name) {
@@ -221,88 +273,161 @@ function mergeAsciiDoc (
221
273
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
222
274
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
223
275
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
224
- if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
276
+ let headerHasBlockAttrs
277
+ if (doc.hasHeader()) {
278
+ for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
279
+ if (entry.type === 'author_line') {
280
+ buffer.splice(1, 0, entry.lines[0])
281
+ buffer.endHeaderIdx += 1
282
+ } else if (entry.type === 'attribute_entry') {
283
+ const name = entry.name
284
+ if (!entry.negated && !doc.isAttributeLocked(name)) {
285
+ let val, newVal
286
+ if (
287
+ name.endsWith('-image') &&
288
+ (val = doc.getAttribute(name)) &&
289
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
290
+ ) {
291
+ if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
292
+ } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
293
+ newVal = val
294
+ if (~newVal.indexOf('image:')) {
295
+ newVal = rewriteInlineImages(
296
+ newVal,
297
+ contentCatalog,
298
+ assemblyModel,
299
+ page.src,
300
+ assembled.assets,
301
+ false,
302
+ doc
303
+ )
304
+ }
305
+ if (~newVal.indexOf('xref:')) {
306
+ newVal = rewriteXrefs(
307
+ newVal,
308
+ contentCatalog,
309
+ assemblyModel,
310
+ page.src,
311
+ false,
312
+ pagesInOutline,
313
+ idSeparators,
314
+ pageIdLeader,
315
+ doc,
316
+ { file: relative, lineno: entry.lineno }
317
+ )
318
+ }
319
+ if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
320
+ }
321
+ }
322
+ if (entry.promote) {
323
+ buffer.splice(buffer.endHeaderIdx + 1, 0, ...entry.lines)
324
+ buffer.endHeaderIdx += entry.lines.length
325
+ } else {
326
+ buffer.push(...entry.lines)
327
+ }
328
+ } else {
329
+ if (entry.type === 'attrlist') headerHasBlockAttrs = true
330
+ buffer.push(...entry.lines)
331
+ }
332
+ }
333
+ pageRoles = doc.getRoles()
334
+ }
335
+ if (roles.length) pageRoles = pageRoles.concat(roles[0] === 'page' ? roles.slice(1) : roles)
225
336
  let heading
226
337
  if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
227
338
  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
- ) {
234
- htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
235
- htitlePlain = sanitize(htitleOverride)
339
+ let notitle
340
+ if (pageStyle === 'preface') {
341
+ const hasPrefaceTitleAttr =
342
+ doc.isAttribute('preface-title') ||
343
+ doc.source_header_attributes['$key?']('preface-title') ||
344
+ doc.isAttributeLocked('preface-title')
345
+ const htitleOverride = hasPrefaceTitleAttr
346
+ ? doc.getAttribute('preface-title', '')
347
+ : atDocumentRoot && (rootLevel ? true : !hasAssemblyNavtitleAttr)
348
+ ? ''
349
+ : undefined
350
+ if (htitleOverride != null) {
351
+ const keepSections = hasSections && sectionMergeStrategy !== 'discrete'
352
+ if (htitleOverride === '') {
353
+ if (headerHasBlockAttrs || keepSections) {
354
+ htitleAsciiDoc = '{empty}'
355
+ notitle = true
356
+ } else {
357
+ if (hasPrefaceTitleAttr) buffer.splice(++buffer.endHeaderIdx, 0, ':preface-title:')
358
+ htitleAsciiDoc = undefined
359
+ }
360
+ } else if (hasPrefaceTitleAttr && !headerHasBlockAttrs && !keepSections) {
361
+ buffer.splice(++buffer.endHeaderIdx, 0, `:preface-title: ${unconvertInlineAsciiDoc(htitleOverride)}`)
362
+ htitleAsciiDoc = undefined
363
+ } else {
364
+ htitleAsciiDoc = unconvertInlineAsciiDoc(htitleOverride)
365
+ }
366
+ }
236
367
  }
237
- if (atDocumentRoot && pageStyle === 'preface' && htitlePlain === componentVersion.title) {
238
- assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
239
- } else {
368
+ if (htitleAsciiDoc != null) {
240
369
  if (atDocumentRoot) {
241
- pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
242
- buffer.unshift(`[#${idScope}]`)
370
+ // NOTE use pageStyle as fallback because this section must have a unique ID
371
+ pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
372
+ buffer.unshift(`[#${pageId}]`)
243
373
  } else {
244
- pageFragment = `#${idScope}`
374
+ pageFragment = `#${pageId}`
245
375
  }
246
376
  heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
247
- nextSectionLevel++
377
+ if (notitle) heading.notitle = true
378
+ nextSectionLevel = 2 // next section level is 2, even for special part
248
379
  }
249
380
  } else if (level) {
250
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
381
+ if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title && !hasAssemblyNavtitleAttr) {
251
382
  level--
252
383
  } else {
253
- pageFragment = `#${idScope}`
384
+ pageFragment = `#${pageId}`
254
385
  if (part && level === 1) level--
255
386
  if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
256
387
  Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
257
388
  }
258
389
  }
390
+ } else if (atDocumentRoot && rootLevel === 0 && hasAssemblyNavtitleAttr) {
391
+ pageFragment = `#${pageId}`
392
+ heading = { title: navtitleAsciiDoc, level: (nextSectionLevel = part ? 1 : 2) }
393
+ } else if (atBookRoot) {
394
+ nextSectionLevel = undefined
259
395
  }
396
+ let enclosed
260
397
  if (heading) {
261
- buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
398
+ const rolesAttr = pageRoles.reduce((str, it) => str + '.' + it, '')
399
+ const notitleAttrs = heading.notitle ? '%notitle,toclevels=0,outlinelevels=0' : ''
400
+ buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${rolesAttr}${notitleAttrs}]`)
262
401
  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)}`
402
+ } else {
403
+ if (atDocumentRoot) buffer.unshift(`[#${pageId}]`)
404
+ if (sectionMergeStrategy === 'enclose' && hasItems && hasSections) {
405
+ enclosed = true
406
+ // TODO: make overview section title configurable
407
+ //let overviewTitle = doc.getDocumentTitle()
408
+ //if (overviewTitle === navtitle) overviewTitle = doc.getAttribute('overview-title', 'Overview')
409
+ const overviewTitle = doc.getAttribute('overview-title', 'Overview')
410
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
411
+ let hlevel = level + 2
412
+ if (hlevel > 6) {
413
+ const blockStyle = `discrete.h${hlevel}`
414
+ hlevel = 6
415
+ buffer.push(`[${blockStyle}#${syntheticId}]`)
279
416
  } else {
280
- buffer.push(':!sectids:')
281
- toggleSectids = true
417
+ buffer.push(`[#${syntheticId}]`)
282
418
  }
419
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
283
420
  }
284
- let hlevel = level + 2
285
- if (hlevel > 6) {
286
- const blockStyle = `discrete.h${hlevel}`
287
- hlevel = 6
288
- buffer.push(syntheticId ? `[${blockStyle}#${syntheticId}]` : `[${blockStyle}]`)
289
- } else if (syntheticId) {
290
- buffer.push(`[#${syntheticId}]`)
291
- }
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)
298
421
  }
422
+ assembled.pages.set(page, pageFragment)
423
+ if (hasSections) fixSectionLevels(doc.getSections(), nextSectionLevel)
299
424
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
300
425
  it.getContext() === 'document'
301
426
  ? it.getDocument().isNested()
302
427
  : !(it.getContext() === 'table_cell' && it.getStyle() === 'asciidoc')
303
428
  )
304
429
  if (doc.getDoctype() === 'manpage') {
305
- const firstSectionIdx = doc.hasSections() ? doc.getSections()[0].getLineNumber() - 1 : lines.length
430
+ const firstSectionIdx = hasSections ? doc.getSections()[0].getLineNumber() - 1 : lines.length
306
431
  for (let idx = 0; idx < firstSectionIdx; idx++) {
307
432
  if (~ignoreLines.indexOf(idx)) continue
308
433
  const line = lines[idx]
@@ -366,7 +491,7 @@ function mergeAsciiDoc (
366
491
  if (
367
492
  line.charAt() === ':' &&
368
493
  ~line.indexOf(':', 2) &&
369
- (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
494
+ (line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
370
495
  ) {
371
496
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
372
497
  lines[idx] = undefined
@@ -378,111 +503,31 @@ function mergeAsciiDoc (
378
503
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
379
504
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
380
505
  }
381
- return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
506
+ return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
382
507
  })
383
508
  }
384
509
  // NOTE: the next check takes care of inline and block anchors
385
510
  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}]`
511
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
512
+ return `[[${pageIdLeader}${refid}${text}]]`
455
513
  })
456
514
  }
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
- })
515
+ if (~line.indexOf('xref:')) {
516
+ line = rewriteXrefs(
517
+ line,
518
+ contentCatalog,
519
+ assemblyModel,
520
+ page.src,
521
+ true,
522
+ pagesInOutline,
523
+ idSeparators,
524
+ pageIdLeader,
525
+ doc,
526
+ { file: relative, lineno: idx + 1 }
527
+ )
471
528
  }
472
529
  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
- })
530
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
486
531
  }
487
532
  lines[idx] = line
488
533
  }
@@ -498,7 +543,7 @@ function mergeAsciiDoc (
498
543
  let idx = lineno - 1
499
544
  if (context === 'section' && !block.getDocument().isNested()) {
500
545
  if (block.getSectionName() === 'header') return
501
- let blockStyle = (assemblyModel.sectionMergeStrategy || 'discrete') === 'discrete' ? 'discrete' : undefined
546
+ let blockStyle = sectionMergeStrategy === 'discrete' ? 'discrete' : undefined
502
547
  lines[idx] = lines[idx].replace(/^=+ (.+)/, (_, rest) => {
503
548
  let targetMarkerLength = block.level + 1 + level + (enclosed ? 1 : 0)
504
549
  if (targetMarkerLength > 6) {
@@ -508,53 +553,43 @@ function mergeAsciiDoc (
508
553
  return '='.repeat(targetMarkerLength) + ' ' + rest
509
554
  })
510
555
  // NOTE: ID will be undefined if sectids are turned off
511
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
556
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
512
557
  } else {
513
558
  if (context === 'image') {
514
559
  let line = lines[idx] || ''
515
560
  let prefix = ''
516
561
  // Q: can we use startsWith('image::') in certain cases?
517
562
  let imageMacroOffset = (
518
- lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
563
+ lastImageMacroAt?.[0] === idx ? line.substring(0, lastImageMacroAt[1]) : line
519
564
  ).lastIndexOf('image::')
520
565
  if (imageMacroOffset > 0) {
521
566
  if (
522
567
  block.getDocument().isNested() &&
523
- (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
568
+ (prefix = line.substring(0, imageMacroOffset)).trimRight().endsWith('|')
524
569
  ) {
525
- line = line.slice(prefix.length)
570
+ line = line.substring(prefix.length)
526
571
  } else {
527
572
  imageMacroOffset = -1
528
573
  }
529
574
  }
530
- if (imageMacroOffset >= 0) {
575
+ if (~imageMacroOffset) {
531
576
  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
- }
577
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
578
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.substring(line.indexOf('['))}`
544
579
  lastImageMacroAt = [idx, imageMacroOffset]
545
580
  }
546
581
  } else if (context === 'document' && block.hasHeader()) {
547
582
  // nested document
548
583
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
549
584
  }
550
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
585
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
551
586
  }
552
587
  })
553
588
  safePush(
554
589
  buffer,
555
590
  lines.filter((it) => it !== undefined)
556
591
  )
557
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
592
+ const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
558
593
  if (attributeEntries.length) {
559
594
  const resolvedAttributeEntries = attributeEntries.reduce(
560
595
  (accum, [name, val]) => {
@@ -566,7 +601,7 @@ function mergeAsciiDoc (
566
601
  } else if (val !== initialVal) {
567
602
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
568
603
  }
569
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
604
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
570
605
  accum.push(`:!${name}:`)
571
606
  }
572
607
  return accum
@@ -576,26 +611,13 @@ function mergeAsciiDoc (
576
611
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
577
612
  }
578
613
  } else if (level) {
579
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
614
+ if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
580
615
  buffer.inBody ??= false
581
616
  level--
582
617
  } else {
583
618
  buffer.inBody = true
584
619
  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
- }
620
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
599
621
  let sectionTitle = navtitleAsciiDoc
600
622
  if (urlType === 'external') {
601
623
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -603,27 +625,19 @@ function mergeAsciiDoc (
603
625
  const resource = files.find((it) => it.pub.url === url)
604
626
  if (resource) {
605
627
  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}`
628
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
609
629
  sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
610
630
  } else if (siteRoot) {
611
- sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
631
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
612
632
  }
613
633
  }
614
634
  }
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}]`)
621
- }
635
+ const hlevel = level > 5 ? 6 : level + 1
636
+ const hroles = urlType === 'internal' ? roles.slice(1) : roles
637
+ buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
622
638
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
623
- if (toggleSectids) buffer.push(':sectids:')
624
639
  }
625
640
  }
626
-
627
641
  if (hasItems) {
628
642
  const nextLevel = level + 1
629
643
  // NOTE: drop first child if same as parent; should we keep if content is different?
@@ -639,6 +653,7 @@ function mergeAsciiDoc (
639
653
  item,
640
654
  files,
641
655
  pagesInOutline,
656
+ idSeparators,
642
657
  asciidocConfig,
643
658
  mutableAttributes,
644
659
  assemblyModel,
@@ -651,11 +666,32 @@ function mergeAsciiDoc (
651
666
  return buffer
652
667
  }
653
668
 
654
- function processDocumentHeader (doc, lines, buffer, ignoreLines) {
669
+ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
670
+ const entries = []
671
+ let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
672
+ assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
673
+ if (assemblyHeaderAttributes.size) {
674
+ if (assemblyHeaderAttributes.has('%authors')) {
675
+ const authors = doc.getAuthors()
676
+ if (authors.length) {
677
+ entries.push({
678
+ type: 'author_line',
679
+ promote: true,
680
+ lines: [
681
+ authors
682
+ .map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
683
+ .join('; '),
684
+ ],
685
+ })
686
+ }
687
+ assemblyHeaderAttributes.delete('%authors')
688
+ }
689
+ const headerAttributes = doc.source_header_attributes
690
+ for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
691
+ }
655
692
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
656
693
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
657
- let belowDoctitle
658
- let open
694
+ let belowDoctitle, current, open
659
695
  const implicitLines = []
660
696
  for (let idx = 0; idx < end; idx++) {
661
697
  if (idx === doctitleIdx) {
@@ -666,41 +702,51 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
666
702
  }
667
703
  const line = lines[idx]
668
704
  if (open === ':' || open === '-:') {
669
- if (line) {
670
- if (open === ':') buffer.push(line)
671
- if (!line.endsWith(' \\')) open = undefined
672
- } else {
673
- open = undefined
674
- }
705
+ if (open === ':') current.lines.push(line)
706
+ if (!line?.endsWith(' \\')) current = open = undefined
675
707
  } else if (line) {
676
- const chr0 = line.charAt()
677
708
  let attributeEntryMatch
709
+ const chr0 = line.charAt()
678
710
  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) === '/') {
711
+ if (line.startsWith('////') && line === '/'.repeat(line.length)) {
712
+ if (open) {
713
+ current.lines.push(line)
714
+ if (open === line) current = open = undefined
715
+ } else {
716
+ entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
717
+ }
718
+ } else if (open) {
719
+ current.lines.push(line)
720
+ } else if (belowDoctitle && line.charAt(2) === '/') {
682
721
  break
722
+ } else {
723
+ entries.push({ type: 'line_comment', lines: [line] })
724
+ current = undefined
683
725
  }
684
- buffer.push(line)
685
726
  } 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 = '-:'
727
+ current.lines.push(line)
728
+ } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
729
+ let name = attributeEntryMatch[1]
730
+ const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
731
+ if (negated) name = name.replace('!', '')
732
+ if (DISCARD_ATTRIBUTE_NAMES.includes(name) && !assemblyHeaderAttributes.has(name)) {
733
+ if (line.endsWith(' \\')) open = '-:' // disallow value continuation
691
734
  } else {
692
735
  if (line.endsWith(' \\')) open = ':'
693
- buffer.push(line)
736
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
737
+ if (assemblyHeaderAttributes.has(name)) current.promote = true
694
738
  }
695
739
  } else if (belowDoctitle) {
696
740
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
697
741
  implicitLines.push(line)
698
742
  } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
743
+ current = undefined
699
744
  const attrlist = line
700
- .slice(1, -1)
745
+ .substring(1, line.length - 1)
701
746
  .trim()
702
- .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
703
- if (attrlist) buffer.push('[' + attrlist + ']')
747
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
748
+ .replace(NAMED_ID_ATTR_RX, '')
749
+ if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
704
750
  }
705
751
  } else if (belowDoctitle) {
706
752
  break
@@ -708,23 +754,26 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
708
754
  lines[idx] = undefined
709
755
  ignoreLines.push(idx)
710
756
  }
711
- return doc
712
- .getRoles()
713
- .map((role) => '.' + role)
714
- .join('')
757
+ return entries
715
758
  }
716
759
 
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')
760
+ function generateSlug (string) {
761
+ if (string === 'index') return string
762
+ return sanitizeSlug(
763
+ string
764
+ .toLowerCase()
765
+ .replace(/<[^>]+>/g, '')
766
+ .replace(CHAR_REF_RX, (_, name, dec, hex) => {
767
+ if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
768
+ return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
769
+ })
770
+ .replace(/[\x27\u2019]/g, '')
771
+ .replace(/[_.]/, '-')
772
+ )
773
+ }
774
+
775
+ function sanitizeSlug (slug) {
776
+ return slug.replace(/[^\p{Alpha}0-9_.-]/gu, '-').replace(/^-+|-+$|(-)-+/g, '$1')
728
777
  }
729
778
 
730
779
  function fixSectionLevels (sections, expectedLevel) {
@@ -735,64 +784,6 @@ function fixSectionLevels (sections, expectedLevel) {
735
784
  })
736
785
  }
737
786
 
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
787
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
797
788
  // IMPORTANT: this must not leave behind lines the parser will drop!
798
789
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -815,35 +806,11 @@ function safePush (onto, entries) {
815
806
  }
816
807
  }
817
808
 
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
- }
839
- }
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
809
+ function generateIdFromTitle (titleAsciiDoc, idSeparators) {
810
+ const Section = this.$class().$const_get('::Asciidoctor::Section')
811
+ const baseId = Section.$generate_id(titleAsciiDoc, this)
812
+ this.getCatalog().refs['$[]='](baseId, true)
813
+ return `_${idSeparators.coordinate}${baseId}`
847
814
  }
848
815
 
849
816
  module.exports = produceAssemblyFile