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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,84 @@ 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, 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 (
235
+ !pageStyle &&
236
+ atBookRoot &&
237
+ rootLevel === 0 &&
238
+ assemblyModel.sectionMergeStrategy === 'fuse' &&
239
+ !doc.source_header_attributes['$key?']('assembly-style')
240
+ ) {
241
+ pageStyle = 'preface'
242
+ }
189
243
  let part
190
244
  if ((part = pageStyle === 'part')) {
191
245
  pageStyle = ''
192
246
  if (!supportsParts) part = undefined
193
247
  } else if ((part = pageStyle.endsWith('-part'))) {
194
- pageStyle = pageStyle.slice(0, -5)
248
+ pageStyle = pageStyle.substring(0, pageStyle.length - 5)
195
249
  if (!supportsParts) part = undefined
196
250
  }
197
251
  let nextSectionLevel = 1
198
252
  const lines = doc.getSourceLines()
199
253
  const ignoreLines = []
200
- buffer.inBody = true
254
+ if (!buffer.inBody) {
255
+ buffer.inBody = true
256
+ buffer.endHeaderIdx = buffer.length - 1
257
+ }
201
258
  buffer.push('')
202
259
  buffer.push(`:docname: ${docname}`)
203
260
  if (component !== lastComponentVersion.name) {
@@ -221,81 +278,153 @@ function mergeAsciiDoc (
221
278
  buffer.push(`:page-origin-refname: ${origin.branch || origin.tag}`)
222
279
  buffer.push(`:page-origin-reftype: ${origin.branch ? 'branch' : 'tag'}`)
223
280
  buffer.push(`:page-origin-refhash: ${origin.worktree ? '(worktree)' : origin.refhash}`)
224
- if (doc.hasHeader()) pageRoles = processDocumentHeader(doc, lines, buffer, ignoreLines)
281
+ let headerHasBlockAttrs
282
+ if (doc.hasHeader()) {
283
+ for (const entry of processDocumentHeader(doc, lines, ignoreLines, isRootPage)) {
284
+ if (entry.type === 'author_line') {
285
+ buffer.splice(1, 0, entry.lines[0])
286
+ buffer.endHeaderIdx += 1
287
+ } else if (entry.type === 'attribute_entry') {
288
+ const name = entry.name
289
+ if (!entry.negated && !doc.isAttributeLocked(name)) {
290
+ let val, newVal
291
+ if (
292
+ name.endsWith('-image') &&
293
+ (val = doc.getAttribute(name)) &&
294
+ (newVal = rewriteImageAttr(val, contentCatalog, assemblyModel, page.src, assembled.assets))
295
+ ) {
296
+ if (newVal !== val) entry.lines = [`:${name}: ${newVal}`]
297
+ } else if (~(val = entry.lines.join('\n').substring(name.length + 3)).indexOf(':')) {
298
+ newVal = val
299
+ if (~newVal.indexOf('image:')) {
300
+ newVal = rewriteInlineImages(
301
+ newVal,
302
+ contentCatalog,
303
+ assemblyModel,
304
+ page.src,
305
+ assembled.assets,
306
+ false,
307
+ doc
308
+ )
309
+ }
310
+ if (~newVal.indexOf('xref:')) {
311
+ newVal = rewriteXrefs(
312
+ newVal,
313
+ contentCatalog,
314
+ assemblyModel,
315
+ page.src,
316
+ false,
317
+ pagesInOutline,
318
+ idSeparators,
319
+ pageIdLeader,
320
+ doc,
321
+ { file: relative, lineno: entry.lineno }
322
+ )
323
+ }
324
+ if (newVal !== val) entry.lines = `:${entry.name}: ${newVal}`.split('\n')
325
+ }
326
+ }
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
+ }
333
+ } else {
334
+ if (entry.type === 'attrlist') headerHasBlockAttrs = true
335
+ buffer.push(...entry.lines)
336
+ }
337
+ }
338
+ pageRoles = doc.getRoles()
339
+ }
340
+ if (roles.length) pageRoles = pageRoles.concat(roles[0] === 'page' ? roles.slice(1) : roles)
225
341
  let heading
226
342
  if (pageStyle && (part && level === 1 ? (level = 0) : level) === 0) {
227
343
  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)
344
+ let notitle
345
+ if (pageStyle === 'preface') {
346
+ const hasPrefaceTitleAttr =
347
+ doc.isAttribute('preface-title') ||
348
+ doc.source_header_attributes['$key?']('preface-title') ||
349
+ 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:')
362
+ htitleAsciiDoc = undefined
363
+ }
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
+ }
370
+ }
236
371
  }
237
- if (atDocumentRoot && pageStyle === 'preface' && htitlePlain === componentVersion.title) {
238
- assemblyModel = Object.assign({}, assemblyModel, { sectionMergeStrategy: 'discrete' })
239
- } else {
372
+ if (htitleAsciiDoc != null) {
240
373
  if (atDocumentRoot) {
241
- pageFragment = `#${idPrefix}${doc.getId() ?? pageStyle}`
242
- buffer.unshift(`[#${idScope}]`)
374
+ // NOTE use pageStyle as fallback because this section must have a unique ID
375
+ pageFragment = `#${pageIdLeader}${doc.getId() ?? pageStyle}`
376
+ buffer.unshift(`[#${pageId}]`)
243
377
  } else {
244
- pageFragment = `#${idScope}`
378
+ pageFragment = `#${pageId}`
245
379
  }
246
380
  heading = { title: htitleAsciiDoc, level: part ? 1 : 2 }
247
- nextSectionLevel++
381
+ if (notitle) heading.notitle = true
382
+ nextSectionLevel = 2 // next section level is 2, even for special part
248
383
  }
249
384
  } else if (level) {
250
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
385
+ if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title && !hasAssemblyNavtitleAttr) {
251
386
  level--
252
387
  } else {
253
- pageFragment = `#${idScope}`
388
+ pageFragment = `#${pageId}`
254
389
  if (part && level === 1) level--
255
390
  if ((heading = { title: navtitleAsciiDoc, level: level + 1 }).level > 6) {
256
391
  Object.assign(heading, { level: 6, style: `discrete.h${heading.level}` })
257
392
  }
258
393
  }
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
259
399
  }
400
+ let enclosed
260
401
  if (heading) {
261
- buffer.push(`[${heading.style ?? pageStyle}${pageFragment}${pageRoles}]`)
402
+ 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}]`)
262
405
  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)}`
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}]`)
279
420
  } else {
280
- buffer.push(':!sectids:')
281
- toggleSectids = true
421
+ buffer.push(`[#${syntheticId}]`)
282
422
  }
423
+ buffer.push(`${'='.repeat(hlevel)} ${overviewTitle}`)
283
424
  }
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
425
  }
426
+ assembled.pages.set(page, pageFragment)
427
+ if (doc.hasSections()) fixSectionLevels(doc.getSections(), nextSectionLevel)
299
428
  const allBlocks = doc.findBy({ traverse_documents: true }, (it) =>
300
429
  it.getContext() === 'document'
301
430
  ? it.getDocument().isNested()
@@ -366,7 +495,7 @@ function mergeAsciiDoc (
366
495
  if (
367
496
  line.charAt() === ':' &&
368
497
  ~line.indexOf(':', 2) &&
369
- (line.match(AttributeEntryRx) || ['', ''])[1].replace('!', '') === 'leveloffset'
498
+ (line.match(ATTR_ENTRY_RX) || ['', ''])[1].replace('!', '') === 'leveloffset'
370
499
  ) {
371
500
  if (lines[idx - 1] === '') lines[idx - 1] = undefined
372
501
  lines[idx] = undefined
@@ -378,111 +507,31 @@ function mergeAsciiDoc (
378
507
  if (!refs['$key?'](refid) && (~refid.indexOf(' ') || refid.toLowerCase() !== refid)) {
379
508
  if ((refid = doc.$resolve_id(refid))['$nil?']()) return m
380
509
  }
381
- return `<<${idPrefix}${refid}${text ? ',' + text : ''}>>`
510
+ return `<<${pageIdLeader}${refid}${text ? ',' + text : ''}>>`
382
511
  })
383
512
  }
384
513
  // NOTE: the next check takes care of inline and block anchors
385
514
  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}]`
515
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
516
+ return `[[${pageIdLeader}${refid}${text}]]`
455
517
  })
456
518
  }
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
- })
519
+ if (~line.indexOf('xref:')) {
520
+ line = rewriteXrefs(
521
+ line,
522
+ contentCatalog,
523
+ assemblyModel,
524
+ page.src,
525
+ true,
526
+ pagesInOutline,
527
+ idSeparators,
528
+ pageIdLeader,
529
+ doc,
530
+ { file: relative, lineno: idx + 1 }
531
+ )
471
532
  }
472
533
  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
- })
534
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
486
535
  }
487
536
  lines[idx] = line
488
537
  }
@@ -508,53 +557,43 @@ function mergeAsciiDoc (
508
557
  return '='.repeat(targetMarkerLength) + ' ' + rest
509
558
  })
510
559
  // NOTE: ID will be undefined if sectids are turned off
511
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
560
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
512
561
  } else {
513
562
  if (context === 'image') {
514
563
  let line = lines[idx] || ''
515
564
  let prefix = ''
516
565
  // Q: can we use startsWith('image::') in certain cases?
517
566
  let imageMacroOffset = (
518
- lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
567
+ lastImageMacroAt?.[0] === idx ? line.substring(0, lastImageMacroAt[1]) : line
519
568
  ).lastIndexOf('image::')
520
569
  if (imageMacroOffset > 0) {
521
570
  if (
522
571
  block.getDocument().isNested() &&
523
- (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
572
+ (prefix = line.substring(0, imageMacroOffset)).trimRight().endsWith('|')
524
573
  ) {
525
- line = line.slice(prefix.length)
574
+ line = line.substring(prefix.length)
526
575
  } else {
527
576
  imageMacroOffset = -1
528
577
  }
529
578
  }
530
- if (imageMacroOffset >= 0) {
579
+ if (~imageMacroOffset) {
531
580
  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
- }
581
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
582
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.substring(line.indexOf('['))}`
544
583
  lastImageMacroAt = [idx, imageMacroOffset]
545
584
  }
546
585
  } else if (context === 'document' && block.hasHeader()) {
547
586
  // nested document
548
587
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
549
588
  }
550
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
589
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
551
590
  }
552
591
  })
553
592
  safePush(
554
593
  buffer,
555
594
  lines.filter((it) => it !== undefined)
556
595
  )
557
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
596
+ const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
558
597
  if (attributeEntries.length) {
559
598
  const resolvedAttributeEntries = attributeEntries.reduce(
560
599
  (accum, [name, val]) => {
@@ -566,7 +605,7 @@ function mergeAsciiDoc (
566
605
  } else if (val !== initialVal) {
567
606
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
568
607
  }
569
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
608
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
570
609
  accum.push(`:!${name}:`)
571
610
  }
572
611
  return accum
@@ -576,26 +615,13 @@ function mergeAsciiDoc (
576
615
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
577
616
  }
578
617
  } else if (level) {
579
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
618
+ if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
580
619
  buffer.inBody ??= false
581
620
  level--
582
621
  } else {
583
622
  buffer.inBody = true
584
623
  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
- }
624
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
599
625
  let sectionTitle = navtitleAsciiDoc
600
626
  if (urlType === 'external') {
601
627
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -603,27 +629,19 @@ function mergeAsciiDoc (
603
629
  const resource = files.find((it) => it.pub.url === url)
604
630
  if (resource) {
605
631
  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}`
632
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
609
633
  sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
610
634
  } else if (siteRoot) {
611
- sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
635
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
612
636
  }
613
637
  }
614
638
  }
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
- }
639
+ const hlevel = level > 5 ? 6 : level + 1
640
+ const hroles = urlType === 'internal' ? roles.slice(1) : roles
641
+ buffer.push(`[${hlevel > 6 ? 'discrete' : ''}#${syntheticId}${hroles.reduce((str, it) => str + '.' + it, '')}]`)
622
642
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
623
- if (toggleSectids) buffer.push(':sectids:')
624
643
  }
625
644
  }
626
-
627
645
  if (hasItems) {
628
646
  const nextLevel = level + 1
629
647
  // NOTE: drop first child if same as parent; should we keep if content is different?
@@ -639,6 +657,7 @@ function mergeAsciiDoc (
639
657
  item,
640
658
  files,
641
659
  pagesInOutline,
660
+ idSeparators,
642
661
  asciidocConfig,
643
662
  mutableAttributes,
644
663
  assemblyModel,
@@ -651,11 +670,32 @@ function mergeAsciiDoc (
651
670
  return buffer
652
671
  }
653
672
 
654
- function processDocumentHeader (doc, lines, buffer, ignoreLines) {
673
+ function processDocumentHeader (doc, lines, ignoreLines, isRootPage) {
674
+ const entries = []
675
+ let assemblyHeaderAttributes = isRootPage && doc.getAttribute('assembly-header-attributes', '%authors')
676
+ assemblyHeaderAttributes = new Set(assemblyHeaderAttributes ? assemblyHeaderAttributes.split(/, */) : undefined)
677
+ if (assemblyHeaderAttributes.size) {
678
+ if (assemblyHeaderAttributes.has('%authors')) {
679
+ const authors = doc.getAuthors()
680
+ if (authors.length) {
681
+ entries.push({
682
+ type: 'author_line',
683
+ promote: true,
684
+ lines: [
685
+ authors
686
+ .map((author) => (author.getEmail() ? `${author.getName()} <${author.getEmail()}>` : author.getName()))
687
+ .join('; '),
688
+ ],
689
+ })
690
+ }
691
+ assemblyHeaderAttributes.delete('%authors')
692
+ }
693
+ const headerAttributes = doc.source_header_attributes
694
+ for (const name of assemblyHeaderAttributes) headerAttributes.$delete(name)
695
+ }
655
696
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
656
697
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
657
- let belowDoctitle
658
- let open
698
+ let belowDoctitle, current, open
659
699
  const implicitLines = []
660
700
  for (let idx = 0; idx < end; idx++) {
661
701
  if (idx === doctitleIdx) {
@@ -666,41 +706,51 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
666
706
  }
667
707
  const line = lines[idx]
668
708
  if (open === ':' || open === '-:') {
669
- if (line) {
670
- if (open === ':') buffer.push(line)
671
- if (!line.endsWith(' \\')) open = undefined
672
- } else {
673
- open = undefined
674
- }
709
+ if (open === ':') current.lines.push(line)
710
+ if (!line?.endsWith(' \\')) current = open = undefined
675
711
  } else if (line) {
676
- const chr0 = line.charAt()
677
712
  let attributeEntryMatch
713
+ const chr0 = line.charAt()
678
714
  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) === '/') {
715
+ if (line.startsWith('////') && line === '/'.repeat(line.length)) {
716
+ if (open) {
717
+ current.lines.push(line)
718
+ if (open === line) current = open = undefined
719
+ } else {
720
+ entries.push((current = { type: 'block_comment', lines: [(open = line)] }))
721
+ }
722
+ } else if (open) {
723
+ current.lines.push(line)
724
+ } else if (belowDoctitle && line.charAt(2) === '/') {
682
725
  break
726
+ } else {
727
+ entries.push({ type: 'line_comment', lines: [line] })
728
+ current = undefined
683
729
  }
684
- buffer.push(line)
685
730
  } 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 = '-:'
731
+ current.lines.push(line)
732
+ } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(ATTR_ENTRY_RX))) {
733
+ let name = attributeEntryMatch[1]
734
+ const negated = name.charAt() === '!' || name.charAt(name.length - 1) === '!'
735
+ if (negated) name = name.replace('!', '')
736
+ if (DISCARD_ATTRIBUTE_NAMES.includes(name) && !assemblyHeaderAttributes.has(name)) {
737
+ if (line.endsWith(' \\')) open = '-:' // disallow value continuation
691
738
  } else {
692
739
  if (line.endsWith(' \\')) open = ':'
693
- buffer.push(line)
740
+ entries.push((current = { type: 'attribute_entry', name, negated, lines: [line], lineno: idx + 1 }))
741
+ if (assemblyHeaderAttributes.has(name)) current.promote = true
694
742
  }
695
743
  } else if (belowDoctitle) {
696
744
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
697
745
  implicitLines.push(line)
698
746
  } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
747
+ current = undefined
699
748
  const attrlist = line
700
- .slice(1, -1)
749
+ .substring(1, line.length - 1)
701
750
  .trim()
702
- .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
703
- if (attrlist) buffer.push('[' + attrlist + ']')
751
+ .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.]\w[\w-]*)*/, '')
752
+ .replace(NAMED_ID_ATTR_RX, '')
753
+ if (attrlist) entries.push({ type: 'attrlist', lines: ['[' + attrlist + ']'] })
704
754
  }
705
755
  } else if (belowDoctitle) {
706
756
  break
@@ -708,23 +758,26 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
708
758
  lines[idx] = undefined
709
759
  ignoreLines.push(idx)
710
760
  }
711
- return doc
712
- .getRoles()
713
- .map((role) => '.' + role)
714
- .join('')
761
+ return entries
715
762
  }
716
763
 
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')
764
+ function generateSlug (string) {
765
+ if (string === 'index') return string
766
+ return sanitizeSlug(
767
+ string
768
+ .toLowerCase()
769
+ .replace(/<[^>]+>/g, '')
770
+ .replace(CHAR_REF_RX, (_, name, dec, hex) => {
771
+ if (name) return BUILT_IN_NAMED_ENTITIES[name] ?? '?'
772
+ return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
773
+ })
774
+ .replace(/[\x27\u2019]/g, '')
775
+ .replace(/[_.]/, '-')
776
+ )
777
+ }
778
+
779
+ function sanitizeSlug (slug) {
780
+ return slug.replace(/[^\p{Alpha}0-9_.-]/gu, '-').replace(/^-+|-+$|(-)-+/g, '$1')
728
781
  }
729
782
 
730
783
  function fixSectionLevels (sections, expectedLevel) {
@@ -735,64 +788,6 @@ function fixSectionLevels (sections, expectedLevel) {
735
788
  })
736
789
  }
737
790
 
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
791
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
797
792
  // IMPORTANT: this must not leave behind lines the parser will drop!
798
793
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -815,35 +810,11 @@ function safePush (onto, entries) {
815
810
  }
816
811
  }
817
812
 
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
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}`
847
818
  }
848
819
 
849
820
  module.exports = produceAssemblyFile