@panagos/jojoscript 0.7.0
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/LICENSE +24 -0
- package/README.md +418 -0
- package/bin/jojo.js +170 -0
- package/package.json +59 -0
- package/src/compiler.js +169 -0
- package/src/index.js +19 -0
- package/src/lexer.js +178 -0
- package/src/match-parser.js +215 -0
- package/src/pipeline-parser.js +418 -0
- package/src/runtime/collections.js +327 -0
- package/src/runtime/index.js +29 -0
- package/src/transforms.js +281 -0
- package/src/type-annotations.js +191 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
function iterable(value) {
|
|
2
|
+
if (value == null) {
|
|
3
|
+
throw new TypeError('Expected an iterable, got null or undefined')
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
if (typeof value[Symbol.iterator] === 'function') {
|
|
7
|
+
return value
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (typeof value.next === 'function') {
|
|
11
|
+
return {
|
|
12
|
+
[Symbol.iterator]() {
|
|
13
|
+
return value
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
throw new TypeError('Expected an iterable or iterator')
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* Like `iterable`, but accepts (and prefers) an async iterable/iterator so
|
|
23
|
+
* `for await` correctly awaits values produced by async generators such as
|
|
24
|
+
* `mapAsync`/`filterAsync`, in addition to ordinary sync iterables.
|
|
25
|
+
*/
|
|
26
|
+
function asyncIterable(value) {
|
|
27
|
+
if (value == null) {
|
|
28
|
+
throw new TypeError('Expected an iterable, got null or undefined')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
typeof value[Symbol.asyncIterator] === 'function' ||
|
|
33
|
+
typeof value[Symbol.iterator] === 'function'
|
|
34
|
+
) {
|
|
35
|
+
return value
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (typeof value.next === 'function') {
|
|
39
|
+
return {
|
|
40
|
+
[Symbol.asyncIterator]() {
|
|
41
|
+
return value
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new TypeError('Expected an iterable or iterator')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function map(items, fn) {
|
|
50
|
+
return (function* () {
|
|
51
|
+
let index = 0
|
|
52
|
+
for (const item of iterable(items)) {
|
|
53
|
+
yield fn(item, index++)
|
|
54
|
+
}
|
|
55
|
+
})()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function filter(items, fn) {
|
|
59
|
+
return (function* () {
|
|
60
|
+
let index = 0
|
|
61
|
+
for (const item of iterable(items)) {
|
|
62
|
+
if (fn(item, index++)) yield item
|
|
63
|
+
}
|
|
64
|
+
})()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function take(items, count) {
|
|
68
|
+
return (function* () {
|
|
69
|
+
if (count <= 0) return
|
|
70
|
+
|
|
71
|
+
let emitted = 0
|
|
72
|
+
for (const item of iterable(items)) {
|
|
73
|
+
yield item
|
|
74
|
+
emitted++
|
|
75
|
+
if (emitted >= count) return
|
|
76
|
+
}
|
|
77
|
+
})()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function skip(items, count) {
|
|
81
|
+
return (function* () {
|
|
82
|
+
let index = 0
|
|
83
|
+
for (const item of iterable(items)) {
|
|
84
|
+
if (index++ < count) continue
|
|
85
|
+
yield item
|
|
86
|
+
}
|
|
87
|
+
})()
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function unique(items) {
|
|
91
|
+
return (function* () {
|
|
92
|
+
const seen = new Set()
|
|
93
|
+
for (const item of iterable(items)) {
|
|
94
|
+
if (seen.has(item)) continue
|
|
95
|
+
seen.add(item)
|
|
96
|
+
yield item
|
|
97
|
+
}
|
|
98
|
+
})()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function flatMap(items, fn) {
|
|
102
|
+
return (function* () {
|
|
103
|
+
let index = 0
|
|
104
|
+
for (const item of iterable(items)) {
|
|
105
|
+
for (const value of iterable(fn(item, index++))) {
|
|
106
|
+
yield value
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
})()
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function reduce(items, fn, initial) {
|
|
113
|
+
const iterator = iterable(items)[Symbol.iterator]()
|
|
114
|
+
let accumulator
|
|
115
|
+
|
|
116
|
+
if (arguments.length >= 3) {
|
|
117
|
+
accumulator = initial
|
|
118
|
+
} else {
|
|
119
|
+
const first = iterator.next()
|
|
120
|
+
if (first.done) {
|
|
121
|
+
throw new TypeError('Reduce of empty iterable with no initial value')
|
|
122
|
+
}
|
|
123
|
+
accumulator = first.value
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let index = arguments.length >= 3 ? 0 : 1
|
|
127
|
+
for (let step = iterator.next(); !step.done; step = iterator.next()) {
|
|
128
|
+
accumulator = fn(accumulator, step.value, index++)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return accumulator
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function toArray(items) {
|
|
135
|
+
return Array.from(iterable(items))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function count(items) {
|
|
139
|
+
let result = 0
|
|
140
|
+
for (const _ of iterable(items)) result++
|
|
141
|
+
return result
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function first(items) {
|
|
145
|
+
for (const item of iterable(items)) return item
|
|
146
|
+
return undefined
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function find(items, predicate) {
|
|
150
|
+
let index = 0
|
|
151
|
+
for (const item of iterable(items)) {
|
|
152
|
+
if (predicate(item, index++)) return item
|
|
153
|
+
}
|
|
154
|
+
return undefined
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function some(items, predicate) {
|
|
158
|
+
let index = 0
|
|
159
|
+
for (const item of iterable(items)) {
|
|
160
|
+
if (predicate(item, index++)) return true
|
|
161
|
+
}
|
|
162
|
+
return false
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function every(items, predicate) {
|
|
166
|
+
let index = 0
|
|
167
|
+
for (const item of iterable(items)) {
|
|
168
|
+
if (!predicate(item, index++)) return false
|
|
169
|
+
}
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function sort(items, fn) {
|
|
174
|
+
return Array.from(iterable(items)).sort(fn)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function sortBy(items, keyFn) {
|
|
178
|
+
return Array.from(iterable(items)).sort((a, b) => {
|
|
179
|
+
const keyA = keyFn(a)
|
|
180
|
+
const keyB = keyFn(b)
|
|
181
|
+
if (keyA < keyB) return -1
|
|
182
|
+
if (keyA > keyB) return 1
|
|
183
|
+
return 0
|
|
184
|
+
})
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function distinctBy(items, keyFn) {
|
|
188
|
+
return (function* () {
|
|
189
|
+
const seen = new Set()
|
|
190
|
+
for (const item of iterable(items)) {
|
|
191
|
+
const key = keyFn(item)
|
|
192
|
+
if (seen.has(key)) continue
|
|
193
|
+
seen.add(key)
|
|
194
|
+
yield item
|
|
195
|
+
}
|
|
196
|
+
})()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function tap(items, fn) {
|
|
200
|
+
return (function* () {
|
|
201
|
+
let index = 0
|
|
202
|
+
for (const item of iterable(items)) {
|
|
203
|
+
fn(item, index++)
|
|
204
|
+
yield item
|
|
205
|
+
}
|
|
206
|
+
})()
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function chunk(items, size) {
|
|
210
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
211
|
+
throw new RangeError('chunk size must be a positive integer')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return (function* () {
|
|
215
|
+
let buffer = []
|
|
216
|
+
for (const item of iterable(items)) {
|
|
217
|
+
buffer.push(item)
|
|
218
|
+
if (buffer.length === size) {
|
|
219
|
+
yield buffer
|
|
220
|
+
buffer = []
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (buffer.length) yield buffer
|
|
224
|
+
})()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function window(items, size) {
|
|
228
|
+
if (!Number.isInteger(size) || size <= 0) {
|
|
229
|
+
throw new RangeError('window size must be a positive integer')
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return (function* () {
|
|
233
|
+
let buffer = []
|
|
234
|
+
for (const item of iterable(items)) {
|
|
235
|
+
buffer.push(item)
|
|
236
|
+
if (buffer.length > size) buffer.shift()
|
|
237
|
+
if (buffer.length === size) yield buffer.slice()
|
|
238
|
+
}
|
|
239
|
+
})()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function zip(items, other) {
|
|
243
|
+
return (function* () {
|
|
244
|
+
const left = iterable(items)[Symbol.iterator]()
|
|
245
|
+
const right = iterable(other)[Symbol.iterator]()
|
|
246
|
+
|
|
247
|
+
while (true) {
|
|
248
|
+
const a = left.next()
|
|
249
|
+
const b = right.next()
|
|
250
|
+
if (a.done || b.done) return
|
|
251
|
+
yield [a.value, b.value]
|
|
252
|
+
}
|
|
253
|
+
})()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function zipWith(items, other, fn) {
|
|
257
|
+
return (function* () {
|
|
258
|
+
const left = iterable(items)[Symbol.iterator]()
|
|
259
|
+
const right = iterable(other)[Symbol.iterator]()
|
|
260
|
+
|
|
261
|
+
while (true) {
|
|
262
|
+
const a = left.next()
|
|
263
|
+
const b = right.next()
|
|
264
|
+
if (a.done || b.done) return
|
|
265
|
+
yield fn(a.value, b.value)
|
|
266
|
+
}
|
|
267
|
+
})()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function scan(items, fn, initial) {
|
|
271
|
+
return (function* () {
|
|
272
|
+
let accumulator = initial
|
|
273
|
+
let index = 0
|
|
274
|
+
for (const item of iterable(items)) {
|
|
275
|
+
accumulator = fn(accumulator, item, index++)
|
|
276
|
+
yield accumulator
|
|
277
|
+
}
|
|
278
|
+
})()
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function partition(items, predicate) {
|
|
282
|
+
const truthy = []
|
|
283
|
+
const falsy = []
|
|
284
|
+
let index = 0
|
|
285
|
+
|
|
286
|
+
for (const item of iterable(items)) {
|
|
287
|
+
if (predicate(item, index++)) {
|
|
288
|
+
truthy.push(item)
|
|
289
|
+
} else {
|
|
290
|
+
falsy.push(item)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return [truthy, falsy]
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function groupBy(items, keyFn) {
|
|
298
|
+
const groups = new Map()
|
|
299
|
+
|
|
300
|
+
for (const item of iterable(items)) {
|
|
301
|
+
const key = keyFn(item)
|
|
302
|
+
if (!groups.has(key)) groups.set(key, [])
|
|
303
|
+
groups.get(key).push(item)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return groups
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export async function* mapAsync(items, fn) {
|
|
310
|
+
let index = 0
|
|
311
|
+
for await (const item of asyncIterable(items)) {
|
|
312
|
+
yield await fn(item, index++)
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export async function* filterAsync(items, fn) {
|
|
317
|
+
let index = 0
|
|
318
|
+
for await (const item of asyncIterable(items)) {
|
|
319
|
+
if (await fn(item, index++)) yield item
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export async function toArrayAsync(items) {
|
|
324
|
+
const result = []
|
|
325
|
+
for await (const item of asyncIterable(items)) result.push(item)
|
|
326
|
+
return result
|
|
327
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export {
|
|
2
|
+
map,
|
|
3
|
+
filter,
|
|
4
|
+
sort,
|
|
5
|
+
unique,
|
|
6
|
+
take,
|
|
7
|
+
skip,
|
|
8
|
+
flatMap,
|
|
9
|
+
reduce,
|
|
10
|
+
toArray,
|
|
11
|
+
count,
|
|
12
|
+
first,
|
|
13
|
+
find,
|
|
14
|
+
some,
|
|
15
|
+
every,
|
|
16
|
+
groupBy,
|
|
17
|
+
partition,
|
|
18
|
+
chunk,
|
|
19
|
+
window,
|
|
20
|
+
zip,
|
|
21
|
+
zipWith,
|
|
22
|
+
scan,
|
|
23
|
+
sortBy,
|
|
24
|
+
distinctBy,
|
|
25
|
+
tap,
|
|
26
|
+
mapAsync,
|
|
27
|
+
filterAsync,
|
|
28
|
+
toArrayAsync
|
|
29
|
+
} from './collections.js'
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { maskNonCode } from './lexer.js'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Applies `regex.replace` only to matches that are real code, i.e. matches
|
|
5
|
+
* that do not overlap a string, template literal or comment. This prevents
|
|
6
|
+
* jojo syntax that merely *looks* like source code (inside a string a
|
|
7
|
+
* comment) from being rewritten.
|
|
8
|
+
*/
|
|
9
|
+
function codeAwareReplace(source, regex, replacer) {
|
|
10
|
+
const mask = maskNonCode(source)
|
|
11
|
+
|
|
12
|
+
return source.replace(regex, (...args) => {
|
|
13
|
+
const match = args[0]
|
|
14
|
+
const offset = args[args.length - 2]
|
|
15
|
+
|
|
16
|
+
if (mask.slice(offset, offset + match.length) !== match) {
|
|
17
|
+
return match
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return replacer(...args)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/*
|
|
25
|
+
* Expression functions capture everything up to the end of the physical
|
|
26
|
+
* line as the body, which may include a trailing `// comment`. Splitting it
|
|
27
|
+
* out keeps the comment from swallowing the generated closing brace, e.g.
|
|
28
|
+
* fn double(x) = x * 2 // doubles the value
|
|
29
|
+
* must not become:
|
|
30
|
+
* function double(x) { return x * 2 // doubles the value; }
|
|
31
|
+
*/
|
|
32
|
+
function splitTrailingComment(expression) {
|
|
33
|
+
const mask = maskNonCode(expression, { comments: false })
|
|
34
|
+
const index = mask.indexOf('//')
|
|
35
|
+
|
|
36
|
+
if (index === -1) {
|
|
37
|
+
return { code: expression, comment: '' }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
code: expression.slice(0, index).trimEnd(),
|
|
42
|
+
comment: expression.slice(index)
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function transformExpressionFunctions(source) {
|
|
47
|
+
const mask = maskNonCode(source)
|
|
48
|
+
|
|
49
|
+
return source.replace(
|
|
50
|
+
/^([ \t]*)(export\s+)?(async\s+)?fn\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(((?:[^()]|\((?:[^()]|\([^()]*\))*\))*)\)\s*=\s*(.+)$/gm,
|
|
51
|
+
(match, indent, exportPart, asyncPart, name, args, expression, offset) => {
|
|
52
|
+
/*
|
|
53
|
+
* Only validate the part of the match that can never legitimately
|
|
54
|
+
* contain a string/comment (indent + export/async + "fn name(").
|
|
55
|
+
* The parameter list and expression body are allowed to contain
|
|
56
|
+
* real string/template literals (e.g. a default value that calls
|
|
57
|
+
* a function returning a string), so they must not be compared
|
|
58
|
+
* against the mask.
|
|
59
|
+
*/
|
|
60
|
+
const keywordLength = match.indexOf('(') + 1
|
|
61
|
+
|
|
62
|
+
if (mask.slice(offset, offset + keywordLength) !== match.slice(0, keywordLength)) {
|
|
63
|
+
return match
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const { code, comment } = splitTrailingComment(expression)
|
|
67
|
+
const suffix = comment ? ` ${comment}` : ''
|
|
68
|
+
return `${indent}${exportPart || ''}${asyncPart || ''}function ${name}(${args}) { return ${code}; }${suffix}`
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function transformFunctionDeclarations(source) {
|
|
74
|
+
return codeAwareReplace(
|
|
75
|
+
codeAwareReplace(
|
|
76
|
+
source,
|
|
77
|
+
/\basync\s+fn\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g,
|
|
78
|
+
(_, name) => `async function ${name}(`
|
|
79
|
+
),
|
|
80
|
+
/\bfn\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*\(/g,
|
|
81
|
+
(_, name) => `function ${name}(`
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function transformMutableDeclarations(source) {
|
|
86
|
+
return codeAwareReplace(
|
|
87
|
+
source,
|
|
88
|
+
/(^|\n)([ \t]*)(export\s+)?mutable\s+([A-Za-z_$][A-Za-z0-9_$]*)\s*:=/g,
|
|
89
|
+
(_, before, indent, exportPart, name) => `${before}${indent}${exportPart || ''}let ${name} =`
|
|
90
|
+
)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function transformConstDeclarations(source) {
|
|
94
|
+
return codeAwareReplace(
|
|
95
|
+
source,
|
|
96
|
+
/(^|\n)([ \t]*)(export\s+)?([A-Za-z_$][A-Za-z0-9_$]*)\s*:=/g,
|
|
97
|
+
(_, before, indent, exportPart, name) => `${before}${indent}${exportPart || ''}const ${name} =`
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Tuple/multi-binding declarations:
|
|
103
|
+
* a, b := 1, 2 -> const [a, b] = [1, 2]
|
|
104
|
+
* mutable a, b := 1, 2 -> let [a, b] = [1, 2]
|
|
105
|
+
*
|
|
106
|
+
* The right-hand side is captured as the remainder of the physical line
|
|
107
|
+
* and wrapped in `[...]` verbatim, so this sugar is intentionally limited
|
|
108
|
+
* to single-line right-hand sides. Use `[a, b] := someArrayExpr()` (see
|
|
109
|
+
* `transformDestructuringDeclarations`) to destructure a single expression
|
|
110
|
+
* instead.
|
|
111
|
+
*/
|
|
112
|
+
export function transformMultiBindingDeclarations(source) {
|
|
113
|
+
const mask = maskNonCode(source)
|
|
114
|
+
|
|
115
|
+
return source.replace(
|
|
116
|
+
/^([ \t]*)(export\s+)?(mutable\s+)?([A-Za-z_$][A-Za-z0-9_$]*(?:\s*,\s*[A-Za-z_$][A-Za-z0-9_$]*)+)\s*:=[ \t]*(.+)$/gm,
|
|
117
|
+
(match, indent, exportPart, mutablePart, names, rhs, offset) => {
|
|
118
|
+
const headLength = match.length - rhs.length
|
|
119
|
+
|
|
120
|
+
if (mask.slice(offset, offset + headLength) !== match.slice(0, headLength)) {
|
|
121
|
+
return match
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const keyword = mutablePart ? 'let' : 'const'
|
|
125
|
+
const list = names.split(',').map(name => name.trim())
|
|
126
|
+
const { code, comment } = splitTrailingComment(rhs)
|
|
127
|
+
const suffix = comment ? ` ${comment}` : ''
|
|
128
|
+
|
|
129
|
+
return `${indent}${exportPart || ''}${keyword} [${list.join(', ')}] = [${code.trim()}]${suffix}`
|
|
130
|
+
}
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
* Finds the index of `close` matching the `open` bracket already known to
|
|
136
|
+
* be at `mask[start]`, scanning the mask so brackets inside strings,
|
|
137
|
+
* template literals and comments are ignored.
|
|
138
|
+
*/
|
|
139
|
+
function findMatchingBracket(mask, start, open, close) {
|
|
140
|
+
let depth = 0
|
|
141
|
+
|
|
142
|
+
for (let i = start; i < mask.length; i++) {
|
|
143
|
+
if (mask[i] === open) depth++
|
|
144
|
+
else if (mask[i] === close) {
|
|
145
|
+
depth--
|
|
146
|
+
if (depth === 0) return i
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return -1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/*
|
|
154
|
+
* Recognizes an object/array destructuring declaration head at `start`,
|
|
155
|
+
* which must be the beginning of a line (ignoring leading whitespace):
|
|
156
|
+
* { a, b } := getUser() -> const { a, b } = getUser()
|
|
157
|
+
* mutable [a, ...rest] := items -> let [a, ...rest] = items
|
|
158
|
+
*
|
|
159
|
+
* Only the declaration head (indent, `export`/`mutable`, pattern, `:=`) is
|
|
160
|
+
* matched and rewritten; the right-hand side is left completely untouched,
|
|
161
|
+
* so it may span multiple lines just like plain `:=`/`mutable :=`.
|
|
162
|
+
*/
|
|
163
|
+
function matchDestructuringHead(source, mask, start) {
|
|
164
|
+
let i = start
|
|
165
|
+
|
|
166
|
+
while (source[i] === ' ' || source[i] === '\t') i++
|
|
167
|
+
const indent = source.slice(start, i)
|
|
168
|
+
|
|
169
|
+
let exportPart = ''
|
|
170
|
+
if (source.startsWith('export', i) && /\s/.test(source[i + 6] || '')) {
|
|
171
|
+
let j = i + 6
|
|
172
|
+
while (/\s/.test(source[j])) j++
|
|
173
|
+
exportPart = source.slice(i, j)
|
|
174
|
+
i = j
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
let isMutable = false
|
|
178
|
+
if (source.startsWith('mutable', i) && /\s/.test(source[i + 7] || '')) {
|
|
179
|
+
let j = i + 7
|
|
180
|
+
while (/\s/.test(source[j])) j++
|
|
181
|
+
isMutable = true
|
|
182
|
+
i = j
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const openChar = source[i]
|
|
186
|
+
if ((openChar !== '{' && openChar !== '[') || mask[i] !== openChar) return null
|
|
187
|
+
|
|
188
|
+
const closeChar = openChar === '{' ? '}' : ']'
|
|
189
|
+
const patternStart = i
|
|
190
|
+
const closeIndex = findMatchingBracket(mask, i, openChar, closeChar)
|
|
191
|
+
if (closeIndex === -1) return null
|
|
192
|
+
|
|
193
|
+
const patternEnd = closeIndex + 1
|
|
194
|
+
let k = patternEnd
|
|
195
|
+
while (/\s/.test(source[k] || '')) k++
|
|
196
|
+
if (source[k] !== ':' || source[k + 1] !== '=') return null
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
indent,
|
|
200
|
+
exportPart,
|
|
201
|
+
mutable: isMutable,
|
|
202
|
+
pattern: source.slice(patternStart, patternEnd),
|
|
203
|
+
end: k + 2
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function transformDestructuringDeclarations(source) {
|
|
208
|
+
const mask = maskNonCode(source)
|
|
209
|
+
let result = ''
|
|
210
|
+
let i = 0
|
|
211
|
+
|
|
212
|
+
while (i < source.length) {
|
|
213
|
+
const atLineStart = i === 0 || source[i - 1] === '\n'
|
|
214
|
+
|
|
215
|
+
if (!atLineStart) {
|
|
216
|
+
result += source[i]
|
|
217
|
+
i++
|
|
218
|
+
continue
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const head = matchDestructuringHead(source, mask, i)
|
|
222
|
+
|
|
223
|
+
if (!head) {
|
|
224
|
+
result += source[i]
|
|
225
|
+
i++
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const keyword = head.mutable ? 'let' : 'const'
|
|
230
|
+
result += `${head.indent}${head.exportPart}${keyword} ${head.pattern} =`
|
|
231
|
+
i = head.end
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return result
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function transformLocalImports(source) {
|
|
238
|
+
const replacer = (_, prefix, specifier, quote) =>
|
|
239
|
+
prefix + normalizeLocalSpecifier(specifier) + quote
|
|
240
|
+
|
|
241
|
+
return [
|
|
242
|
+
/(from\s+["'])(\.{1,2}\/[^"']+)(["'])/g,
|
|
243
|
+
/(import\s+["'])(\.{1,2}\/[^"']+)(["'])/g,
|
|
244
|
+
/(import\(\s*["'])(\.{1,2}\/[^"']+)(["']\s*\))/g
|
|
245
|
+
].reduce(
|
|
246
|
+
(result, regex) => codeAwareReplaceIgnoringComments(result, regex, replacer),
|
|
247
|
+
source
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/*
|
|
252
|
+
* Like codeAwareReplace, but only guards against matches inside comments.
|
|
253
|
+
* Local import specifiers legitimately live inside string literals, so
|
|
254
|
+
* strings must stay eligible for replacement here.
|
|
255
|
+
*/
|
|
256
|
+
function codeAwareReplaceIgnoringComments(source, regex, replacer) {
|
|
257
|
+
const mask = maskNonCode(source, { strings: false })
|
|
258
|
+
|
|
259
|
+
return source.replace(regex, (...args) => {
|
|
260
|
+
const match = args[0]
|
|
261
|
+
const offset = args[args.length - 2]
|
|
262
|
+
|
|
263
|
+
if (mask.slice(offset, offset + match.length) !== match) {
|
|
264
|
+
return match
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return replacer(...args)
|
|
268
|
+
})
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function normalizeLocalSpecifier(specifier) {
|
|
272
|
+
if (specifier.endsWith('.jojo')) {
|
|
273
|
+
specifier = specifier.slice(0, -'.jojo'.length)
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!specifier.endsWith('.js')) {
|
|
277
|
+
specifier += '.js'
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return specifier
|
|
281
|
+
}
|