@antora/assembler 1.0.0-beta.8 → 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,114 +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
- if (!relativePart) {
404
- // Q: should we validate the internal ID here?
405
- return text && ~text.indexOf('=')
406
- ? `xref:${idPrefix}${fragment}[${text}]`
407
- : `<<${idPrefix}${fragment}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
408
- }
409
- if (~dollarIdx) {
410
- if (relativePart.slice(dollarIdx).startsWith('$./')) {
411
- relativePart = relativePart.slice(0, dollarIdx + 1) + topicPrefix + relativePart.slice(dollarIdx + 3)
412
- }
413
- if ((isPage = /\bpage\$/.test(relativePart))) relativePart = relativePart.replace('page$', '')
414
- } else {
415
- isPage = true
416
- if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2)
417
- if (~hashIdx && !relativePart.endsWith('.adoc')) relativePart += '.adoc'
418
- }
419
- if (!isPage || ~relativePart.indexOf('@') || /:.*:/.test(relativePart)) {
420
- if (siteRoot && (resource = contentCatalog.resolveResource(relativePart, page.src, 'page'))?.pub) {
421
- text ||= resource.asciidoc?.xreftext || target
422
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
423
- }
424
- // TODO: handle unresolved resource better
425
- return m
426
- }
427
- let targetModule
428
- const colonIdx = relativePart.indexOf(':')
429
- if (~colonIdx) {
430
- targetModule = relativePart.slice(0, colonIdx)
431
- relativePart = relativePart.slice(colonIdx + 1)
432
- if (relativePart.startsWith('./')) relativePart = topicPrefix + relativePart.slice(2)
433
- } else {
434
- targetModule = module_
435
- }
436
- const pageResourceRef = targetModule === 'ROOT' ? relativePart : `${targetModule}:${relativePart}`
437
- if (!(resource = pagesInOutline.get(pageResourceRef))) {
438
- if (siteRoot && (resource = contentCatalog.resolvePage(pageResourceRef, page.src)) && resource.out) {
439
- text ||= resource.asciidoc?.xreftext || target
440
- return `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}${fragment && '#' + fragment}[${text}]`
441
- }
442
- // TODO: handle unresolved page better
443
- return m
444
- }
445
- if (targetModule !== 'ROOT') relativePart = `${targetModule}${idCoordinateSeparator}${relativePart}`
446
- relativePart = relativePart.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
447
- const refid = fragment
448
- ? `${relativePart}${idScopeSeparator}${fragment}`
449
- : relativePart + (ReservedIdNames.includes(relativePart) ? idScopeSeparator : '')
450
- if (
451
- text &&
452
- (assemblyModel.dropExplicitXrefText === 'always' ||
453
- (assemblyModel.dropExplicitXrefText === 'if-redundant' && text === resource.title))
454
- ) {
455
- text = ''
456
- }
457
- return `<<${refid}${text ? ',' + text.replace(/\\]/g, ']') : ''}>>`
515
+ line = line.replace(/\[\[([\p{Alpha}_:][\p{Alpha}0-9_\-:.]*)(|, *.+?)\]\]/gu, (_, refid, text) => {
516
+ return `[[${pageIdLeader}${refid}${text}]]`
458
517
  })
459
518
  }
460
- if (~line.indexOf('link:{attachmentsdir}/')) {
461
- line = line.replace(/(?<![\\+])link:\{attachmentsdir\}\/([^\s[]+)\[(|.*?[^\\])\]/g, (m, relative, text) => {
462
- const attachment =
463
- siteRoot &&
464
- contentCatalog.getById({
465
- component: componentVersion.name,
466
- version: componentVersion.version,
467
- module: module_,
468
- family: 'attachment',
469
- relative,
470
- })
471
- // TODO: handle unresolved attachment page
472
- return attachment?.out ? `${resolveLinkTarget(attachment, siteRoot, pubRoot, linkRefStyle)}[${text}]` : m
473
- })
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
+ )
474
532
  }
475
533
  if (~line.indexOf('image:') && !line.startsWith('image::')) {
476
- line = line.replace(/(?<![\\+])image:([^:\s[](?:[^[]*[^\s[])?)\[([^\]]*)\]/g, (m, target, attrlist) => {
477
- if (isResourceSpec(target)) {
478
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
479
- // TODO: handle (or report) unresolved image better
480
- if (image?.out && (filetype !== 'html' || siteRoot)) {
481
- pagesInOutline.assembled.assets.add(image)
482
- return filetype === 'html'
483
- ? `image:${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle)}[${attrlist}]`
484
- : `image:${resolveEmbedTarget(image, outDirname, embedRefStyle, true)}[${attrlist}]`
485
- }
486
- }
487
- return m
488
- })
534
+ line = rewriteInlineImages(line, contentCatalog, assemblyModel, page.src, assembled.assets, true, doc)
489
535
  }
490
536
  lines[idx] = line
491
537
  }
@@ -511,53 +557,43 @@ function mergeAsciiDoc (
511
557
  return '='.repeat(targetMarkerLength) + ' ' + rest
512
558
  })
513
559
  // NOTE: ID will be undefined if sectids are turned off
514
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix, blockStyle)
560
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader, blockStyle)
515
561
  } else {
516
562
  if (context === 'image') {
517
563
  let line = lines[idx] || ''
518
564
  let prefix = ''
519
565
  // Q: can we use startsWith('image::') in certain cases?
520
566
  let imageMacroOffset = (
521
- lastImageMacroAt?.[0] === idx ? line.slice(0, lastImageMacroAt[1]) : line
567
+ lastImageMacroAt?.[0] === idx ? line.substring(0, lastImageMacroAt[1]) : line
522
568
  ).lastIndexOf('image::')
523
569
  if (imageMacroOffset > 0) {
524
570
  if (
525
571
  block.getDocument().isNested() &&
526
- (prefix = line.slice(0, imageMacroOffset)).trimRight().endsWith('|')
572
+ (prefix = line.substring(0, imageMacroOffset)).trimRight().endsWith('|')
527
573
  ) {
528
- line = line.slice(prefix.length)
574
+ line = line.substring(prefix.length)
529
575
  } else {
530
576
  imageMacroOffset = -1
531
577
  }
532
578
  }
533
- if (imageMacroOffset >= 0) {
579
+ if (~imageMacroOffset) {
534
580
  const target = block.getAttribute('target')
535
- if (isResourceSpec(target)) {
536
- const image = contentCatalog.resolveResource(target, page.src, 'image', ['image'])
537
- // FIXME: handle (or report) case when image is not resolved
538
- if (image?.out && (filetype !== 'html' || siteRoot)) {
539
- const attrlist = line.slice(line.indexOf('[') + 1, -1)
540
- pagesInOutline.assembled.assets.add(image)
541
- lines[idx] =
542
- filetype === 'html'
543
- ? `${prefix}image::${resolveLinkTarget(image, siteRoot, pubRoot, linkRefStyle, false)}[${attrlist}]`
544
- : `${prefix}image::${resolveEmbedTarget(image, outDirname, embedRefStyle)}[${attrlist}]`
545
- }
546
- }
581
+ const newTarget = rewriteImageRef(target, contentCatalog, assemblyModel, page.src, assembled.assets)
582
+ if (newTarget) lines[idx] = `${prefix}image::${newTarget}${line.substring(line.indexOf('['))}`
547
583
  lastImageMacroAt = [idx, imageMacroOffset]
548
584
  }
549
585
  } else if (context === 'document' && block.hasHeader()) {
550
586
  // nested document
551
587
  idx = (block.getHeader().getLineNumber() || idx + 1) - 1
552
588
  }
553
- if (block.getId()) rewriteStyleAttribute(block, lines, idx, idPrefix)
589
+ if (block.getId()) rewriteStyleAttribute(block, lines, idx, pageIdLeader)
554
590
  }
555
591
  })
556
592
  safePush(
557
593
  buffer,
558
594
  lines.filter((it) => it !== undefined)
559
595
  )
560
- const attributeEntries = Object.entries(doc.source_header_attributes?.$$smap || {})
596
+ const attributeEntries = Object.entries(doc.source_header_attributes.$$smap)
561
597
  if (attributeEntries.length) {
562
598
  const resolvedAttributeEntries = attributeEntries.reduce(
563
599
  (accum, [name, val]) => {
@@ -569,7 +605,7 @@ function mergeAsciiDoc (
569
605
  } else if (val !== initialVal) {
570
606
  accum.push(`:${name}:${initialVal ? ' ' + initialVal : ''}`)
571
607
  }
572
- } else if (!(val == null || doc.isAttributeLocked(name) || DiscardAttributes.includes(name))) {
608
+ } else if (!(val == null || doc.isAttributeLocked(name) || DISCARD_ATTRIBUTE_NAMES.includes(name))) {
573
609
  accum.push(`:!${name}:`)
574
610
  }
575
611
  return accum
@@ -579,26 +615,13 @@ function mergeAsciiDoc (
579
615
  if (resolvedAttributeEntries.length > 1) safePush(buffer, resolvedAttributeEntries)
580
616
  }
581
617
  } else if (level) {
582
- if (atDocumentRoot && navtitlePlain === componentVersion.title) {
618
+ if (atDocumentRoot && rootLevel === 0 && navtitlePlain === componentVersion.title) {
583
619
  buffer.inBody ??= false
584
620
  level--
585
621
  } else {
586
622
  buffer.inBody = true
587
623
  buffer.push('')
588
- // NOTE: try to toggle sectids; otherwise, fallback to globally unique synthetic ID
589
- // Q: should we unset docname, page-module, etc?
590
- let toggleSectids, syntheticId
591
- if (!('sectids' in asciidocConfig.attributes)) {
592
- buffer.push(':!sectids:')
593
- toggleSectids = true
594
- } else if (typeof asciidocConfig.attributes.sectids === 'string') {
595
- if ('sectids' in mutableAttributes) {
596
- buffer.push(':!sectids:')
597
- toggleSectids = true
598
- } else {
599
- syntheticId = `__object-id-${global.Opal.hash(outlineEntry).$object_id()}`
600
- }
601
- }
624
+ const syntheticId = idSeparators.generateIdFromTitle(navtitleAsciiDoc, idSeparators)
602
625
  let sectionTitle = navtitleAsciiDoc
603
626
  if (urlType === 'external') {
604
627
  sectionTitle = `${url}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
@@ -606,27 +629,19 @@ function mergeAsciiDoc (
606
629
  const resource = files.find((it) => it.pub.url === url)
607
630
  if (resource) {
608
631
  if (resource.src.family === 'page' && pagesInOutline.has(resource.pub.url)) {
609
- let refid = resource.src.relative.replace(/\.adoc$/, '').replace(/[/.]/g, '-')
610
- if (refid.startsWith('./')) refid = topicPrefix + refid.slice(2)
611
- if (resource.src.module !== 'ROOT') refid = `${resource.src.module}${idCoordinateSeparator}${refid}`
612
- sectionTitle = `<<${refid},${navtitleAsciiDoc}>>`
632
+ const refid = generateScopedId(resource.src, componentVersion, idSeparators, filetype, true)
633
+ sectionTitle = `xref:${refid}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
613
634
  } else if (siteRoot) {
614
- sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkRefStyle)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
635
+ sectionTitle = `${resolveLinkTarget(resource, siteRoot, pubRoot, linkReferenceStyle, true)}[${navtitleAsciiDoc.replace(/\]/g, '\\]')}]`
615
636
  }
616
637
  }
617
638
  }
618
- let hlevel = level + 1
619
- if (hlevel > 6) {
620
- hlevel = 6
621
- buffer.push(syntheticId ? `[discrete#${syntheticId}]` : '[discrete]')
622
- } else if (syntheticId) {
623
- buffer.push(`[#${syntheticId}]`)
624
- }
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, '')}]`)
625
642
  buffer.push(`${'='.repeat(hlevel)} ${sectionTitle}`)
626
- if (toggleSectids) buffer.push(':sectids:')
627
643
  }
628
644
  }
629
-
630
645
  if (hasItems) {
631
646
  const nextLevel = level + 1
632
647
  // NOTE: drop first child if same as parent; should we keep if content is different?
@@ -642,6 +657,7 @@ function mergeAsciiDoc (
642
657
  item,
643
658
  files,
644
659
  pagesInOutline,
660
+ idSeparators,
645
661
  asciidocConfig,
646
662
  mutableAttributes,
647
663
  assemblyModel,
@@ -654,11 +670,32 @@ function mergeAsciiDoc (
654
670
  return buffer
655
671
  }
656
672
 
657
- 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
+ }
658
696
  const doctitleIdx = doc.getHeader().getLineNumber() - 1
659
697
  const end = doc.getBlocks()[0]?.getLineNumber() ?? lines.length
660
- let belowDoctitle
661
- let open
698
+ let belowDoctitle, current, open
662
699
  const implicitLines = []
663
700
  for (let idx = 0; idx < end; idx++) {
664
701
  if (idx === doctitleIdx) {
@@ -669,41 +706,51 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
669
706
  }
670
707
  const line = lines[idx]
671
708
  if (open === ':' || open === '-:') {
672
- if (line) {
673
- if (open === ':') buffer.push(line)
674
- if (!line.endsWith(' \\')) open = undefined
675
- } else {
676
- open = undefined
677
- }
709
+ if (open === ':') current.lines.push(line)
710
+ if (!line?.endsWith(' \\')) current = open = undefined
678
711
  } else if (line) {
679
- const chr0 = line.charAt()
680
712
  let attributeEntryMatch
713
+ const chr0 = line.charAt()
681
714
  if (chr0 === '/' && line.charAt(1) === '/') {
682
- if (line.startsWith('////')) {
683
- open = open ? (open === line ? undefined : open) : line
684
- } 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) === '/') {
685
725
  break
726
+ } else {
727
+ entries.push({ type: 'line_comment', lines: [line] })
728
+ current = undefined
686
729
  }
687
- buffer.push(line)
688
730
  } else if (open) {
689
- buffer.push(line)
690
- } else if (chr0 === ':' && ~line.indexOf(':', 2) && (attributeEntryMatch = line.match(AttributeEntryRx))) {
691
- const attributeName = attributeEntryMatch[1].replace('!', '')
692
- if (DiscardAttributes.includes(attributeName)) {
693
- 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
694
738
  } else {
695
739
  if (line.endsWith(' \\')) open = ':'
696
- 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
697
742
  }
698
743
  } else if (belowDoctitle) {
699
744
  if (implicitLines.length === 2 || !/[\p{Alpha}0-9]/u.test(chr0)) break
700
745
  implicitLines.push(line)
701
746
  } else if (chr0 === '[' && line.charAt(line.length - 1) === ']') {
747
+ current = undefined
702
748
  const attrlist = line
703
- .slice(1, -1)
749
+ .substring(1, line.length - 1)
704
750
  .trim()
705
- .replace(/(?:^\w[\w-]*(?!=|[\w-]))?([#.%]\w[\w-]*)*/, '')
706
- 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 + ']'] })
707
754
  }
708
755
  } else if (belowDoctitle) {
709
756
  break
@@ -711,23 +758,26 @@ function processDocumentHeader (doc, lines, buffer, ignoreLines) {
711
758
  lines[idx] = undefined
712
759
  ignoreLines.push(idx)
713
760
  }
714
- return doc
715
- .getRoles()
716
- .map((role) => '.' + role)
717
- .join('')
761
+ return entries
718
762
  }
719
763
 
720
- function generateSlug (title) {
721
- return title
722
- .toLowerCase()
723
- .replace(/<[^>]+>/g, '')
724
- .replace(CharRefRx, (_, name, dec, hex) => {
725
- if (name) return BuiltInNamedEntities[name] ?? '?'
726
- return String.fromCharCode(dec ? parseInt(dec, 10) : parseInt(hex, 16))
727
- })
728
- .replace(/[\x27\u2019]/g, '')
729
- .replace(/[^\p{Alpha}0-9-]/gu, '-')
730
- .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')
731
781
  }
732
782
 
733
783
  function fixSectionLevels (sections, expectedLevel) {
@@ -738,64 +788,6 @@ function fixSectionLevels (sections, expectedLevel) {
738
788
  })
739
789
  }
740
790
 
741
- function rewriteStyleAttribute (block, lines, idx, idPrefix, replacementStyle = '') {
742
- let prevLine = lines[idx - 1]
743
- const char0 = prevLine?.charAt()
744
- if (char0) {
745
- if (
746
- (char0 === '.' && /^\.\.?[^ \t.]/.test(prevLine)) ||
747
- (char0 === '[' &&
748
- prevLine.charAt(1) === '[' &&
749
- /^\[\[(?:|[\p{Alpha}_:][\p{Alpha}0-9_\-:.]*(?:, *.+)?)\]\]$/u.test(prevLine))
750
- ) {
751
- return rewriteStyleAttribute(block, lines, idx - 1, idPrefix, replacementStyle)
752
- }
753
- }
754
- let cellSpec
755
- if (
756
- char0 &&
757
- (char0 === '[' || (block.getDocument().isNested() && (cellSpec = prevLine.match(/^([^[|]*)\| *(\[.+)/)))) &&
758
- prevLine.charAt(prevLine.length - 1) === ']'
759
- ) {
760
- if (cellSpec) {
761
- prevLine = cellSpec[2]
762
- cellSpec = cellSpec[1]
763
- }
764
- let rawStyle
765
- const commaIdx = prevLine.indexOf(',')
766
- if (~commaIdx) {
767
- rawStyle = prevLine.slice(1, commaIdx)
768
- if (~rawStyle.indexOf('=')) rawStyle = undefined
769
- } else if (!~prevLine.indexOf('=')) {
770
- rawStyle = prevLine.slice(1, prevLine.length - 1)
771
- }
772
- if (rawStyle) {
773
- if (~rawStyle.indexOf('#')) {
774
- prevLine = prevLine.replace(/#[^.%,\]]+/, `#${idPrefix}${block.getId()}`)
775
- if (replacementStyle) prevLine = prevLine.replace(/^[^#.%,\]]+/, `[${replacementStyle}`)
776
- } else {
777
- prevLine = `[${
778
- replacementStyle ? rawStyle.replace(/^[^.%]*/, replacementStyle) : rawStyle
779
- }#${idPrefix}${block.getId()}${prevLine.slice(rawStyle.length + 1)}`
780
- }
781
- } else {
782
- prevLine = `[${replacementStyle}#${idPrefix}${block.getId()}${rawStyle == null ? ',' : ''}${prevLine.slice(1)}`
783
- }
784
- if (cellSpec) prevLine = `${cellSpec}|${prevLine}`
785
- lines[idx - 1] = prevLine
786
- } else {
787
- lines.splice(idx, 0, `[${replacementStyle}#${idPrefix}${block.getId()}]`)
788
- }
789
- }
790
-
791
- function isResourceSpec (str) {
792
- return !(~str.indexOf(':') && (~str.indexOf('://') || (str.startsWith('data:') && ~str.indexOf(','))))
793
- }
794
-
795
- function getObjectId (obj) {
796
- return global.Opal.id(obj)
797
- }
798
-
799
791
  // NOTE: blank lines at top and bottom of document create mismatch when using line numbers to navigate source lines
800
792
  // IMPORTANT: this must not leave behind lines the parser will drop!
801
793
  // IDEA: another option is to capture initial lineno of reader and use as offset (but preseves those blank lines)
@@ -818,35 +810,11 @@ function safePush (onto, entries) {
818
810
  }
819
811
  }
820
812
 
821
- function resolveEmbedTarget (resource, outDirname, referenceStyle, escapeForInline) {
822
- const target =
823
- referenceStyle === 'output-relative' ? resource.out.path : path.relative(outDirname + '/', resource.out.path)
824
- return escapeForInline ? target.replace(/_/g, '{underscore}') : target
825
- }
826
-
827
- function resolveLinkTarget (resource, siteRoot, pubRoot, referenceStyle, escapeForInline = true) {
828
- let target
829
- if (resource.site?.url) {
830
- target = ['', resource.pub.url]
831
- } else {
832
- switch (referenceStyle) {
833
- case 'absolute':
834
- target = ['', siteRoot.url + resource.pub.url]
835
- break
836
- case 'root-relative':
837
- target = ['link:', siteRoot.path + resource.pub.url]
838
- break
839
- default:
840
- target = ['link:', computeRelativeUrl(pubRoot + '/', resource.pub.url)]
841
- }
842
- }
843
- if (escapeForInline) target[1] = target[1].replace(/_/g, '{underscore}')
844
- return target.join('')
845
- }
846
-
847
- function computeRelativeUrl (from, to) {
848
- const rel = path.relative(from, to)
849
- 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}`
850
818
  }
851
819
 
852
820
  module.exports = produceAssemblyFile