@asciidoctor/core 3.0.3 → 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.
- package/README.md +42 -10
- package/build/browser/index.js +24154 -0
- package/build/node/index.cjs +24735 -0
- package/{dist/css/asciidoctor.css → data/asciidoctor-default.css} +54 -53
- package/package.json +53 -100
- package/src/abstract_block.js +849 -0
- package/src/abstract_node.js +954 -0
- package/src/attribute_entry.js +12 -0
- package/src/attribute_list.js +380 -0
- package/src/block.js +168 -0
- package/src/browser/asset.js +22 -0
- package/src/browser/reader.js +138 -0
- package/src/browser.js +121 -0
- package/src/callouts.js +85 -0
- package/src/compliance.js +54 -0
- package/src/constants.js +665 -0
- package/src/convert.js +370 -0
- package/src/converter/composite.js +83 -0
- package/src/converter/docbook5.js +1031 -0
- package/src/converter/html5.js +1899 -0
- package/src/converter/manpage.js +935 -0
- package/src/converter/template.js +459 -0
- package/src/converter.js +478 -0
- package/src/data/stylesheet-data.js +2 -0
- package/src/document.js +2134 -0
- package/src/extensions.js +1952 -0
- package/src/footnote.js +28 -0
- package/src/helpers.js +355 -0
- package/src/index.js +138 -0
- package/src/inline.js +158 -0
- package/src/list.js +240 -0
- package/src/load.js +276 -0
- package/src/logging.js +526 -0
- package/src/parser.js +3661 -0
- package/src/path_resolver.js +472 -0
- package/src/reader.js +1755 -0
- package/src/rx.js +829 -0
- package/src/section.js +354 -0
- package/src/stylesheets.js +30 -0
- package/src/substitutors.js +2241 -0
- package/src/syntaxHighlighter/highlightjs.js +90 -0
- package/src/syntaxHighlighter/html_pipeline.js +33 -0
- package/src/syntax_highlighter.js +304 -0
- package/src/table.js +952 -0
- package/src/timings.js +78 -0
- package/types/abstract_block.d.ts +343 -0
- package/types/abstract_node.d.ts +471 -0
- package/types/attribute_entry.d.ts +7 -0
- package/types/attribute_list.d.ts +52 -0
- package/types/block.d.ts +55 -0
- package/types/browser/asset.d.ts +7 -0
- package/types/browser/reader.d.ts +29 -0
- package/types/callouts.d.ts +36 -0
- package/types/compliance.d.ts +23 -0
- package/types/constants.d.ts +268 -0
- package/types/convert.d.ts +34 -0
- package/types/converter/composite.d.ts +20 -0
- package/types/converter/docbook5.d.ts +41 -0
- package/types/converter/html5.d.ts +51 -0
- package/types/converter/manpage.d.ts +59 -0
- package/types/converter/template.d.ts +83 -0
- package/types/converter.d.ts +150 -0
- package/types/data/stylesheet-data.d.ts +2 -0
- package/types/document.d.ts +495 -0
- package/types/extensions.d.ts +876 -0
- package/types/footnote.d.ts +18 -0
- package/types/helpers.d.ts +146 -0
- package/types/index.d.ts +73 -3731
- package/types/inline.d.ts +69 -0
- package/types/list.d.ts +114 -0
- package/types/load.d.ts +39 -0
- package/types/logging.d.ts +187 -0
- package/types/parser.d.ts +114 -0
- package/types/path_resolver.d.ts +103 -0
- package/types/reader.d.ts +184 -0
- package/types/rx.d.ts +513 -0
- package/types/section.d.ts +122 -0
- package/types/stylesheets.d.ts +10 -0
- package/types/substitutors.d.ts +208 -0
- package/types/syntaxHighlighter/highlightjs.d.ts +33 -0
- package/types/syntaxHighlighter/html_pipeline.d.ts +16 -0
- package/types/syntax_highlighter.d.ts +167 -0
- package/types/table.d.ts +231 -0
- package/types/timings.d.ts +25 -0
- package/types/tsconfig.json +9 -0
- package/LICENSE +0 -21
- package/dist/browser/asciidoctor.js +0 -47654
- package/dist/browser/asciidoctor.min.js +0 -1452
- package/dist/graalvm/asciidoctor.js +0 -47402
- package/dist/node/asciidoctor.cjs +0 -21567
- 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
|