agent-flutter 0.1.6 → 0.1.8
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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +68 -4
- package/templates/shared/tool/generate_model_registry.dart +111 -0
- package/templates/shared/tool/generate_ui_workflow_spec.dart +173 -0
- package/templates/shared/tool/update_specs.dart +40 -0
- package/templates/shared/tools/jsx2flutter/convert.mjs +318 -0
- package/templates/shared/tools/jsx2flutter/icons_map.json +16 -0
- package/templates/shared/tools/jsx2flutter/jsx2flutter.mjs +1997 -0
- package/templates/shared/tools/jsx2flutter/package-lock.json +288 -0
- package/templates/shared/tools/jsx2flutter/package.json +19 -0
- package/templates/shared/vscode/tasks.json +121 -0
|
@@ -0,0 +1,1997 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { createRequire } from 'node:module'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { parse } from '@babel/parser'
|
|
6
|
+
import traverseModule from '@babel/traverse'
|
|
7
|
+
const traverse = traverseModule.default
|
|
8
|
+
import postcss from 'postcss'
|
|
9
|
+
import safeParser from 'postcss-safe-parser'
|
|
10
|
+
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
12
|
+
const __dirname = path.dirname(__filename)
|
|
13
|
+
const require = createRequire(import.meta.url)
|
|
14
|
+
let sass = null
|
|
15
|
+
try {
|
|
16
|
+
sass = require('sass')
|
|
17
|
+
} catch {}
|
|
18
|
+
|
|
19
|
+
function resolveProjectRoot() {
|
|
20
|
+
const fromCwd = process.cwd()
|
|
21
|
+
if (fs.existsSync(path.resolve(fromCwd, 'pubspec.yaml'))) {
|
|
22
|
+
return fromCwd
|
|
23
|
+
}
|
|
24
|
+
let current = __dirname
|
|
25
|
+
while (true) {
|
|
26
|
+
if (fs.existsSync(path.resolve(current, 'pubspec.yaml'))) {
|
|
27
|
+
return current
|
|
28
|
+
}
|
|
29
|
+
const parent = path.dirname(current)
|
|
30
|
+
if (parent === current) break
|
|
31
|
+
current = parent
|
|
32
|
+
}
|
|
33
|
+
return fromCwd
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function readPackageName(projectRoot) {
|
|
37
|
+
const pubspecPath = path.resolve(projectRoot, 'pubspec.yaml')
|
|
38
|
+
if (!fs.existsSync(pubspecPath)) return 'app'
|
|
39
|
+
const content = fs.readFileSync(pubspecPath, 'utf8')
|
|
40
|
+
const matched = content.match(/^name:\s*([A-Za-z0-9_]+)/m)
|
|
41
|
+
return matched?.[1] || 'app'
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const REPO_ROOT = resolveProjectRoot()
|
|
45
|
+
const PROJECT_PACKAGE_NAME = readPackageName(REPO_ROOT)
|
|
46
|
+
const APP_ASSETS_FILE = path.resolve(REPO_ROOT, 'lib/src/utils/app_assets.dart')
|
|
47
|
+
let ICONS_MAP = {}
|
|
48
|
+
try {
|
|
49
|
+
const mapPath = path.resolve(__dirname, 'icons_map.json')
|
|
50
|
+
if (fs.existsSync(mapPath)) ICONS_MAP = JSON.parse(fs.readFileSync(mapPath, 'utf8'))
|
|
51
|
+
} catch {}
|
|
52
|
+
|
|
53
|
+
let ASSET_CONTEXT = null
|
|
54
|
+
const DEFAULT_ASSET_DIR = process.env.JSX2FLUTTER_ASSET_DIR || 'assets/figma'
|
|
55
|
+
const GENERIC_ASSET_TOKENS = new Set([
|
|
56
|
+
'asset',
|
|
57
|
+
'auto',
|
|
58
|
+
'div',
|
|
59
|
+
'ellipse',
|
|
60
|
+
'frame',
|
|
61
|
+
'group',
|
|
62
|
+
'icon',
|
|
63
|
+
'image',
|
|
64
|
+
'img',
|
|
65
|
+
'line',
|
|
66
|
+
'path',
|
|
67
|
+
'rect',
|
|
68
|
+
'shape',
|
|
69
|
+
'svg',
|
|
70
|
+
'vector',
|
|
71
|
+
'wrapper',
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
function normalizeAssetPath(p) {
|
|
75
|
+
return p.replace(/\\/g, '/')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function sanitizeAssetToken(value) {
|
|
79
|
+
return String(value || '')
|
|
80
|
+
.trim()
|
|
81
|
+
.toLowerCase()
|
|
82
|
+
.replace(/[^a-z0-9]+/g, '_')
|
|
83
|
+
.replace(/^_+|_+$/g, '')
|
|
84
|
+
.replace(/_+/g, '_')
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeSemanticAssetToken(value) {
|
|
88
|
+
const base = sanitizeAssetToken(value)
|
|
89
|
+
if (!base) return ''
|
|
90
|
+
const withoutPrefix = base.replace(/^(icon|image|img|ic)_+/, '')
|
|
91
|
+
const trimmed = withoutPrefix
|
|
92
|
+
.replace(/(_?\d+)+$/g, '')
|
|
93
|
+
.replace(/_+/g, '_')
|
|
94
|
+
.replace(/^_+|_+$/g, '')
|
|
95
|
+
return trimmed || withoutPrefix || base
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isGenericSemanticToken(token) {
|
|
99
|
+
const normalized = normalizeSemanticAssetToken(token)
|
|
100
|
+
if (!normalized) return true
|
|
101
|
+
if (GENERIC_ASSET_TOKENS.has(normalized)) return true
|
|
102
|
+
if (/^(frame|group|rect|ellipse|vector|path|shape|line|icon|img|image)\d*$/.test(normalized)) return true
|
|
103
|
+
if (/^[a-z0-9]{6,}(?:_[a-z0-9]{5,})+$/.test(normalized) && /\d/.test(normalized)) return true
|
|
104
|
+
if (/^\d+$/.test(normalized)) return true
|
|
105
|
+
return false
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function pickSemanticAssetToken(absPath, semanticHint, stem) {
|
|
109
|
+
const directHint = normalizeSemanticAssetToken(semanticHint)
|
|
110
|
+
if (directHint && !isGenericSemanticToken(directHint)) return directHint
|
|
111
|
+
|
|
112
|
+
const mappedHint = normalizeSemanticAssetToken(ASSET_CONTEXT?.semanticHintByAbs?.get(absPath))
|
|
113
|
+
if (mappedHint && !isGenericSemanticToken(mappedHint)) return mappedHint
|
|
114
|
+
|
|
115
|
+
const stemHint = normalizeSemanticAssetToken(stem)
|
|
116
|
+
if (stemHint && !isGenericSemanticToken(stemHint)) return stemHint
|
|
117
|
+
|
|
118
|
+
return ''
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function toAssetAbsPath(src, baseDir) {
|
|
122
|
+
if (!src || !baseDir) return null
|
|
123
|
+
const cleaned = String(src).split('?')[0].split('#')[0]
|
|
124
|
+
if (!cleaned || /^https?:\/\//i.test(cleaned) || /^data:/i.test(cleaned)) return null
|
|
125
|
+
const abs = path.resolve(baseDir, cleaned)
|
|
126
|
+
if (!fs.existsSync(abs)) return null
|
|
127
|
+
return abs
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function buildAssetOutputName(absPath, semanticHint = '') {
|
|
131
|
+
if (!ASSET_CONTEXT || !ASSET_CONTEXT.renameAssets) {
|
|
132
|
+
return path.basename(absPath)
|
|
133
|
+
}
|
|
134
|
+
const existing = ASSET_CONTEXT.fileNameByAbs.get(absPath)
|
|
135
|
+
if (existing) return existing
|
|
136
|
+
|
|
137
|
+
const extRaw = path.extname(absPath).toLowerCase()
|
|
138
|
+
const ext = extRaw || '.bin'
|
|
139
|
+
const stemRaw = path.basename(absPath, extRaw)
|
|
140
|
+
const stem = sanitizeAssetToken(stemRaw) || 'asset'
|
|
141
|
+
const prefix = sanitizeAssetToken(ASSET_CONTEXT.assetPrefix)
|
|
142
|
+
const kind = ext === '.svg' ? 'ic' : 'img'
|
|
143
|
+
const semanticPart = pickSemanticAssetToken(absPath, semanticHint, stem) || (ext === '.svg' ? 'icon' : 'image')
|
|
144
|
+
const base = [kind, prefix, semanticPart].filter(Boolean).join('_')
|
|
145
|
+
|
|
146
|
+
let candidate = `${base}${ext}`
|
|
147
|
+
let index = 2
|
|
148
|
+
while (ASSET_CONTEXT.usedFileNames.has(candidate)) {
|
|
149
|
+
candidate = `${base}_${index}${ext}`
|
|
150
|
+
index += 1
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
ASSET_CONTEXT.usedFileNames.add(candidate)
|
|
154
|
+
ASSET_CONTEXT.fileNameByAbs.set(absPath, candidate)
|
|
155
|
+
return candidate
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function registerAsset(absPath, relPath) {
|
|
159
|
+
if (!ASSET_CONTEXT) return
|
|
160
|
+
ASSET_CONTEXT.assets.set(absPath, relPath)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function resolveLocalAsset(src, semanticHint = '') {
|
|
164
|
+
if (!src || !ASSET_CONTEXT) return null
|
|
165
|
+
const abs = toAssetAbsPath(src, ASSET_CONTEXT.jsxDir)
|
|
166
|
+
if (!abs) return null
|
|
167
|
+
const fileName = buildAssetOutputName(abs, semanticHint)
|
|
168
|
+
const rel = normalizeAssetPath(path.posix.join(ASSET_CONTEXT.assetDir, fileName))
|
|
169
|
+
const assetRef = resolveAssetReference(rel)
|
|
170
|
+
registerAsset(abs, rel)
|
|
171
|
+
return {
|
|
172
|
+
rel,
|
|
173
|
+
isSvg: fileName.toLowerCase().endsWith('.svg'),
|
|
174
|
+
assetRef,
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function copyCollectedAssets() {
|
|
179
|
+
if (!ASSET_CONTEXT || !ASSET_CONTEXT.copyAssets) return
|
|
180
|
+
if (!ASSET_CONTEXT.assets.size) return
|
|
181
|
+
for (const [abs, rel] of ASSET_CONTEXT.assets.entries()) {
|
|
182
|
+
const dest = path.resolve(REPO_ROOT, rel)
|
|
183
|
+
ensureDir(path.dirname(dest))
|
|
184
|
+
try {
|
|
185
|
+
fs.copyFileSync(abs, dest)
|
|
186
|
+
} catch {}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function ensureDir(dir) {
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function semanticTokenFromClassName(className) {
|
|
195
|
+
const token = normalizeSemanticAssetToken(className)
|
|
196
|
+
if (!token || isGenericSemanticToken(token)) return ''
|
|
197
|
+
return token
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function isBetterSemanticToken(nextToken, currentToken) {
|
|
201
|
+
if (!currentToken) return true
|
|
202
|
+
const currentGeneric = isGenericSemanticToken(currentToken)
|
|
203
|
+
const nextGeneric = isGenericSemanticToken(nextToken)
|
|
204
|
+
if (currentGeneric !== nextGeneric) return !nextGeneric
|
|
205
|
+
if (nextToken.length !== currentToken.length) return nextToken.length < currentToken.length
|
|
206
|
+
return nextToken < currentToken
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function collectAssetSemanticHints(ast, jsxDir) {
|
|
210
|
+
const semanticHints = new Map()
|
|
211
|
+
traverse(ast, {
|
|
212
|
+
JSXElement(pathNode) {
|
|
213
|
+
const el = pathNode.node
|
|
214
|
+
if (jsxElementName(el).toLowerCase() !== 'img') return
|
|
215
|
+
const src = getAttr(el, 'src') || ''
|
|
216
|
+
const abs = toAssetAbsPath(src, jsxDir)
|
|
217
|
+
if (!abs) return
|
|
218
|
+
const hint = semanticTokenFromClassName(getClassNameAttr(el))
|
|
219
|
+
if (!hint) return
|
|
220
|
+
const existing = semanticHints.get(abs)
|
|
221
|
+
if (!existing || isBetterSemanticToken(hint, existing)) {
|
|
222
|
+
semanticHints.set(abs, hint)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
return semanticHints
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function toPascalCaseToken(raw) {
|
|
230
|
+
const token = sanitizeAssetToken(raw)
|
|
231
|
+
if (!token) return ''
|
|
232
|
+
return token
|
|
233
|
+
.split('_')
|
|
234
|
+
.filter(Boolean)
|
|
235
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
236
|
+
.join('')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toWidgetClassName(raw) {
|
|
240
|
+
const normalized = String(raw || '')
|
|
241
|
+
.trim()
|
|
242
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
243
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
244
|
+
.replace(/^_+|_+$/g, '')
|
|
245
|
+
.toLowerCase()
|
|
246
|
+
if (!normalized) return 'FigmaWidget'
|
|
247
|
+
const className = normalized
|
|
248
|
+
.split('_')
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.map(part => part[0].toUpperCase() + part.slice(1))
|
|
251
|
+
.join('')
|
|
252
|
+
if (!className) return 'FigmaWidget'
|
|
253
|
+
return /^[A-Za-z_]/.test(className) ? className : `Figma${className}`
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function buildAppAssetConstName(assetPath) {
|
|
257
|
+
const normalized = normalizeAssetPath(assetPath).replace(/^assets\//, '')
|
|
258
|
+
const parts = normalized
|
|
259
|
+
.split(/[\/._-]+/)
|
|
260
|
+
.map(part => sanitizeAssetToken(part))
|
|
261
|
+
.filter(Boolean)
|
|
262
|
+
if (!parts.length) return 'assetGenerated'
|
|
263
|
+
const [head, ...tail] = parts
|
|
264
|
+
const headName = /^[a-z]/.test(head) ? head : `asset${toPascalCaseToken(head)}`
|
|
265
|
+
return `${headName}${tail.map(toPascalCaseToken).join('')}`
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function loadAppAssetsRegistry() {
|
|
269
|
+
if (!fs.existsSync(APP_ASSETS_FILE)) return null
|
|
270
|
+
let content = ''
|
|
271
|
+
try {
|
|
272
|
+
content = fs.readFileSync(APP_ASSETS_FILE, 'utf8')
|
|
273
|
+
} catch {
|
|
274
|
+
return null
|
|
275
|
+
}
|
|
276
|
+
const pathToConst = new Map()
|
|
277
|
+
const constToPath = new Map()
|
|
278
|
+
const regex = /static const String (\w+)\s*=\s*'([^']+)';/g
|
|
279
|
+
let match
|
|
280
|
+
while ((match = regex.exec(content)) !== null) {
|
|
281
|
+
const constName = match[1]
|
|
282
|
+
const assetPath = normalizeAssetPath(match[2])
|
|
283
|
+
constToPath.set(constName, assetPath)
|
|
284
|
+
if (!pathToConst.has(assetPath)) pathToConst.set(assetPath, constName)
|
|
285
|
+
}
|
|
286
|
+
return {
|
|
287
|
+
pathToConst,
|
|
288
|
+
constToPath,
|
|
289
|
+
pendingPathToConst: new Map(),
|
|
290
|
+
pendingConstNames: new Set(),
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function resolveAppAssetConstName(assetPath) {
|
|
295
|
+
const registry = ASSET_CONTEXT?.appAssetsRegistry
|
|
296
|
+
if (!registry) return null
|
|
297
|
+
const normalized = normalizeAssetPath(assetPath)
|
|
298
|
+
const existing = registry.pathToConst.get(normalized) || registry.pendingPathToConst.get(normalized)
|
|
299
|
+
if (existing) return existing
|
|
300
|
+
const base = buildAppAssetConstName(normalized)
|
|
301
|
+
let candidate = base
|
|
302
|
+
let index = 2
|
|
303
|
+
while (
|
|
304
|
+
registry.constToPath.has(candidate) ||
|
|
305
|
+
registry.pendingConstNames.has(candidate)
|
|
306
|
+
) {
|
|
307
|
+
candidate = `${base}${index}`
|
|
308
|
+
index += 1
|
|
309
|
+
}
|
|
310
|
+
registry.pathToConst.set(normalized, candidate)
|
|
311
|
+
registry.pendingPathToConst.set(normalized, candidate)
|
|
312
|
+
registry.pendingConstNames.add(candidate)
|
|
313
|
+
return candidate
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function resolveAssetReference(assetPath) {
|
|
317
|
+
const constName = resolveAppAssetConstName(assetPath)
|
|
318
|
+
if (constName) return `AppAssets.${constName}`
|
|
319
|
+
return toDartLiteral(assetPath)
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function syncAppAssetsFile() {
|
|
323
|
+
const registry = ASSET_CONTEXT?.appAssetsRegistry
|
|
324
|
+
if (!registry || !registry.pendingPathToConst.size) return
|
|
325
|
+
if (!fs.existsSync(APP_ASSETS_FILE)) return
|
|
326
|
+
let content = ''
|
|
327
|
+
try {
|
|
328
|
+
content = fs.readFileSync(APP_ASSETS_FILE, 'utf8')
|
|
329
|
+
} catch {
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
const insertAt = content.lastIndexOf('\n}')
|
|
333
|
+
if (insertAt < 0) return
|
|
334
|
+
const lines = [...registry.pendingPathToConst.entries()]
|
|
335
|
+
.sort((a, b) => a[1].localeCompare(b[1]))
|
|
336
|
+
.map(([assetPath, constName]) => ` static const String ${constName} = '${assetPath}';`)
|
|
337
|
+
if (!lines.length) return
|
|
338
|
+
const block = `\n${lines.join('\n')}\n`
|
|
339
|
+
const updated = `${content.slice(0, insertAt)}${block}${content.slice(insertAt)}`
|
|
340
|
+
fs.writeFileSync(APP_ASSETS_FILE, updated, 'utf8')
|
|
341
|
+
registry.pendingPathToConst.clear()
|
|
342
|
+
registry.pendingConstNames.clear()
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function readFile(p) {
|
|
346
|
+
return fs.readFileSync(p, 'utf8')
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function writeFile(p, content) {
|
|
350
|
+
ensureDir(path.dirname(p))
|
|
351
|
+
fs.writeFileSync(p, content, 'utf8')
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function compileScssIfNeeded(css, cssPath) {
|
|
355
|
+
// If dart-sass is available, use it (most accurate).
|
|
356
|
+
if (!sass) return css
|
|
357
|
+
try {
|
|
358
|
+
const result = sass.compileString(css, {
|
|
359
|
+
style: 'expanded',
|
|
360
|
+
loadPaths: cssPath ? [path.dirname(cssPath)] : []
|
|
361
|
+
})
|
|
362
|
+
return result.css || css
|
|
363
|
+
} catch {
|
|
364
|
+
return css
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function stripInlineComment(line) {
|
|
369
|
+
// Keep URLs like http://... by only treating '//' as a comment start when it's not in quotes.
|
|
370
|
+
const s = String(line)
|
|
371
|
+
let inSingle = false
|
|
372
|
+
let inDouble = false
|
|
373
|
+
for (let i = 0; i < s.length - 1; i++) {
|
|
374
|
+
const ch = s[i]
|
|
375
|
+
const next = s[i + 1]
|
|
376
|
+
if (ch === "'" && !inDouble) inSingle = !inSingle
|
|
377
|
+
if (ch === '"' && !inSingle) inDouble = !inDouble
|
|
378
|
+
if (!inSingle && !inDouble && ch === '/' && next === '/') {
|
|
379
|
+
return s.slice(0, i)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return s
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function splitSelectors(sel) {
|
|
386
|
+
return String(sel)
|
|
387
|
+
.split(',')
|
|
388
|
+
.map(s => s.trim())
|
|
389
|
+
.filter(Boolean)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function combineSelectors(parents, current) {
|
|
393
|
+
const cur = splitSelectors(current)
|
|
394
|
+
if (!cur.length) return parents
|
|
395
|
+
const out = []
|
|
396
|
+
for (const p of parents) {
|
|
397
|
+
for (const c of cur) {
|
|
398
|
+
if (!p) {
|
|
399
|
+
out.push(c)
|
|
400
|
+
} else if (c.includes('&')) {
|
|
401
|
+
out.push(c.replace(/&/g, p))
|
|
402
|
+
} else {
|
|
403
|
+
out.push(`${p} ${c}`)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
return out
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function flattenScssToCss(scss) {
|
|
411
|
+
// Minimal SCSS flattener: handles nested selectors with braces.
|
|
412
|
+
// This is enough for Figma/Trae-generated SCSS modules (no mixins/functions).
|
|
413
|
+
const lines = String(scss).replace(/\r\n/g, '\n').split('\n')
|
|
414
|
+
const stack = [{ selectors: [''], decls: [] }]
|
|
415
|
+
let inBlockComment = false
|
|
416
|
+
const outRules = []
|
|
417
|
+
let pendingDecl = null
|
|
418
|
+
|
|
419
|
+
function flushTop() {
|
|
420
|
+
const top = stack[stack.length - 1]
|
|
421
|
+
if (!top || !top.decls.length) return
|
|
422
|
+
const decls = top.decls.join('\n')
|
|
423
|
+
for (const sel of top.selectors) {
|
|
424
|
+
if (!sel) continue
|
|
425
|
+
outRules.push(`${sel} {\n${decls}\n}`)
|
|
426
|
+
}
|
|
427
|
+
top.decls = []
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
for (let rawLine of lines) {
|
|
431
|
+
let line = String(rawLine)
|
|
432
|
+
// Block comments /* ... */
|
|
433
|
+
if (inBlockComment) {
|
|
434
|
+
const end = line.indexOf('*/')
|
|
435
|
+
if (end >= 0) {
|
|
436
|
+
inBlockComment = false
|
|
437
|
+
line = line.slice(end + 2)
|
|
438
|
+
} else {
|
|
439
|
+
continue
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
while (true) {
|
|
443
|
+
const start = line.indexOf('/*')
|
|
444
|
+
if (start < 0) break
|
|
445
|
+
const end = line.indexOf('*/', start + 2)
|
|
446
|
+
if (end >= 0) {
|
|
447
|
+
line = line.slice(0, start) + line.slice(end + 2)
|
|
448
|
+
} else {
|
|
449
|
+
inBlockComment = true
|
|
450
|
+
line = line.slice(0, start)
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
line = stripInlineComment(line).trim()
|
|
456
|
+
if (!line) continue
|
|
457
|
+
|
|
458
|
+
if (pendingDecl) {
|
|
459
|
+
pendingDecl.value = `${pendingDecl.value} ${line}`.trim()
|
|
460
|
+
if (pendingDecl.value.includes(';')) {
|
|
461
|
+
const semicolonIndex = pendingDecl.value.indexOf(';')
|
|
462
|
+
const declValue = pendingDecl.value.slice(0, semicolonIndex + 1).trim()
|
|
463
|
+
stack[stack.length - 1].decls.push(` ${pendingDecl.prop}: ${declValue}`)
|
|
464
|
+
pendingDecl = null
|
|
465
|
+
}
|
|
466
|
+
continue
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// Handle multiple braces on same line crudely by iterating chars.
|
|
470
|
+
// We avoid full tokenization by treating declarations as whole lines.
|
|
471
|
+
// Common generator output keeps '{' on selector line and '}' on its own line.
|
|
472
|
+
if (line.endsWith('{')) {
|
|
473
|
+
const selector = line.slice(0, -1).trim()
|
|
474
|
+
const parentSelectors = stack[stack.length - 1]?.selectors || ['']
|
|
475
|
+
const selectors = combineSelectors(parentSelectors, selector)
|
|
476
|
+
stack.push({ selectors, decls: [] })
|
|
477
|
+
continue
|
|
478
|
+
}
|
|
479
|
+
if (line === '}') {
|
|
480
|
+
if (pendingDecl) pendingDecl = null
|
|
481
|
+
flushTop()
|
|
482
|
+
stack.pop()
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
// Declaration line (supports multiline values like linear-gradient(...))
|
|
486
|
+
if (line.includes(':')) {
|
|
487
|
+
// Only accept standard declarations (avoid mis-parsing nested selectors)
|
|
488
|
+
const idx = line.indexOf(':')
|
|
489
|
+
const prop = line.slice(0, idx).trim()
|
|
490
|
+
const val = line.slice(idx + 1).trim()
|
|
491
|
+
if (prop && /^[a-zA-Z_-][a-zA-Z0-9_-]*$/.test(prop)) {
|
|
492
|
+
if (val.includes(';')) {
|
|
493
|
+
const semicolonIndex = val.indexOf(';')
|
|
494
|
+
const declValue = val.slice(0, semicolonIndex + 1).trim()
|
|
495
|
+
stack[stack.length - 1].decls.push(` ${prop}: ${declValue}`)
|
|
496
|
+
} else {
|
|
497
|
+
pendingDecl = { prop, value: val }
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
continue
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// Flush any remaining
|
|
505
|
+
while (stack.length > 1) {
|
|
506
|
+
flushTop()
|
|
507
|
+
stack.pop()
|
|
508
|
+
}
|
|
509
|
+
return outRules.join('\n\n')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function compileOrFlattenScss(css, cssPath) {
|
|
513
|
+
if (sass) return compileScssIfNeeded(css, cssPath)
|
|
514
|
+
try {
|
|
515
|
+
return flattenScssToCss(css)
|
|
516
|
+
} catch {
|
|
517
|
+
return css
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
function parseCssModules(css, cssPath) {
|
|
522
|
+
const normalizedCss = compileOrFlattenScss(css, cssPath)
|
|
523
|
+
const root = postcss.parse(normalizedCss, { parser: safeParser })
|
|
524
|
+
const map = {}
|
|
525
|
+
root.walkRules(rule => {
|
|
526
|
+
const selector = rule.selector
|
|
527
|
+
if (!selector) return
|
|
528
|
+
const props = {}
|
|
529
|
+
rule.walkDecls(decl => {
|
|
530
|
+
props[decl.prop] = decl.value
|
|
531
|
+
})
|
|
532
|
+
const classes = [...selector.matchAll(/\.([A-Za-z0-9_-]+)/g)].map(m => m[1])
|
|
533
|
+
if (classes.length) {
|
|
534
|
+
const key = classes[classes.length - 1]
|
|
535
|
+
map[key] = { ...(map[key] || {}), ...props }
|
|
536
|
+
}
|
|
537
|
+
})
|
|
538
|
+
return map
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function cssColorToAppColor(v) {
|
|
542
|
+
const raw = v.trim().toLowerCase()
|
|
543
|
+
// CSS variable tokens mapping -> AppColors
|
|
544
|
+
const varMatch = raw.match(/^var\(\s*--?([a-z0-9\-_]+)\s*(?:,\s*([^)]+))?\)$/i)
|
|
545
|
+
if (varMatch) {
|
|
546
|
+
const token = varMatch[1]
|
|
547
|
+
const tok = token.replace(/[^a-z0-9_]/gi, '').toLowerCase()
|
|
548
|
+
const dict = {
|
|
549
|
+
primary: 'AppColors.primary',
|
|
550
|
+
buttondarkbtndarkcolor: 'AppColors.white',
|
|
551
|
+
text2: 'AppColors.colorB8BCC6',
|
|
552
|
+
tabdisabletext: 'AppColors.colorB7B7B7',
|
|
553
|
+
neutrals200: 'AppColors.colorEAECF0',
|
|
554
|
+
titletext: 'AppColors.color667394',
|
|
555
|
+
bg: 'AppColors.background',
|
|
556
|
+
}
|
|
557
|
+
const mapped = dict[tok]
|
|
558
|
+
if (mapped) return mapped
|
|
559
|
+
const fallback = varMatch[2]
|
|
560
|
+
if (fallback) return cssColorToAppColor(fallback)
|
|
561
|
+
return null
|
|
562
|
+
}
|
|
563
|
+
const hex = raw
|
|
564
|
+
if (/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/.test(hex)) {
|
|
565
|
+
// Expand short hex (#abc / #rgba) and normalize alpha order
|
|
566
|
+
if (/^#[0-9a-f]{3}$/.test(hex)) {
|
|
567
|
+
const r = hex[1], g = hex[2], b = hex[3]
|
|
568
|
+
return `AppColors.fromHex('#${r}${r}${g}${g}${b}${b}')`
|
|
569
|
+
}
|
|
570
|
+
if (/^#[0-9a-f]{4}$/.test(hex)) {
|
|
571
|
+
const r = hex[1], g = hex[2], b = hex[3], a = hex[4]
|
|
572
|
+
// CSS #RGBA -> Flutter expects #AARRGGBB
|
|
573
|
+
return `AppColors.fromHex('#${a}${a}${r}${r}${g}${g}${b}${b}')`
|
|
574
|
+
}
|
|
575
|
+
if (/^#[0-9a-f]{8}$/.test(hex)) {
|
|
576
|
+
const rr = hex.slice(1, 3)
|
|
577
|
+
const gg = hex.slice(3, 5)
|
|
578
|
+
const bb = hex.slice(5, 7)
|
|
579
|
+
const aa = hex.slice(7, 9)
|
|
580
|
+
return `AppColors.fromHex('#${aa}${rr}${gg}${bb}')`
|
|
581
|
+
}
|
|
582
|
+
return `AppColors.fromHex('${hex}')`
|
|
583
|
+
}
|
|
584
|
+
const rgbMatch = raw.match(/^rgba?\((\s*\d+\s*),(\s*\d+\s*),(\s*\d+\s*)(?:,\s*(\d*\.?\d+)\s*)?\)$/i)
|
|
585
|
+
if (rgbMatch) {
|
|
586
|
+
const r = Math.max(0, Math.min(255, parseInt(rgbMatch[1])))
|
|
587
|
+
const g = Math.max(0, Math.min(255, parseInt(rgbMatch[2])))
|
|
588
|
+
const b = Math.max(0, Math.min(255, parseInt(rgbMatch[3])))
|
|
589
|
+
const a = rgbMatch[4] != null ? Math.max(0, Math.min(1, parseFloat(rgbMatch[4]))) : 1
|
|
590
|
+
const aa = Math.round(a * 255)
|
|
591
|
+
const hexStr = `#${aa.toString(16).padStart(2,'0')}${r.toString(16).padStart(2,'0')}${g.toString(16).padStart(2,'0')}${b.toString(16).padStart(2,'0')}`
|
|
592
|
+
return `AppColors.fromHex('${hexStr}')`
|
|
593
|
+
}
|
|
594
|
+
if (hex === 'white') return 'AppColors.white'
|
|
595
|
+
if (hex === 'black') return 'AppColors.black'
|
|
596
|
+
if (hex === 'transparent') return 'AppColors.transparent'
|
|
597
|
+
return null
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function extractLinearGradientArgs(str) {
|
|
601
|
+
const source = String(str || '')
|
|
602
|
+
const marker = 'linear-gradient('
|
|
603
|
+
const start = source.toLowerCase().indexOf(marker)
|
|
604
|
+
if (start < 0) return null
|
|
605
|
+
|
|
606
|
+
let depth = 1
|
|
607
|
+
let args = ''
|
|
608
|
+
for (let i = start + marker.length; i < source.length; i += 1) {
|
|
609
|
+
const ch = source[i]
|
|
610
|
+
if (ch === '(') {
|
|
611
|
+
depth += 1
|
|
612
|
+
args += ch
|
|
613
|
+
continue
|
|
614
|
+
}
|
|
615
|
+
if (ch === ')') {
|
|
616
|
+
depth -= 1
|
|
617
|
+
if (depth === 0) break
|
|
618
|
+
args += ch
|
|
619
|
+
continue
|
|
620
|
+
}
|
|
621
|
+
args += ch
|
|
622
|
+
}
|
|
623
|
+
if (depth !== 0) return null
|
|
624
|
+
return args
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function parseGradientColors(str) {
|
|
628
|
+
const args = extractLinearGradientArgs(str)
|
|
629
|
+
if (!args) return []
|
|
630
|
+
const rawColors = [...args.matchAll(/#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3})/ig)].map(x => x[0])
|
|
631
|
+
return rawColors.map(c => cssColorToAppColor(c)).filter(Boolean)
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function parseLinearGradient(str) {
|
|
635
|
+
const args = extractLinearGradientArgs(str)
|
|
636
|
+
if (!args) return null
|
|
637
|
+
const colors = parseGradientColors(str)
|
|
638
|
+
if (!colors.length) return null
|
|
639
|
+
const angleMatch = args.match(/(\d+)\s*deg/i)
|
|
640
|
+
const angle = angleMatch ? parseInt(angleMatch[1]) : 180
|
|
641
|
+
let begin = 'Alignment.topLeft', end = 'Alignment.bottomRight'
|
|
642
|
+
if (angle === 180) { begin = 'Alignment.topCenter'; end = 'Alignment.bottomCenter' }
|
|
643
|
+
if (angle === 90) { begin = 'Alignment.centerLeft'; end = 'Alignment.centerRight' }
|
|
644
|
+
return `LinearGradient(begin: ${begin}, end: ${end}, colors: [${colors.join(', ')}])`
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function gradientTextFromProps(props) {
|
|
648
|
+
const clip = `${props['background-clip'] || ''} ${props['-webkit-background-clip'] || ''}`.toLowerCase()
|
|
649
|
+
if (!clip.includes('text')) return null
|
|
650
|
+
const gradientSource = props['background-image'] || props['background']
|
|
651
|
+
if (!gradientSource || !/linear-gradient/i.test(String(gradientSource))) return null
|
|
652
|
+
return parseLinearGradient(gradientSource)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function cssPxToDouble(v) {
|
|
656
|
+
const m = String(v).match(/([0-9.]+)px/)
|
|
657
|
+
if (m) return parseFloat(m[1])
|
|
658
|
+
const n = parseFloat(v)
|
|
659
|
+
if (!Number.isNaN(n)) return n
|
|
660
|
+
return null
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function parseBoxValues(v) {
|
|
664
|
+
if (!v) return null
|
|
665
|
+
const cleaned = String(v).split('/')[0]
|
|
666
|
+
const parts = cleaned.trim().split(/\s+/).filter(Boolean)
|
|
667
|
+
const nums = parts.map(cssPxToDouble).filter(n => n != null)
|
|
668
|
+
if (!nums.length) return null
|
|
669
|
+
if (nums.length === 1) return { top: nums[0], right: nums[0], bottom: nums[0], left: nums[0] }
|
|
670
|
+
if (nums.length === 2) return { top: nums[0], right: nums[1], bottom: nums[0], left: nums[1] }
|
|
671
|
+
if (nums.length === 3) return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[1] }
|
|
672
|
+
return { top: nums[0], right: nums[1], bottom: nums[2], left: nums[3] }
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
function fontWeightFromCss(v) {
|
|
676
|
+
const raw = String(v || '').trim().toLowerCase()
|
|
677
|
+
if (raw === 'bold') return 'FontWeight.w700'
|
|
678
|
+
if (raw === 'normal') return 'FontWeight.w400'
|
|
679
|
+
const n = parseInt(raw, 10)
|
|
680
|
+
if (n >= 700) return 'FontWeight.w700'
|
|
681
|
+
if (n >= 600) return 'FontWeight.w600'
|
|
682
|
+
if (n >= 500) return 'FontWeight.w500'
|
|
683
|
+
return 'FontWeight.w400'
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
function fontFamilyFromCss(v) {
|
|
687
|
+
if (!v) return null
|
|
688
|
+
const fontMap = {
|
|
689
|
+
'zen maru gothic': 'ZenMaruGothic',
|
|
690
|
+
'noto sans jp': 'ZenMaruGothic',
|
|
691
|
+
'sf pro text': null,
|
|
692
|
+
'inter': null,
|
|
693
|
+
}
|
|
694
|
+
const candidates = String(v)
|
|
695
|
+
.split(',')
|
|
696
|
+
.map(s => s.trim().replace(/^['"]|['"]$/g, ''))
|
|
697
|
+
.filter(Boolean)
|
|
698
|
+
for (const name of candidates) {
|
|
699
|
+
const key = name.toLowerCase()
|
|
700
|
+
if (Object.prototype.hasOwnProperty.call(fontMap, key)) {
|
|
701
|
+
const mapped = fontMap[key]
|
|
702
|
+
return mapped ? toDartLiteral(mapped) : null
|
|
703
|
+
}
|
|
704
|
+
if (key.includes('sans-serif') || key.includes('serif') || key.includes('monospace')) continue
|
|
705
|
+
return toDartLiteral(name)
|
|
706
|
+
}
|
|
707
|
+
return null
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function textStyleFromProps(props) {
|
|
711
|
+
const size = cssPxToDouble(props['font-size'])
|
|
712
|
+
const weight = props['font-weight'] ? fontWeightFromCss(props['font-weight']) : null
|
|
713
|
+
let color = props['color'] ? cssColorToAppColor(props['color']) : null
|
|
714
|
+
if (color === 'AppColors.transparent') {
|
|
715
|
+
// Figma CSS often exports gradient text as:
|
|
716
|
+
// color: transparent; background: linear-gradient(...); background-clip: text.
|
|
717
|
+
// Flutter Text doesn't support CSS background-clip, so fallback to first gradient color.
|
|
718
|
+
const clip = `${props['background-clip'] || ''} ${props['-webkit-background-clip'] || ''}`.toLowerCase()
|
|
719
|
+
if (clip.includes('text')) {
|
|
720
|
+
const gradientSource = props['background-image'] || props['background']
|
|
721
|
+
const gradientColors = parseGradientColors(gradientSource)
|
|
722
|
+
if (gradientColors.length) color = gradientColors[0]
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
const lineHeight = cssPxToDouble(props['line-height'])
|
|
726
|
+
const heightRatio = size && lineHeight ? (lineHeight / size) : null
|
|
727
|
+
const fontFamily = fontFamilyFromCss(props['font-family'])
|
|
728
|
+
const letterSpacing = cssPxToDouble(props['letter-spacing'])
|
|
729
|
+
if (!size && !weight && !color && !heightRatio && !fontFamily && letterSpacing == null) return null
|
|
730
|
+
const args = []
|
|
731
|
+
if (fontFamily) args.push(`fontFamily: ${fontFamily}`)
|
|
732
|
+
if (size) args.push(`fontSize: ${size}`)
|
|
733
|
+
if (color) args.push(`color: ${color}`)
|
|
734
|
+
if (weight) args.push(`fontWeight: ${weight}`)
|
|
735
|
+
if (heightRatio) args.push(`height: ${heightRatio}`)
|
|
736
|
+
if (letterSpacing != null) args.push(`letterSpacing: ${letterSpacing}`)
|
|
737
|
+
return `TextStyle(${args.join(', ')})`
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
function textAlignFromProps(props) {
|
|
741
|
+
const align = String(props['text-align'] || '').toLowerCase()
|
|
742
|
+
if (align === 'center') return 'TextAlign.center'
|
|
743
|
+
if (align === 'right' || align === 'end') return 'TextAlign.right'
|
|
744
|
+
if (align === 'left' || align === 'start') return 'TextAlign.left'
|
|
745
|
+
return null
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function paddingFromProps(props) {
|
|
749
|
+
const p = props['padding']
|
|
750
|
+
const pt = props['padding-top']
|
|
751
|
+
const pr = props['padding-right']
|
|
752
|
+
const pb = props['padding-bottom']
|
|
753
|
+
const pl = props['padding-left']
|
|
754
|
+
if (p) {
|
|
755
|
+
const box = parseBoxValues(p)
|
|
756
|
+
if (box) return `EdgeInsets.fromLTRB(${box.left}, ${box.top}, ${box.right}, ${box.bottom})`
|
|
757
|
+
const v = cssPxToDouble(p)
|
|
758
|
+
if (v !== null) return `EdgeInsets.all(${v})`
|
|
759
|
+
}
|
|
760
|
+
const top = cssPxToDouble(pt) || 0
|
|
761
|
+
const right = cssPxToDouble(pr) || 0
|
|
762
|
+
const bottom = cssPxToDouble(pb) || 0
|
|
763
|
+
const left = cssPxToDouble(pl) || 0
|
|
764
|
+
if (top || right || bottom || left) return `EdgeInsets.fromLTRB(${left}, ${top}, ${right}, ${bottom})`
|
|
765
|
+
return null
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
function borderRadiusFromProps(props) {
|
|
769
|
+
const v = props['border-radius']
|
|
770
|
+
const box = parseBoxValues(v)
|
|
771
|
+
if (box) {
|
|
772
|
+
if (box.top === box.right && box.top === box.bottom && box.top === box.left) {
|
|
773
|
+
return `BorderRadius.circular(${box.top})`
|
|
774
|
+
}
|
|
775
|
+
return `BorderRadius.only(topLeft: Radius.circular(${box.top}), topRight: Radius.circular(${box.right}), bottomRight: Radius.circular(${box.bottom}), bottomLeft: Radius.circular(${box.left}))`
|
|
776
|
+
}
|
|
777
|
+
const n = cssPxToDouble(v)
|
|
778
|
+
if (n !== null) return `BorderRadius.circular(${n})`
|
|
779
|
+
return null
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function marginFromProps(props) {
|
|
783
|
+
const m = props['margin']
|
|
784
|
+
const mt = props['margin-top']
|
|
785
|
+
const mr = props['margin-right']
|
|
786
|
+
const mb = props['margin-bottom']
|
|
787
|
+
const ml = props['margin-left']
|
|
788
|
+
if (m) {
|
|
789
|
+
const box = parseBoxValues(m)
|
|
790
|
+
if (box) return `EdgeInsets.fromLTRB(${box.left}, ${box.top}, ${box.right}, ${box.bottom})`
|
|
791
|
+
const v = cssPxToDouble(m)
|
|
792
|
+
if (v !== null) return `EdgeInsets.all(${v})`
|
|
793
|
+
}
|
|
794
|
+
const top = cssPxToDouble(mt) || 0
|
|
795
|
+
const right = cssPxToDouble(mr) || 0
|
|
796
|
+
const bottom = cssPxToDouble(mb) || 0
|
|
797
|
+
const left = cssPxToDouble(ml) || 0
|
|
798
|
+
if (top || right || bottom || left) return `EdgeInsets.fromLTRB(${left}, ${top}, ${right}, ${bottom})`
|
|
799
|
+
return null
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function borderFromProps(props) {
|
|
803
|
+
const border = props['border']
|
|
804
|
+
const borderWidth = props['border-width']
|
|
805
|
+
const borderColor = props['border-color']
|
|
806
|
+
let width = borderWidth ? cssPxToDouble(borderWidth) : null
|
|
807
|
+
let color = borderColor ? cssColorToAppColor(borderColor) : null
|
|
808
|
+
if (border) {
|
|
809
|
+
if (width == null) {
|
|
810
|
+
const m = String(border).match(/(-?\d*\.?\d+)px/)
|
|
811
|
+
if (m) width = parseFloat(m[1])
|
|
812
|
+
}
|
|
813
|
+
if (!color) {
|
|
814
|
+
const colorMatch = String(border).match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
|
|
815
|
+
if (colorMatch) color = cssColorToAppColor(colorMatch[1])
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
if (width == null && !color) return null
|
|
819
|
+
if (width == null) width = 1
|
|
820
|
+
if (!color) color = 'AppColors.black'
|
|
821
|
+
return `Border.all(color: ${color}, width: ${width})`
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function borderColorFromProps(props) {
|
|
825
|
+
if (!props) return null
|
|
826
|
+
const border = props['border']
|
|
827
|
+
const borderColor = props['border-color']
|
|
828
|
+
let color = borderColor ? cssColorToAppColor(borderColor) : null
|
|
829
|
+
if (!color && border) {
|
|
830
|
+
const colorMatch = String(border).match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
|
|
831
|
+
if (colorMatch) color = cssColorToAppColor(colorMatch[1])
|
|
832
|
+
}
|
|
833
|
+
return color
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
function boxShadowFromProps(props) {
|
|
837
|
+
const shadow = props['box-shadow']
|
|
838
|
+
if (!shadow) return null
|
|
839
|
+
const parts = String(shadow).split(/,(?![^(]*\))/).map(s => s.trim()).filter(Boolean)
|
|
840
|
+
const shadows = []
|
|
841
|
+
for (const part of parts) {
|
|
842
|
+
const colorMatch = part.match(/(rgba?\([^)]*\)|#[0-9a-f]{3,8}|var\([^)]*\))/i)
|
|
843
|
+
const color = colorMatch ? cssColorToAppColor(colorMatch[1]) : null
|
|
844
|
+
const numbers = part.replace(colorMatch ? colorMatch[1] : '', '').trim().split(/\s+/).filter(Boolean)
|
|
845
|
+
const ox = cssPxToDouble(numbers[0]) ?? 0
|
|
846
|
+
const oy = cssPxToDouble(numbers[1]) ?? 0
|
|
847
|
+
const blur = cssPxToDouble(numbers[2]) ?? 0
|
|
848
|
+
const spread = cssPxToDouble(numbers[3]) ?? 0
|
|
849
|
+
if (!color) continue
|
|
850
|
+
shadows.push(`BoxShadow(offset: Offset(${ox}, ${oy}), blurRadius: ${blur}, spreadRadius: ${spread}, color: ${color})`)
|
|
851
|
+
}
|
|
852
|
+
if (!shadows.length) return null
|
|
853
|
+
return shadows.join(', ')
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function boxDecorationFromProps(props, options = {}) {
|
|
857
|
+
const { omitBackgroundColor = false } = options
|
|
858
|
+
const bg = props['background'] || props['background-color']
|
|
859
|
+
const bgImg = props['background-image'] || props['backgroundImage']
|
|
860
|
+
const bgGradientSource = bg && /linear-gradient/i.test(bg) ? bg : null
|
|
861
|
+
const gradientSource = bgImg || bgGradientSource
|
|
862
|
+
const gradient = gradientSource && /linear-gradient/i.test(gradientSource)
|
|
863
|
+
? (parseLinearGradient(gradientSource) || 'AppColors.primaryBackgroundGradient()')
|
|
864
|
+
: null
|
|
865
|
+
const color = bg && !bgGradientSource ? cssColorToAppColor(bg) : null
|
|
866
|
+
const radius = borderRadiusFromProps(props)
|
|
867
|
+
const border = borderFromProps(props)
|
|
868
|
+
const shadows = boxShadowFromProps(props)
|
|
869
|
+
const parts = []
|
|
870
|
+
if (color && !omitBackgroundColor) parts.push(`color: ${color}`)
|
|
871
|
+
if (gradient) parts.push(`gradient: ${gradient}`)
|
|
872
|
+
if (radius) parts.push(`borderRadius: ${radius}`)
|
|
873
|
+
if (border) parts.push(`border: ${border}`)
|
|
874
|
+
if (shadows) parts.push(`boxShadow: [${shadows}]`)
|
|
875
|
+
return parts.length ? `BoxDecoration(${parts.join(', ')})` : null
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function usesAppOwnedInputBackground(widget) {
|
|
879
|
+
if (!widget) return false
|
|
880
|
+
const normalized = String(widget).trim()
|
|
881
|
+
return normalized.startsWith('AppInput(') || normalized.startsWith('_GeneratedDateTimeField(')
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
function usesAppOwnedProgressBarBackground(widget) {
|
|
885
|
+
if (!widget) return false
|
|
886
|
+
const normalized = String(widget).trim()
|
|
887
|
+
return normalized.startsWith('AppProgressBar(')
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function toDartLiteral(s) {
|
|
891
|
+
const t = String(s || '')
|
|
892
|
+
return `'${t.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
function isTextNode(node) {
|
|
896
|
+
return node.type === 'JSXText'
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function jsxElementName(el) {
|
|
900
|
+
const n = el.openingElement.name
|
|
901
|
+
if (n.type === 'JSXIdentifier') return n.name
|
|
902
|
+
return 'div'
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
function getClassNameAttr(el) {
|
|
906
|
+
const attrs = el.openingElement.attributes || []
|
|
907
|
+
for (const a of attrs) {
|
|
908
|
+
if (a.type === 'JSXAttribute' && a.name && a.name.name === 'className') {
|
|
909
|
+
if (a.value && a.value.type === 'StringLiteral') return a.value.value
|
|
910
|
+
if (a.value && a.value.type === 'JSXExpressionContainer') {
|
|
911
|
+
const expr = a.value.expression
|
|
912
|
+
if (expr.type === 'StringLiteral') return expr.value
|
|
913
|
+
if (expr.type === 'MemberExpression') {
|
|
914
|
+
const obj = expr.object
|
|
915
|
+
const prop = expr.property
|
|
916
|
+
if ((obj.type === 'Identifier' && obj.name === 'styles') && prop.type === 'Identifier') {
|
|
917
|
+
return prop.name
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
return null
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function assetForClassName(className) {
|
|
927
|
+
if (!className) return null
|
|
928
|
+
if (ICONS_MAP[className]) return ICONS_MAP[className]
|
|
929
|
+
const n = className.toLowerCase()
|
|
930
|
+
const map = [
|
|
931
|
+
{ key: 'calendar', asset: 'AppAssets.iconsCalendarSvg' },
|
|
932
|
+
{ key: 'search', asset: 'AppAssets.iconsSearchSvg' },
|
|
933
|
+
{ key: 'user', asset: 'AppAssets.iconsUserSvg' },
|
|
934
|
+
{ key: 'key', asset: 'AppAssets.iconsUserKeySvg' },
|
|
935
|
+
{ key: 'family', asset: 'AppAssets.iconsFamilySvg' },
|
|
936
|
+
{ key: 'phone', asset: 'AppAssets.iconsPhoneSvg' },
|
|
937
|
+
{ key: 'location', asset: 'AppAssets.iconsLocationSvg' },
|
|
938
|
+
{ key: 'battery', asset: 'AppAssets.iconsBatterySvg' },
|
|
939
|
+
{ key: 'solar', asset: 'AppAssets.iconsSolarSvg' },
|
|
940
|
+
{ key: 'water', asset: 'AppAssets.iconsWaterSystemSvg' },
|
|
941
|
+
{ key: 'home', asset: 'AppAssets.iconsHomeSvg' },
|
|
942
|
+
{ key: 'notification', asset: 'AppAssets.iconsNotificationSvg' },
|
|
943
|
+
{ key: 'percent', asset: 'AppAssets.iconsPercentSvg' },
|
|
944
|
+
{ key: 'plus', asset: 'AppAssets.iconsPlusSvg' },
|
|
945
|
+
]
|
|
946
|
+
for (const m of map) {
|
|
947
|
+
if (n.includes(m.key)) return m.asset
|
|
948
|
+
}
|
|
949
|
+
return null
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function sizeFromProps(props) {
|
|
953
|
+
const w = cssPxToDouble(props['width'])
|
|
954
|
+
const h = cssPxToDouble(props['height'])
|
|
955
|
+
return { w, h }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function positionFromProps(props) {
|
|
959
|
+
const pos = (props['position'] || '').toLowerCase()
|
|
960
|
+
if (pos !== 'absolute') return null
|
|
961
|
+
const top = cssPxToDouble(props['top'])
|
|
962
|
+
const left = cssPxToDouble(props['left'])
|
|
963
|
+
const right = cssPxToDouble(props['right'])
|
|
964
|
+
const bottom = cssPxToDouble(props['bottom'])
|
|
965
|
+
if (top == null && left == null && right == null && bottom == null) return null
|
|
966
|
+
return { top, left, right, bottom }
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
function zIndexFromProps(props) {
|
|
970
|
+
const raw = props['z-index']
|
|
971
|
+
const n = raw == null ? NaN : parseInt(raw, 10)
|
|
972
|
+
return Number.isFinite(n) ? n : 0
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
function flexGrowFromProps(props) {
|
|
976
|
+
const raw = props['flex-grow'] ?? props['flex']
|
|
977
|
+
if (raw == null) return 0
|
|
978
|
+
const m = String(raw).trim().match(/-?\d+(\.\d+)?/)
|
|
979
|
+
if (!m) return 0
|
|
980
|
+
const n = parseFloat(m[0])
|
|
981
|
+
return Number.isFinite(n) ? n : 0
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function rotationFromProps(props) {
|
|
985
|
+
const raw = props['rotate'] || props['transform']
|
|
986
|
+
if (!raw) return null
|
|
987
|
+
const m = String(raw).match(/(-?\d+(\.\d+)?)deg/i)
|
|
988
|
+
if (!m) return null
|
|
989
|
+
const deg = parseFloat(m[1])
|
|
990
|
+
if (!Number.isFinite(deg)) return null
|
|
991
|
+
return deg * Math.PI / 180
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function estimateStackSize(absKids) {
|
|
995
|
+
let maxRight = 0
|
|
996
|
+
let maxBottom = 0
|
|
997
|
+
for (const child of absKids) {
|
|
998
|
+
const pos = child.position || {}
|
|
999
|
+
const w = child.size?.w ?? 0
|
|
1000
|
+
const h = child.size?.h ?? 0
|
|
1001
|
+
const left = pos.left ?? 0
|
|
1002
|
+
const top = pos.top ?? 0
|
|
1003
|
+
const rightEdge = (pos.left != null ? left : (pos.right != null ? pos.right : 0)) + w
|
|
1004
|
+
const bottomEdge = (pos.top != null ? top : (pos.bottom != null ? pos.bottom : 0)) + h
|
|
1005
|
+
if (rightEdge > maxRight) maxRight = rightEdge
|
|
1006
|
+
if (bottomEdge > maxBottom) maxBottom = bottomEdge
|
|
1007
|
+
}
|
|
1008
|
+
const width = maxRight > 0 ? maxRight : null
|
|
1009
|
+
const height = maxBottom > 0 ? maxBottom : null
|
|
1010
|
+
if (width == null && height == null) return null
|
|
1011
|
+
return { w: width, h: height }
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function flexConfigFromProps(props) {
|
|
1015
|
+
if ((props['display'] || '').includes('flex')) {
|
|
1016
|
+
// CSS default is `row` when `flex-direction` is omitted.
|
|
1017
|
+
const dir = (props['flex-direction'] || 'row').toLowerCase()
|
|
1018
|
+
const align = (props['align-items'] || '').toLowerCase()
|
|
1019
|
+
const justify = (props['justify-content'] || '').toLowerCase()
|
|
1020
|
+
const gapCol = cssPxToDouble(props['column-gap'])
|
|
1021
|
+
const gapRow = cssPxToDouble(props['row-gap'])
|
|
1022
|
+
const isRow = dir.includes('row')
|
|
1023
|
+
const main = (() => {
|
|
1024
|
+
switch (justify) {
|
|
1025
|
+
case 'center': return 'MainAxisAlignment.center'
|
|
1026
|
+
case 'space-between': return 'MainAxisAlignment.spaceBetween'
|
|
1027
|
+
case 'space-around': return 'MainAxisAlignment.spaceAround'
|
|
1028
|
+
case 'flex-end': return 'MainAxisAlignment.end'
|
|
1029
|
+
default: return 'MainAxisAlignment.start'
|
|
1030
|
+
}
|
|
1031
|
+
})()
|
|
1032
|
+
const cross = (() => {
|
|
1033
|
+
switch (align) {
|
|
1034
|
+
case 'center': return 'CrossAxisAlignment.center'
|
|
1035
|
+
case 'flex-end': return 'CrossAxisAlignment.end'
|
|
1036
|
+
case 'stretch': return 'CrossAxisAlignment.stretch'
|
|
1037
|
+
default: return 'CrossAxisAlignment.start'
|
|
1038
|
+
}
|
|
1039
|
+
})()
|
|
1040
|
+
return { isRow, main, cross, gap: isRow ? gapCol : gapRow }
|
|
1041
|
+
}
|
|
1042
|
+
return null
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function getAttr(el, name) {
|
|
1046
|
+
const attrs = el.openingElement.attributes || []
|
|
1047
|
+
for (const a of attrs) {
|
|
1048
|
+
if (a.type === 'JSXAttribute' && a.name && a.name.name === name) {
|
|
1049
|
+
if (a.value && a.value.type === 'StringLiteral') return a.value.value
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
return null
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function normalizeTextValue(raw) {
|
|
1056
|
+
return String(raw || '').replace(/\s+/g, ' ').trim()
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function extractTextsFromElement(el, out = []) {
|
|
1060
|
+
if (!el || !el.children) return out
|
|
1061
|
+
for (const child of el.children) {
|
|
1062
|
+
if (child.type === 'JSXText') {
|
|
1063
|
+
const text = normalizeTextValue(child.value)
|
|
1064
|
+
if (text) out.push(text)
|
|
1065
|
+
continue
|
|
1066
|
+
}
|
|
1067
|
+
if (child.type === 'JSXExpressionContainer' && child.expression?.type === 'StringLiteral') {
|
|
1068
|
+
const text = normalizeTextValue(child.expression.value)
|
|
1069
|
+
if (text) out.push(text)
|
|
1070
|
+
continue
|
|
1071
|
+
}
|
|
1072
|
+
if (child.type === 'JSXElement') {
|
|
1073
|
+
extractTextsFromElement(child, out)
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return out
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function uniqueTexts(texts) {
|
|
1080
|
+
const result = []
|
|
1081
|
+
const seen = new Set()
|
|
1082
|
+
for (const t of texts) {
|
|
1083
|
+
const key = normalizeTextValue(t)
|
|
1084
|
+
if (!key || seen.has(key)) continue
|
|
1085
|
+
seen.add(key)
|
|
1086
|
+
result.push(key)
|
|
1087
|
+
}
|
|
1088
|
+
return result
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
function isHintLikeText(text) {
|
|
1092
|
+
const raw = normalizeTextValue(text)
|
|
1093
|
+
if (!raw) return false
|
|
1094
|
+
if (/^\d[\d\s./:-]*$/.test(raw)) return true
|
|
1095
|
+
if (/^(yyyy|yy|mm|dd)/i.test(raw)) return true
|
|
1096
|
+
if (raw.includes('YYYY') || raw.includes('MM') || raw.includes('DD')) return true
|
|
1097
|
+
if (raw.length >= 24) return true
|
|
1098
|
+
return false
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function deriveInputLabelHint(el) {
|
|
1102
|
+
const texts = uniqueTexts(extractTextsFromElement(el, []))
|
|
1103
|
+
if (!texts.length) return { label: null, hint: null }
|
|
1104
|
+
const labelCandidate = texts.find(t => !isHintLikeText(t)) || texts[0]
|
|
1105
|
+
const hintCandidate = texts.find(t => t !== labelCandidate) || null
|
|
1106
|
+
return { label: labelCandidate || null, hint: hintCandidate || null }
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function looksLikeDateText(rawValue) {
|
|
1110
|
+
const raw = normalizeTextValue(rawValue)
|
|
1111
|
+
if (!raw) return false
|
|
1112
|
+
const lower = raw.toLowerCase()
|
|
1113
|
+
if (raw.includes('生年')) return true
|
|
1114
|
+
if (raw.includes('生年月日')) return true
|
|
1115
|
+
if (raw.includes('誕生日')) return true
|
|
1116
|
+
if (raw.includes('出生')) return true
|
|
1117
|
+
if (raw.includes('設置年')) return true
|
|
1118
|
+
if (raw.includes('施工年')) return true
|
|
1119
|
+
if (raw.includes('実施年')) return true
|
|
1120
|
+
if (raw.includes('年度')) return true
|
|
1121
|
+
if (/\bdob\b/.test(lower)) return true
|
|
1122
|
+
if (lower.includes('birth')) return true
|
|
1123
|
+
if (/(yyyy|yy).*(mm).*(dd)/i.test(raw)) return true
|
|
1124
|
+
if (/\b\d{4}[\/.-]\d{1,2}[\/.-]\d{1,2}\b/.test(raw)) return true
|
|
1125
|
+
if (/\d{4}年\d{1,2}月\d{1,2}日/.test(raw)) return true
|
|
1126
|
+
if (raw.includes('年月日')) return true
|
|
1127
|
+
if (raw.includes('日付')) return true
|
|
1128
|
+
if (/\bdate\b/.test(lower)) return true
|
|
1129
|
+
if (raw.includes('カレンダー')) return true
|
|
1130
|
+
if (lower.includes('calendar')) return true
|
|
1131
|
+
if (raw.includes('日程')) return true
|
|
1132
|
+
const hasYear = raw.includes('年')
|
|
1133
|
+
const hasMonth = raw.includes('月')
|
|
1134
|
+
const hasDay = raw.includes('日')
|
|
1135
|
+
if (hasYear && hasMonth && hasDay) return true
|
|
1136
|
+
if (hasMonth && hasDay) return true
|
|
1137
|
+
return false
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
function looksLikeDateField({ className, inputType, label, hint }) {
|
|
1141
|
+
if (inputType === 'date' || inputType === 'datetime-local' || inputType === 'month') return true
|
|
1142
|
+
return [className, label, hint].some(value => looksLikeDateText(value))
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function extractSvgAssetCall(widget) {
|
|
1146
|
+
if (!widget) return null
|
|
1147
|
+
const match = String(widget).match(/SvgPicture\.asset\([^)]*\)/)
|
|
1148
|
+
return match ? match[0] : null
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
function isSmallIconSize(size) {
|
|
1152
|
+
if (!size) return false
|
|
1153
|
+
const hasWidth = size.w != null
|
|
1154
|
+
const hasHeight = size.h != null
|
|
1155
|
+
if (!hasWidth && !hasHeight) return false
|
|
1156
|
+
const widthOk = !hasWidth || size.w <= 128
|
|
1157
|
+
const heightOk = !hasHeight || size.h <= 128
|
|
1158
|
+
return widthOk && heightOk
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
function normalizeGeneratedSize(size) {
|
|
1162
|
+
if (!size) return size
|
|
1163
|
+
if (isSmallIconSize(size)) return size
|
|
1164
|
+
if (size.h != null && size.h > 1200) {
|
|
1165
|
+
return { ...size, h: null }
|
|
1166
|
+
}
|
|
1167
|
+
return size
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function simplifySvgWidget(widget, size) {
|
|
1171
|
+
const svgCall = extractSvgAssetCall(widget)
|
|
1172
|
+
if (!svgCall) return null
|
|
1173
|
+
if (!size || (size.w == null && size.h == null)) return svgCall
|
|
1174
|
+
const width = size.w != null ? `width: ${size.w}, ` : ''
|
|
1175
|
+
const height = size.h != null ? `height: ${size.h}, ` : ''
|
|
1176
|
+
return `SizedBox(${width}${height}child: ${svgCall})`
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function shouldStripIconPositioning(size, position, rotate) {
|
|
1180
|
+
return (position != null || rotate != null) && isSmallIconSize(size)
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
function looksLikeInputClass(className) {
|
|
1184
|
+
if (!className) return false
|
|
1185
|
+
const normalized = String(className).toLowerCase()
|
|
1186
|
+
return normalized.includes('textfield')
|
|
1187
|
+
|| normalized.includes('input')
|
|
1188
|
+
|| normalized.includes('textarea')
|
|
1189
|
+
|| normalized.includes('formfield')
|
|
1190
|
+
|| normalized.includes('form_field')
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function looksLikeButtonClass(className) {
|
|
1194
|
+
if (!className) return false
|
|
1195
|
+
const normalized = String(className).toLowerCase()
|
|
1196
|
+
return normalized.includes('nextbutton')
|
|
1197
|
+
|| /(^|[-_])button([0-9]|$|[-_])/.test(normalized)
|
|
1198
|
+
|| normalized.endsWith('btn')
|
|
1199
|
+
|| normalized.includes('btn_')
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function isCheckboxClassName(className) {
|
|
1203
|
+
if (!className) return false
|
|
1204
|
+
const normalized = String(className).toLowerCase()
|
|
1205
|
+
return normalized.includes('checkbox') || normalized.includes('check_box')
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function isUncheckedCheckboxClass(className) {
|
|
1209
|
+
if (!className) return false
|
|
1210
|
+
const normalized = String(className).toLowerCase()
|
|
1211
|
+
return normalized.includes('checkbox2')
|
|
1212
|
+
|| normalized.includes('unchecked')
|
|
1213
|
+
|| normalized.includes('uncheck')
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function buildCheckboxWidgetFromElement(el, cssMap) {
|
|
1217
|
+
if (!el || !Array.isArray(el.children)) return null
|
|
1218
|
+
const directElements = el.children.filter(c => c.type === 'JSXElement')
|
|
1219
|
+
if (directElements.length < 2 || directElements.length > 3) return null
|
|
1220
|
+
const checkboxNode = directElements.find(child => isCheckboxClassName(getClassNameAttr(child)))
|
|
1221
|
+
if (!checkboxNode) return null
|
|
1222
|
+
const label = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
|
|
1223
|
+
if (!label) return null
|
|
1224
|
+
const checkboxClass = getClassNameAttr(checkboxNode)
|
|
1225
|
+
const initialChecked = !isUncheckedCheckboxClass(checkboxClass)
|
|
1226
|
+
const checkboxProps = (checkboxClass && cssMap && cssMap[checkboxClass]) ? cssMap[checkboxClass] : {}
|
|
1227
|
+
const borderColor = borderColorFromProps(checkboxProps)
|
|
1228
|
+
const widgetArgs = [
|
|
1229
|
+
`title: ${toDartLiteral(label)}`,
|
|
1230
|
+
`initialChecked: ${initialChecked ? 'true' : 'false'}`
|
|
1231
|
+
]
|
|
1232
|
+
if (borderColor) widgetArgs.push(`borderColor: ${borderColor}`)
|
|
1233
|
+
return `_GeneratedCheckbox(${widgetArgs.join(', ')})`
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
function isRadioClassName(className) {
|
|
1237
|
+
if (!className) return false
|
|
1238
|
+
const normalized = String(className).toLowerCase()
|
|
1239
|
+
return /^radio\d*$/.test(normalized)
|
|
1240
|
+
|| /(^|[-_])radio\d*($|[-_])/.test(normalized)
|
|
1241
|
+
|| normalized.includes('radiobutton')
|
|
1242
|
+
|| normalized.includes('radio_button')
|
|
1243
|
+
|| normalized.includes('radio-option')
|
|
1244
|
+
|| normalized.includes('radiooption')
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
function findRadioClassInTree(el) {
|
|
1248
|
+
if (!el || el.type !== 'JSXElement') return null
|
|
1249
|
+
const cls = getClassNameAttr(el)
|
|
1250
|
+
if (isRadioClassName(cls)) return cls
|
|
1251
|
+
for (const child of el.children || []) {
|
|
1252
|
+
if (child.type !== 'JSXElement') continue
|
|
1253
|
+
const match = findRadioClassInTree(child)
|
|
1254
|
+
if (match) return match
|
|
1255
|
+
}
|
|
1256
|
+
return null
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function extractRadioOptionFromElement(el, cssMap) {
|
|
1260
|
+
if (!el || el.type !== 'JSXElement') return null
|
|
1261
|
+
const radioClass = findRadioClassInTree(el)
|
|
1262
|
+
if (!radioClass) return null
|
|
1263
|
+
const texts = uniqueTexts(extractTextsFromElement(el, []))
|
|
1264
|
+
const label = texts.find(Boolean)
|
|
1265
|
+
if (!label) return null
|
|
1266
|
+
const radioProps = (radioClass && cssMap && cssMap[radioClass]) ? cssMap[radioClass] : {}
|
|
1267
|
+
const borderColor = borderColorFromProps(radioProps)
|
|
1268
|
+
const normalizedRadio = radioClass.toLowerCase()
|
|
1269
|
+
const selected = normalizedRadio === 'radio2' || normalizedRadio === 'radio3' || normalizedRadio.includes('checked')
|
|
1270
|
+
return { label, selected, borderColor }
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function buildRadioGroupWidgetFromElement(el, cssMap) {
|
|
1274
|
+
if (!el || !Array.isArray(el.children)) return null
|
|
1275
|
+
const directElements = el.children.filter(c => c.type === 'JSXElement')
|
|
1276
|
+
if (directElements.length < 2) return null
|
|
1277
|
+
const options = []
|
|
1278
|
+
for (const child of directElements) {
|
|
1279
|
+
const option = extractRadioOptionFromElement(child, cssMap)
|
|
1280
|
+
if (!option) return null
|
|
1281
|
+
options.push(option)
|
|
1282
|
+
}
|
|
1283
|
+
if (options.length < 2) return null
|
|
1284
|
+
const optionLiterals = options.map(option => {
|
|
1285
|
+
const valueLiteral = toDartLiteral(option.label)
|
|
1286
|
+
const labelLiteral = toDartLiteral(option.label)
|
|
1287
|
+
return `AppRadioOption(value: ${valueLiteral}, label: ${labelLiteral})`
|
|
1288
|
+
})
|
|
1289
|
+
const selected = options.find(o => o.selected) || options[0]
|
|
1290
|
+
const inactive = options.find(o => !o.selected && o.borderColor)
|
|
1291
|
+
const widgetArgs = [
|
|
1292
|
+
`initialValue: ${toDartLiteral(selected.label)}`,
|
|
1293
|
+
`options: [${optionLiterals.join(', ')}]`
|
|
1294
|
+
]
|
|
1295
|
+
if (selected.borderColor) widgetArgs.push(`activeColor: ${selected.borderColor}`)
|
|
1296
|
+
if (inactive?.borderColor) widgetArgs.push(`inactiveColor: ${inactive.borderColor}`)
|
|
1297
|
+
return `_GeneratedRadioGroup(${widgetArgs.join(', ')})`
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function roundNumber(value, digits = 4) {
|
|
1301
|
+
const factor = 10 ** digits
|
|
1302
|
+
return Math.round(value * factor) / factor
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function parsePaddingValues(props) {
|
|
1306
|
+
const fromPadding = parseBoxValues(props['padding'])
|
|
1307
|
+
if (fromPadding) return fromPadding
|
|
1308
|
+
return {
|
|
1309
|
+
top: cssPxToDouble(props['padding-top']) || 0,
|
|
1310
|
+
right: cssPxToDouble(props['padding-right']) || 0,
|
|
1311
|
+
bottom: cssPxToDouble(props['padding-bottom']) || 0,
|
|
1312
|
+
left: cssPxToDouble(props['padding-left']) || 0,
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function solidColorFromBackground(props) {
|
|
1317
|
+
const bg = props['background-color'] || props['background']
|
|
1318
|
+
if (!bg || /linear-gradient/i.test(String(bg))) return null
|
|
1319
|
+
return cssColorToAppColor(bg)
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function gradientOrSolidFromBackground(props) {
|
|
1323
|
+
const bgSource = props['background-image'] || props['background']
|
|
1324
|
+
if (bgSource && /linear-gradient/i.test(String(bgSource))) {
|
|
1325
|
+
const gradient = parseLinearGradient(bgSource)
|
|
1326
|
+
if (gradient) return gradient
|
|
1327
|
+
}
|
|
1328
|
+
const solid = solidColorFromBackground(props)
|
|
1329
|
+
if (solid) return `LinearGradient(colors: [${solid}, ${solid}])`
|
|
1330
|
+
return null
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function buildProgressBarWidgetFromElement(el, cssMap) {
|
|
1334
|
+
if (!el || !Array.isArray(el.children)) return null
|
|
1335
|
+
const directElements = el.children.filter(c => c.type === 'JSXElement')
|
|
1336
|
+
if (directElements.length !== 1) return null
|
|
1337
|
+
|
|
1338
|
+
const parentClass = getClassNameAttr(el)
|
|
1339
|
+
const child = directElements[0]
|
|
1340
|
+
const childClass = getClassNameAttr(child)
|
|
1341
|
+
if (!parentClass || !childClass) return null
|
|
1342
|
+
|
|
1343
|
+
const parentProps = (cssMap && cssMap[parentClass]) ? cssMap[parentClass] : {}
|
|
1344
|
+
const childProps = (cssMap && cssMap[childClass]) ? cssMap[childClass] : {}
|
|
1345
|
+
|
|
1346
|
+
const parentSize = normalizeGeneratedSize(sizeFromProps(parentProps))
|
|
1347
|
+
const childSize = normalizeGeneratedSize(sizeFromProps(childProps))
|
|
1348
|
+
if (childSize.w == null || childSize.h == null) return null
|
|
1349
|
+
|
|
1350
|
+
const hasProgressLikeBackground = !!solidColorFromBackground(parentProps)
|
|
1351
|
+
const fillGradient = gradientOrSolidFromBackground(childProps)
|
|
1352
|
+
if (!hasProgressLikeBackground || !fillGradient) return null
|
|
1353
|
+
|
|
1354
|
+
const padding = parsePaddingValues(parentProps)
|
|
1355
|
+
let totalWidth = parentSize.w
|
|
1356
|
+
if (totalWidth == null) {
|
|
1357
|
+
const inferred = childSize.w + (padding.left || 0) + (padding.right || 0)
|
|
1358
|
+
if (inferred > childSize.w) totalWidth = inferred
|
|
1359
|
+
}
|
|
1360
|
+
if (totalWidth == null || totalWidth <= 0) return null
|
|
1361
|
+
|
|
1362
|
+
let value = childSize.w / totalWidth
|
|
1363
|
+
if (!Number.isFinite(value)) return null
|
|
1364
|
+
if (value <= 0 || value > 1.05) return null
|
|
1365
|
+
value = Math.max(0, Math.min(1, value))
|
|
1366
|
+
|
|
1367
|
+
const height = parentSize.h ?? childSize.h
|
|
1368
|
+
if (height == null || height <= 0) return null
|
|
1369
|
+
|
|
1370
|
+
const backgroundColor = solidColorFromBackground(parentProps)
|
|
1371
|
+
const radius = borderRadiusFromProps(parentProps) || borderRadiusFromProps(childProps)
|
|
1372
|
+
const args = [
|
|
1373
|
+
`value: ${roundNumber(value)}`,
|
|
1374
|
+
`height: ${roundNumber(height, 2)}`,
|
|
1375
|
+
`width: ${roundNumber(totalWidth, 2)}`,
|
|
1376
|
+
`progressGradient: ${fillGradient}`,
|
|
1377
|
+
'padding: 0',
|
|
1378
|
+
'showDot: false',
|
|
1379
|
+
]
|
|
1380
|
+
if (backgroundColor) args.push(`backgroundColor: ${backgroundColor}`)
|
|
1381
|
+
if (radius) args.push(`borderRadius: ${radius}`)
|
|
1382
|
+
|
|
1383
|
+
return `AppProgressBar(${args.join(', ')})`
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function wrapOverflowIfSized(body, size) {
|
|
1387
|
+
if (process.env.JSX2FLUTTER_ENABLE_OVERFLOW !== '1') return body
|
|
1388
|
+
if (!size || (!size.w && !size.h)) return body
|
|
1389
|
+
const trimmed = body.trim()
|
|
1390
|
+
const isColumn = trimmed.startsWith('Column(')
|
|
1391
|
+
const isRow = trimmed.startsWith('Row(')
|
|
1392
|
+
if (!isColumn && !isRow) return body
|
|
1393
|
+
if (/\b(?:Expanded|Flexible|Spacer)\s*\(/.test(trimmed)) return body
|
|
1394
|
+
if (trimmed.includes('SvgPicture.asset(')) return body
|
|
1395
|
+
|
|
1396
|
+
// Only allow overflow on the main axis of the flex container:
|
|
1397
|
+
// - Column: vertical overflow
|
|
1398
|
+
// - Row: horizontal overflow
|
|
1399
|
+
//
|
|
1400
|
+
// To keep constraints valid, only apply overflow when the cross-axis size is known.
|
|
1401
|
+
// Otherwise, OverflowBox defaults can make the cross-axis unbounded and produce NaN during paint.
|
|
1402
|
+
const overflowWidth = isRow && size.w != null && size.h != null
|
|
1403
|
+
const overflowHeight = isColumn && size.h != null && size.w != null
|
|
1404
|
+
if (!overflowWidth && !overflowHeight) return body
|
|
1405
|
+
|
|
1406
|
+
const overflowArgs = [
|
|
1407
|
+
'alignment: Alignment.topLeft',
|
|
1408
|
+
'minWidth: 0',
|
|
1409
|
+
'minHeight: 0',
|
|
1410
|
+
]
|
|
1411
|
+
overflowArgs.push(`maxWidth: ${overflowWidth ? 'double.infinity' : size.w}`)
|
|
1412
|
+
overflowArgs.push(`maxHeight: ${overflowHeight ? 'double.infinity' : size.h}`)
|
|
1413
|
+
return `OverflowBox(${overflowArgs.join(', ')}, child: ${body})`
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function buildGeneratedRippleButton(titleExpr) {
|
|
1417
|
+
return `RippleButton(title: ${titleExpr}, backgroundColor: Colors.transparent, minWidth: 0, padding: EdgeInsets.zero, onTap: () {})`
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function buildWidgetFromElement(el, cssMap, topClassName, skipRootDecoration) {
|
|
1421
|
+
const tag = jsxElementName(el).toLowerCase()
|
|
1422
|
+
const className = getClassNameAttr(el)
|
|
1423
|
+
const props = className && cssMap[className] ? cssMap[className] : {}
|
|
1424
|
+
let size = sizeFromProps(props)
|
|
1425
|
+
size = normalizeGeneratedSize(size)
|
|
1426
|
+
const position = positionFromProps(props)
|
|
1427
|
+
const zIndex = zIndexFromProps(props)
|
|
1428
|
+
const flexGrow = flexGrowFromProps(props)
|
|
1429
|
+
const rotate = rotationFromProps(props)
|
|
1430
|
+
const overflowHidden = (props['overflow'] || '').toLowerCase() === 'hidden'
|
|
1431
|
+
const inputType = (getAttr(el, 'type') || '').toLowerCase()
|
|
1432
|
+
const inputPlaceholder = getAttr(el, 'placeholder')
|
|
1433
|
+
const inputLabel = getAttr(el, 'label')
|
|
1434
|
+
const inputValue = getAttr(el, 'value')
|
|
1435
|
+
let semanticWidget = null
|
|
1436
|
+
if (tag === 'input' || tag === 'textarea' || (tag === 'div' && looksLikeInputClass(className))) {
|
|
1437
|
+
if (inputType === 'radio') {
|
|
1438
|
+
const radioLabel = inputLabel || inputValue || className || 'Option'
|
|
1439
|
+
semanticWidget = `_GeneratedRadioGroup(initialValue: ${toDartLiteral(radioLabel)}, options: [AppRadioOption(value: ${toDartLiteral(radioLabel)}, label: ${toDartLiteral(radioLabel)})])`
|
|
1440
|
+
} else {
|
|
1441
|
+
const derived = deriveInputLabelHint(el)
|
|
1442
|
+
const finalLabel = inputLabel || (tag === 'textarea' ? (derived.label || className || '内容') : derived.label)
|
|
1443
|
+
const finalHint = inputPlaceholder || inputValue || derived.hint
|
|
1444
|
+
const isDateField = tag !== 'textarea' && looksLikeDateField({
|
|
1445
|
+
className,
|
|
1446
|
+
inputType,
|
|
1447
|
+
label: finalLabel,
|
|
1448
|
+
hint: finalHint
|
|
1449
|
+
})
|
|
1450
|
+
const widgetArgs = []
|
|
1451
|
+
if (finalLabel) widgetArgs.push(`label: ${toDartLiteral(finalLabel)}`)
|
|
1452
|
+
if (finalHint) widgetArgs.push(`hint: ${toDartLiteral(finalHint)}`)
|
|
1453
|
+
if (isDateField) {
|
|
1454
|
+
semanticWidget = `_GeneratedDateTimeField(${widgetArgs.join(', ')})`
|
|
1455
|
+
} else {
|
|
1456
|
+
const maxLines = tag === 'textarea' ? 4 : 1
|
|
1457
|
+
if (maxLines > 1) widgetArgs.push(`maxLines: ${maxLines}`)
|
|
1458
|
+
semanticWidget = `AppInput(${widgetArgs.join(', ')})`
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
} else if (tag === 'div') {
|
|
1462
|
+
const checkboxWidget = buildCheckboxWidgetFromElement(el, cssMap)
|
|
1463
|
+
if (checkboxWidget) {
|
|
1464
|
+
semanticWidget = checkboxWidget
|
|
1465
|
+
}
|
|
1466
|
+
if (!semanticWidget) {
|
|
1467
|
+
const radioGroupWidget = buildRadioGroupWidgetFromElement(el, cssMap)
|
|
1468
|
+
if (radioGroupWidget) {
|
|
1469
|
+
semanticWidget = radioGroupWidget
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (!semanticWidget) {
|
|
1473
|
+
const progressBarWidget = buildProgressBarWidgetFromElement(el, cssMap)
|
|
1474
|
+
if (progressBarWidget) {
|
|
1475
|
+
semanticWidget = progressBarWidget
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
if (!semanticWidget && looksLikeButtonClass(className)) {
|
|
1479
|
+
const buttonLabel = uniqueTexts(extractTextsFromElement(el, [])).find(Boolean)
|
|
1480
|
+
if (buttonLabel) {
|
|
1481
|
+
semanticWidget = buildGeneratedRippleButton(toDartLiteral(buttonLabel))
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
const children = []
|
|
1486
|
+
if (!semanticWidget) {
|
|
1487
|
+
for (const c of el.children || []) {
|
|
1488
|
+
if (isTextNode(c)) {
|
|
1489
|
+
const text = c.value.trim()
|
|
1490
|
+
if (text) {
|
|
1491
|
+
const style = textStyleFromProps(props)
|
|
1492
|
+
const align = textAlignFromProps(props)
|
|
1493
|
+
const textLiteral = toDartLiteral(text)
|
|
1494
|
+
const gradientText = gradientTextFromProps(props)
|
|
1495
|
+
if (gradientText) {
|
|
1496
|
+
const parts = [`text: ${textLiteral}`]
|
|
1497
|
+
if (style) parts.push(`style: ${style}`)
|
|
1498
|
+
if (align) parts.push(`textAlign: ${align}`)
|
|
1499
|
+
parts.push(`gradient: ${gradientText}`)
|
|
1500
|
+
children.push({ widget: `AppTextGradient(${parts.join(', ')})`, isAbsolute: false, position: null, zIndex: 0, flexGrow: 0, size: { w: null, h: null } })
|
|
1501
|
+
} else {
|
|
1502
|
+
const parts = [textLiteral]
|
|
1503
|
+
if (style) parts.push(`style: ${style}`)
|
|
1504
|
+
if (align) parts.push(`textAlign: ${align}`)
|
|
1505
|
+
children.push({ widget: `Text(${parts.join(', ')})`, isAbsolute: false, position: null, zIndex: 0, flexGrow: 0, size: { w: null, h: null } })
|
|
1506
|
+
}
|
|
1507
|
+
}
|
|
1508
|
+
} else if (c.type === 'JSXElement') {
|
|
1509
|
+
children.push(buildWidgetFromElement(c, cssMap, topClassName, skipRootDecoration))
|
|
1510
|
+
}
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
if (!semanticWidget && tag !== 'img' && shouldStripIconPositioning(size, position, rotate)) {
|
|
1514
|
+
const hasDirectText = (el.children || []).some(child => isTextNode(child) && child.value.trim())
|
|
1515
|
+
const flowOnlyChildren = children.filter(child => !child.isAbsolute)
|
|
1516
|
+
const hasAbsoluteChildren = children.some(child => child.isAbsolute)
|
|
1517
|
+
if (!hasDirectText && !hasAbsoluteChildren && flowOnlyChildren.length === 1) {
|
|
1518
|
+
const simplifiedIcon = simplifySvgWidget(flowOnlyChildren[0].widget, size)
|
|
1519
|
+
if (simplifiedIcon) {
|
|
1520
|
+
return { widget: simplifiedIcon, isAbsolute: false, position: null, zIndex, flexGrow, size }
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
if (tag === 'img') {
|
|
1525
|
+
const src = getAttr(el, 'src') || ''
|
|
1526
|
+
if (/^https?:\/\//.test(src)) {
|
|
1527
|
+
const widget = `Image.network(${toDartLiteral(src)}, fit: BoxFit.cover)`
|
|
1528
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1529
|
+
}
|
|
1530
|
+
const local = resolveLocalAsset(src, className)
|
|
1531
|
+
if (local) {
|
|
1532
|
+
const widget = local.isSvg
|
|
1533
|
+
? `SvgPicture.asset(${local.assetRef})`
|
|
1534
|
+
: `Image.asset(${local.assetRef}, fit: BoxFit.contain)`
|
|
1535
|
+
if (local.isSvg) {
|
|
1536
|
+
const simplified = simplifySvgWidget(widget, size) || widget
|
|
1537
|
+
const stripPlacement = shouldStripIconPositioning(size, position, rotate)
|
|
1538
|
+
const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
|
|
1539
|
+
return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
|
|
1540
|
+
}
|
|
1541
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1542
|
+
}
|
|
1543
|
+
const asset = assetForClassName(className)
|
|
1544
|
+
if (asset) {
|
|
1545
|
+
const widget = `SvgPicture.asset(${asset})`
|
|
1546
|
+
const simplified = simplifySvgWidget(widget, size) || widget
|
|
1547
|
+
const stripPlacement = shouldStripIconPositioning(size, position, rotate)
|
|
1548
|
+
const finalWidget = (rotate != null && !stripPlacement) ? `Transform.rotate(angle: ${rotate}, child: ${simplified})` : simplified
|
|
1549
|
+
return { widget: finalWidget, isAbsolute: stripPlacement ? false : !!position, position: stripPlacement ? null : position, zIndex, flexGrow, size }
|
|
1550
|
+
}
|
|
1551
|
+
const widget = 'SizedBox.shrink()'
|
|
1552
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1553
|
+
}
|
|
1554
|
+
if (tag === 'button') {
|
|
1555
|
+
let labelText = toDartLiteral('Button')
|
|
1556
|
+
if (children.length) {
|
|
1557
|
+
const m = children[0]?.widget?.match(/Text\('([^']*)'/)
|
|
1558
|
+
if (m && m[1]) labelText = `'${m[1]}'`
|
|
1559
|
+
}
|
|
1560
|
+
const widget = buildGeneratedRippleButton(labelText)
|
|
1561
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1562
|
+
}
|
|
1563
|
+
if (tag === 'span' || tag === 'p') {
|
|
1564
|
+
if (children.length === 1) {
|
|
1565
|
+
const widget = children[0].widget
|
|
1566
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1567
|
+
}
|
|
1568
|
+
const widget = children.length
|
|
1569
|
+
? `Row(children: [${children.map(c => c.widget).join(', ')}])`
|
|
1570
|
+
: 'SizedBox.shrink()'
|
|
1571
|
+
return { widget: rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${widget})` : widget, isAbsolute: !!position, position, zIndex, flexGrow, size }
|
|
1572
|
+
}
|
|
1573
|
+
const padding = paddingFromProps(props)
|
|
1574
|
+
const margin = marginFromProps(props)
|
|
1575
|
+
const flowKids = semanticWidget ? [] : children.filter(c => !c.isAbsolute)
|
|
1576
|
+
const absKids = semanticWidget ? [] : children.filter(c => c.isAbsolute).sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0))
|
|
1577
|
+
const inputOwnedWidget = usesAppOwnedInputBackground(semanticWidget)
|
|
1578
|
+
const progressBarOwnedWidget = usesAppOwnedProgressBarBackground(semanticWidget)
|
|
1579
|
+
const wrappedInputOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedInputBackground(flowKids[0].widget)
|
|
1580
|
+
const wrappedProgressBarOwnedWidget = !semanticWidget && flowKids.length === 1 && usesAppOwnedProgressBarBackground(flowKids[0].widget)
|
|
1581
|
+
const appOwnedInputWidget = inputOwnedWidget || wrappedInputOwnedWidget
|
|
1582
|
+
const appOwnedProgressBarWidget = progressBarOwnedWidget || wrappedProgressBarOwnedWidget
|
|
1583
|
+
const appOwnedStyledWidget = appOwnedInputWidget || appOwnedProgressBarWidget
|
|
1584
|
+
const omitBackgroundColor = appOwnedStyledWidget
|
|
1585
|
+
const decoration = (skipRootDecoration && className === topClassName)
|
|
1586
|
+
? null
|
|
1587
|
+
: boxDecorationFromProps(props, { omitBackgroundColor })
|
|
1588
|
+
const effectivePadding = appOwnedStyledWidget ? null : padding
|
|
1589
|
+
const effectiveDecoration = appOwnedStyledWidget ? null : decoration
|
|
1590
|
+
const layoutSize = (appOwnedInputWidget && size) ? { ...size, h: null } : size
|
|
1591
|
+
const flex = flexConfigFromProps(props)
|
|
1592
|
+
let flowBody = semanticWidget
|
|
1593
|
+
if (!flowBody && flowKids.length) {
|
|
1594
|
+
if (flex) {
|
|
1595
|
+
const alignSelf = String(props['align-self'] || '').toLowerCase()
|
|
1596
|
+
const hasMainAxisSize = flex.isRow
|
|
1597
|
+
? (layoutSize.w != null || alignSelf === 'stretch')
|
|
1598
|
+
: (layoutSize.h != null)
|
|
1599
|
+
const gapLiteral = flex.gap ? (flex.isRow ? `${flex.gap}.width` : `${flex.gap}.height`) : null
|
|
1600
|
+
const baseKids = flowKids.map(c => {
|
|
1601
|
+
if (flex && c.flexGrow > 0 && hasMainAxisSize) {
|
|
1602
|
+
const flexValue = Math.max(1, Math.round(c.flexGrow))
|
|
1603
|
+
return `Expanded(flex: ${flexValue}, child: ${c.widget})`
|
|
1604
|
+
}
|
|
1605
|
+
return c.widget
|
|
1606
|
+
})
|
|
1607
|
+
const kids = gapLiteral && baseKids.length > 1
|
|
1608
|
+
? baseKids.map((c, i) => i < baseKids.length - 1 ? `${c}, ${gapLiteral}` : c).join(', ')
|
|
1609
|
+
: baseKids.join(', ')
|
|
1610
|
+
flowBody = flex.isRow
|
|
1611
|
+
? `Row(mainAxisAlignment: ${flex.main}, crossAxisAlignment: ${flex.cross}, children: [${kids}])`
|
|
1612
|
+
: `Column(crossAxisAlignment: ${flex.cross}, mainAxisAlignment: ${flex.main}, children: [${kids}])`
|
|
1613
|
+
} else if (flowKids.length === 1 && absKids.length === 0) {
|
|
1614
|
+
flowBody = flowKids[0].widget
|
|
1615
|
+
} else {
|
|
1616
|
+
flowBody = `Column(crossAxisAlignment: CrossAxisAlignment.start, children: [${flowKids.map(c => c.widget).join(', ')}])`
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
let body = flowBody || 'SizedBox.shrink()'
|
|
1620
|
+
if (absKids.length) {
|
|
1621
|
+
const stackChildren = []
|
|
1622
|
+
let primaryAbsIndex = -1
|
|
1623
|
+
if (flowBody) {
|
|
1624
|
+
stackChildren.push(flowBody)
|
|
1625
|
+
} else {
|
|
1626
|
+
for (let i = 0; i < absKids.length; i += 1) {
|
|
1627
|
+
const child = absKids[i]
|
|
1628
|
+
const pos = child.position || {}
|
|
1629
|
+
const isAnchoredTopLeft = (pos.top == null || pos.top === 0)
|
|
1630
|
+
&& (pos.left == null || pos.left === 0)
|
|
1631
|
+
&& pos.right == null
|
|
1632
|
+
&& pos.bottom == null
|
|
1633
|
+
if (isAnchoredTopLeft && !isSmallIconSize(child.size)) {
|
|
1634
|
+
primaryAbsIndex = i
|
|
1635
|
+
stackChildren.push(child.widget)
|
|
1636
|
+
break
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
if (primaryAbsIndex === -1) {
|
|
1640
|
+
const derived = normalizeGeneratedSize(estimateStackSize(absKids))
|
|
1641
|
+
const derivedHeight = className === topClassName ? null : derived?.h
|
|
1642
|
+
if (derived && (derived.w != null || derivedHeight != null)) {
|
|
1643
|
+
const w = derived.w != null ? `width: ${derived.w}, ` : ''
|
|
1644
|
+
const h = derivedHeight != null ? `height: ${derivedHeight}, ` : ''
|
|
1645
|
+
stackChildren.push(`SizedBox(${w}${h})`)
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
for (let i = 0; i < absKids.length; i += 1) {
|
|
1650
|
+
if (i === primaryAbsIndex) continue
|
|
1651
|
+
const child = absKids[i]
|
|
1652
|
+
if (isSmallIconSize(child.size)) {
|
|
1653
|
+
const simplifiedIcon = simplifySvgWidget(child.widget, child.size)
|
|
1654
|
+
if (simplifiedIcon) {
|
|
1655
|
+
stackChildren.push(simplifiedIcon)
|
|
1656
|
+
continue
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
const pos = child.position || {}
|
|
1660
|
+
const args = []
|
|
1661
|
+
if (pos.top != null) args.push(`top: ${pos.top}`)
|
|
1662
|
+
if (pos.left != null) args.push(`left: ${pos.left}`)
|
|
1663
|
+
if (pos.right != null) args.push(`right: ${pos.right}`)
|
|
1664
|
+
if (pos.bottom != null) args.push(`bottom: ${pos.bottom}`)
|
|
1665
|
+
const positioned = args.length
|
|
1666
|
+
? `Positioned(${args.join(', ')}, child: ${child.widget})`
|
|
1667
|
+
: child.widget
|
|
1668
|
+
stackChildren.push(positioned)
|
|
1669
|
+
}
|
|
1670
|
+
body = `Stack(clipBehavior: Clip.none, children: [${stackChildren.join(', ')}])`
|
|
1671
|
+
}
|
|
1672
|
+
if (effectivePadding || margin || effectiveDecoration) {
|
|
1673
|
+
const padLine = effectivePadding ? `padding: ${effectivePadding},` : ''
|
|
1674
|
+
const marLine = margin ? `margin: ${margin},` : ''
|
|
1675
|
+
const decLine = effectiveDecoration ? `decoration: ${effectiveDecoration},` : ''
|
|
1676
|
+
const sizedChild = (layoutSize.w || layoutSize.h) && className !== topClassName
|
|
1677
|
+
? `SizedBox(${layoutSize.w ? `width: ${layoutSize.w}, ` : ''}${layoutSize.h ? `height: ${layoutSize.h}, ` : ''}child: ${wrapOverflowIfSized(body, layoutSize)})`
|
|
1678
|
+
: body
|
|
1679
|
+
let container = `Container(${padLine}${marLine}${decLine} child: ${sizedChild})`
|
|
1680
|
+
if (overflowHidden) {
|
|
1681
|
+
const radius = borderRadiusFromProps(props)
|
|
1682
|
+
container = radius
|
|
1683
|
+
? `ClipRRect(borderRadius: ${radius}, child: ${container})`
|
|
1684
|
+
: `ClipRect(child: ${container})`
|
|
1685
|
+
}
|
|
1686
|
+
const widget = rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${container})` : container
|
|
1687
|
+
return { widget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
|
|
1688
|
+
}
|
|
1689
|
+
if ((layoutSize.w || layoutSize.h) && className !== topClassName) {
|
|
1690
|
+
let sized = `SizedBox(${layoutSize.w ? `width: ${layoutSize.w}, ` : ''}${layoutSize.h ? `height: ${layoutSize.h}, ` : ''}child: ${wrapOverflowIfSized(body, layoutSize)})`
|
|
1691
|
+
if (overflowHidden) {
|
|
1692
|
+
const radius = borderRadiusFromProps(props)
|
|
1693
|
+
sized = radius
|
|
1694
|
+
? `ClipRRect(borderRadius: ${radius}, child: ${sized})`
|
|
1695
|
+
: `ClipRect(child: ${sized})`
|
|
1696
|
+
}
|
|
1697
|
+
const widget = rotate != null ? `Transform.rotate(angle: ${rotate}, child: ${sized})` : sized
|
|
1698
|
+
return { widget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
|
|
1699
|
+
}
|
|
1700
|
+
let finalWidget = body
|
|
1701
|
+
if (overflowHidden) {
|
|
1702
|
+
const radius = borderRadiusFromProps(props)
|
|
1703
|
+
finalWidget = radius
|
|
1704
|
+
? `ClipRRect(borderRadius: ${radius}, child: ${finalWidget})`
|
|
1705
|
+
: `ClipRect(child: ${finalWidget})`
|
|
1706
|
+
}
|
|
1707
|
+
if (rotate != null) finalWidget = `Transform.rotate(angle: ${rotate}, child: ${finalWidget})`
|
|
1708
|
+
return { widget: finalWidget, isAbsolute: !!position, position, zIndex, flexGrow, size: layoutSize }
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1711
|
+
function generateDart(ast, cssMap, outClassName, outPath) {
|
|
1712
|
+
let rootWidget = null
|
|
1713
|
+
let topClassName = null
|
|
1714
|
+
traverse(ast, {
|
|
1715
|
+
JSXElement(pathNode) {
|
|
1716
|
+
if (!rootWidget) {
|
|
1717
|
+
topClassName = getClassNameAttr(pathNode.node)
|
|
1718
|
+
const mode = process.env.JSX2FLUTTER_MODE
|
|
1719
|
+
rootWidget = buildWidgetFromElement(pathNode.node, cssMap, topClassName, mode !== 'classic')
|
|
1720
|
+
}
|
|
1721
|
+
}
|
|
1722
|
+
})
|
|
1723
|
+
// Root wrapper: background + scroll
|
|
1724
|
+
const rootProps = topClassName && cssMap[topClassName] ? cssMap[topClassName] : {}
|
|
1725
|
+
const rootSize = sizeFromProps(rootProps)
|
|
1726
|
+
const rootWidth = rootSize.w
|
|
1727
|
+
const rootDeco = boxDecorationFromProps(rootProps)
|
|
1728
|
+
const mode = process.env.JSX2FLUTTER_MODE
|
|
1729
|
+
let inner = rootWidget?.widget || 'const SizedBox.shrink()'
|
|
1730
|
+
let finalRoot
|
|
1731
|
+
if (mode === 'classic') {
|
|
1732
|
+
finalRoot = inner
|
|
1733
|
+
} else if (mode === 'scaffold') {
|
|
1734
|
+
finalRoot = rootDeco
|
|
1735
|
+
? `Scaffold(body: Container(width: Get.width, decoration: ${rootDeco}, child: SingleChildScrollView(child: ${inner})))`
|
|
1736
|
+
: `Scaffold(body: Container(width: Get.width, child: SingleChildScrollView(child: ${inner})))`
|
|
1737
|
+
} else {
|
|
1738
|
+
finalRoot = rootDeco
|
|
1739
|
+
? `DecoratedBox(decoration: ${rootDeco}, child: SingleChildScrollView(child: FittedBox(alignment: Alignment.topLeft, fit: BoxFit.fitWidth, child: ${inner})))`
|
|
1740
|
+
: `SingleChildScrollView(child: FittedBox(alignment: Alignment.topLeft, fit: BoxFit.fitWidth, child: ${inner}))`
|
|
1741
|
+
}
|
|
1742
|
+
if (rootWidth != null) {
|
|
1743
|
+
const numericRootWidth = Number(rootWidth)
|
|
1744
|
+
const escapedRootWidth = String(rootWidth).replace('.', '\\.')
|
|
1745
|
+
const rootWidthPattern = Number.isInteger(numericRootWidth) ? `${escapedRootWidth}(?:\\.0+)?` : escapedRootWidth
|
|
1746
|
+
finalRoot = finalRoot.replace(new RegExp(`width:\\s*${rootWidthPattern}(?=\\s*[,\\)])`, 'g'), 'width: Get.width')
|
|
1747
|
+
finalRoot = finalRoot.replace(new RegExp(`maxWidth:\\s*${rootWidthPattern}(?=\\s*[,\\)])`, 'g'), 'maxWidth: Get.width')
|
|
1748
|
+
}
|
|
1749
|
+
const usesDateTimeField = finalRoot.includes('_GeneratedDateTimeField(')
|
|
1750
|
+
const usesInput = finalRoot.includes('AppInput(')
|
|
1751
|
+
const usesRadio = finalRoot.includes('_GeneratedRadioGroup(') || finalRoot.includes('AppRadioGroup(') || finalRoot.includes('AppRadioOption(')
|
|
1752
|
+
const usesCheckbox = finalRoot.includes('_GeneratedCheckbox(') || finalRoot.includes('AppCheckbox(')
|
|
1753
|
+
const usesRippleButton = finalRoot.includes('RippleButton(')
|
|
1754
|
+
const usesTextGradient = finalRoot.includes('AppTextGradient(')
|
|
1755
|
+
const usesProgressBar = finalRoot.includes('AppProgressBar(')
|
|
1756
|
+
const usesGetWidth = finalRoot.includes('Get.width')
|
|
1757
|
+
const imports = [
|
|
1758
|
+
"import 'package:flutter/material.dart';",
|
|
1759
|
+
`import 'package:${PROJECT_PACKAGE_NAME}/src/utils/app_colors.dart';`,
|
|
1760
|
+
`import 'package:${PROJECT_PACKAGE_NAME}/src/extensions/int_extensions.dart';`,
|
|
1761
|
+
"import 'package:flutter_svg/flutter_svg.dart';",
|
|
1762
|
+
`import 'package:${PROJECT_PACKAGE_NAME}/src/utils/app_assets.dart';`,
|
|
1763
|
+
]
|
|
1764
|
+
if (usesInput) {
|
|
1765
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_input.dart';`)
|
|
1766
|
+
}
|
|
1767
|
+
if (usesDateTimeField) {
|
|
1768
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_input_full_time.dart';`)
|
|
1769
|
+
}
|
|
1770
|
+
if (usesRadio) {
|
|
1771
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_radio_button.dart';`)
|
|
1772
|
+
}
|
|
1773
|
+
if (usesCheckbox) {
|
|
1774
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/base/checkbox/app_checkbox.dart';`)
|
|
1775
|
+
}
|
|
1776
|
+
if (usesRippleButton) {
|
|
1777
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/base/ripple_button.dart';`)
|
|
1778
|
+
}
|
|
1779
|
+
if (usesTextGradient) {
|
|
1780
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_text_gradient.dart';`)
|
|
1781
|
+
}
|
|
1782
|
+
if (usesProgressBar) {
|
|
1783
|
+
imports.push(`import 'package:${PROJECT_PACKAGE_NAME}/src/ui/widgets/app_progressbar.dart';`)
|
|
1784
|
+
}
|
|
1785
|
+
if (usesGetWidth) {
|
|
1786
|
+
imports.push("import 'package:get/get.dart';")
|
|
1787
|
+
}
|
|
1788
|
+
const helperBlocks = []
|
|
1789
|
+
if (usesDateTimeField) {
|
|
1790
|
+
helperBlocks.push(
|
|
1791
|
+
[
|
|
1792
|
+
"class _GeneratedDateTimeField extends StatefulWidget {",
|
|
1793
|
+
" final String? label;",
|
|
1794
|
+
" final String? hint;",
|
|
1795
|
+
" const _GeneratedDateTimeField({this.label, this.hint});",
|
|
1796
|
+
" @override",
|
|
1797
|
+
" State<_GeneratedDateTimeField> createState() => _GeneratedDateTimeFieldState();",
|
|
1798
|
+
"}",
|
|
1799
|
+
"",
|
|
1800
|
+
"class _GeneratedDateTimeFieldState extends State<_GeneratedDateTimeField> {",
|
|
1801
|
+
" late DateTime _selectedDate;",
|
|
1802
|
+
" @override",
|
|
1803
|
+
" void initState() {",
|
|
1804
|
+
" super.initState();",
|
|
1805
|
+
" final parsed = _parseDate(widget.hint);",
|
|
1806
|
+
" _selectedDate = parsed ?? DateTime.now();",
|
|
1807
|
+
" }",
|
|
1808
|
+
" DateTime? _parseDate(String? value) {",
|
|
1809
|
+
" final raw = (value ?? '').trim();",
|
|
1810
|
+
" if (raw.isEmpty) {",
|
|
1811
|
+
" return null;",
|
|
1812
|
+
" }",
|
|
1813
|
+
" final normalized = raw",
|
|
1814
|
+
" .replaceAll('年', '/')",
|
|
1815
|
+
" .replaceAll('月', '/')",
|
|
1816
|
+
" .replaceAll('日', '')",
|
|
1817
|
+
" .replaceAll('.', '/')",
|
|
1818
|
+
" .replaceAll('-', '/');",
|
|
1819
|
+
" final full = RegExp(r'(\\\\d{4})/(\\\\d{1,2})/(\\\\d{1,2})').firstMatch(normalized);",
|
|
1820
|
+
" if (full != null) {",
|
|
1821
|
+
" final year = int.tryParse(full.group(1)!);",
|
|
1822
|
+
" final month = int.tryParse(full.group(2)!);",
|
|
1823
|
+
" final day = int.tryParse(full.group(3)!);",
|
|
1824
|
+
" if (year != null && month != null && day != null) {",
|
|
1825
|
+
" return DateTime(year, month, day);",
|
|
1826
|
+
" }",
|
|
1827
|
+
" }",
|
|
1828
|
+
" final yearMonth = RegExp(r'(\\\\d{4})/(\\\\d{1,2})').firstMatch(normalized);",
|
|
1829
|
+
" if (yearMonth != null) {",
|
|
1830
|
+
" final year = int.tryParse(yearMonth.group(1)!);",
|
|
1831
|
+
" final month = int.tryParse(yearMonth.group(2)!);",
|
|
1832
|
+
" if (year != null && month != null) {",
|
|
1833
|
+
" return DateTime(year, month, 1);",
|
|
1834
|
+
" }",
|
|
1835
|
+
" }",
|
|
1836
|
+
" final yearOnly = RegExp(r'\\\\b(\\\\d{4})\\\\b').firstMatch(normalized);",
|
|
1837
|
+
" if (yearOnly != null) {",
|
|
1838
|
+
" final year = int.tryParse(yearOnly.group(1)!);",
|
|
1839
|
+
" if (year != null) {",
|
|
1840
|
+
" return DateTime(year, 1, 1);",
|
|
1841
|
+
" }",
|
|
1842
|
+
" }",
|
|
1843
|
+
" return null;",
|
|
1844
|
+
" }",
|
|
1845
|
+
" @override",
|
|
1846
|
+
" Widget build(BuildContext context) {",
|
|
1847
|
+
" return AppInputFullTime(",
|
|
1848
|
+
" label: widget.label,",
|
|
1849
|
+
" hint: widget.hint ?? 'YYYY/MM/DD',",
|
|
1850
|
+
" initialTime: _selectedDate,",
|
|
1851
|
+
" onTimeChanged: (next) {",
|
|
1852
|
+
" if (!mounted) {",
|
|
1853
|
+
" return;",
|
|
1854
|
+
" }",
|
|
1855
|
+
" setState(() => _selectedDate = next);",
|
|
1856
|
+
" },",
|
|
1857
|
+
" minimumYear: 1900,",
|
|
1858
|
+
" maximumYear: 2100,",
|
|
1859
|
+
" );",
|
|
1860
|
+
" }",
|
|
1861
|
+
"}",
|
|
1862
|
+
].join('\n')
|
|
1863
|
+
)
|
|
1864
|
+
}
|
|
1865
|
+
if (usesRadio) {
|
|
1866
|
+
helperBlocks.push(
|
|
1867
|
+
[
|
|
1868
|
+
"class _GeneratedRadioGroup extends StatefulWidget {",
|
|
1869
|
+
" final String initialValue;",
|
|
1870
|
+
" final List<AppRadioOption> options;",
|
|
1871
|
+
" final Color? activeColor;",
|
|
1872
|
+
" final Color? inactiveColor;",
|
|
1873
|
+
" const _GeneratedRadioGroup({required this.initialValue, required this.options, this.activeColor, this.inactiveColor});",
|
|
1874
|
+
" @override",
|
|
1875
|
+
" State<_GeneratedRadioGroup> createState() => _GeneratedRadioGroupState();",
|
|
1876
|
+
"}",
|
|
1877
|
+
"",
|
|
1878
|
+
"class _GeneratedRadioGroupState extends State<_GeneratedRadioGroup> {",
|
|
1879
|
+
" late String _value;",
|
|
1880
|
+
" @override",
|
|
1881
|
+
" void initState() {",
|
|
1882
|
+
" super.initState();",
|
|
1883
|
+
" if (widget.initialValue.isNotEmpty) {",
|
|
1884
|
+
" _value = widget.initialValue;",
|
|
1885
|
+
" } else if (widget.options.isNotEmpty) {",
|
|
1886
|
+
" _value = widget.options.first.value;",
|
|
1887
|
+
" } else {",
|
|
1888
|
+
" _value = '';",
|
|
1889
|
+
" }",
|
|
1890
|
+
" }",
|
|
1891
|
+
" @override",
|
|
1892
|
+
" Widget build(BuildContext context) {",
|
|
1893
|
+
" if (widget.options.isEmpty) {",
|
|
1894
|
+
" return const SizedBox.shrink();",
|
|
1895
|
+
" }",
|
|
1896
|
+
" return AppRadioGroup(",
|
|
1897
|
+
" value: _value,",
|
|
1898
|
+
" options: widget.options,",
|
|
1899
|
+
" activeColor: widget.activeColor,",
|
|
1900
|
+
" inactiveColor: widget.inactiveColor,",
|
|
1901
|
+
" onChanged: (next) {",
|
|
1902
|
+
" if (!mounted) {",
|
|
1903
|
+
" return;",
|
|
1904
|
+
" }",
|
|
1905
|
+
" setState(() => _value = next);",
|
|
1906
|
+
" },",
|
|
1907
|
+
" );",
|
|
1908
|
+
" }",
|
|
1909
|
+
"}",
|
|
1910
|
+
].join('\n')
|
|
1911
|
+
)
|
|
1912
|
+
}
|
|
1913
|
+
if (usesCheckbox) {
|
|
1914
|
+
helperBlocks.push(
|
|
1915
|
+
[
|
|
1916
|
+
"class _GeneratedCheckbox extends StatefulWidget {",
|
|
1917
|
+
" final String title;",
|
|
1918
|
+
" final bool initialChecked;",
|
|
1919
|
+
" final Color? borderColor;",
|
|
1920
|
+
" const _GeneratedCheckbox({required this.title, required this.initialChecked, this.borderColor});",
|
|
1921
|
+
" @override",
|
|
1922
|
+
" State<_GeneratedCheckbox> createState() => _GeneratedCheckboxState();",
|
|
1923
|
+
"}",
|
|
1924
|
+
"",
|
|
1925
|
+
"class _GeneratedCheckboxState extends State<_GeneratedCheckbox> {",
|
|
1926
|
+
" late bool _checked;",
|
|
1927
|
+
" @override",
|
|
1928
|
+
" void initState() {",
|
|
1929
|
+
" super.initState();",
|
|
1930
|
+
" _checked = widget.initialChecked;",
|
|
1931
|
+
" }",
|
|
1932
|
+
" @override",
|
|
1933
|
+
" Widget build(BuildContext context) {",
|
|
1934
|
+
" return AppCheckbox(",
|
|
1935
|
+
" title: widget.title,",
|
|
1936
|
+
" isChecked: _checked,",
|
|
1937
|
+
" borderColor: widget.borderColor,",
|
|
1938
|
+
" onTap: () {",
|
|
1939
|
+
" if (!mounted) {",
|
|
1940
|
+
" return;",
|
|
1941
|
+
" }",
|
|
1942
|
+
" setState(() => _checked = !_checked);",
|
|
1943
|
+
" },",
|
|
1944
|
+
" );",
|
|
1945
|
+
" }",
|
|
1946
|
+
"}",
|
|
1947
|
+
].join('\n')
|
|
1948
|
+
)
|
|
1949
|
+
}
|
|
1950
|
+
const content = [
|
|
1951
|
+
...imports,
|
|
1952
|
+
"",
|
|
1953
|
+
`class ${outClassName} extends StatelessWidget {`,
|
|
1954
|
+
" const " + outClassName + "({super.key});",
|
|
1955
|
+
" @override",
|
|
1956
|
+
" Widget build(BuildContext context) {",
|
|
1957
|
+
" return " + finalRoot + ";",
|
|
1958
|
+
" }",
|
|
1959
|
+
"}",
|
|
1960
|
+
...(helperBlocks.length ? ["", ...helperBlocks] : []),
|
|
1961
|
+
""
|
|
1962
|
+
].join('\n')
|
|
1963
|
+
writeFile(outPath, content)
|
|
1964
|
+
}
|
|
1965
|
+
|
|
1966
|
+
function run(inJsx, inCss, outDart) {
|
|
1967
|
+
const jsx = readFile(inJsx)
|
|
1968
|
+
const css = readFile(inCss)
|
|
1969
|
+
const assetPrefix = process.env.JSX2FLUTTER_ASSET_PREFIX || ''
|
|
1970
|
+
ASSET_CONTEXT = {
|
|
1971
|
+
jsxDir: path.dirname(inJsx),
|
|
1972
|
+
assetDir: DEFAULT_ASSET_DIR,
|
|
1973
|
+
assetPrefix,
|
|
1974
|
+
assets: new Map(),
|
|
1975
|
+
appAssetsRegistry: loadAppAssetsRegistry(),
|
|
1976
|
+
semanticHintByAbs: new Map(),
|
|
1977
|
+
fileNameByAbs: new Map(),
|
|
1978
|
+
usedFileNames: new Set(),
|
|
1979
|
+
copyAssets: process.env.JSX2FLUTTER_COPY_ASSETS !== '0',
|
|
1980
|
+
renameAssets: !!sanitizeAssetToken(assetPrefix),
|
|
1981
|
+
}
|
|
1982
|
+
const cssMap = parseCssModules(css, inCss)
|
|
1983
|
+
const ast = parse(jsx, { sourceType: 'module', plugins: ['jsx'] })
|
|
1984
|
+
ASSET_CONTEXT.semanticHintByAbs = collectAssetSemanticHints(ast, path.dirname(inJsx))
|
|
1985
|
+
const baseName = path.basename(outDart, '.dart')
|
|
1986
|
+
const className = toWidgetClassName(baseName)
|
|
1987
|
+
generateDart(ast, cssMap, className, outDart)
|
|
1988
|
+
copyCollectedAssets()
|
|
1989
|
+
syncAppAssetsFile()
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
if (process.argv.length < 5) {
|
|
1993
|
+
process.stderr.write('Usage: node jsx2flutter.mjs <index.jsx> <index.module.scss> <out.dart>\\n')
|
|
1994
|
+
process.exit(1)
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
run(process.argv[2], process.argv[3], process.argv[4])
|