@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.
@@ -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
+ }