wuchale 0.1.0 → 0.2.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/README.md CHANGED
@@ -35,7 +35,10 @@ minimal and constant-time.
35
35
  ### 🧩 Full Nesting Support
36
36
 
37
37
  Handles deeply nested markup and interpolations — mixed conditionals, loops,
38
- and awaits — by compiling them into nested Svelte snippets.
38
+ and awaits — by compiling them into nested Svelte snippets. That means you can
39
+ go as crazy as
40
+ [this test](https://github.com/K1DV5/wuchale/blob/main/tests/complicated/app.svelte)
41
+ and it will still extract the correct texts.
39
42
 
40
43
  ### 📦 No String Parsing at Runtime
41
44
 
@@ -56,8 +59,9 @@ optional integration with external tools.
56
59
 
57
60
  ### 🚀 Tiny Footprint
58
61
 
59
- Adds just 2 packages (your own) to `node_modules`, and only a few kilobytes to
60
- the bundle. No 90MB dependency trees like some existing solutions.
62
+ Adds just 2 packages (itself and `pofile`) to `node_modules`, as the other
63
+ dependency is Svelte itself. No 200 packages and 90MB dependency trees like
64
+ some existing solutions.
61
65
 
62
66
  ### ✨ Ready for Svelte 5
63
67
 
@@ -79,11 +83,39 @@ Add to your Vite config:
79
83
  import { svelte } from '@sveltejs/vite-plugin-svelte'
80
84
  import { wuchale } from 'wuchale'
81
85
 
82
- export default { plugins: [ wuchale(), svelte(), ] }
86
+ export default {
87
+ plugins: [
88
+ wuchale(),
89
+ svelte(),
90
+ ]
91
+ // ...your other config
92
+ }
83
93
 
84
94
  ```
85
95
 
86
- Use in your Svelte files:
96
+ Create `/locales/` if it doesn't exist, and then set it up in your main
97
+ component. Assuming `/src/App.svelte`:
98
+
99
+ ```svelte
100
+ <script>
101
+ import {setTranslations} from 'wuchale/runtime.svelte'
102
+
103
+ let locale = $state('en')
104
+
105
+ $effect.pre(() => {
106
+ import(`../locales/${locale}.json`).then(mod => {
107
+ setTranslations(mod.default)
108
+ })
109
+ })
110
+ </script>
111
+ ```
112
+
113
+ Note that you manage the state of which locale is active and how to download
114
+ the compiled `.json`. This is to allow maximum flexibility, meaning you can use
115
+ lazy loading (like this example) or you can import it directly and it will be
116
+ bundled by Vite. After that, you notify `wuchale` to set it as the current one.
117
+
118
+ Then finally you write your Svelte files naturally:
87
119
 
88
120
  ```svelte
89
121
 
@@ -98,6 +130,8 @@ import WuchaleTrans, { wuchaleTrans } from 'wuchale/runtime.svelte'
98
130
  <h1>{wuchaleTrans(0)}</h1> <!-- Extracted "Hello" as index 0 -->
99
131
  ```
100
132
 
133
+ Full example below.
134
+
101
135
  ## 📦 How It Works
102
136
 
103
137
  ### Process
@@ -135,6 +169,8 @@ Input:
135
169
 
136
170
  ```svelte
137
171
 
172
+ <p>Hi there!</p>
173
+
138
174
  <p>Hello <b>{userName}</b></p>
139
175
 
140
176
  ```
@@ -142,19 +178,62 @@ Input:
142
178
  Output:
143
179
 
144
180
  ```svelte
181
+ <script>
182
+ import WuchaleTrans, { wuchaleTrans } from 'wuchale/runtime.svelte'
183
+ </script>
145
184
 
146
- <p>{wuchaleTrans(0, <b>{userName}</b>)}</p>
185
+ <p>{wuchaleTrans(0)}</p>
186
+
187
+ <p>
188
+ {#snippet wuchaleSnippet0(ctx)}
189
+ <b>{ctx[1]}</b>
190
+ {/snippet}
191
+ <WuchaleTrans tags={[wuchaleSnippet0]} id={1} args={[userName]} />
192
+ </p>
147
193
 
148
194
  ```
149
195
 
150
- Catalog (PO):
196
+ Extracted catalog (PO) for `en`:
151
197
 
152
198
  ```nginx
153
199
 
154
- msgid "Hello {0}" msgstr "Bonjour {0}"
200
+ msgid "Hi there!"
201
+ msgstr "Hi there!"
202
+
203
+ msgid "Hello {0}"
204
+ msgstr "Hello {0}"
155
205
 
156
206
  ```
157
207
 
208
+ Extracted catalog (PO) for `es`, initially empty `msgstr`, but after a translator or Gemini translates it:
209
+
210
+ ```nginx
211
+
212
+ msgid "Hi there!"
213
+ msgstr "¡Hola!"
214
+
215
+ msgid "Hello {0}"
216
+ msgstr "Hola {0}"
217
+
218
+ ```
219
+
220
+ Which is then automatically compiled to:
221
+
222
+ ```json
223
+ [
224
+ "¡Hola!",
225
+ [
226
+ "Hola ",
227
+ [
228
+ 0,
229
+ 0
230
+ ]
231
+ ]
232
+ ]
233
+ ```
234
+
235
+ This is what you import when you set it up above in `App.svelte`.
236
+
158
237
  ## Supported syntax
159
238
 
160
239
  Text can be in three places: markup, script and attributes. Script means not
@@ -196,6 +275,35 @@ necessary to have a separate plurals support because you can do something like:
196
275
  And they will be extracted separately. You can also make a reusable function
197
276
  yourself.
198
277
 
278
+ ## Configuration
279
+
280
+ To configure `wuchale`, you pass an object that looks like the following (the
281
+ default) to `wuchale()` in your `vite.config.js` `vite.config.ts`:
282
+
283
+ ```javascript
284
+ export const defaultOptions = {
285
+ sourceLocale: 'en',
286
+ otherLocales: ['am'],
287
+ localesDir: './locales',
288
+ importFrom: 'wuchale/runtime.svelte',
289
+ heuristic: defaultHeuristic,
290
+ geminiAPIKey: 'env',
291
+ }
292
+ ```
293
+
294
+ While the others are self explanatory, the `heuristic` is a function that
295
+ decides what text to extract and what not to. The `defaultHeuristic` is the
296
+ implementation of the above rules, but you can roll your own and provide it
297
+ here. The function should receive the following arguments:
298
+
299
+ - `text`: The candidate text
300
+ - `scope`: Where the text is located, i.e. it can be one of `markup`, `script`, and `attribute`
301
+
302
+ And it should return an object with two properties:
303
+
304
+ - `extract` (boolean): Whether to extract it or not
305
+ - `replace` (`string`): The string to replace it with. This is how you specify how to remove parts such as the prefixes above (`+` and `-`).
306
+
199
307
  ## 🧹 Cleaning
200
308
 
201
309
  Unused keys are marked as obsolete during a production build. Obsoletes are
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuchale",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "i18n for svelte without turning your codebase upside down",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -1,4 +1,4 @@
1
- import Preprocess, { IndexTracker } from "./prep.js"
1
+ import Preprocess, { defaultHeuristic, IndexTracker } from "./prep.js"
2
2
  import { parse } from "svelte/compiler"
3
3
  import {writeFile} from 'node:fs/promises'
4
4
  import compileTranslation from "./compile.js"
@@ -6,30 +6,6 @@ import setupGemini from "./gemini.js"
6
6
  import PO from "pofile"
7
7
  import { relative } from "node:path"
8
8
 
9
- /**
10
- * @param {string} text
11
- * @param {string} scope
12
- * @returns {{extract: boolean, replace: string}}
13
- */
14
- export function defaultHeuristic(text, scope = 'markup') {
15
- if (scope === 'markup') {
16
- if (text.startsWith('-')) {
17
- return {extract: false, replace: text.slice(1).trim()}
18
- }
19
- return {extract: true, replace: text}
20
- }
21
- // script and attribute
22
- if (text.startsWith('+')) {
23
- return {extract: true, replace: text.slice(1).trim()}
24
- }
25
- const range = 'AZ'
26
- const startCode = text.charCodeAt(0)
27
- if (startCode >= range.charCodeAt(0) && startCode <= range.charCodeAt(1)) {
28
- return {extract: true, replace: text}
29
- }
30
- return {extract: false, replace: text}
31
- }
32
-
33
9
  export const defaultOptions = {
34
10
  sourceLocale: 'en',
35
11
  otherLocales: ['am'],
@@ -45,24 +21,22 @@ export const defaultOptions = {
45
21
  async function loadPONoFail(filename) {
46
22
  return new Promise((res) => {
47
23
  PO.load(filename, (err, po) => {
48
- if (err) {
49
- res({})
50
- return
51
- }
52
24
  const translations = {}
53
25
  let total = 0
54
26
  let obsolete = 0
55
27
  let untranslated = 0
56
- for (const item of po.items) {
57
- total++
58
- if (item.obsolete) {
59
- obsolete++
60
- continue
61
- }
62
- if (!item.msgstr[0]) {
63
- untranslated++
28
+ if (!err) {
29
+ for (const item of po.items) {
30
+ total++
31
+ if (item.obsolete) {
32
+ obsolete++
33
+ continue
34
+ }
35
+ if (!item.msgstr[0]) {
36
+ untranslated++
37
+ }
38
+ translations[item.msgid] = item
64
39
  }
65
- translations[item.msgid] = item
66
40
  }
67
41
  res({translations, total, obsolete, untranslated})
68
42
  })
@@ -142,7 +116,7 @@ export default async function wuchale(options = defaultOptions) {
142
116
 
143
117
  /**
144
118
  * @param {string} content
145
- * @param {import("./prep.js").Node} ast
119
+ * @param {import('estree').Program | import("svelte/compiler").AST.Root} ast
146
120
  * @param {string} filename
147
121
  */
148
122
  async function preprocess(content, ast, filename) {
@@ -219,6 +193,9 @@ export default async function wuchale(options = defaultOptions) {
219
193
  * @param {string} id
220
194
  */
221
195
  handler: async function(code, id) {
196
+ if (!id.startsWith(projectRoot)) {
197
+ return
198
+ }
222
199
  const isModule = id.endsWith('.svelte.js')
223
200
  if (!id.endsWith('.svelte') && !isModule) {
224
201
  return
@@ -227,8 +204,7 @@ export default async function wuchale(options = defaultOptions) {
227
204
  if (isModule) {
228
205
  ast = this.parse(code)
229
206
  } else {
230
- ast = parse(code)
231
- ast.type = 'SvelteComponent'
207
+ ast = parse(code, {modern: true})
232
208
  }
233
209
  const filename = relative(projectRoot, id)
234
210
  const processed = await preprocess(code, ast, filename)
@@ -7,31 +7,36 @@ const rtComponent = 'WuchaleTrans'
7
7
  const rtFunc = 'wuchaleTrans'
8
8
 
9
9
  /**
10
- * @typedef {object} Node
11
- * @property {string} type
12
- * @property {number} start
13
- * @property {number} end
10
+ * @typedef {"script" | "markup" | "attribute"} TxtScope
11
+ * @typedef {(text: string, scope: TxtScope) => {extract: boolean; replace: string}} HeuristicFunc
14
12
  */
15
13
 
16
14
  /**
17
- * @typedef {Node & {expression: Node, value: boolean | (Node & {data: string})[]}} Attribute
18
- * @typedef {Node & {value: string}} NodeWithVal
19
- * @typedef {Node & {data: string}} NodeWithData
20
- * @typedef {Node & {
21
- * attributes: Attribute[],
22
- * children: (Element & NodeWithData & { expression: Node })[]
23
- * inCompoundText: boolean | null,
24
- * }} Element
25
- * @typedef {(text: string, scope?: string) => {extract: boolean; replace: string}} HeuristicFunc
26
- * @typedef {Node & {
27
- * properties: Node & { key: Node, value: Node }[]
28
- * }} ObjExpr
15
+ * @type {HeuristicFunc}
29
16
  */
17
+ export function defaultHeuristic(text, scope = 'markup') {
18
+ if (scope === 'markup') {
19
+ if (text.startsWith('-')) {
20
+ return {extract: false, replace: text.slice(1).trim()}
21
+ }
22
+ return {extract: true, replace: text}
23
+ }
24
+ // script and attribute
25
+ if (text.startsWith('+')) {
26
+ return {extract: true, replace: text.slice(1).trim()}
27
+ }
28
+ const range = 'AZ'
29
+ const startCode = text.charCodeAt(0)
30
+ if (startCode >= range.charCodeAt(0) && startCode <= range.charCodeAt(1)) {
31
+ return {extract: true, replace: text}
32
+ }
33
+ return {extract: false, replace: text}
34
+ }
30
35
 
31
36
  class NestText extends String {
32
37
  /**
33
38
  * @param {string} txt
34
- * @param {string} scope
39
+ * @param {TxtScope} scope
35
40
  */
36
41
  constructor(txt, scope) {
37
42
  super(txt)
@@ -80,12 +85,13 @@ export default class Preprocess {
80
85
  }
81
86
 
82
87
  /**
83
- * @param {{ start: number; end: number; }} node
88
+ * @param {number} start
89
+ * @param {number} end
84
90
  * @param {string} text
85
- * @param {string} scope
91
+ * @param {TxtScope} scope
86
92
  * @returns {Array<*> & {0: boolean, 1: NestText}}
87
93
  */
88
- modifyCheck = (node, text, scope) => {
94
+ modifyCheck = (start, end, text, scope) => {
89
95
  text = text.replace(/\s+/g, ' ').trim()
90
96
  if (text === '') {
91
97
  // nothing to ask
@@ -94,7 +100,7 @@ export default class Preprocess {
94
100
  let {extract, replace} = this.heuristic(text, scope)
95
101
  replace = replace.trim()
96
102
  if (!extract && text !== replace) {
97
- this.mstr.update(node.start, node.end, replace)
103
+ this.mstr.update(start, end, replace)
98
104
  }
99
105
  return [extract, new NestText(replace, scope)]
100
106
  }
@@ -104,23 +110,24 @@ export default class Preprocess {
104
110
  // visitImportDeclaration = () => []
105
111
 
106
112
  /**
107
- * @param {NodeWithVal} node
113
+ * @param {import('estree').Literal & {start: number, end: number}} node
108
114
  * @returns {NestText[]}
109
115
  */
110
116
  visitLiteral = node => {
111
117
  if (typeof node.value !== 'string') {
112
118
  return []
113
119
  }
114
- const [pass, txt] = this.modifyCheck(node, node.value, 'script')
120
+ const { start, end } = node
121
+ const [pass, txt] = this.modifyCheck(start, end, node.value, 'script')
115
122
  if (!pass) {
116
123
  return []
117
124
  }
118
- this.mstr.update(node.start, node.end, `${rtFunc}(${this.index.get(txt.toString())})`)
125
+ this.mstr.update(start, end, `${rtFunc}(${this.index.get(txt.toString())})`)
119
126
  return [txt]
120
127
  }
121
128
 
122
129
  /**
123
- * @param {Node & { elements: Node[] }} node
130
+ * @param {import('estree').ArrayExpression} node
124
131
  * @returns {NestText[]}
125
132
  */
126
133
  visitArrayExpression = node => {
@@ -132,20 +139,22 @@ export default class Preprocess {
132
139
  }
133
140
 
134
141
  /**
135
- * @param {ObjExpr} node
142
+ * @param {import('estree').ObjectExpression} node
136
143
  * @returns {NestText[]}
137
144
  */
138
145
  visitObjectExpression = node => {
139
146
  const txts = []
140
147
  for (const prop of node.properties) {
148
+ // @ts-ignore
141
149
  txts.push(...this.visit(prop.key))
150
+ // @ts-ignore
142
151
  txts.push(...this.visit(prop.value))
143
152
  }
144
153
  return txts
145
154
  }
146
155
 
147
156
  /**
148
- * @param {Node & { object: Node, property: Node }} node
157
+ * @param {import('estree').MemberExpression} node
149
158
  * @returns {NestText[]}
150
159
  */
151
160
  visitMemberExpression = node => {
@@ -156,7 +165,7 @@ export default class Preprocess {
156
165
  }
157
166
 
158
167
  /**
159
- * @param {Node & { callee: Node, arguments: Node[] }} node
168
+ * @param {import('estree').CallExpression} node
160
169
  * @returns {NestText[]}
161
170
  */
162
171
  visitCallExpression = node => {
@@ -168,13 +177,7 @@ export default class Preprocess {
168
177
  }
169
178
 
170
179
  /**
171
- * @param {Node & {
172
- * declarations: Node & {
173
- * init: Node & {
174
- * callee: Node & { name: string },
175
- * },
176
- * }[]
177
- * }} node
180
+ * @param {import('estree').VariableDeclaration} node
178
181
  * @returns {NestText[]}
179
182
  */
180
183
  visitVariableDeclaration = node => {
@@ -197,24 +200,21 @@ export default class Preprocess {
197
200
  }
198
201
 
199
202
  /**
200
- * @param {Node & { declaration: Node }} node
203
+ * @param {import('estree').ExportDefaultDeclaration} node
201
204
  * @returns {NestText[]}
202
205
  */
203
206
  visitExportDefaultDeclaration = node => this.visit(node.declaration)
204
207
 
205
208
  /**
206
- * @param {Node & {
207
- * quasis: (Node & {
208
- * value: {cooked: string}
209
- * })[],
210
- * expressions: Node[]
211
- * }} node
209
+ * @param {import('estree').TemplateLiteral} node
212
210
  * @returns {NestText[]}
213
211
  */
214
212
  visitTemplateLiteral = node => {
215
213
  const txts = []
216
214
  const quasi0 = node.quasis[0]
217
- const [pass, txt] = this.modifyCheck(quasi0, quasi0.value.cooked, 'script')
215
+ // @ts-ignore
216
+ const {start: start0, end: end0} = quasi0
217
+ const [pass, txt] = this.modifyCheck(start0, end0, quasi0.value.cooked, 'script')
218
218
  if (!pass) {
219
219
  return txts
220
220
  }
@@ -223,17 +223,20 @@ export default class Preprocess {
223
223
  txts.push(...this.visit(expr))
224
224
  const quasi = node.quasis[i + 1]
225
225
  nTxt += `{${i}}${quasi.value.cooked}`
226
- this.mstr.remove(quasi.start - 1, quasi.end)
226
+ // @ts-ignore
227
+ const {start, end} = quasi
228
+ this.mstr.remove(start - 1, end)
227
229
  if (i + 1 === node.expressions.length) {
228
230
  continue
229
231
  }
230
- this.mstr.update(quasi.end, quasi.end + 2, ', ')
232
+ this.mstr.update(end, end + 2, ', ')
231
233
  }
232
234
  let repl = `${rtFunc}(${this.index.get(txt.toString())}`
233
235
  if (node.expressions.length) {
234
236
  repl += ', '
235
237
  }
236
- this.mstr.update(quasi0.start - 1, quasi0.end + 2, repl)
238
+ this.mstr.update(start0 - 1, end0 + 2, repl)
239
+ // @ts-ignore
237
240
  this.mstr.update(node.end - 1, node.end, ')')
238
241
  txts.push(new NestText(nTxt, 'script'))
239
242
  return txts
@@ -241,19 +244,19 @@ export default class Preprocess {
241
244
 
242
245
 
243
246
  /**
244
- * @param {Node & {expression: Node}} node
247
+ * @param {import("svelte/compiler").AST.ExpressionTag} node
245
248
  * @returns {NestText[]}
246
249
  */
247
- visitMustacheTag = node => this.visit(node.expression)
250
+ visitExpressionTag = node => this.visit(node.expression)
248
251
 
249
252
  /**
250
- * @param {Node & {children: NodeWithData[]}} node
253
+ * @param {import("svelte/compiler").AST.ElementLike} node
251
254
  * @returns {boolean}
252
255
  */
253
256
  checkHasCompoundText = node => {
254
257
  let text = false
255
258
  let nonText = false
256
- for (const child of node.children ?? []) {
259
+ for (const child of node.fragment.nodes ?? []) {
257
260
  if (child.type === 'Text') {
258
261
  if (child.data.trim()) {
259
262
  text = true
@@ -266,23 +269,29 @@ export default class Preprocess {
266
269
  }
267
270
 
268
271
  /**
269
- * @param {Element} node
272
+ * @typedef {import("svelte/compiler").AST.ElementLike & {inCompoundText: boolean}} ElementNode
273
+ * @param {ElementNode & {
274
+ * fragment: import("svelte/compiler").AST.Fragment & {
275
+ * nodes: ElementNode[]
276
+ * },
277
+ * }} node
270
278
  * @returns {NestText[]}
271
279
  */
272
- visitElement = node => {
280
+ visitRegularElement = node => {
273
281
  const txts = []
274
282
  for (const attrib of node.attributes) {
275
- txts.push(...this.visitAttribute(attrib))
283
+ txts.push(...this.visit(attrib))
276
284
  }
277
- if (node.children.length === 0) {
285
+ if (node.fragment.nodes.length === 0) {
278
286
  return txts
279
287
  }
280
288
  let hasTextChild = false
281
289
  let hasNonTextChild = false
282
290
  const textNodesToModify = {}
283
- for (const [i, child] of node.children.entries()) {
291
+ for (const [i, child] of node.fragment.nodes.entries()) {
284
292
  if (child.type === 'Text') {
285
- const [pass, modify] = this.modifyCheck(child, child.data, 'markup')
293
+ const { start, end } = child
294
+ const [pass, modify] = this.modifyCheck(start, end, child.data, 'markup')
286
295
  if (pass) {
287
296
  hasTextChild = true
288
297
  textNodesToModify[i] = modify
@@ -298,8 +307,8 @@ export default class Preprocess {
298
307
  let txt = ''
299
308
  let iArg = 0
300
309
  let iTag = 0
301
- const lastChildEnd = node.children.slice(-1)[0].end
302
- for (const [i, child] of node.children.entries()) {
310
+ const lastChildEnd = node.fragment.nodes.slice(-1)[0].end
311
+ for (const [i, child] of node.fragment.nodes.entries()) {
303
312
  if (child.type === 'Comment') {
304
313
  continue
305
314
  }
@@ -309,7 +318,7 @@ export default class Preprocess {
309
318
  continue
310
319
  }
311
320
  txt += ' ' + modify
312
- if (node.inCompoundText && node.children.length === 1) {
321
+ if (node.inCompoundText && node.fragment.nodes.length === 1) {
313
322
  this.mstr.update(child.start, child.end, `{ctx[1]}`)
314
323
  } else {
315
324
  this.mstr.remove(child.start, child.end)
@@ -320,8 +329,8 @@ export default class Preprocess {
320
329
  txts.push(...this.visit(child))
321
330
  continue
322
331
  }
323
- if (child.type === 'MustacheTag') {
324
- txts.push(...this.visitMustacheTag(child))
332
+ if (child.type === 'ExpressionTag') {
333
+ txts.push(...this.visitExpressionTag(child))
325
334
  txt += ` {${iArg}}`
326
335
  this.mstr.move(child.start + 1, child.end - 1, lastChildEnd)
327
336
  if (iArg > 0) {
@@ -334,16 +343,17 @@ export default class Preprocess {
334
343
  continue
335
344
  }
336
345
  // elements and components
346
+ // @ts-ignore
337
347
  child.inCompoundText = true
338
348
  let chTxt = ''
339
349
  for (const txt of this.visit(child)) {
340
- if (child.type === 'Element' && txt.scope === 'markup') {
350
+ if (['RegularElement', 'Component'].includes(child.type) && txt.scope === 'markup') {
341
351
  chTxt += txt.toString()
342
352
  } else { // attributes, blocks
343
353
  txts.push(txt)
344
354
  }
345
355
  }
346
- if (child.type === 'Element') {
356
+ if (['RegularElement', 'Component'].includes(child.type)) {
347
357
  chTxt = `<${iTag}>${chTxt}</${iTag}>`
348
358
  } else {
349
359
  // InlineComponent
@@ -392,14 +402,15 @@ export default class Preprocess {
392
402
  return txts
393
403
  }
394
404
 
395
- visitInlineComponent = this.visitElement
405
+ visitComponent = this.visitRegularElement
396
406
 
397
407
  /**
398
- * @param {NodeWithData} node
408
+ * @param {import("svelte/compiler").AST.Text} node
399
409
  * @returns {NestText[]}
400
410
  */
401
411
  visitText = node => {
402
- const [pass, txt] = this.modifyCheck(node, node.data, 'markup')
412
+ const { start, end } = node
413
+ const [pass, txt] = this.modifyCheck(start, end, node.data, 'markup')
403
414
  if (!pass) {
404
415
  return []
405
416
  }
@@ -408,29 +419,41 @@ export default class Preprocess {
408
419
  }
409
420
 
410
421
  /**
411
- * @param {Attribute} node
422
+ * @param {import("svelte/compiler").AST.SpreadAttribute} node
423
+ * @returns {NestText[]}
424
+ */
425
+ visitSpreadAttribute = node => {
426
+ return this.visit(node.expression)
427
+ }
428
+
429
+ /**
430
+ * @param {import("svelte/compiler").AST.Attribute} node
412
431
  * @returns {NestText[]}
413
432
  */
414
433
  visitAttribute = node => {
415
- if (node.type === 'Spread') {
416
- return this.visit(node.expression)
417
- }
418
- if (node.type !== 'Attribute' || typeof node.value === 'boolean') {
434
+ if (node.value === true) {
419
435
  return []
420
436
  }
421
437
  const txts = []
422
- for (const value of node.value) {
423
- if (value.type !== 'Text') {
438
+ let values
439
+ if (Array.isArray(node.value)) {
440
+ values = node.value
441
+ } else {
442
+ values = [node.value]
443
+ }
444
+ for (const value of values) {
445
+ if (value.type !== 'Text') { // ExpressionTag
424
446
  txts.push(...this.visit(value))
425
447
  continue
426
448
  }
427
- const [pass, txt] = this.modifyCheck(value, value.data, 'attribute')
449
+ // Text
450
+ const {start, end} = value
451
+ const [pass, txt] = this.modifyCheck(start, end, value.data, 'attribute')
428
452
  if (!pass) {
429
453
  continue
430
454
  }
431
455
  txts.push(txt)
432
456
  this.mstr.update(value.start, value.end, `{${rtFunc}(${this.index.get(txt.toString())})}`)
433
- let {start, end} = value
434
457
  if (!`'"`.includes(this.content[start - 1])) {
435
458
  continue
436
459
  }
@@ -441,60 +464,85 @@ export default class Preprocess {
441
464
  }
442
465
 
443
466
  /**
444
- * @param {Node & {children: Node[]}} node
467
+ * @param {import("svelte/compiler").AST.Fragment} node
445
468
  * @returns {NestText[]}
446
469
  */
447
470
  visitFragment = node => {
448
471
  const txts = []
449
- for (const child of node.children) {
472
+ for (const child of node.nodes) {
450
473
  txts.push(...this.visit(child))
451
474
  }
452
475
  return txts
453
476
  }
454
477
 
455
- visitSnippetBlock = this.visitFragment
478
+ /**
479
+ * @param {import("svelte/compiler").AST.SnippetBlock} node
480
+ * @returns {NestText[]}
481
+ */
482
+ visitSnippetBlock = node => this.visitFragment(node.body)
456
483
 
457
484
  /**
458
- * @param {Node & {children: Node[], expression: Node}} node
485
+ * @param {import("svelte/compiler").AST.IfBlock} node
459
486
  * @returns {NestText[]}
460
487
  */
461
488
  visitIfBlock = node => {
462
- const txts = this.visit(node.expression)
463
- for (const child of node.children) {
464
- txts.push(...this.visit(child))
489
+ const txts = this.visit(node.test)
490
+ txts.push(...this.visit(node.consequent))
491
+ if (node.alternate) {
492
+ txts.push(...this.visit(node.alternate))
465
493
  }
466
494
  return txts
467
495
  }
468
496
 
469
- visitEachBlock = this.visitIfBlock
497
+ /**
498
+ * @param {import("svelte/compiler").AST.EachBlock} node
499
+ * @returns {NestText[]}
500
+ */
501
+ visitEachBlock = node => {
502
+ const txts = [
503
+ ...this.visit(node.expression),
504
+ ...this.visit(node.body),
505
+ ]
506
+ if (node.fallback) {
507
+ txts.push(...this.visit(node.fallback),)
508
+ }
509
+ if (node.key) {
510
+ txts.push(...this.visit(node.key),)
511
+ }
512
+ return txts
513
+ }
470
514
 
471
- visitKeyBlock = this.visitFragment
515
+ /**
516
+ * @param {import("svelte/compiler").AST.KeyBlock} node
517
+ * @returns {NestText[]}
518
+ */
519
+ visitKeyBlock = node => {
520
+ return [
521
+ ...this.visit(node.expression),
522
+ ...this.visit(node.fragment),
523
+ ]
524
+ }
472
525
 
473
526
  /**
474
- * @typedef {Node & {children: Node[]}} Block
475
- * @param {Node & {
476
- * expression: Node
477
- * value: ObjExpr
478
- * pending: Block
479
- * then: Block
480
- * catch: Block
481
- * }} node
527
+ * @param {import("svelte/compiler").AST.AwaitBlock} node
482
528
  * @returns {NestText[]}
483
529
  */
484
530
  visitAwaitBlock = node => {
485
- const txts = this.visit(node.expression)
486
- txts.push(
487
- ...this.visit(node.value),
488
- ...this.visitFragment(node.pending),
531
+ const txts = [
532
+ ...this.visit(node.expression),
489
533
  ...this.visitFragment(node.then),
490
- ...this.visitFragment(node.catch),
491
- )
534
+ ]
535
+ if (node.pending) {
536
+ txts.push(...this.visitFragment(node.pending),)
537
+ }
538
+ if (node.catch) {
539
+ txts.push(...this.visitFragment(node.catch),)
540
+ }
492
541
  return txts
493
542
  }
494
543
 
495
544
  /**
496
- * @typedef {Node & {body: Node[]}} Program
497
- * @param {Program} node
545
+ * @param {import('estree').Program} node
498
546
  * @returns {NestText[]}
499
547
  */
500
548
  visitProgram = (node, needImport = true) => {
@@ -504,20 +552,18 @@ export default class Preprocess {
504
552
  }
505
553
  if (needImport) {
506
554
  const importStmt = `import {${rtFunc}} from "${this.importFrom}"\n`
555
+ // @ts-ignore
507
556
  this.mstr.appendRight(node.start, importStmt)
508
557
  }
509
558
  return txts
510
559
  }
511
560
 
512
561
  /**
513
- * @param {Node & {
514
- * html: Node & {children: Node[]}
515
- * instance: Node & {content: Program}}
516
- * } node
562
+ * @param {import("svelte/compiler").AST.Root} node
517
563
  * @returns {NestText[]}
518
564
  */
519
- visitSvelteComponent = node => {
520
- const txts = this.visitFragment(node.html)
565
+ visitRoot = node => {
566
+ const txts = this.visitFragment(node.fragment)
521
567
  if (node.instance) {
522
568
  txts.push(...this.visitProgram(node.instance.content, false))
523
569
  }
@@ -528,8 +574,8 @@ export default class Preprocess {
528
574
  }
529
575
  const importStmt = `import ${rtComponent}, {${rtFunc}} from "${this.importFrom}"\n`
530
576
  if (node.instance) {
577
+ // @ts-ignore
531
578
  this.mstr.appendRight(node.instance.content.start, importStmt)
532
- // @ts-ignore
533
579
  } else if (node.module) {
534
580
  // @ts-ignore
535
581
  this.mstr.appendRight(node.module.content.start, importStmt)
@@ -540,7 +586,7 @@ export default class Preprocess {
540
586
  }
541
587
 
542
588
  /**
543
- * @param {Node} node
589
+ * @param {import("svelte/compiler").AST.SvelteNode} node
544
590
  * @returns {NestText[]}
545
591
  */
546
592
  visit = node => {
@@ -554,7 +600,7 @@ export default class Preprocess {
554
600
 
555
601
  /**
556
602
  * @param {string} content
557
- * @param {Node} ast
603
+ * @param {import('estree').Program | import("svelte/compiler").AST.Root} ast
558
604
  * @returns {NestText[]}
559
605
  */
560
606
  process = (content, ast) => {