@asciidoctor/core 3.0.4 → 4.0.0-alpha.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.
Files changed (91) hide show
  1. package/README.md +42 -10
  2. package/build/browser/index.js +24154 -0
  3. package/build/node/index.cjs +24735 -0
  4. package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
  5. package/package.json +53 -100
  6. package/src/abstract_block.js +849 -0
  7. package/src/abstract_node.js +954 -0
  8. package/src/attribute_entry.js +12 -0
  9. package/src/attribute_list.js +380 -0
  10. package/src/block.js +168 -0
  11. package/src/browser/asset.js +22 -0
  12. package/src/browser/reader.js +138 -0
  13. package/src/browser.js +121 -0
  14. package/src/callouts.js +85 -0
  15. package/src/compliance.js +54 -0
  16. package/src/constants.js +665 -0
  17. package/src/convert.js +370 -0
  18. package/src/converter/composite.js +83 -0
  19. package/src/converter/docbook5.js +1031 -0
  20. package/src/converter/html5.js +1899 -0
  21. package/src/converter/manpage.js +935 -0
  22. package/src/converter/template.js +459 -0
  23. package/src/converter.js +478 -0
  24. package/src/data/stylesheet-data.js +2 -0
  25. package/src/document.js +2134 -0
  26. package/src/extensions.js +1952 -0
  27. package/src/footnote.js +28 -0
  28. package/src/helpers.js +355 -0
  29. package/src/index.js +138 -0
  30. package/src/inline.js +158 -0
  31. package/src/list.js +240 -0
  32. package/src/load.js +276 -0
  33. package/src/logging.js +526 -0
  34. package/src/parser.js +3661 -0
  35. package/src/path_resolver.js +472 -0
  36. package/src/reader.js +1755 -0
  37. package/src/rx.js +829 -0
  38. package/src/section.js +354 -0
  39. package/src/stylesheets.js +30 -0
  40. package/src/substitutors.js +2241 -0
  41. package/src/syntaxHighlighter/highlightjs.js +90 -0
  42. package/src/syntaxHighlighter/html_pipeline.js +33 -0
  43. package/src/syntax_highlighter.js +304 -0
  44. package/src/table.js +952 -0
  45. package/src/timings.js +78 -0
  46. package/types/abstract_block.d.ts +343 -0
  47. package/types/abstract_node.d.ts +471 -0
  48. package/types/attribute_entry.d.ts +7 -0
  49. package/types/attribute_list.d.ts +52 -0
  50. package/types/block.d.ts +55 -0
  51. package/types/browser/asset.d.ts +7 -0
  52. package/types/browser/reader.d.ts +29 -0
  53. package/types/callouts.d.ts +36 -0
  54. package/types/compliance.d.ts +23 -0
  55. package/types/constants.d.ts +268 -0
  56. package/types/convert.d.ts +34 -0
  57. package/types/converter/composite.d.ts +20 -0
  58. package/types/converter/docbook5.d.ts +41 -0
  59. package/types/converter/html5.d.ts +51 -0
  60. package/types/converter/manpage.d.ts +59 -0
  61. package/types/converter/template.d.ts +83 -0
  62. package/types/converter.d.ts +150 -0
  63. package/types/data/stylesheet-data.d.ts +2 -0
  64. package/types/document.d.ts +495 -0
  65. package/types/extensions.d.ts +876 -0
  66. package/types/footnote.d.ts +18 -0
  67. package/types/helpers.d.ts +146 -0
  68. package/types/index.d.ts +73 -3731
  69. package/types/inline.d.ts +69 -0
  70. package/types/list.d.ts +114 -0
  71. package/types/load.d.ts +39 -0
  72. package/types/logging.d.ts +187 -0
  73. package/types/parser.d.ts +114 -0
  74. package/types/path_resolver.d.ts +103 -0
  75. package/types/reader.d.ts +184 -0
  76. package/types/rx.d.ts +513 -0
  77. package/types/section.d.ts +122 -0
  78. package/types/stylesheets.d.ts +10 -0
  79. package/types/substitutors.d.ts +208 -0
  80. package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
  81. package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
  82. package/types/syntax_highlighter.d.ts +167 -0
  83. package/types/table.d.ts +231 -0
  84. package/types/timings.d.ts +25 -0
  85. package/types/tsconfig.json +9 -0
  86. package/LICENSE +0 -21
  87. package/dist/browser/asciidoctor.js +0 -47654
  88. package/dist/browser/asciidoctor.min.js +0 -1452
  89. package/dist/graalvm/asciidoctor.js +0 -47402
  90. package/dist/node/asciidoctor.cjs +0 -21567
  91. package/dist/node/asciidoctor.js +0 -23037
@@ -0,0 +1,459 @@
1
+ // ESM conversion of converter/template.rb
2
+ //
3
+ // Ruby-to-JavaScript notes:
4
+ // - Tilt template engine (haml/slim/erb) → replaced by nunjucks/handlebars/ejs/pug/js/cjs.
5
+ // Reference: packages/core/lib/asciidoctor/js/asciidoctor_ext/node/template.rb
6
+ // - Static @caches → TemplateConverter._caches (class-level static property).
7
+ // - Ruby Dir.glob(pattern) → readdirSync(dir) filtered by extension.
8
+ // - Ruby File.directory?/File.file? → fsp.stat().isDirectory()/isFile() (async).
9
+ // - Ruby File.basename / File.expand_path → node:path basename / resolve.
10
+ // - PathResolver.system_path → pathResolver.systemPath().
11
+ // - template.render(node, opts) → template.render({node, opts, helpers}).
12
+ // - Helpers loaded from helpers.js/helpers.cjs; can export configure(enginesContext).
13
+ // - Custom engines registered via TemplateConverter.TemplateEngine.register(ext, adapter).
14
+ // - Thread safety / Mutex → not needed (single-threaded JS).
15
+ // - load_eruby / eRuby support → not applicable in JS environment.
16
+ // - require_library → _nodeRequire() with a user-friendly IOError on missing module.
17
+ // - Constructor is sync; use TemplateConverter.create() factory for async _scan().
18
+
19
+ import { ConverterBase } from '../converter.js'
20
+ import { PathResolver } from '../path_resolver.js'
21
+ import { createRequire } from 'node:module'
22
+ import { promises as fsp } from 'node:fs'
23
+ import path from 'node:path'
24
+
25
+ const _require = createRequire(import.meta.url)
26
+
27
+ // ── TemplateEngine registry ───────────────────────────────────────────────────
28
+ // Allows users to plug in custom template engine adapters.
29
+ //
30
+ // Adapters must implement:
31
+ // compile(file, name) → { render(ctx) }
32
+ //
33
+ // Example:
34
+ // TemplateConverter.TemplateEngine.register('dot', {
35
+ // compile(file, _name) {
36
+ // const doT = require('dot')
37
+ // const fn = doT.template(fs.readFileSync(file, 'utf8'))
38
+ // return { render: (ctx) => fn(ctx) }
39
+ // }
40
+ // })
41
+ const TemplateEngine = {
42
+ registry: {},
43
+ register(names, adapter) {
44
+ const list = Array.isArray(names) ? names : [names]
45
+ for (const name of list) this.registry[name] = adapter
46
+ },
47
+ }
48
+
49
+ export class TemplateConverter extends ConverterBase {
50
+ // Class-level scan + template caches (shared across instances when template_cache: true).
51
+ static _caches = { scans: {}, templates: {} }
52
+
53
+ // Expose the TemplateEngine registry on the class for external registration.
54
+ static TemplateEngine = TemplateEngine
55
+
56
+ static clearCaches() {
57
+ TemplateConverter._caches.scans = {}
58
+ TemplateConverter._caches.templates = {}
59
+ }
60
+
61
+ /** Alias for clearCaches() — matches the Ruby/core API name. */
62
+ static clearCache() {
63
+ TemplateConverter.clearCaches()
64
+ }
65
+
66
+ /** Return the class-level cache object. */
67
+ static getCache() {
68
+ return TemplateConverter._caches
69
+ }
70
+
71
+ /**
72
+ * Construct a new TemplateConverter (synchronous setup only).
73
+ * Prefer TemplateConverter.create() which also runs the async _scan() step.
74
+ * @param {string} backend - the backend name (e.g. 'html5')
75
+ * @param {string|string[]} templateDirs - paths to scan for templates
76
+ * @param {Object} [opts={}]
77
+ * @param {string} [opts.template_engine] - engine name restriction (e.g. 'nunjucks')
78
+ * @param {Object} [opts.template_engine_options] - per-engine option objects
79
+ * @param {boolean|Object} [opts.template_cache] - true → class-level cache, Object → supplied cache
80
+ */
81
+ constructor(backend, templateDirs, opts = {}) {
82
+ super(backend, opts)
83
+ this._templates = {}
84
+ this.templateDirs = Array.isArray(templateDirs)
85
+ ? templateDirs
86
+ : [templateDirs]
87
+ this.engine = opts.template_engine ?? null
88
+ this.engineOptions = opts.template_engine_options ?? {}
89
+
90
+ if (opts.template_cache === true) {
91
+ this.caches = TemplateConverter._caches
92
+ } else if (opts.template_cache && typeof opts.template_cache === 'object') {
93
+ this.caches = opts.template_cache
94
+ } else {
95
+ this.caches = {} // empty object effectively disables caching
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Async factory — create and fully initialize a TemplateConverter.
101
+ * @param {string} backend
102
+ * @param {string|string[]} templateDirs
103
+ * @param {Object} [opts={}]
104
+ * @returns {Promise<TemplateConverter>}
105
+ */
106
+ static async create(backend, templateDirs, opts = {}) {
107
+ const converter = new TemplateConverter(backend, templateDirs, opts)
108
+ await converter._scan()
109
+ return converter
110
+ }
111
+
112
+ /**
113
+ * Convert an AbstractNode to the backend format using the named template.
114
+ *
115
+ * Note: convert() is async because getContent() / applySubs() in the core package
116
+ * is async throughout. We pre-resolve content here and expose it synchronously to
117
+ * template engines (which are all synchronous) via a Proxy wrapper.
118
+ * @param {object} node - the AbstractNode to convert
119
+ * @param {string|null} [templateName=null] - the template name to use (default: node.nodeName)
120
+ * @param {object|null} [opts=null] - optional plain object passed as locals to the template
121
+ * @returns {Promise<string>}
122
+ */
123
+ async convert(node, templateName = null, opts = null) {
124
+ const name = templateName ?? node.nodeName
125
+ const template = this._templates[name]
126
+ if (!template)
127
+ throw new Error(
128
+ `Could not find a custom template to handle transform: ${name}`
129
+ )
130
+
131
+ const helpersEntry = this._templates['helpers.js']
132
+ const helpers = helpersEntry ? helpersEntry.ctx : undefined
133
+
134
+ // Pre-resolve async content so synchronous template engines receive a plain string.
135
+ const content =
136
+ typeof node.getContent === 'function'
137
+ ? await node.getContent()
138
+ : undefined
139
+
140
+ // Pre-resolve list items when the node exposes getItems().
141
+ let resolvedItems = null
142
+ if (typeof node.getItems === 'function') {
143
+ const rawItems = node.getItems()
144
+ if (Array.isArray(rawItems) && rawItems.length > 0) {
145
+ resolvedItems = await Promise.all(
146
+ rawItems.map(async (item) => {
147
+ const itemContent =
148
+ typeof item.getContent === 'function'
149
+ ? await item.getContent()
150
+ : undefined
151
+ return _makeContentProxy(item, itemContent)
152
+ })
153
+ )
154
+ } else {
155
+ resolvedItems = rawItems ?? []
156
+ }
157
+ }
158
+
159
+ const proxyNode = _makeContentProxy(node, content, resolvedItems)
160
+ const result = template.render({ node: proxyNode, opts, helpers })
161
+ return name === 'document' ? result.trim() : result.trimEnd()
162
+ }
163
+
164
+ /**
165
+ * Check whether there is a template registered for the given name.
166
+ * @param {string} name
167
+ * @returns {boolean}
168
+ */
169
+ handles(name) {
170
+ return Object.hasOwn(this._templates, name)
171
+ }
172
+
173
+ /**
174
+ * Retrieve a shallow copy of the templates map.
175
+ * @returns {Object} plain object keyed by template name
176
+ */
177
+ get templates() {
178
+ return { ...this._templates }
179
+ }
180
+
181
+ /**
182
+ * Method alias for the templates getter — matches the core/Ruby API.
183
+ * @returns {Object} plain object keyed by template name
184
+ */
185
+ getTemplates() {
186
+ return { ...this._templates }
187
+ }
188
+
189
+ /**
190
+ * Register a template with this converter (and optionally the template cache).
191
+ * @param {string} name - the template name
192
+ * @param {{ render: Function, file: string }} template - the template object
193
+ * @returns {{ render: Function, file: string }} the template object
194
+ */
195
+ register(name, template) {
196
+ if (this.caches.templates && template.file) {
197
+ this.caches.templates[template.file] = template
198
+ }
199
+ this._templates[name] = template
200
+ return template
201
+ }
202
+
203
+ // ── Private ──────────────────────────────────────────────────────────────────
204
+
205
+ /**
206
+ * Scan all template directories and populate this._templates.
207
+ * @returns {Promise<void>}
208
+ * @internal
209
+ */
210
+ async _scan() {
211
+ const pathResolver = new PathResolver()
212
+ const backend = this.backend
213
+ const engine = this.engine
214
+
215
+ for (let templateDir of this.templateDirs) {
216
+ // Resolve and verify the directory exists.
217
+ templateDir = pathResolver.systemPath(templateDir)
218
+ if (!(await _isDirectory(templateDir))) continue
219
+
220
+ // If a specific engine is requested, check for an engine subdirectory.
221
+ let fileExtFilter = null
222
+ if (engine) {
223
+ fileExtFilter = engine
224
+ const engineDir = path.join(templateDir, engine)
225
+ if (await _isDirectory(engineDir)) templateDir = engineDir
226
+ }
227
+
228
+ // Check for a backend subdirectory.
229
+ const backendDir = path.join(templateDir, backend)
230
+ if (await _isDirectory(backendDir)) templateDir = backendDir
231
+
232
+ const cacheKey = fileExtFilter
233
+ ? `${templateDir}/*.${fileExtFilter}`
234
+ : `${templateDir}/*`
235
+
236
+ const scanCache = this.caches.scans
237
+ if (scanCache) {
238
+ const templateCache = this.caches.templates
239
+ let scanned = scanCache[cacheKey]
240
+ if (!scanned) {
241
+ scanned = scanCache[cacheKey] = await this._scanDir(
242
+ templateDir,
243
+ fileExtFilter,
244
+ templateCache
245
+ )
246
+ }
247
+ for (const [name, template] of Object.entries(scanned)) {
248
+ this._templates[name] = template
249
+ if (templateCache && template.file) {
250
+ templateCache[template.file] = template
251
+ }
252
+ }
253
+ } else {
254
+ const scanned = await this._scanDir(
255
+ templateDir,
256
+ fileExtFilter,
257
+ this.caches.templates
258
+ )
259
+ Object.assign(this._templates, scanned)
260
+ }
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Scan templateDir for template files and return a map of name → template.
266
+ * @param {string} templateDir - absolute path to the directory to scan
267
+ * @param {string|null} fileExtFilter - extension restriction (without leading dot), or null for all
268
+ * @param {Object|null} [templateCache=null] - cache of file → template, or falsy if no cache
269
+ * @returns {Promise<Object>} plain object { [name]: template }
270
+ * @internal
271
+ */
272
+ async _scanDir(templateDir, fileExtFilter, templateCache = null) {
273
+ const result = {}
274
+ let helpersFile = null
275
+ const enginesCtx = {}
276
+
277
+ let entries
278
+ try {
279
+ entries = await fsp.readdir(templateDir)
280
+ } catch {
281
+ return result
282
+ }
283
+
284
+ for (const basename of entries) {
285
+ const file = path.join(templateDir, basename)
286
+
287
+ // Skip non-files.
288
+ if (!(await _isFile(file))) continue
289
+
290
+ // Collect helpers separately; process after all templates.
291
+ if (basename === 'helpers.js' || basename === 'helpers.cjs') {
292
+ helpersFile = file
293
+ continue
294
+ }
295
+
296
+ // Require at least one dot (i.e. a recognisable extension).
297
+ const segments = basename.split('.')
298
+ if (segments.length < 2) continue
299
+
300
+ // Filter by engine if requested.
301
+ if (fileExtFilter && segments[segments.length - 1] !== fileExtFilter)
302
+ continue
303
+
304
+ // Map file stem to transform name.
305
+ let name = segments[0]
306
+ if (name === 'block_ruler') {
307
+ name = 'thematic_break'
308
+ } else if (name.startsWith('block_')) {
309
+ name = name.slice('block_'.length)
310
+ }
311
+
312
+ // Return cached template when available.
313
+ if (templateCache?.[file]) {
314
+ result[name] = templateCache[file]
315
+ continue
316
+ }
317
+
318
+ const ext = segments[segments.length - 1]
319
+ let template
320
+
321
+ if (ext === 'nunjucks' || ext === 'njk') {
322
+ const nunjucks = this._nodeRequire('nunjucks')
323
+ let env
324
+ if (enginesCtx.nunjucks?.environment) {
325
+ env = enginesCtx.nunjucks.environment
326
+ } else {
327
+ const nunjucksOpts = { ...(this.engineOptions.nunjucks ?? {}) }
328
+ delete nunjucksOpts.web // unsupported in Node.js
329
+ env = nunjucks.configure(templateDir, nunjucksOpts)
330
+ enginesCtx.nunjucks = { environment: env }
331
+ }
332
+ const compiled = nunjucks.compile(await fsp.readFile(file, 'utf8'), env)
333
+ template = Object.assign(compiled, { file })
334
+ } else if (ext === 'handlebars' || ext === 'hbs') {
335
+ const handlebars = this._nodeRequire('handlebars')
336
+ const hbsOpts = { ...(this.engineOptions.handlebars ?? {}) }
337
+ let env
338
+ if (enginesCtx.handlebars?.environment) {
339
+ env = enginesCtx.handlebars.environment
340
+ } else {
341
+ env = handlebars.create()
342
+ enginesCtx.handlebars = { environment: env }
343
+ }
344
+ const renderFn = env.compile(await fsp.readFile(file, 'utf8'), hbsOpts)
345
+ template = { render: renderFn, file }
346
+ } else if (ext === 'ejs') {
347
+ const ejs = this._nodeRequire('ejs')
348
+ const ejsOpts = { ...(this.engineOptions.ejs ?? {}), filename: file }
349
+ // Unsupported EJS options in synchronous compile.
350
+ delete ejsOpts.async
351
+ delete ejsOpts.client
352
+ const renderFn = ejs.compile(await fsp.readFile(file, 'utf8'), ejsOpts)
353
+ template = { render: renderFn, file }
354
+ } else if (ext === 'pug') {
355
+ const pug = this._nodeRequire('pug')
356
+ const pugOpts = { ...(this.engineOptions.pug ?? {}), filename: file }
357
+ const renderFn = pug.compileFile(file, pugOpts)
358
+ template = { render: renderFn, file }
359
+ } else if (ext === 'js' || ext === 'cjs') {
360
+ const renderFn = _require(file)
361
+ template = { render: renderFn, file }
362
+ } else {
363
+ // Fall back to custom TemplateEngine registry.
364
+ const adapter = TemplateEngine.registry[ext]
365
+ if (adapter && typeof adapter.compile === 'function') {
366
+ template = Object.assign(adapter.compile(file, name), { file })
367
+ } else {
368
+ continue
369
+ }
370
+ }
371
+
372
+ result[name] = template
373
+ }
374
+
375
+ // Load helpers if found (or if a helpers.js exists at the top of the dir).
376
+ const fallbackHelpers = path.join(templateDir, 'helpers.js')
377
+ const fallbackHelpersCjs = path.join(templateDir, 'helpers.cjs')
378
+ if (!helpersFile && (await _isFile(fallbackHelpers)))
379
+ helpersFile = fallbackHelpers
380
+ if (!helpersFile && (await _isFile(fallbackHelpersCjs)))
381
+ helpersFile = fallbackHelpersCjs
382
+
383
+ if (helpersFile) {
384
+ const ctx = _require(helpersFile)
385
+ if (typeof ctx.configure === 'function') ctx.configure(enginesCtx)
386
+ result['helpers.js'] = { file: helpersFile, ctx }
387
+ }
388
+
389
+ return result
390
+ }
391
+
392
+ /**
393
+ * Load an optional Node.js module by name.
394
+ * @param {string} moduleName
395
+ * @returns {*} the required module
396
+ * @throws {Error} with a friendly message if the module is not installed
397
+ * @internal
398
+ */
399
+ _nodeRequire(moduleName) {
400
+ try {
401
+ return _require(moduleName)
402
+ } catch {
403
+ throw new Error(
404
+ `Unable to require the module '${moduleName}', please make sure that the module is installed.`
405
+ )
406
+ }
407
+ }
408
+ }
409
+
410
+ // ── Helpers ───────────────────────────────────────────────────────────────────
411
+
412
+ /**
413
+ * @internal
414
+ * @private
415
+ */
416
+ async function _isDirectory(p) {
417
+ try {
418
+ return (await fsp.stat(p)).isDirectory()
419
+ } catch {
420
+ return false
421
+ }
422
+ }
423
+
424
+ /**
425
+ * @internal
426
+ * @private
427
+ */
428
+ async function _isFile(p) {
429
+ try {
430
+ return (await fsp.stat(p)).isFile()
431
+ } catch {
432
+ return false
433
+ }
434
+ }
435
+
436
+ /**
437
+ * Wrap an AST node in a Proxy that intercepts getContent() / content
438
+ * (and optionally getItems() / items) to return pre-resolved synchronous values.
439
+ *
440
+ * This lets synchronous template engines (nunjucks, ejs, pug, handlebars, …)
441
+ * receive plain strings even though the core applySubs() pipeline is async.
442
+ * @internal
443
+ * @private
444
+ */
445
+ function _makeContentProxy(node, content, resolvedItems = null) {
446
+ return new Proxy(node, {
447
+ get(target, prop) {
448
+ if (prop === 'getContent') return () => content
449
+ if (prop === 'content') return content
450
+ if (resolvedItems !== null && prop === 'getItems')
451
+ return () => resolvedItems
452
+ if (resolvedItems !== null && prop === 'items') return resolvedItems
453
+ const val = Reflect.get(target, prop)
454
+ return typeof val === 'function' ? val.bind(target) : val
455
+ },
456
+ })
457
+ }
458
+
459
+ export default TemplateConverter