wuchale 0.3.2 → 0.4.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.
@@ -1,652 +0,0 @@
1
- // $$ cd .. && npm run test
2
-
3
- import MagicString from "magic-string"
4
-
5
- const snipPrefix = 'wuchaleSnippet'
6
- const rtComponent = 'WuchaleTrans'
7
- const rtFunc = 'wuchaleTrans'
8
-
9
- /**
10
- * @typedef {"script" | "markup" | "attribute"} TxtScope
11
- * @typedef {(text: string, scope: TxtScope) => boolean} HeuristicFunc
12
- */
13
-
14
- /**
15
- * @type {HeuristicFunc}
16
- */
17
- export function defaultHeuristic(text, scope = 'markup') {
18
- if (scope === 'markup') {
19
- return true
20
- }
21
- // script and attribute
22
- const range = 'AZ'
23
- const startCode = text.charCodeAt(0)
24
- return startCode >= range.charCodeAt(0) && startCode <= range.charCodeAt(1)
25
- }
26
-
27
- export class NestText extends String {
28
- /**
29
- * @param {string} txt
30
- * @param {TxtScope} scope
31
- * @param {string | null} [context]
32
- */
33
- constructor(txt, scope, context) {
34
- super(txt)
35
- this.scope = scope
36
- /** @type {string} */
37
- this.context = context ?? null
38
- }
39
-
40
- toKey = () => `${this.toString()}\n${this.context ?? ''}`.trim()
41
-
42
- }
43
-
44
- export class IndexTracker {
45
- /**
46
- * @param {object} sourceTranslations
47
- */
48
- constructor(sourceTranslations) {
49
- this.indices = {}
50
- this.nextIndex = 0
51
- for (const txt of Object.keys(sourceTranslations)) {
52
- // guaranteed order for strings since ES2015
53
- this.indices[txt] = this.nextIndex
54
- this.nextIndex++
55
- }
56
- }
57
-
58
- get = (/** @type {string} */ txt) => {
59
- if (txt in this.indices) {
60
- return this.indices[txt]
61
- }
62
- const index = this.nextIndex
63
- this.indices[txt] = index
64
- this.nextIndex++
65
- return index
66
- }
67
- }
68
-
69
- export default class Preprocess {
70
- /**
71
- * @param {IndexTracker} index
72
- * @param {HeuristicFunc} heuristic
73
- * @param {string} importFrom
74
- */
75
- constructor(index, heuristic = defaultHeuristic, importFrom = 'wuchale/runtime.svelte') {
76
- this.index = index
77
- this.importFrom = importFrom
78
- this.heuristic = heuristic
79
- this.content = ''
80
- /** @type {MagicString} */
81
- this.mstr = null
82
- /** @type {boolean | null} */
83
- this.forceInclude = null
84
- /** @type {string} */
85
- this.context = null
86
- }
87
-
88
- /**
89
- * @param {string} text
90
- * @param {TxtScope} scope
91
- * @returns {Array<*> & {0: boolean, 1: NestText}}
92
- */
93
- checkHeuristic = (text, scope) => {
94
- text = text.replace(/\s+/g, ' ').trim()
95
- if (text === '') {
96
- // nothing to ask
97
- return [false, null]
98
- }
99
- const extract = this.forceInclude || this.heuristic(text, scope)
100
- return [extract, new NestText(text, scope)]
101
- }
102
-
103
- // visitComment = () => []
104
- // visitIdentifier = () => []
105
- // visitImportDeclaration = () => []
106
-
107
- /**
108
- * @param {import('estree').Literal & {start: number, end: number}} node
109
- * @returns {NestText[]}
110
- */
111
- visitLiteral = node => {
112
- if (typeof node.value !== 'string') {
113
- return []
114
- }
115
- const { start, end } = node
116
- const [pass, txt] = this.checkHeuristic(node.value, 'script')
117
- if (!pass) {
118
- return []
119
- }
120
- txt.context = this.context
121
- this.mstr.update(start, end, `${rtFunc}(${this.index.get(txt.toKey())})`)
122
- return [txt]
123
- }
124
-
125
- /**
126
- * @param {import('estree').ArrayExpression} node
127
- * @returns {NestText[]}
128
- */
129
- visitArrayExpression = node => {
130
- const txts = []
131
- for (const elm of node.elements) {
132
- txts.push(...this.visit(elm))
133
- }
134
- return txts
135
- }
136
-
137
- /**
138
- * @param {import('estree').ObjectExpression} node
139
- * @returns {NestText[]}
140
- */
141
- visitObjectExpression = node => {
142
- const txts = []
143
- for (const prop of node.properties) {
144
- // @ts-ignore
145
- txts.push(...this.visit(prop.key))
146
- // @ts-ignore
147
- txts.push(...this.visit(prop.value))
148
- }
149
- return txts
150
- }
151
-
152
- /**
153
- * @param {import('estree').MemberExpression} node
154
- * @returns {NestText[]}
155
- */
156
- visitMemberExpression = node => {
157
- return [
158
- ...this.visit(node.object),
159
- ...this.visit(node.property),
160
- ]
161
- }
162
-
163
- /**
164
- * @param {import('estree').CallExpression} node
165
- * @returns {NestText[]}
166
- */
167
- visitCallExpression = node => {
168
- const txts = [...this.visit(node.callee)]
169
- for (const arg of node.arguments) {
170
- txts.push(...this.visit(arg))
171
- }
172
- return txts
173
- }
174
-
175
- /**
176
- * @param {import('estree').BinaryExpression} node
177
- * @returns {NestText[]}
178
- */
179
- visitBinaryExpression = node => {
180
- return [
181
- ...this.visit(node.left),
182
- ...this.visit(node.right),
183
- ]
184
- }
185
-
186
- /**
187
- * @param {import('estree').VariableDeclaration} node
188
- * @returns {NestText[]}
189
- */
190
- visitVariableDeclaration = node => {
191
- const txts = []
192
- for (const dec of node.declarations) {
193
- if (!dec.init) {
194
- continue
195
- }
196
- // visit only contents inside $derived
197
- if (dec.init.type !== 'CallExpression' || dec.init.callee.type !== 'Identifier' || dec.init.callee.name !== '$derived') {
198
- continue
199
- }
200
- const decVisit = this.visit(dec.init)
201
- if (!decVisit.length) {
202
- continue
203
- }
204
- txts.push(...decVisit)
205
- }
206
- return txts
207
- }
208
-
209
- /**
210
- * @param {import('estree').ExportDefaultDeclaration} node
211
- * @returns {NestText[]}
212
- */
213
- visitExportDefaultDeclaration = node => this.visit(node.declaration)
214
-
215
- /**
216
- * @param {import('estree').TemplateLiteral} node
217
- * @returns {NestText[]}
218
- */
219
- visitTemplateLiteral = node => {
220
- const txts = []
221
- const quasi0 = node.quasis[0]
222
- // @ts-ignore
223
- const {start: start0, end: end0} = quasi0
224
- const [pass, txt0] = this.checkHeuristic(quasi0.value.cooked, 'script')
225
- if (!pass) {
226
- return txts
227
- }
228
- let txt = txt0.toString()
229
- for (const [i, expr] of node.expressions.entries()) {
230
- txts.push(...this.visit(expr))
231
- const quasi = node.quasis[i + 1]
232
- txt += `{${i}}${quasi.value.cooked}`
233
- // @ts-ignore
234
- const {start, end} = quasi
235
- this.mstr.remove(start - 1, end)
236
- if (i + 1 === node.expressions.length) {
237
- continue
238
- }
239
- this.mstr.update(end, end + 2, ', ')
240
- }
241
- const nTxt = new NestText(txt, txt0.scope, this.context)
242
- let repl = `${rtFunc}(${this.index.get(nTxt.toKey())}`
243
- if (node.expressions.length) {
244
- repl += ', '
245
- }
246
- this.mstr.update(start0 - 1, end0 + 2, repl)
247
- // @ts-ignore
248
- this.mstr.update(node.end - 1, node.end, ')')
249
- txts.push(nTxt)
250
- return txts
251
- }
252
-
253
-
254
- /**
255
- * @param {import("svelte/compiler").AST.ExpressionTag} node
256
- * @returns {NestText[]}
257
- */
258
- visitExpressionTag = node => this.visit(node.expression)
259
-
260
- /**
261
- * @param {import("svelte/compiler").AST.ElementLike} node
262
- * @returns {boolean}
263
- */
264
- checkHasCompoundText = node => {
265
- let text = false
266
- let nonText = false
267
- for (const child of node.fragment.nodes ?? []) {
268
- if (child.type === 'Text') {
269
- if (child.data.trim()) {
270
- text = true
271
- }
272
- } else if (child.type !== 'Comment') {
273
- nonText = true
274
- }
275
- }
276
- return text && nonText // mixed content
277
- }
278
-
279
- /**
280
- * @typedef {import("svelte/compiler").AST.ElementLike & {inCompoundText: boolean}} ElementNode
281
- * @param {ElementNode & {
282
- * fragment: import("svelte/compiler").AST.Fragment & {
283
- * nodes: ElementNode[]
284
- * },
285
- * }} node
286
- * @returns {NestText[]}
287
- */
288
- visitRegularElement = node => {
289
- const txts = []
290
- for (const attrib of node.attributes) {
291
- txts.push(...this.visit(attrib))
292
- }
293
- if (node.fragment.nodes.length === 0) {
294
- return txts
295
- }
296
- let hasTextChild = false
297
- let hasNonTextChild = false
298
- const textNodesToModify = {}
299
- for (const [i, child] of node.fragment.nodes.entries()) {
300
- if (child.type === 'Text') {
301
- const [pass, modify] = this.checkHeuristic(child.data, 'markup')
302
- if (pass) {
303
- hasTextChild = true
304
- textNodesToModify[i] = modify
305
- } else if (i === 0 && modify != null) { // non whitespace
306
- return txts // explicitly to ignore
307
- }
308
- } else if (child.type !== 'Comment') {
309
- hasNonTextChild = true
310
- }
311
- // no break because of textNodesToModify, already started, finish it
312
- }
313
- let hasCompoundText = hasTextChild && hasNonTextChild
314
- let txt = ''
315
- let iArg = 0
316
- let iTag = 0
317
- const lastChildEnd = node.fragment.nodes.slice(-1)[0].end
318
- for (const [i, child] of node.fragment.nodes.entries()) {
319
- if (child.type === 'Comment') {
320
- continue
321
- }
322
- if (child.type === 'Text') {
323
- const modify = textNodesToModify[i]
324
- if (modify == null) { // whitespace
325
- continue
326
- }
327
- txt += ' ' + modify
328
- if (node.inCompoundText && node.fragment.nodes.length === 1) {
329
- this.mstr.update(child.start, child.end, `{ctx[1]}`)
330
- } else {
331
- this.mstr.remove(child.start, child.end)
332
- }
333
- continue
334
- }
335
- if (!node.inCompoundText && !hasCompoundText) {
336
- txts.push(...this.visit(child))
337
- continue
338
- }
339
- if (child.type === 'ExpressionTag') {
340
- txts.push(...this.visitExpressionTag(child))
341
- txt += ` {${iArg}}`
342
- this.mstr.move(child.start + 1, child.end - 1, lastChildEnd)
343
- if (iArg > 0) {
344
- this.mstr.update(child.start, child.start + 1, ', ')
345
- } else {
346
- this.mstr.remove(child.start, child.start + 1)
347
- }
348
- this.mstr.remove(child.end - 1, child.end)
349
- iArg++
350
- continue
351
- }
352
- // elements and components
353
- // @ts-ignore
354
- child.inCompoundText = true
355
- let chTxt = ''
356
- for (const txt of this.visit(child)) {
357
- if (['RegularElement', 'Component'].includes(child.type) && txt.scope === 'markup') {
358
- chTxt += txt.toString()
359
- } else { // attributes, blocks
360
- txts.push(txt)
361
- }
362
- }
363
- if (['RegularElement', 'Component'].includes(child.type)) {
364
- chTxt = `<${iTag}>${chTxt}</${iTag}>`
365
- } else {
366
- // InlineComponent
367
- chTxt = `<${iTag}/>`
368
- }
369
- const snippetName = `${snipPrefix}${iTag}`
370
- const snippetBegin = `\n{#snippet ${snippetName}(ctx)}\n`
371
- const snippetEnd = '\n{/snippet}'
372
- this.mstr.appendRight(child.start, snippetBegin)
373
- this.mstr.prependLeft(child.end, snippetEnd)
374
- iTag++
375
- if (!txt.endsWith(' ')) {
376
- txt += ' '
377
- }
378
- txt += chTxt
379
- }
380
- txt = txt.trim()
381
- if (!txt) {
382
- return txts
383
- }
384
- const nTxt = new NestText(txt, 'markup', this.context)
385
- txts.push(nTxt)
386
- if (iTag > 0) {
387
- const snippets = []
388
- // reference all new snippets added
389
- for (let i = 0; i < iTag; i++) {
390
- snippets.push(`${snipPrefix}${i}`)
391
- }
392
- let begin = `\n<${rtComponent} tags={[${snippets.join(', ')}]} `
393
- if (node.inCompoundText) {
394
- begin += `ctx={ctx}`
395
- } else {
396
- begin += `id={${this.index.get(nTxt.toKey())}}`
397
- }
398
- let end = ' />\n'
399
- if (iArg > 0) {
400
- begin += ' args={['
401
- end = ']}' + end
402
- }
403
- this.mstr.appendLeft(lastChildEnd, begin)
404
- this.mstr.appendRight(lastChildEnd, end)
405
- } else if (!node.inCompoundText) {
406
- this.mstr.appendLeft(lastChildEnd, `{${rtFunc}(${this.index.get(nTxt.toKey())}, `)
407
- this.mstr.appendRight(lastChildEnd, ')}')
408
- }
409
- return txts
410
- }
411
-
412
- visitComponent = this.visitRegularElement
413
-
414
- /**
415
- * @param {import("svelte/compiler").AST.Text} node
416
- * @returns {NestText[]}
417
- */
418
- visitText = node => {
419
- const [pass, txt] = this.checkHeuristic(node.data, 'markup')
420
- if (!pass) {
421
- return []
422
- }
423
- txt.context = this.context
424
- this.mstr.update(node.start, node.end, `{${rtFunc}(${this.index.get(txt.toKey())})}`)
425
- return [txt]
426
- }
427
-
428
- /**
429
- * @param {import("svelte/compiler").AST.SpreadAttribute} node
430
- * @returns {NestText[]}
431
- */
432
- visitSpreadAttribute = node => {
433
- return this.visit(node.expression)
434
- }
435
-
436
- /**
437
- * @param {import("svelte/compiler").AST.Attribute} node
438
- * @returns {NestText[]}
439
- */
440
- visitAttribute = node => {
441
- if (node.value === true) {
442
- return []
443
- }
444
- const txts = []
445
- let values
446
- if (Array.isArray(node.value)) {
447
- values = node.value
448
- } else {
449
- values = [node.value]
450
- }
451
- for (const value of values) {
452
- if (value.type !== 'Text') { // ExpressionTag
453
- txts.push(...this.visit(value))
454
- continue
455
- }
456
- // Text
457
- const {start, end} = value
458
- const [pass, txt] = this.checkHeuristic(value.data, 'attribute')
459
- if (!pass) {
460
- continue
461
- }
462
- txt.context = this.context
463
- txts.push(txt)
464
- this.mstr.update(value.start, value.end, `{${rtFunc}(${this.index.get(txt.toKey())})}`)
465
- if (!`'"`.includes(this.content[start - 1])) {
466
- continue
467
- }
468
- this.mstr.remove(start - 1, start)
469
- this.mstr.remove(end, end + 1)
470
- }
471
- return txts
472
- }
473
-
474
- /**
475
- * @param {import("svelte/compiler").AST.Fragment} node
476
- * @returns {NestText[]}
477
- */
478
- visitFragment = node => {
479
- const txts = []
480
- for (const child of node.nodes) {
481
- txts.push(...this.visit(child))
482
- }
483
- return txts
484
- }
485
-
486
- /**
487
- * @param {import("svelte/compiler").AST.SnippetBlock} node
488
- * @returns {NestText[]}
489
- */
490
- visitSnippetBlock = node => this.visitFragment(node.body)
491
-
492
- /**
493
- * @param {import("svelte/compiler").AST.IfBlock} node
494
- * @returns {NestText[]}
495
- */
496
- visitIfBlock = node => {
497
- const txts = this.visit(node.test)
498
- txts.push(...this.visit(node.consequent))
499
- if (node.alternate) {
500
- txts.push(...this.visit(node.alternate))
501
- }
502
- return txts
503
- }
504
-
505
- /**
506
- * @param {import("svelte/compiler").AST.EachBlock} node
507
- * @returns {NestText[]}
508
- */
509
- visitEachBlock = node => {
510
- const txts = [
511
- ...this.visit(node.expression),
512
- ...this.visit(node.body),
513
- ]
514
- if (node.fallback) {
515
- txts.push(...this.visit(node.fallback),)
516
- }
517
- if (node.key) {
518
- txts.push(...this.visit(node.key),)
519
- }
520
- return txts
521
- }
522
-
523
- /**
524
- * @param {import("svelte/compiler").AST.KeyBlock} node
525
- * @returns {NestText[]}
526
- */
527
- visitKeyBlock = node => {
528
- return [
529
- ...this.visit(node.expression),
530
- ...this.visit(node.fragment),
531
- ]
532
- }
533
-
534
- /**
535
- * @param {import("svelte/compiler").AST.AwaitBlock} node
536
- * @returns {NestText[]}
537
- */
538
- visitAwaitBlock = node => {
539
- const txts = [
540
- ...this.visit(node.expression),
541
- ...this.visitFragment(node.then),
542
- ]
543
- if (node.pending) {
544
- txts.push(...this.visitFragment(node.pending),)
545
- }
546
- if (node.catch) {
547
- txts.push(...this.visitFragment(node.catch),)
548
- }
549
- return txts
550
- }
551
-
552
- /**
553
- * @param {import('estree').Program} node
554
- * @returns {NestText[]}
555
- */
556
- visitProgram = (node, needImport = true) => {
557
- const txts = []
558
- for (const child of node.body) {
559
- txts.push(...this.visit(child))
560
- }
561
- if (needImport) {
562
- const importStmt = `import {${rtFunc}} from "${this.importFrom}"\n`
563
- // @ts-ignore
564
- this.mstr.appendRight(node.start, importStmt)
565
- }
566
- return txts
567
- }
568
-
569
- /**
570
- * @param {import("svelte/compiler").AST.Root} node
571
- * @returns {NestText[]}
572
- */
573
- visitRoot = node => {
574
- const txts = this.visitFragment(node.fragment)
575
- if (node.instance) {
576
- txts.push(...this.visitProgram(node.instance.content, false))
577
- }
578
- // @ts-ignore: module is a reserved keyword, not sure how to specify the type
579
- if (node.module) {
580
- // @ts-ignore
581
- txts.push(...this.visitProgram(node.module.content, false))
582
- }
583
- const importStmt = `import ${rtComponent}, {${rtFunc}} from "${this.importFrom}"\n`
584
- if (node.instance) {
585
- // @ts-ignore
586
- this.mstr.appendRight(node.instance.content.start, importStmt)
587
- } else if (node.module) {
588
- // @ts-ignore
589
- this.mstr.appendRight(node.module.content.start, importStmt)
590
- } else {
591
- this.mstr.prepend(`<script>${importStmt}</script>\n`)
592
- }
593
- return txts
594
- }
595
-
596
- /**
597
- * @param {string} data
598
- */
599
- processCommentDirectives = data => {
600
- if (data === '@wc-ignore') {
601
- this.forceInclude = false
602
- }
603
- if (data === '@wc-include') {
604
- this.forceInclude = true
605
- }
606
- const ctxStart = '@wc-context:'
607
- if (data.startsWith(ctxStart)) {
608
- this.context = data.slice(ctxStart.length).trim()
609
- }
610
- }
611
-
612
- /**
613
- * @param {import("svelte/compiler").AST.SvelteNode & import('estree').BaseNode} node
614
- * @returns {NestText[]}
615
- */
616
- visit = node => {
617
- if (node.type === 'Comment') {
618
- this.processCommentDirectives(node.data.trim())
619
- return []
620
- }
621
- // for estree
622
- for (const comment of node.leadingComments ?? []) {
623
- this.processCommentDirectives(comment.value.trim())
624
- }
625
- let txts = []
626
- if (this.forceInclude !== false) {
627
- const methodName = `visit${node.type}`
628
- if (methodName in this) {
629
- txts = this[methodName](node)
630
- }
631
- }
632
- this.forceInclude = null
633
- if (this.context != null) {
634
- for (const txt of txts) {
635
- txt.context = this.context
636
- }
637
- }
638
- this.context = null
639
- return txts
640
- }
641
-
642
- /**
643
- * @param {string} content
644
- * @param {import('estree').Program | import("svelte/compiler").AST.Root} ast
645
- * @returns {NestText[]}
646
- */
647
- process = (content, ast) => {
648
- this.content = content
649
- this.mstr = new MagicString(content)
650
- return this.visit(ast)
651
- }
652
- }