UNPKG

3.9 kBJavaScriptView Raw
1const { Transform } = require('stream');
2const { checkRule, hideStack } = require('./lib');
3let cleanStack = require('@artdeco/clean-stack'); if (cleanStack && cleanStack.__esModule) cleanStack = cleanStack.default;
4
5 class Replaceable extends Transform {
6 /**
7 * Replaceable class that extends Transform and pushes data when it's done replacing each incoming chunk. If the replacement is passed as a function, it will work in the same way as the replacer for `string.replace` method (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace), taking the `match` as the first argument, and matched `p1`, `p2`, _etc_ parameters as following arguments. The replacer can also be an async function.
8 * @param {(Rule|Array<Rule>)} rules A single replacement rule, or multiple rules.
9 * @param {TransformOptions=} [options] The options for the transform stream.
10 * @example
11 *
12 * // markdown __ to html emphasize implementation
13 * const stream = replaceStream({
14 * re: /__(\S+)__/g,
15 * replacement(match, p1) {
16 * return `<em>${p1}</em>`
17 * },
18 * })
19 */
20 constructor(rules, options) {
21 super(options)
22 const re = Array.isArray(rules) ? rules : [rules]
23 const fr = re.filter(checkRule)
24 this.rules = fr
25 }
26
27 /**
28 * Stop executing further after the current rule.
29 */
30 brake() {
31 this._broke = true
32 }
33
34 async reduce(chunk) {
35 /** @type {string} */
36 const s = await this.rules.reduce(async (acc, { re, replacement }) => {
37 /** @type {string} */
38 let string = await acc
39 if (this._broke) return string
40
41 if (typeof replacement == 'string') {
42 string = string.replace(re, replacement)
43 } else {
44 /** @type {(Replacer|AsyncReplacer)} */
45 const R = replacement.bind(this)
46 const promises = []
47 let commonError
48 const t = string.replace(re, (match, ...args) => {
49 commonError = new Error()
50 try {
51 if (this._broke) return match
52 const p = R(match, ...args)
53 if (p instanceof Promise) {
54 promises.push(p)
55 }
56 return p
57 } catch (e) { // hide stack for sync stack traces
58 hideStack(commonError, e)
59 }
60 })
61 if (promises.length) {
62 try { // hide stack only for when throw happens before awaits
63 const data = await Promise.all(promises)
64 string = string.replace(re, () => data.shift())
65 } catch (e) {
66 hideStack(commonError, e)
67 }
68 } else {
69 string = t
70 }
71 }
72 return string
73 }, `${chunk}`)
74
75 return s
76 }
77 async _transform(chunk, _, next) {
78 try {
79 const s = await this.reduce(chunk)
80 this.push(s)
81 next()
82 } catch (e) {
83 const s = cleanStack(e.stack)
84 e.stack = s
85 next(e)
86 }
87 }
88}
89
90/**
91 * The class for when serial execution of asynchronous replacements withing the same rule are needed.
92 */
93 class SerialAsyncReplaceable extends Replaceable {
94 /**
95 * @param {(Rule|Array<Rule>)} rules
96 */
97 constructor(rules) {
98 super(rules)
99 this.promise = Promise.resolve()
100 }
101 addItem(fn) {
102 const pp = this.promise.then(fn)
103 this.promise = pp
104 return pp
105 }
106}
107
108/**
109 * @typedef {import('stream').TransformOptions} TransformOptions
110 */
111
112/* documentary types/rules.xml */
113/**
114 * @typedef {(match: string, ...params: string[]) => string} Replacer
115 *
116 * @typedef {(match: string, ...params: string[]) => Promise.<string>} AsyncReplacer
117 *
118 * @typedef {Object} Rule A replacement rule.
119 * @prop {RegExp} re A Regular expression to match against.
120 * @prop {string|Replacer|AsyncReplacer} replacement A replacement string, or a replacement string function.
121 */
122
123
124module.exports = Replaceable
125module.exports.SerialAsyncReplaceable = SerialAsyncReplaceable
\No newline at end of file