1 | const { Transform } = require('stream');
|
2 | const { checkRule, hideStack } = require('./lib');
|
3 | let 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 |
|
124 | module.exports = Replaceable
|
125 | module.exports.SerialAsyncReplaceable = SerialAsyncReplaceable |
\ | No newline at end of file |