@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.
- 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
package/src/convert.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
// ESM conversion of convert.rb
|
|
2
|
+
//
|
|
3
|
+
// Ruby-to-JavaScript notes:
|
|
4
|
+
// - Ruby module methods on Asciidoctor → named exports convert() and convertFile()
|
|
5
|
+
// (deprecated aliases: render, renderFile).
|
|
6
|
+
// - Ruby File === input → duck-type: any object with a .path property is treated as a file.
|
|
7
|
+
// - Ruby File.absolute_path / File.dirname / File.expand_path → node:path.resolve / .dirname.
|
|
8
|
+
// - Ruby Dir.pwd → process.cwd().
|
|
9
|
+
// - Ruby File.directory? → async _isDirectory() helper via node:fs/promises.
|
|
10
|
+
// - Ruby File.file? → async _isFile() helper via node:fs/promises.
|
|
11
|
+
// - Ruby File.write → async writeFile() via node:fs/promises.
|
|
12
|
+
// - Ruby Helpers.mkdir_p → mkdirP() from helpers.js.
|
|
13
|
+
// - Ruby Helpers.uriish? → isUriish() from helpers.js.
|
|
14
|
+
// - Ruby Stylesheets.instance.write_primary_stylesheet → not yet ported to JS; skipped.
|
|
15
|
+
// - Ruby doc.syntax_highlighter → doc.syntaxHighlighter.
|
|
16
|
+
// - Ruby syntax_hl.write_stylesheet? doc → syntaxHl.writeStylesheet(doc).
|
|
17
|
+
// - Ruby syntax_hl.write_stylesheet doc, dir → syntaxHl.writeStylesheetToDisk(doc, dir).
|
|
18
|
+
// - Ruby doc.normalize_system_path → doc.normalizeSystemPath.
|
|
19
|
+
// - Ruby doc.attr? 'x' → doc.hasAttribute('x').
|
|
20
|
+
// - Ruby doc.attr 'x' → doc.getAttribute('x').
|
|
21
|
+
// - Ruby doc.basebackend? 'html' → doc.basebackend('html').
|
|
22
|
+
// - The entire function is async because load() is async.
|
|
23
|
+
|
|
24
|
+
import { load } from './load.js'
|
|
25
|
+
import { isUriish, mkdirP } from './helpers.js'
|
|
26
|
+
import { SafeMode, DEFAULT_STYLESHEET_KEYS } from './constants.js'
|
|
27
|
+
|
|
28
|
+
// ── convert ───────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse the AsciiDoc source input into a Document and convert it to the specified backend format.
|
|
32
|
+
*
|
|
33
|
+
* Accepts input as a Node.js Readable stream (or any object with a read() method), a String,
|
|
34
|
+
* or a String Array. If the input is a file-like object with a `.path` property, it is treated
|
|
35
|
+
* as a file: the output is written to a file adjacent to the input by default.
|
|
36
|
+
*
|
|
37
|
+
* If `to_file` is true or omitted and the input is a file-like object, the output is written
|
|
38
|
+
* next to the input file. If `to_file` is a String path, the output is written there.
|
|
39
|
+
* If `to_file` is false, the converted String is returned.
|
|
40
|
+
* If `to_file` is `'/dev/null'`, the document is loaded but neither converted nor written.
|
|
41
|
+
*
|
|
42
|
+
* @param {string|string[]|object} input - the AsciiDoc source (String, Array, Readable, or
|
|
43
|
+
* file-like object with a `.path` property)
|
|
44
|
+
* @param {object} [options={}] - a plain Object of options (mirrors Ruby API):
|
|
45
|
+
* - `to_file` {string|boolean|object} - String path, Boolean, stream object, or `'/dev/null'`
|
|
46
|
+
* - `to_dir` {string} - output directory
|
|
47
|
+
* - `mkdirs` {boolean} - create missing directories if true
|
|
48
|
+
* - `standalone` {boolean} - include header/footer
|
|
49
|
+
* - `header_footer` {boolean} - deprecated alias for `standalone`
|
|
50
|
+
* - `base_dir` {string} - base directory
|
|
51
|
+
* @returns {Promise<import('./document.js').Document|string>} the Document if output was written to a file, otherwise the converted String
|
|
52
|
+
*/
|
|
53
|
+
export async function convert(input, options = {}) {
|
|
54
|
+
options = Object.assign({}, options)
|
|
55
|
+
delete options.parse
|
|
56
|
+
const toDir = options.to_dir
|
|
57
|
+
delete options.to_dir
|
|
58
|
+
const mkdirs = options.mkdirs
|
|
59
|
+
delete options.mkdirs
|
|
60
|
+
|
|
61
|
+
let toFile = options.to_file
|
|
62
|
+
delete options.to_file
|
|
63
|
+
let siblingPath = null
|
|
64
|
+
let writeToTarget = null
|
|
65
|
+
let streamOutput = false
|
|
66
|
+
|
|
67
|
+
if (toFile === true || toFile == null) {
|
|
68
|
+
writeToTarget = toDir || null
|
|
69
|
+
if (!writeToTarget && input && typeof input === 'object' && input.path) {
|
|
70
|
+
const nodePath = await _requirePath()
|
|
71
|
+
siblingPath = nodePath.resolve(input.path)
|
|
72
|
+
}
|
|
73
|
+
toFile = null
|
|
74
|
+
} else if (toFile === false) {
|
|
75
|
+
toFile = null
|
|
76
|
+
} else if (toFile === '/dev/null') {
|
|
77
|
+
return load(input, options)
|
|
78
|
+
} else {
|
|
79
|
+
if (typeof toFile === 'object' && typeof toFile.write === 'function') {
|
|
80
|
+
streamOutput = true
|
|
81
|
+
} else {
|
|
82
|
+
options.to_file = toFile
|
|
83
|
+
writeToTarget = toFile
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Normalise :header_footer → :standalone when writing to a target.
|
|
88
|
+
if (!('standalone' in options)) {
|
|
89
|
+
if (siblingPath || writeToTarget) {
|
|
90
|
+
options.standalone =
|
|
91
|
+
'header_footer' in options ? options.header_footer : true
|
|
92
|
+
} else if ('header_footer' in options) {
|
|
93
|
+
options.standalone = options.header_footer
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// NOTE outfile may be controlled by document attributes, so resolve outfile after loading.
|
|
98
|
+
// NOTE the :to_dir option is always set when outputting to a file.
|
|
99
|
+
// NOTE the :to_file option is only passed if assigned an explicit path.
|
|
100
|
+
if (siblingPath) {
|
|
101
|
+
const nodePath = await _requirePath()
|
|
102
|
+
options.to_dir = nodePath.dirname(siblingPath)
|
|
103
|
+
} else if (writeToTarget) {
|
|
104
|
+
const nodePath = await _requirePath()
|
|
105
|
+
if (toDir) {
|
|
106
|
+
if (toFile) {
|
|
107
|
+
options.to_dir = nodePath.dirname(nodePath.resolve(toDir, toFile))
|
|
108
|
+
} else {
|
|
109
|
+
options.to_dir = nodePath.resolve(toDir)
|
|
110
|
+
}
|
|
111
|
+
} else if (toFile) {
|
|
112
|
+
options.to_dir = nodePath.dirname(nodePath.resolve(toFile))
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const doc = await load(input, options)
|
|
117
|
+
|
|
118
|
+
let outfile, outdir
|
|
119
|
+
|
|
120
|
+
if (siblingPath) {
|
|
121
|
+
const nodePath = await _requirePath()
|
|
122
|
+
outdir = nodePath.dirname(siblingPath)
|
|
123
|
+
outfile = nodePath.join(
|
|
124
|
+
outdir,
|
|
125
|
+
`${doc.attributes.docname}${doc.outfilesuffix}`
|
|
126
|
+
)
|
|
127
|
+
if (outfile === siblingPath) {
|
|
128
|
+
throw new Error(
|
|
129
|
+
`input file and output file cannot be the same: ${outfile}`
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
} else if (writeToTarget) {
|
|
133
|
+
const nodePath = await _requirePath()
|
|
134
|
+
const workingDir = options.base_dir
|
|
135
|
+
? nodePath.resolve(options.base_dir)
|
|
136
|
+
: process.cwd()
|
|
137
|
+
// QUESTION should the jail be workingDir or doc.baseDir?
|
|
138
|
+
const jail = doc.safe >= SafeMode.SAFE ? workingDir : null
|
|
139
|
+
|
|
140
|
+
if (toDir) {
|
|
141
|
+
outdir = doc.normalizeSystemPath(toDir, workingDir, jail, {
|
|
142
|
+
targetName: 'to_dir',
|
|
143
|
+
recover: false,
|
|
144
|
+
})
|
|
145
|
+
if (toFile) {
|
|
146
|
+
outfile = doc.normalizeSystemPath(toFile, outdir, null, {
|
|
147
|
+
targetName: 'to_dir',
|
|
148
|
+
recover: false,
|
|
149
|
+
})
|
|
150
|
+
// reestablish outdir as the final target directory (in case to_file had directory segments)
|
|
151
|
+
outdir = nodePath.dirname(outfile)
|
|
152
|
+
} else {
|
|
153
|
+
outfile = nodePath.join(
|
|
154
|
+
outdir,
|
|
155
|
+
`${doc.attributes.docname}${doc.outfilesuffix}`
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
} else if (toFile) {
|
|
159
|
+
outfile = doc.normalizeSystemPath(toFile, workingDir, jail, {
|
|
160
|
+
targetName: 'to_dir',
|
|
161
|
+
recover: false,
|
|
162
|
+
})
|
|
163
|
+
// establish outdir as the final target directory (in case to_file had directory segments)
|
|
164
|
+
outdir = nodePath.dirname(outfile)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (input && typeof input === 'object' && input.path) {
|
|
168
|
+
const absInputPath = nodePath.resolve(input.path)
|
|
169
|
+
if (nodePath.normalize(outfile) === nodePath.normalize(absInputPath)) {
|
|
170
|
+
throw new Error(
|
|
171
|
+
`input file and output file cannot be the same: ${outfile}`
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (mkdirs) {
|
|
177
|
+
await mkdirP(outdir)
|
|
178
|
+
} else {
|
|
179
|
+
if (!(await _isDirectory(outdir))) {
|
|
180
|
+
// NOTE we intentionally refer to the directory as it was passed to the API
|
|
181
|
+
throw new Error(
|
|
182
|
+
`target directory does not exist: ${toDir} (hint: set mkdirs option)`
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// write to stream
|
|
188
|
+
outfile = streamOutput ? toFile : null
|
|
189
|
+
outdir = null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let output
|
|
193
|
+
if (outfile && !streamOutput) {
|
|
194
|
+
output = await doc.convert({ outfile, outdir })
|
|
195
|
+
} else {
|
|
196
|
+
output = await doc.convert()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (outfile) {
|
|
200
|
+
await doc.write(output, outfile)
|
|
201
|
+
|
|
202
|
+
// NOTE document cannot control this behavior if safe >= SafeMode.SERVER
|
|
203
|
+
// NOTE skip if stylesdir is a URI
|
|
204
|
+
if (
|
|
205
|
+
!streamOutput &&
|
|
206
|
+
doc.safe < SafeMode.SECURE &&
|
|
207
|
+
doc.hasAttribute('linkcss') &&
|
|
208
|
+
doc.hasAttribute('copycss') &&
|
|
209
|
+
doc.basebackend('html') &&
|
|
210
|
+
!(
|
|
211
|
+
doc.getAttribute('stylesdir') && isUriish(doc.getAttribute('stylesdir'))
|
|
212
|
+
)
|
|
213
|
+
) {
|
|
214
|
+
let copyAsciidoctorStylesheet = false
|
|
215
|
+
let copyUserStylesheet = false
|
|
216
|
+
const stylesheet = doc.getAttribute('stylesheet')
|
|
217
|
+
if (stylesheet) {
|
|
218
|
+
if (DEFAULT_STYLESHEET_KEYS.has(stylesheet)) {
|
|
219
|
+
copyAsciidoctorStylesheet = true
|
|
220
|
+
} else if (!isUriish(stylesheet)) {
|
|
221
|
+
copyUserStylesheet = true
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const syntaxHl = doc.syntaxHighlighter
|
|
225
|
+
const copySyntaxHlStylesheet = syntaxHl?.writeStylesheet(doc)
|
|
226
|
+
|
|
227
|
+
if (
|
|
228
|
+
copyAsciidoctorStylesheet ||
|
|
229
|
+
copyUserStylesheet ||
|
|
230
|
+
copySyntaxHlStylesheet
|
|
231
|
+
) {
|
|
232
|
+
const stylesdir = doc.getAttribute('stylesdir')
|
|
233
|
+
const stylesoutdir = doc.normalizeSystemPath(
|
|
234
|
+
stylesdir,
|
|
235
|
+
outdir,
|
|
236
|
+
doc.safe >= SafeMode.SAFE ? outdir : null
|
|
237
|
+
)
|
|
238
|
+
if (mkdirs) {
|
|
239
|
+
await mkdirP(stylesoutdir)
|
|
240
|
+
} else {
|
|
241
|
+
if (!(await _isDirectory(stylesoutdir))) {
|
|
242
|
+
throw new Error(
|
|
243
|
+
`target stylesheet directory does not exist: ${stylesoutdir} (hint: set mkdirs option)`
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (copyAsciidoctorStylesheet) {
|
|
249
|
+
// NOTE Stylesheets.instance.write_primary_stylesheet is not yet ported to JS
|
|
250
|
+
} else if (copyUserStylesheet) {
|
|
251
|
+
let stylesheetSrc = doc.getAttribute('copycss')
|
|
252
|
+
if (stylesheetSrc === '' || stylesheetSrc === true) {
|
|
253
|
+
stylesheetSrc = doc.normalizeSystemPath(stylesheet)
|
|
254
|
+
} else {
|
|
255
|
+
// NOTE in this case, copycss is a source location (but cannot be a URI)
|
|
256
|
+
stylesheetSrc = doc.normalizeSystemPath(String(stylesheetSrc))
|
|
257
|
+
}
|
|
258
|
+
const stylesheetDest = doc.normalizeSystemPath(
|
|
259
|
+
stylesheet,
|
|
260
|
+
stylesoutdir,
|
|
261
|
+
doc.safe >= SafeMode.SAFE ? outdir : null
|
|
262
|
+
)
|
|
263
|
+
// NOTE don't warn if src can't be read and dest already exists (see #2323)
|
|
264
|
+
if (stylesheetSrc !== stylesheetDest) {
|
|
265
|
+
const warnOnFailure = !(await _isFile(stylesheetDest))
|
|
266
|
+
const stylesheetData = await doc.readAsset(stylesheetSrc, {
|
|
267
|
+
warnOnFailure,
|
|
268
|
+
label: 'stylesheet',
|
|
269
|
+
})
|
|
270
|
+
if (stylesheetData) {
|
|
271
|
+
const { writeFile } = await import('node:fs/promises')
|
|
272
|
+
const nodePath = await _requirePath()
|
|
273
|
+
const stylesheetOutdir = nodePath.dirname(stylesheetDest)
|
|
274
|
+
if (
|
|
275
|
+
stylesheetOutdir !== stylesoutdir &&
|
|
276
|
+
!(await _isDirectory(stylesheetOutdir))
|
|
277
|
+
) {
|
|
278
|
+
if (!mkdirs) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`target stylesheet directory does not exist: ${stylesheetOutdir} (hint: set mkdirs option)`
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
await mkdirP(stylesheetOutdir)
|
|
284
|
+
}
|
|
285
|
+
await writeFile(stylesheetDest, stylesheetData, 'utf8')
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (copySyntaxHlStylesheet) {
|
|
290
|
+
await syntaxHl.writeStylesheetToDisk(doc, stylesoutdir)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return doc
|
|
295
|
+
} else {
|
|
296
|
+
return output
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ── convertFile ───────────────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Parse the contents of the AsciiDoc source file into a Document and convert it
|
|
304
|
+
* to the specified backend format.
|
|
305
|
+
*
|
|
306
|
+
* @param {string} filename - the path to the AsciiDoc source file
|
|
307
|
+
* @param {object} [options={}] - a plain Object of options (see {@link convert})
|
|
308
|
+
* @returns {Promise<import('./document.js').Document|string>} the Document if output was written to a file, otherwise the converted String
|
|
309
|
+
*/
|
|
310
|
+
export async function convertFile(filename, options = {}) {
|
|
311
|
+
const { readFile, stat } = await import('node:fs/promises')
|
|
312
|
+
const nodePath = await _requirePath()
|
|
313
|
+
const absPath = nodePath.resolve(filename)
|
|
314
|
+
const content = await readFile(absPath, 'utf8')
|
|
315
|
+
let mtime
|
|
316
|
+
try {
|
|
317
|
+
mtime = (await stat(absPath)).mtime
|
|
318
|
+
} catch {
|
|
319
|
+
/* ignore */
|
|
320
|
+
}
|
|
321
|
+
const fileObj = {
|
|
322
|
+
path: absPath,
|
|
323
|
+
mtime,
|
|
324
|
+
read() {
|
|
325
|
+
return content
|
|
326
|
+
},
|
|
327
|
+
}
|
|
328
|
+
return convert(fileObj, options)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Deprecated aliases
|
|
332
|
+
export { convert as render, convertFile as renderFile }
|
|
333
|
+
|
|
334
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* @internal
|
|
338
|
+
* Lazily import node:path to avoid issues in browser / Opal environments.
|
|
339
|
+
*/
|
|
340
|
+
async function _requirePath() {
|
|
341
|
+
return import('node:path')
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* @internal
|
|
346
|
+
* @param {string} dir
|
|
347
|
+
* @returns {Promise<boolean>}
|
|
348
|
+
*/
|
|
349
|
+
async function _isDirectory(dir) {
|
|
350
|
+
try {
|
|
351
|
+
const { stat } = await import('node:fs/promises')
|
|
352
|
+
return (await stat(dir)).isDirectory()
|
|
353
|
+
} catch {
|
|
354
|
+
return false
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* @internal
|
|
360
|
+
* @param {string} path
|
|
361
|
+
* @returns {Promise<boolean>}
|
|
362
|
+
*/
|
|
363
|
+
async function _isFile(path) {
|
|
364
|
+
try {
|
|
365
|
+
const { stat } = await import('node:fs/promises')
|
|
366
|
+
return (await stat(path)).isFile()
|
|
367
|
+
} catch {
|
|
368
|
+
return false
|
|
369
|
+
}
|
|
370
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// ESM conversion of converter/composite.rb
|
|
2
|
+
//
|
|
3
|
+
// Ruby-to-JavaScript notes:
|
|
4
|
+
// - Ruby Hash.new { |h,k| h[k] = find_converter(k) } → Map with lazy population in converterFor().
|
|
5
|
+
// - Ruby respond_to?(:composed) → typeof converter.composed === 'function'.
|
|
6
|
+
// - Ruby raise → throw new Error(…).
|
|
7
|
+
// - backend_traits_source keyword arg → options object { backendTraitsSource }.
|
|
8
|
+
// - init_backend_traits(source.backend_traits) → this.initBackendTraits(source.backendInfo()).
|
|
9
|
+
|
|
10
|
+
import { applyBackendTraits } from '../converter.js'
|
|
11
|
+
|
|
12
|
+
// ── CompositeConverter ────────────────────────────────────────────────────────
|
|
13
|
+
// Delegates to the first converter in the chain that handles a given transform.
|
|
14
|
+
|
|
15
|
+
export class CompositeConverter {
|
|
16
|
+
constructor(backend, ...args) {
|
|
17
|
+
// Last argument may be an options object { backendTraitsSource }
|
|
18
|
+
let opts = {}
|
|
19
|
+
if (
|
|
20
|
+
args.length > 0 &&
|
|
21
|
+
args[args.length - 1] !== null &&
|
|
22
|
+
typeof args[args.length - 1] === 'object' &&
|
|
23
|
+
!args[args.length - 1].convert
|
|
24
|
+
) {
|
|
25
|
+
opts = args.pop()
|
|
26
|
+
}
|
|
27
|
+
this.backend = backend
|
|
28
|
+
this.converters = args
|
|
29
|
+
applyBackendTraits(this)
|
|
30
|
+
for (const converter of this.converters) {
|
|
31
|
+
if (typeof converter.composed === 'function') converter.composed(this)
|
|
32
|
+
}
|
|
33
|
+
if (opts.backendTraitsSource) {
|
|
34
|
+
this.initBackendTraits(opts.backendTraitsSource.backendInfo())
|
|
35
|
+
}
|
|
36
|
+
this._converterCache = new Map()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Delegates to the first converter that handles the given transform.
|
|
41
|
+
* @param {object} node - the AbstractNode to convert
|
|
42
|
+
* @param {string|null} [transform=null] - the optional transform (default: node.nodeName)
|
|
43
|
+
* @param {object|null} [opts=null] - optional hints passed to the delegate's convert method
|
|
44
|
+
* @returns {Promise<string>} the result from the delegate's convert method
|
|
45
|
+
*/
|
|
46
|
+
convert(node, transform = null, opts = null) {
|
|
47
|
+
const t = transform ?? node.nodeName
|
|
48
|
+
return this.converterFor(t).convert(node, t, opts)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Retrieve the converter for the specified transform (cached).
|
|
53
|
+
* @param {string} transform
|
|
54
|
+
* @returns {object} the matching converter
|
|
55
|
+
*/
|
|
56
|
+
converterFor(transform) {
|
|
57
|
+
if (this._converterCache.has(transform))
|
|
58
|
+
return this._converterCache.get(transform)
|
|
59
|
+
const converter = this._findConverter(transform)
|
|
60
|
+
this._converterCache.set(transform, converter)
|
|
61
|
+
return converter
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Find the converter for the specified transform.
|
|
66
|
+
* @param {string} transform
|
|
67
|
+
* @returns {object} the matching converter
|
|
68
|
+
* @throws {Error} if no converter handles the transform
|
|
69
|
+
* @internal
|
|
70
|
+
*/
|
|
71
|
+
_findConverter(transform) {
|
|
72
|
+
for (const candidate of this.converters) {
|
|
73
|
+
if (
|
|
74
|
+
typeof candidate.handles === 'function' &&
|
|
75
|
+
candidate.handles(transform)
|
|
76
|
+
)
|
|
77
|
+
return candidate
|
|
78
|
+
}
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Could not find a converter to handle transform: ${transform}`
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|