@geastack/core 0.1.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +36 -0
- package/app_frame_perf.cpp +77 -0
- package/css.d.ts +1 -0
- package/events.cpp +67 -0
- package/gea_app_entry.cpp +317 -0
- package/gea_sources.mjs +126 -0
- package/gea_sources.sh +36 -0
- package/include/app.h +60 -0
- package/include/apps.h +46 -0
- package/include/audio.h +90 -0
- package/include/ble.h +212 -0
- package/include/bluetooth/hid_profile.h +92 -0
- package/include/camera.h +107 -0
- package/include/canvas.h +290 -0
- package/include/display.h +390 -0
- package/include/display_present.h +869 -0
- package/include/event.h +32 -0
- package/include/events.h +116 -0
- package/include/gea/embedded.h +13 -0
- package/include/gea/reactive_runtime.h +32 -0
- package/include/gea.h +14 -0
- package/include/gea_perf_config.h +96 -0
- package/include/geaos/abi.h +165 -0
- package/include/geolocation.h +126 -0
- package/include/gpio.h +61 -0
- package/include/graphics/font.h +122 -0
- package/include/i2c.h +20 -0
- package/include/image.h +156 -0
- package/include/imu.h +23 -0
- package/include/input.h +33 -0
- package/include/memory.h +52 -0
- package/include/pixel.h +907 -0
- package/include/platform/file_cache.h +18 -0
- package/include/power.h +11 -0
- package/include/runtime.h +83 -0
- package/include/services/app_runner.h +10 -0
- package/include/services/app_state.h +14 -0
- package/include/services/battery_service.h +12 -0
- package/include/services/bluetooth_service.h +13 -0
- package/include/services/diagnostics.h +76 -0
- package/include/services/diagnostics_platform.h +54 -0
- package/include/services/frame_scheduler.h +71 -0
- package/include/services/mirror.h +49 -0
- package/include/services/network_services.h +25 -0
- package/include/services/ota.h +32 -0
- package/include/services/runtime_log.h +11 -0
- package/include/services/storage_service.h +26 -0
- package/include/touch.h +38 -0
- package/include/ui/signal.h +149 -0
- package/include/wifi.h +67 -0
- package/index.d.ts +1940 -0
- package/jsx.d.ts +1 -0
- package/package.json +70 -0
- package/runtime/ble.ts +49 -0
- package/runtime/color.ts +21 -0
- package/runtime/compiler.ts +43 -0
- package/runtime/host.ts +1083 -0
- package/runtime/images.ts +129 -0
- package/runtime/primitives.ts +49 -0
- package/runtime-surface.ts +180 -0
- package/runtime.cpp +431 -0
- package/runtime.ts +34 -0
- package/scripts/build-gea-vite-geatsc.mjs +4514 -0
- package/scripts/dotenv-defines.mjs +98 -0
- package/scripts/gea-bundle-type-hints.mjs +546 -0
- package/scripts/gea-embedded-compat-staging.mjs +17 -0
- package/scripts/gea-embedded-compat-transform.mjs +427 -0
- package/scripts/gea-vite-module-graph-plugin.mjs +578 -0
- package/scripts/generate-gea-embedded-assets.mjs +296 -0
- package/scripts/generate-gea-embedded-fonts.mjs +1252 -0
- package/scripts/generate-installed-app-plan.mjs +55 -0
- package/scripts/native-object-cache.sh +244 -0
|
@@ -0,0 +1,4514 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import process from 'node:process'
|
|
5
|
+
import { spawnSync } from 'node:child_process'
|
|
6
|
+
import { createRequire } from 'node:module'
|
|
7
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
8
|
+
import { inlineJsonImports, transformGeaEmbeddedCompatSource } from './gea-embedded-compat-transform.mjs'
|
|
9
|
+
import { shouldIgnoreCompatStagingDirectory } from './gea-embedded-compat-staging.mjs'
|
|
10
|
+
import { dotEnvDefines, inlineProcessEnv } from './dotenv-defines.mjs'
|
|
11
|
+
import {
|
|
12
|
+
MODULE_HINT_SCOPE,
|
|
13
|
+
applyBundledTypeHints,
|
|
14
|
+
collectReachableSourceFiles,
|
|
15
|
+
collectSourceFiles,
|
|
16
|
+
discoverLocalImport,
|
|
17
|
+
parseTsx,
|
|
18
|
+
resolveFunctionHintName,
|
|
19
|
+
restoreModuleGraphElAnnotations,
|
|
20
|
+
} from './gea-bundle-type-hints.mjs'
|
|
21
|
+
|
|
22
|
+
const args = process.argv.slice(2)
|
|
23
|
+
const requireFromLib = createRequire(new URL('../package.json', import.meta.url))
|
|
24
|
+
const coreRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
|
|
25
|
+
const elementsRoot = path.resolve(coreRoot, '..', 'elements')
|
|
26
|
+
const engineRoot = path.resolve(coreRoot, '..', 'engine')
|
|
27
|
+
|
|
28
|
+
// Where @geastack/apple actually is, rather than where a repository layout
|
|
29
|
+
// would put it. The Apple target ships INSIDE that package, so the repo root it
|
|
30
|
+
// reports is the package itself; an app that installs it from npm has it in a
|
|
31
|
+
// node_modules chain instead, and `<repo>/packages/geastack-apple` describes
|
|
32
|
+
// neither. Guessing the path is what made an installed app fail to resolve
|
|
33
|
+
// @geastack/apple/AppKit at all.
|
|
34
|
+
function resolveApplePackageRoot(appDir = '') {
|
|
35
|
+
// GEA_APPLE_ROOT is for a caller that IS the apple package: its own target
|
|
36
|
+
// scripts build apps against the working copy, not an installed copy.
|
|
37
|
+
const declared = process.env.GEA_APPLE_ROOT || ''
|
|
38
|
+
if (declared && readPackageName(declared) === '@geastack/apple') return fs.realpathSync(declared)
|
|
39
|
+
for (const start of [appDir, coreRoot]) {
|
|
40
|
+
if (!start) continue
|
|
41
|
+
let dir = path.resolve(start)
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (readPackageName(dir) === '@geastack/apple') return fs.realpathSync(dir)
|
|
44
|
+
const candidate = path.join(dir, 'node_modules', '@geastack', 'apple')
|
|
45
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) return fs.realpathSync(candidate)
|
|
46
|
+
const parent = path.dirname(dir)
|
|
47
|
+
if (parent === dir) break
|
|
48
|
+
dir = parent
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// No guessed fallback. The old one returned `<repo>/packages/geastack-apple`,
|
|
52
|
+
// a layout that existed only in this project's own working copy, so an
|
|
53
|
+
// installed app was handed a path that was not there and failed later,
|
|
54
|
+
// somewhere else, with a message about the wrong thing.
|
|
55
|
+
fail(`cannot resolve @geastack/apple from ${appDir || coreRoot} — run \`npm install\` there, or set GEA_APPLE_ROOT`)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readPackageName(dir) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).name
|
|
61
|
+
} catch {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// The module identity a build pipeline must resolve `@geastack/core` to. The
|
|
66
|
+
// package answers "." with two files (`types: ./index.d.ts` for an editor,
|
|
67
|
+
// `default: ./runtime.ts` for anything that loads it), and everything that
|
|
68
|
+
// compiles the framework from source needs the second one -- see the comment
|
|
69
|
+
// at the top of runtime.ts. Stated to Vite as an alias and to TypeScript as a
|
|
70
|
+
// `paths` entry, so both consumers of the staged tree agree.
|
|
71
|
+
const runtimePath = path.join(coreRoot, 'runtime.ts')
|
|
72
|
+
const tryResolve = (m) => { try { return requireFromLib.resolve(m) } catch { return '' } }
|
|
73
|
+
const geaPluginResolved = fileURLToPath(import.meta.resolve('@geajs/vite-plugin'))
|
|
74
|
+
const { parse } = requireFromLib('@babel/parser')
|
|
75
|
+
const traverseModule = requireFromLib('@babel/traverse')
|
|
76
|
+
const t = requireFromLib('@babel/types')
|
|
77
|
+
const traverse = traverseModule.default || traverseModule
|
|
78
|
+
const useStaticCssRules = !['0', 'false', 'no'].includes(String(process.env.GEA_STATIC_CSS_RULES ?? '1').toLowerCase())
|
|
79
|
+
const useStaticCssTape = !['0', 'false', 'no'].includes(String(process.env.GEA_STATIC_CSS_TAPE ?? '1').toLowerCase())
|
|
80
|
+
const staticCssTapeMinChunk = Math.max(1, Number.parseInt(process.env.GEA_STATIC_CSS_TAPE_MIN_CHUNK ?? '5', 10) || 1)
|
|
81
|
+
|
|
82
|
+
function readOption(name) {
|
|
83
|
+
const index = args.indexOf(name)
|
|
84
|
+
if (index === -1) return undefined
|
|
85
|
+
return args[index + 1]
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function hasFlag(name) {
|
|
89
|
+
return args.includes(name)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function readAllOptions(name) {
|
|
93
|
+
const values = []
|
|
94
|
+
for (let i = 0; i < args.length; i++) {
|
|
95
|
+
if (args[i] === name) values.push(args[i + 1])
|
|
96
|
+
}
|
|
97
|
+
return values.filter(Boolean)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fail(message) {
|
|
101
|
+
process.stderr.write(`${message}\n`)
|
|
102
|
+
process.exit(1)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function acquireOutputPipelineLock(outDir) {
|
|
106
|
+
const lockDir = path.join(path.dirname(outDir), `.${path.basename(outDir)}.gea-pipeline.lock`)
|
|
107
|
+
const pidFile = path.join(lockDir, 'pid')
|
|
108
|
+
const tryAcquire = () => {
|
|
109
|
+
try {
|
|
110
|
+
fs.mkdirSync(lockDir)
|
|
111
|
+
fs.writeFileSync(pidFile, `${process.pid}\n`)
|
|
112
|
+
return true
|
|
113
|
+
} catch (error) {
|
|
114
|
+
if (error?.code !== 'EEXIST') throw error
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!tryAcquire()) {
|
|
120
|
+
const ownerText = fs.existsSync(pidFile) ? fs.readFileSync(pidFile, 'utf8').trim() : ''
|
|
121
|
+
const owner = /^[0-9]+$/.test(ownerText) ? Number(ownerText) : null
|
|
122
|
+
let ownerIsAlive = false
|
|
123
|
+
if (owner !== null) {
|
|
124
|
+
try {
|
|
125
|
+
process.kill(owner, 0)
|
|
126
|
+
ownerIsAlive = true
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
if (!ownerIsAlive) {
|
|
130
|
+
fs.rmSync(lockDir, { recursive: true, force: true })
|
|
131
|
+
if (!tryAcquire()) fail(`generation output became busy while recovering a stale lock: ${outDir}`)
|
|
132
|
+
} else {
|
|
133
|
+
fail(`generation output is already owned by pid ${owner}: ${outDir}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
let released = false
|
|
138
|
+
const release = () => {
|
|
139
|
+
if (released) return
|
|
140
|
+
released = true
|
|
141
|
+
const ownerText = fs.existsSync(pidFile) ? fs.readFileSync(pidFile, 'utf8').trim() : ''
|
|
142
|
+
if (ownerText === String(process.pid)) fs.rmSync(lockDir, { recursive: true, force: true })
|
|
143
|
+
}
|
|
144
|
+
process.on('exit', release)
|
|
145
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
146
|
+
process.once(signal, () => {
|
|
147
|
+
release()
|
|
148
|
+
process.kill(process.pid, signal)
|
|
149
|
+
})
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function writeFileIfChanged(filePath, contents) {
|
|
154
|
+
if (fs.existsSync(filePath) && fs.readFileSync(filePath, 'utf8') === contents) return
|
|
155
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
156
|
+
fs.writeFileSync(filePath, contents)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function run(command, commandArgs, options = {}) {
|
|
160
|
+
const result = spawnSync(command, commandArgs, {
|
|
161
|
+
stdio: 'inherit',
|
|
162
|
+
...options,
|
|
163
|
+
})
|
|
164
|
+
if (result.status !== 0) {
|
|
165
|
+
const rendered = [command, ...commandArgs].join(' ')
|
|
166
|
+
fail(`command failed (${result.status ?? result.signal}): ${rendered}`)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function findFirstJsFile(dir) {
|
|
171
|
+
const stack = [dir]
|
|
172
|
+
const files = []
|
|
173
|
+
while (stack.length > 0) {
|
|
174
|
+
const current = stack.pop()
|
|
175
|
+
if (!current || !fs.existsSync(current)) continue
|
|
176
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
177
|
+
const full = path.join(current, entry.name)
|
|
178
|
+
if (entry.isDirectory()) stack.push(full)
|
|
179
|
+
else if (entry.isFile() && entry.name.endsWith('.js')) files.push(full)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
files.sort()
|
|
183
|
+
return files[0]
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function copyCssUrlAssets(srcCss, dstCss) {
|
|
187
|
+
const css = fs.readFileSync(srcCss, 'utf8')
|
|
188
|
+
const srcDir = path.dirname(srcCss)
|
|
189
|
+
const dstDir = path.dirname(dstCss)
|
|
190
|
+
const urlRe = /url\(\s*(?:"([^"]+)"|'([^']+)'|([^)"']+))\s*\)/gi
|
|
191
|
+
let match
|
|
192
|
+
while ((match = urlRe.exec(css)) !== null) {
|
|
193
|
+
const raw = (match[1] ?? match[2] ?? match[3] ?? '').trim()
|
|
194
|
+
if (!raw || raw.startsWith('#') || raw.startsWith('/') || /^[a-z][a-z0-9+.-]*:/i.test(raw)) continue
|
|
195
|
+
const clean = raw.split(/[?#]/, 1)[0]
|
|
196
|
+
if (!clean) continue
|
|
197
|
+
const srcAsset = path.resolve(srcDir, clean)
|
|
198
|
+
if (!fs.existsSync(srcAsset) || !fs.statSync(srcAsset).isFile()) continue
|
|
199
|
+
const dstAsset = path.resolve(dstDir, clean)
|
|
200
|
+
if (dstAsset === srcAsset) continue
|
|
201
|
+
fs.mkdirSync(path.dirname(dstAsset), { recursive: true })
|
|
202
|
+
fs.copyFileSync(srcAsset, dstAsset)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Stages one source tree into the compat dir; the caller clears the compat
|
|
207
|
+
// dir first (an app plus its escaped shared trees stage as multiple calls).
|
|
208
|
+
function copyCompatSourceTree(srcDir, dstDir, envDefines = {}) {
|
|
209
|
+
fs.mkdirSync(dstDir, { recursive: true })
|
|
210
|
+
const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
|
|
211
|
+
const dstRoot = path.resolve(dstDir)
|
|
212
|
+
const stack = ['.']
|
|
213
|
+
while (stack.length > 0) {
|
|
214
|
+
const rel = stack.pop()
|
|
215
|
+
const currentSrc = path.join(srcDir, rel)
|
|
216
|
+
const currentDst = path.join(dstDir, rel)
|
|
217
|
+
for (const entry of fs.readdirSync(currentSrc, { withFileTypes: true })) {
|
|
218
|
+
if (entry.isDirectory()) {
|
|
219
|
+
if (shouldIgnoreCompatStagingDirectory(entry.name)) continue
|
|
220
|
+
const childRel = path.join(rel, entry.name)
|
|
221
|
+
const childSrc = path.resolve(srcDir, childRel)
|
|
222
|
+
const relativeToDst = path.relative(dstRoot, childSrc)
|
|
223
|
+
if (relativeToDst === '' || (!relativeToDst.startsWith('..') && !path.isAbsolute(relativeToDst))) continue
|
|
224
|
+
fs.mkdirSync(path.join(dstDir, childRel), { recursive: true })
|
|
225
|
+
stack.push(childRel)
|
|
226
|
+
continue
|
|
227
|
+
}
|
|
228
|
+
if (!entry.isFile()) continue
|
|
229
|
+
const childRel = path.join(rel, entry.name)
|
|
230
|
+
const src = path.join(srcDir, childRel)
|
|
231
|
+
const dst = path.join(dstDir, childRel)
|
|
232
|
+
fs.mkdirSync(path.dirname(dst), { recursive: true })
|
|
233
|
+
if (transformable.has(path.extname(entry.name))) {
|
|
234
|
+
const code = fs.readFileSync(src, 'utf8')
|
|
235
|
+
// JSON-import folding first, so the compat transform and the
|
|
236
|
+
// vite-plugin-gea IR extractor both see the data as inline literals
|
|
237
|
+
// (the fully native store/record lowering path). Then bake in .env vars
|
|
238
|
+
// (process.env.<KEY> -> literal) so the module-graph snapshots geatsc
|
|
239
|
+
// compiles carry the value — Vite's `define` only rewrites the bundle.
|
|
240
|
+
fs.writeFileSync(dst, inlineProcessEnv(transformGeaEmbeddedCompatSource(inlineJsonImports(code, src), src), envDefines))
|
|
241
|
+
} else {
|
|
242
|
+
fs.copyFileSync(src, dst)
|
|
243
|
+
if (path.extname(entry.name) === '.css') copyCssUrlAssets(src, dst)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The workspace `node_modules` the app's bare specifiers actually resolve
|
|
250
|
+
// through, mirrored beside the staged copy of the app.
|
|
251
|
+
//
|
|
252
|
+
// Staging copies SOURCES only, so `@geastack/core` -- which resolves in the
|
|
253
|
+
// real tree through the hoisted `examples/node_modules/@geastack/core`
|
|
254
|
+
// workspace symlink -- resolves through nothing at all in the staged tree. The
|
|
255
|
+
// module graph hides that for every module it TRACKS, because it hands geatsc
|
|
256
|
+
// each tracked edge's resolved id; a module the graph shook out but the
|
|
257
|
+
// typechecker still loads (a barrel's `export { X } from './raster/pipeline'`
|
|
258
|
+
// whose exports the app never uses) falls through to ordinary node resolution
|
|
259
|
+
// and finds nothing. `examples/apps/gea3d-cube` compiled to "Cannot find module
|
|
260
|
+
// '@geastack/core'" for exactly two such files.
|
|
261
|
+
//
|
|
262
|
+
// EVERY ancestor level is mirrored, not just the nearest: node resolution walks
|
|
263
|
+
// up through all of them, and in a workspace the app's own `node_modules` holds
|
|
264
|
+
// its direct deps while the hoisted one a few levels up holds the framework.
|
|
265
|
+
// Linking only the first found reproduces the failure with a link in place.
|
|
266
|
+
function linkCompatNodeModules(appDir, stagedAppDir, compatSrcDir) {
|
|
267
|
+
const stagingRoot = path.resolve(compatSrcDir)
|
|
268
|
+
let real = path.resolve(appDir)
|
|
269
|
+
let staged = path.resolve(stagedAppDir)
|
|
270
|
+
for (;;) {
|
|
271
|
+
const source = path.join(real, 'node_modules')
|
|
272
|
+
if (fs.existsSync(source)) {
|
|
273
|
+
const link = path.join(staged, 'node_modules')
|
|
274
|
+
if (!fs.existsSync(link)) {
|
|
275
|
+
fs.mkdirSync(staged, { recursive: true })
|
|
276
|
+
try {
|
|
277
|
+
fs.symlinkSync(source, link, 'dir')
|
|
278
|
+
} catch {
|
|
279
|
+
// A filesystem without symlinks is not a reason to fail the build:
|
|
280
|
+
// every module the graph tracked still resolves through its stated id.
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if (staged === stagingRoot) return
|
|
285
|
+
const nextReal = path.dirname(real)
|
|
286
|
+
const nextStaged = path.dirname(staged)
|
|
287
|
+
if (nextReal === real || nextStaged === staged) return
|
|
288
|
+
real = nextReal
|
|
289
|
+
staged = nextStaged
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Directories OUTSIDE the app dir reached through relative imports (the
|
|
294
|
+
// examples/shared component trees). Outermost-only: a nested hit is covered by
|
|
295
|
+
// staging its containing directory wholesale (which also brings the css/asset
|
|
296
|
+
// files discoverLocalImport skips).
|
|
297
|
+
function escapedCompatImportDirs(appDir, entry) {
|
|
298
|
+
const escaped = collectReachableSourceFiles(appDir, entry).filter((file) => path.relative(appDir, file).startsWith('..'))
|
|
299
|
+
const dirs = [...new Set(escaped.map((file) => path.dirname(file)))].sort()
|
|
300
|
+
return dirs.filter((dir, index) => !dirs.slice(0, index).some((outer) => outer !== dir && !path.relative(outer, dir).startsWith('..')))
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function commonAncestorDir(dirs) {
|
|
304
|
+
let ancestor = dirs[0]
|
|
305
|
+
for (const dir of dirs.slice(1)) {
|
|
306
|
+
while (path.relative(ancestor, dir).startsWith('..')) ancestor = path.dirname(ancestor)
|
|
307
|
+
}
|
|
308
|
+
return ancestor
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// The compat transform rewrites core imports to the bare specifier
|
|
312
|
+
// `gea-embedded`, because that is the name the generated Vite config aliases to
|
|
313
|
+
// `runtime.ts` -- the module identity a source-compiling pipeline needs, rather
|
|
314
|
+
// than the `index.d.ts` that `@geastack/core` answers with.
|
|
315
|
+
//
|
|
316
|
+
// geatsc typechecks this same staged tree, and nothing had ever told TypeScript
|
|
317
|
+
// that name. `gea-embedded` resolved to nothing for any app: the `paths` entry
|
|
318
|
+
// eleven of them still carry points at `examples/lib/gea-embedded`, a directory
|
|
319
|
+
// the repo split removed. One authority renaming a module the other cannot see
|
|
320
|
+
// is how `Cannot find module 'gea-embedded'` reaches a program whose Vite build
|
|
321
|
+
// succeeded (examples/hid-clicker, through the shared Settings tree).
|
|
322
|
+
//
|
|
323
|
+
// So state the alias to TypeScript too, against the staged runtime when the
|
|
324
|
+
// staging mirrored core into the tree -- the module graph is rooted there, and
|
|
325
|
+
// pointing the checker outside it would hand back the second module identity
|
|
326
|
+
// this whole rename exists to avoid.
|
|
327
|
+
function stateRuntimeAliasToTypeScript(stagedAppDir, compatSrcDir, stagingRoot) {
|
|
328
|
+
const mirrored = path.join(compatSrcDir, path.relative(stagingRoot, runtimePath))
|
|
329
|
+
const target = !path.relative(compatSrcDir, mirrored).startsWith('..') && fs.existsSync(mirrored) ? mirrored : runtimePath
|
|
330
|
+
const configPath = path.join(stagedAppDir, 'tsconfig.json')
|
|
331
|
+
// Six apps under examples/ ship no tsconfig at all (ttf-bench and the ios-*
|
|
332
|
+
// family). Handing the checker no project at all is not neutral: it defaults
|
|
333
|
+
// `lib` to include lib.dom, and `@geastack/core` declares `document`,
|
|
334
|
+
// `window`, `console`, `Image`, `WebSocket` and the rest itself -- so every
|
|
335
|
+
// one of them collides ("Definitions of the following identifiers conflict")
|
|
336
|
+
// and `jsx` defaults to unset, which discards the element tree the plugin
|
|
337
|
+
// reads. What those apps need is not a new policy: it is the one their forty
|
|
338
|
+
// five siblings already state in their own tsconfig, so state it for them.
|
|
339
|
+
let config = {
|
|
340
|
+
compilerOptions: {
|
|
341
|
+
target: 'ES2022',
|
|
342
|
+
module: 'ESNext',
|
|
343
|
+
moduleResolution: 'Bundler',
|
|
344
|
+
lib: ['ES2022'],
|
|
345
|
+
strict: true,
|
|
346
|
+
noEmit: true,
|
|
347
|
+
skipLibCheck: true,
|
|
348
|
+
jsx: 'preserve',
|
|
349
|
+
},
|
|
350
|
+
include: ['**/*.ts', '**/*.tsx', '**/*.d.ts'],
|
|
351
|
+
}
|
|
352
|
+
if (fs.existsSync(configPath)) {
|
|
353
|
+
try {
|
|
354
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf8'))
|
|
355
|
+
} catch (error) {
|
|
356
|
+
// An app tsconfig this script cannot read is the app's to fix, not a
|
|
357
|
+
// reason to fail a build that would otherwise resolve every other module.
|
|
358
|
+
process.stdout.write(`gea-embedded-compat: leaving ${configPath} alone (${error.message})\n`)
|
|
359
|
+
return
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
const options = config.compilerOptions ?? (config.compilerOptions = {})
|
|
363
|
+
options.baseUrl = options.baseUrl ?? '.'
|
|
364
|
+
const base = path.resolve(stagedAppDir, options.baseUrl)
|
|
365
|
+
// Both specifiers, because the Vite config aliases both to the same
|
|
366
|
+
// `runtimePath` and an app may write either. `@geastack/core` is the one that
|
|
367
|
+
// matters for `declare module '@geastack/core' { interface
|
|
368
|
+
// GeaIntrinsicElements { ... } }`: left unmapped it resolves to the package's
|
|
369
|
+
// `index.d.ts` OUTSIDE the staged tree, while `JSX.IntrinsicElements` is wired
|
|
370
|
+
// to the staged `runtime.ts` -- so an app's own tags merge into a copy the JSX
|
|
371
|
+
// namespace never reads, and every use of one is `Property 'x' does not exist
|
|
372
|
+
// on type 'JSX.IntrinsicElements'` (companion/examples/gea-companion's
|
|
373
|
+
// `<symbol>`). That is the two-module-identities trap runtime.ts's own header
|
|
374
|
+
// warns about, reached here through a third copy that staging made.
|
|
375
|
+
const stagedRuntime = [path.relative(base, target)]
|
|
376
|
+
options.paths = { ...(options.paths ?? {}), 'gea-embedded': stagedRuntime, '@geastack/core': stagedRuntime }
|
|
377
|
+
// Staging mirrors `examples/node_modules` into the tree, and `@types/react`
|
|
378
|
+
// lives there. TypeScript auto-includes EVERY `@types/*` package it can reach
|
|
379
|
+
// unless `types` is stated, and React's declares a GLOBAL `JSX` namespace --
|
|
380
|
+
// so it does not merely add tags, it supplies the whole namespace. An app
|
|
381
|
+
// whose own JSX declarations resolve never notices. An app whose do not takes
|
|
382
|
+
// React's instead, and every `class` attribute becomes `Property 'class' does
|
|
383
|
+
// not exist ... Did you mean 'className'?` against `DetailedHTMLProps`.
|
|
384
|
+
//
|
|
385
|
+
// `virtual-list/env.d.ts` is the case: it states its types with
|
|
386
|
+
// `/// <reference types="@geastack/core" />`, and a triple-slash TYPES
|
|
387
|
+
// reference resolves through `typeRoots`/`node_modules` ONLY -- never through
|
|
388
|
+
// the `paths` set just above -- while no reachable `node_modules` carries an
|
|
389
|
+
// `@geastack` scope at all. So the reference silently resolves to nothing and
|
|
390
|
+
// React wins by default. These apps declare what they need in their own
|
|
391
|
+
// `.d.ts` and in the staged `runtime.ts`; none of them wants an ambient
|
|
392
|
+
// package. Say so, and keep an app's own `types` if it states one.
|
|
393
|
+
options.types = options.types ?? []
|
|
394
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`)
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function resolveAppleNativeAliasModules(appDir) {
|
|
398
|
+
const appPackageBoundary = path.join(appDir, 'package.json')
|
|
399
|
+
const appRequire = createRequire(appPackageBoundary)
|
|
400
|
+
let threeSrcDir = ''
|
|
401
|
+
let threeUtilsModule = ''
|
|
402
|
+
let threeWebGLAnimationModule = ''
|
|
403
|
+
let threeWebXRManagerModule = ''
|
|
404
|
+
let troikaThreeTextModule = ''
|
|
405
|
+
let browserTroikaModule = ''
|
|
406
|
+
try {
|
|
407
|
+
threeSrcDir = path.dirname(fs.realpathSync(appRequire.resolve('three/src/Three.js')))
|
|
408
|
+
} catch {
|
|
409
|
+
// Native apps that do not use Three need no source-identity alias.
|
|
410
|
+
}
|
|
411
|
+
try {
|
|
412
|
+
threeWebGLAnimationModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeWebGLAnimation'))
|
|
413
|
+
threeWebXRManagerModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeWebXRManager'))
|
|
414
|
+
threeUtilsModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/nativeThreeUtils'))
|
|
415
|
+
} catch {
|
|
416
|
+
// A native app that does not use the ANGLE package needs no Three XR replacement.
|
|
417
|
+
}
|
|
418
|
+
try {
|
|
419
|
+
browserTroikaModule = appRequire.resolve('troika-three-text')
|
|
420
|
+
} catch {
|
|
421
|
+
// No browser Troika package means there is no package surface to replace.
|
|
422
|
+
}
|
|
423
|
+
try {
|
|
424
|
+
troikaThreeTextModule = fs.realpathSync(appRequire.resolve('@geastack/native-webgl-angle/troika-three-text'))
|
|
425
|
+
} catch {
|
|
426
|
+
if (browserTroikaModule) {
|
|
427
|
+
fail(
|
|
428
|
+
`Apple-native build resolved "troika-three-text" from ${appPackageBoundary}, but could not resolve ` +
|
|
429
|
+
`"@geastack/native-webgl-angle/troika-three-text" from the same application package boundary. ` +
|
|
430
|
+
'Install the native package in the application; a bare Vite alias cannot be chained safely.'
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule }
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function writeCompatViteConfig({ configPath, compatSrcDir, appDir, entry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines }) {
|
|
438
|
+
const engineComponentsPath = path.join(engineRoot, 'components/index.ts')
|
|
439
|
+
const engineComponentSubpath = path.join(engineRoot, 'components/$1')
|
|
440
|
+
const elementsPath = path.join(elementsRoot, 'components/index.ts')
|
|
441
|
+
const elementSubpath = path.join(elementsRoot, 'components/$1')
|
|
442
|
+
// Only an apple-native build needs the apple package, and only an
|
|
443
|
+
// apple-native build emits the aliases below. Resolving it unconditionally
|
|
444
|
+
// made the (fail-closed) lookup a hard requirement of every ESP32, web and
|
|
445
|
+
// Linux build too.
|
|
446
|
+
const applePackageRoot = appleNative ? resolveApplePackageRoot(appDir) : ''
|
|
447
|
+
const appleRuntimeSubpath = applePackageRoot ? path.join(applePackageRoot, 'runtime/$1.js') : ''
|
|
448
|
+
const applePackagePath = applePackageRoot ? path.join(applePackageRoot, 'dist/index.js') : ''
|
|
449
|
+
const moduleGraphPluginPath = path.join(coreRoot, 'scripts/gea-vite-module-graph-plugin.mjs')
|
|
450
|
+
const { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule } = appleNative
|
|
451
|
+
? resolveAppleNativeAliasModules(appDir)
|
|
452
|
+
: { threeSrcDir: '', threeUtilsModule: '', threeWebGLAnimationModule: '', threeWebXRManagerModule: '', troikaThreeTextModule: '' }
|
|
453
|
+
const q = (value) => JSON.stringify(value)
|
|
454
|
+
// Compile the @geajs/core reactive runtime (Component/Store/compiler-runtime)
|
|
455
|
+
// from its TYPED SOURCE, not the minified dist. The minified build reuses short
|
|
456
|
+
// identifiers across nested scopes, which the C++ symbol collector cannot
|
|
457
|
+
// disambiguate, and — with all types erased — geatsc is forced to lower the
|
|
458
|
+
// whole reactive runtime dynamically (gea_cpp_value, ill-formed auto-vectors,
|
|
459
|
+
// proxy derefs). From source, everything is typed and lowers natively, exactly
|
|
460
|
+
// like three.js/hono/mongodb. The env var still wins when explicitly set;
|
|
461
|
+
// otherwise resolve the package's source entry via its node_modules symlink.
|
|
462
|
+
let compilerRuntimeEntry = process.env.GEA_COMPILER_RUNTIME_SOURCE || ''
|
|
463
|
+
if (!compilerRuntimeEntry) {
|
|
464
|
+
for (const base of [appDir, coreRoot]) {
|
|
465
|
+
try {
|
|
466
|
+
const pkgDir = fs.realpathSync(path.join(base, 'node_modules/@geajs/core'))
|
|
467
|
+
const srcIndex = path.join(pkgDir, 'src/index.ts')
|
|
468
|
+
if (fs.existsSync(srcIndex) && fs.existsSync(path.join(pkgDir, 'src/compiler-runtime.ts'))) {
|
|
469
|
+
compilerRuntimeEntry = srcIndex
|
|
470
|
+
break
|
|
471
|
+
}
|
|
472
|
+
} catch {
|
|
473
|
+
// No @geajs/core source under this base (e.g. published-only install) —
|
|
474
|
+
// fall through; the alias stays empty and the dist runtime is used.
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
const compilerRuntimeAlias = compilerRuntimeEntry && fs.existsSync(compilerRuntimeEntry)
|
|
479
|
+
? ` { find: new RegExp('^@geajs/core$'), replacement: ${q(compilerRuntimeEntry)} },\n`
|
|
480
|
+
: ''
|
|
481
|
+
const appleAliases = appleNative
|
|
482
|
+
? ` { find: /^@geajs\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
|
|
483
|
+
{ find: /^@geastack\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
|
|
484
|
+
{ find: '@geajs/apple', replacement: ${q(applePackagePath)} },
|
|
485
|
+
{ find: '@geastack/apple', replacement: ${q(applePackagePath)} },
|
|
486
|
+
`
|
|
487
|
+
: ''
|
|
488
|
+
// App .env vars, inlined as process.env.<KEY> string literals. geatsc compiles
|
|
489
|
+
// this bundle (not the browser), so the values are baked in at compile time and
|
|
490
|
+
// no `process` object exists at runtime — process.env.X is already the literal.
|
|
491
|
+
const defineEntries = Object.entries(envDefines || {})
|
|
492
|
+
.map(([key, literal]) => ` ${q(key)}: ${q(literal)},`)
|
|
493
|
+
.join('\n')
|
|
494
|
+
const config = `import { resolve } from 'node:path'
|
|
495
|
+
import { geaPlugin } from ${q(geaPluginResolved)}
|
|
496
|
+
import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
|
|
497
|
+
|
|
498
|
+
const root = ${q(compatSrcDir)}
|
|
499
|
+
const geaModuleGraph = geaModuleGraphPlugins({ outDir: ${q(moduleGraphOutDir || '')}, entryReachableOnly: true })
|
|
500
|
+
const geaAppleNativeAliases = geaAppleNativeModuleAliases({
|
|
501
|
+
threeSrcDir: ${q(threeSrcDir)},
|
|
502
|
+
threeUtilsModule: ${q(threeUtilsModule)},
|
|
503
|
+
threeWebGLAnimationModule: ${q(threeWebGLAnimationModule)},
|
|
504
|
+
threeWebXRManagerModule: ${q(threeWebXRManagerModule)},
|
|
505
|
+
troikaThreeTextModule: ${q(troikaThreeTextModule)},
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
export default {
|
|
509
|
+
root,
|
|
510
|
+
define: {
|
|
511
|
+
${defineEntries}
|
|
512
|
+
},
|
|
513
|
+
plugins: [geaPlugin({ ir: { enabled: true, outFile: ${q(geaIrPath)} } }), ...geaModuleGraph],
|
|
514
|
+
resolve: {
|
|
515
|
+
alias: [
|
|
516
|
+
${compilerRuntimeAlias}
|
|
517
|
+
{ find: new RegExp('^@geastack/core$'), replacement: ${q(runtimePath)} },
|
|
518
|
+
{ find: new RegExp('^@geastack/engine$'), replacement: ${q(engineComponentsPath)} },
|
|
519
|
+
{ find: new RegExp('^@geastack/engine/components$'), replacement: ${q(engineComponentsPath)} },
|
|
520
|
+
{ find: new RegExp('^@geastack/engine/components/(.+)$'), replacement: ${q(engineComponentSubpath)} },
|
|
521
|
+
{ find: new RegExp('^@geastack/elements$'), replacement: ${q(elementsPath)} },
|
|
522
|
+
{ find: new RegExp('^@geastack/elements/components$'), replacement: ${q(elementsPath)} },
|
|
523
|
+
{ find: new RegExp('^@geastack/elements/components/(.+)$'), replacement: ${q(elementSubpath)} },
|
|
524
|
+
{ find: new RegExp('^gea-embedded$'), replacement: ${q(runtimePath)} },
|
|
525
|
+
${appleAliases}${appleNative ? ' ...geaAppleNativeAliases,\n' : ''} ],
|
|
526
|
+
},
|
|
527
|
+
build: {
|
|
528
|
+
lib: {
|
|
529
|
+
entry: resolve(root, ${q(entry)}),
|
|
530
|
+
formats: ['es'],
|
|
531
|
+
fileName: () => 'index.js',
|
|
532
|
+
cssFileName: 'index',
|
|
533
|
+
},
|
|
534
|
+
outDir: ${q(viteOutDir)},
|
|
535
|
+
emptyOutDir: true,
|
|
536
|
+
modulePreload: { polyfill: false },
|
|
537
|
+
minify: false,
|
|
538
|
+
cssCodeSplit: false,
|
|
539
|
+
// Vite 8 / rolldown defaults output.topLevelVar to true, which rewrites
|
|
540
|
+
// top-level \`const\`/\`let\` to \`var\` (a JS-runtime TDZ tweak). That is
|
|
541
|
+
// pointless here — geatsc compiles this bundle to C++, not a browser — and
|
|
542
|
+
// it erases const-ness that geatsc's static analysis relies on (e.g. a
|
|
543
|
+
// const array length / loop bound, which gates dropping typed-array bounds
|
|
544
|
+
// checks). Keep declarations as authored.
|
|
545
|
+
rollupOptions: { output: { topLevelVar: false } },
|
|
546
|
+
},
|
|
547
|
+
// JSON imports that survive source-level folding (see inlineJsonImports)
|
|
548
|
+
// must reach geatsc as an inline object literal, never as the
|
|
549
|
+
// JSON.parse("...") string form vite's stringify:'auto' emits past 10kB —
|
|
550
|
+
// geatsc can only statically type the literal form.
|
|
551
|
+
json: { stringify: false },
|
|
552
|
+
}
|
|
553
|
+
`
|
|
554
|
+
fs.writeFileSync(configPath, config)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function posixPath(value) {
|
|
558
|
+
return value.split(path.sep).join('/')
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function relativeModuleSpecifier(fromDir, toFile) {
|
|
562
|
+
let specifier = posixPath(path.relative(fromDir, toFile))
|
|
563
|
+
if (!specifier.startsWith('.')) specifier = `./${specifier}`
|
|
564
|
+
return specifier
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function writeAppendJsCompatEntry({ compatSrcDir, compatEntry, appendFiles }) {
|
|
568
|
+
if (appendFiles.length === 0) return compatEntry
|
|
569
|
+
const entryDirRel = path.dirname(compatEntry)
|
|
570
|
+
const entryDirAbs = path.join(compatSrcDir, entryDirRel)
|
|
571
|
+
const entryAbs = path.join(compatSrcDir, compatEntry)
|
|
572
|
+
const wrapperRel = path.join(entryDirRel, '__gea_appended_entry.tsx')
|
|
573
|
+
const wrapperAbs = path.join(compatSrcDir, wrapperRel)
|
|
574
|
+
const entryImport = relativeModuleSpecifier(path.dirname(wrapperAbs), entryAbs)
|
|
575
|
+
const parts = [`import ${JSON.stringify(entryImport)}\n`]
|
|
576
|
+
for (const appendFile of appendFiles) {
|
|
577
|
+
if (!fs.existsSync(appendFile)) fail(`missing appended JS file: ${appendFile}`)
|
|
578
|
+
parts.push(`\n// appended from ${posixPath(appendFile)}\n${fs.readFileSync(appendFile, 'utf8')}\n`)
|
|
579
|
+
}
|
|
580
|
+
writeFileIfChanged(wrapperAbs, parts.join(''))
|
|
581
|
+
return wrapperRel
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function stripCssComments(css) {
|
|
585
|
+
return css.replace(/\/\*[\s\S]*?\*\//g, '')
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function stripAtBlocks(css) {
|
|
589
|
+
return css.replace(/@[^{;]+(?:;|\{[\s\S]*?\})/g, '')
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function splitTopLevelCss(value, delimiter) {
|
|
593
|
+
const out = []
|
|
594
|
+
let start = 0
|
|
595
|
+
let depth = 0
|
|
596
|
+
for (let i = 0; i < value.length; i++) {
|
|
597
|
+
const char = value[i]
|
|
598
|
+
if (char === '(') depth++
|
|
599
|
+
else if (char === ')' && depth > 0) depth--
|
|
600
|
+
else if (char === delimiter && depth === 0) {
|
|
601
|
+
out.push(value.slice(start, i).trim())
|
|
602
|
+
start = i + 1
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
out.push(value.slice(start).trim())
|
|
606
|
+
return out.filter(Boolean)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function splitCssWords(value) {
|
|
610
|
+
const out = []
|
|
611
|
+
let i = 0
|
|
612
|
+
while (i < value.length) {
|
|
613
|
+
while (i < value.length && /\s/.test(value[i])) i++
|
|
614
|
+
const start = i
|
|
615
|
+
let depth = 0
|
|
616
|
+
while (i < value.length) {
|
|
617
|
+
const char = value[i]
|
|
618
|
+
if (char === '(') depth++
|
|
619
|
+
else if (char === ')' && depth > 0) depth--
|
|
620
|
+
else if (/\s/.test(char) && depth === 0) break
|
|
621
|
+
i++
|
|
622
|
+
}
|
|
623
|
+
if (i > start) out.push(value.slice(start, i))
|
|
624
|
+
}
|
|
625
|
+
return out
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function findCssFiles(dir) {
|
|
629
|
+
const files = []
|
|
630
|
+
const stack = [dir]
|
|
631
|
+
while (stack.length > 0) {
|
|
632
|
+
const current = stack.pop()
|
|
633
|
+
if (!current || !fs.existsSync(current)) continue
|
|
634
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
635
|
+
const full = path.join(current, entry.name)
|
|
636
|
+
if (entry.isDirectory()) stack.push(full)
|
|
637
|
+
else if (entry.isFile() && entry.name.endsWith('.css')) files.push(full)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
files.sort()
|
|
641
|
+
return files
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function usesGeaEmbeddedCompat(appDir) {
|
|
645
|
+
const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
|
|
646
|
+
const stack = [appDir]
|
|
647
|
+
while (stack.length > 0) {
|
|
648
|
+
const current = stack.pop()
|
|
649
|
+
if (!current || !fs.existsSync(current)) continue
|
|
650
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
651
|
+
const full = path.join(current, entry.name)
|
|
652
|
+
if (entry.isDirectory()) {
|
|
653
|
+
if (['node_modules', 'dist', 'build', '.vite'].includes(entry.name)) continue
|
|
654
|
+
stack.push(full)
|
|
655
|
+
continue
|
|
656
|
+
}
|
|
657
|
+
if (!entry.isFile() || !transformable.has(path.extname(entry.name))) continue
|
|
658
|
+
const code = fs.readFileSync(full, 'utf8')
|
|
659
|
+
if (code.includes("'@geastack/core'") || code.includes('"@geastack/core"')) return true
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
return false
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function usesAppleNative(appDir) {
|
|
666
|
+
const transformable = new Set(['.js', '.jsx', '.ts', '.tsx'])
|
|
667
|
+
const stack = [appDir]
|
|
668
|
+
while (stack.length > 0) {
|
|
669
|
+
const current = stack.pop()
|
|
670
|
+
if (!current || !fs.existsSync(current)) continue
|
|
671
|
+
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
|
|
672
|
+
const full = path.join(current, entry.name)
|
|
673
|
+
if (entry.isDirectory()) {
|
|
674
|
+
if (['node_modules', 'dist', 'build', '.vite'].includes(entry.name)) continue
|
|
675
|
+
stack.push(full)
|
|
676
|
+
continue
|
|
677
|
+
}
|
|
678
|
+
if (!entry.isFile() || !transformable.has(path.extname(entry.name))) continue
|
|
679
|
+
const code = fs.readFileSync(full, 'utf8')
|
|
680
|
+
if (
|
|
681
|
+
code.includes("'@geajs/apple/") ||
|
|
682
|
+
code.includes('"@geajs/apple/') ||
|
|
683
|
+
code.includes("'@geastack/apple/") ||
|
|
684
|
+
code.includes('"@geastack/apple/')
|
|
685
|
+
) return true
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return false
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
function escapeRegExp(value) {
|
|
692
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function typeTextReferencesName(typeText, name) {
|
|
696
|
+
return new RegExp(`\\b${escapeRegExp(name)}\\b`).test(typeText)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function resolveTypeImportPath(base) {
|
|
700
|
+
for (const candidate of [
|
|
701
|
+
base,
|
|
702
|
+
`${base}.d.ts`,
|
|
703
|
+
`${base}.ts`,
|
|
704
|
+
`${base}.tsx`,
|
|
705
|
+
path.join(base, 'index.d.ts'),
|
|
706
|
+
path.join(base, 'index.ts'),
|
|
707
|
+
path.join(base, 'index.tsx'),
|
|
708
|
+
]) {
|
|
709
|
+
if (fs.existsSync(candidate)) return candidate
|
|
710
|
+
}
|
|
711
|
+
return null
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function typeImportModuleSpecifier(resolvedPath) {
|
|
715
|
+
return resolvedPath
|
|
716
|
+
.replace(/\\/g, '/')
|
|
717
|
+
.replace(/\.d\.ts$/, '')
|
|
718
|
+
.replace(/\.(?:tsx?|jsx?)$/, '')
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
function resolveTypeImportModule(importingFile, source) {
|
|
722
|
+
if (source === '@geastack/core') {
|
|
723
|
+
return typeImportModuleSpecifier(path.join(coreRoot, 'index.d.ts'))
|
|
724
|
+
}
|
|
725
|
+
if (source === '@geastack/engine' || source === '@geastack/engine/components') {
|
|
726
|
+
return typeImportModuleSpecifier(path.join(engineRoot, 'components/index.d.ts'))
|
|
727
|
+
}
|
|
728
|
+
if (source === '@geastack/elements' || source === '@geastack/elements/components') {
|
|
729
|
+
return typeImportModuleSpecifier(path.join(elementsRoot, 'components/index.d.ts'))
|
|
730
|
+
}
|
|
731
|
+
if (source.startsWith('@geastack/core/')) {
|
|
732
|
+
const resolved = resolveTypeImportPath(path.join(coreRoot, source.slice('@geastack/core/'.length)))
|
|
733
|
+
return resolved ? typeImportModuleSpecifier(resolved) : source
|
|
734
|
+
}
|
|
735
|
+
if (source.startsWith('@geastack/engine/components/')) {
|
|
736
|
+
const resolved = resolveTypeImportPath(path.join(engineRoot, 'components', source.slice('@geastack/engine/components/'.length)))
|
|
737
|
+
return resolved ? typeImportModuleSpecifier(resolved) : source
|
|
738
|
+
}
|
|
739
|
+
if (source.startsWith('@geastack/engine/')) {
|
|
740
|
+
const resolved = resolveTypeImportPath(path.join(engineRoot, source.slice('@geastack/engine/'.length)))
|
|
741
|
+
return resolved ? typeImportModuleSpecifier(resolved) : source
|
|
742
|
+
}
|
|
743
|
+
if (source.startsWith('@geastack/elements/components/')) {
|
|
744
|
+
const resolved = resolveTypeImportPath(path.join(elementsRoot, 'components', source.slice('@geastack/elements/components/'.length)))
|
|
745
|
+
return resolved ? typeImportModuleSpecifier(resolved) : source
|
|
746
|
+
}
|
|
747
|
+
if (source.startsWith('@geastack/elements/')) {
|
|
748
|
+
const resolved = resolveTypeImportPath(path.join(elementsRoot, source.slice('@geastack/elements/'.length)))
|
|
749
|
+
return resolved ? typeImportModuleSpecifier(resolved) : source
|
|
750
|
+
}
|
|
751
|
+
if (source.startsWith('.') || source.startsWith('/')) {
|
|
752
|
+
const base = source.startsWith('/') ? source : path.resolve(path.dirname(importingFile), source)
|
|
753
|
+
const resolved = resolveTypeImportPath(base)
|
|
754
|
+
return typeImportModuleSpecifier(resolved ?? base)
|
|
755
|
+
}
|
|
756
|
+
return source
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function renderTypeImports(imports) {
|
|
760
|
+
const bySource = new Map()
|
|
761
|
+
for (const info of imports) {
|
|
762
|
+
const source = resolveTypeImportModule(info.file, info.source)
|
|
763
|
+
const group = bySource.get(source) ?? []
|
|
764
|
+
group.push(info)
|
|
765
|
+
bySource.set(source, group)
|
|
766
|
+
}
|
|
767
|
+
// Emit each external type as a global `type X = import('mod').Y` ALIAS rather
|
|
768
|
+
// than an `import type { … }` statement. An import statement would make the
|
|
769
|
+
// sidecar a module, so its declarations (and the bundle's `/// <reference>` to
|
|
770
|
+
// it) would no longer be ambient — the restored JSDoc `@type {GeaEmbeddedImage[]}`
|
|
771
|
+
// in the bundle could not resolve the name. An import-type alias is a type
|
|
772
|
+
// expression (not an import statement), so the sidecar stays a script and the
|
|
773
|
+
// alias is globally visible.
|
|
774
|
+
return [...bySource.entries()]
|
|
775
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
776
|
+
.flatMap(([source, group]) =>
|
|
777
|
+
group
|
|
778
|
+
.sort((a, b) => a.local.localeCompare(b.local))
|
|
779
|
+
.map((info) => `type ${info.local} = import(${JSON.stringify(source)}).${info.imported}`)
|
|
780
|
+
)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// True for `T[]` / `Array<T>` / `ReadonlyArray<T>` where T is a named type
|
|
784
|
+
// reference (an interface/type alias). Excludes primitive-element arrays
|
|
785
|
+
// (`number[]`, `string[]`) and non-array annotations. Babel TS AST.
|
|
786
|
+
function isNamedInterfaceArrayTypeNode(typeNode) {
|
|
787
|
+
if (t.isTSArrayType(typeNode)) {
|
|
788
|
+
return t.isTSTypeReference(typeNode.elementType) && t.isIdentifier(typeNode.elementType.typeName)
|
|
789
|
+
}
|
|
790
|
+
if (t.isTSTypeReference(typeNode) && t.isIdentifier(typeNode.typeName)) {
|
|
791
|
+
const name = typeNode.typeName.name
|
|
792
|
+
if (name !== 'Array' && name !== 'ReadonlyArray') return false
|
|
793
|
+
const args = typeNode.typeParameters?.params
|
|
794
|
+
if (!args || args.length !== 1) return false
|
|
795
|
+
return t.isTSTypeReference(args[0]) && t.isIdentifier(args[0].typeName)
|
|
796
|
+
}
|
|
797
|
+
return false
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// True for `string[]` / `number[]` / `boolean[]` (and the `Array<…>` form). An
|
|
801
|
+
// empty primitive-element array annotation is just as lossy in the bundle as a
|
|
802
|
+
// named one — `const keys: string[] = []` infers `any[]` once the pushes live in
|
|
803
|
+
// other functions — so it gets the same JSDoc restoration.
|
|
804
|
+
function isPrimitiveArrayTypeNode(typeNode) {
|
|
805
|
+
const isPrimitiveElement = (el) =>
|
|
806
|
+
!!el && (t.isTSStringKeyword(el) || t.isTSNumberKeyword(el) || t.isTSBooleanKeyword(el))
|
|
807
|
+
if (t.isTSArrayType(typeNode)) return isPrimitiveElement(typeNode.elementType)
|
|
808
|
+
if (t.isTSTypeReference(typeNode) && t.isIdentifier(typeNode.typeName)) {
|
|
809
|
+
const name = typeNode.typeName.name
|
|
810
|
+
if (name !== 'Array' && name !== 'ReadonlyArray') return false
|
|
811
|
+
const args = typeNode.typeParameters?.params
|
|
812
|
+
return !!args && args.length === 1 && isPrimitiveElement(args[0])
|
|
813
|
+
}
|
|
814
|
+
return false
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Array annotations worth restoring as JSDoc: named-element (interface/alias) or
|
|
818
|
+
// primitive-element. Both lower to a typed `std::vector<…>` instead of a boxed
|
|
819
|
+
// `std::vector<gea_cpp_value>` once the element type is known.
|
|
820
|
+
function isHintableArrayTypeNode(typeNode) {
|
|
821
|
+
return isNamedInterfaceArrayTypeNode(typeNode) || isPrimitiveArrayTypeNode(typeNode)
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// A bare named-type reference (`GeaEmbeddedImage`, `FetchResponse`, an app
|
|
825
|
+
// interface) — NOT an array. Bundling erases it to `any` -> `gea_cpp_value`, so a
|
|
826
|
+
// field/var/return typed this way loses its static shape (and any `nativeTypes`
|
|
827
|
+
// host-handle lowering). Restored as JSDoc, it lowers to the typed struct/native
|
|
828
|
+
// type instead. Excludes `Array`/`ReadonlyArray` (handled by the array forms).
|
|
829
|
+
function isNamedTypeReferenceNode(typeNode) {
|
|
830
|
+
if (!t.isTSTypeReference(typeNode) || !t.isIdentifier(typeNode.typeName)) return false
|
|
831
|
+
const name = typeNode.typeName.name
|
|
832
|
+
if (name === 'Array' || name === 'ReadonlyArray') return false
|
|
833
|
+
return !typeNode.typeParameters
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
// Field/var/return annotations worth restoring: typed arrays or bare named types.
|
|
837
|
+
function isHintableTypeNode(typeNode) {
|
|
838
|
+
return isHintableArrayTypeNode(typeNode) || isNamedTypeReferenceNode(typeNode)
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
function isJsonParseCallNode(expr) {
|
|
843
|
+
return (
|
|
844
|
+
t.isCallExpression(expr) &&
|
|
845
|
+
t.isMemberExpression(expr.callee) &&
|
|
846
|
+
!expr.callee.computed &&
|
|
847
|
+
t.isIdentifier(expr.callee.object, { name: 'JSON' }) &&
|
|
848
|
+
t.isIdentifier(expr.callee.property, { name: 'parse' })
|
|
849
|
+
)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
// `const x = JSON.parse(...) as T` (or `const x: T = JSON.parse(...)`): the
|
|
853
|
+
// bundler erases the `as` cast / annotation, so capture the target type text and
|
|
854
|
+
// restore it as a JSDoc `@type`. geatsc then decodes the parse straight into the
|
|
855
|
+
// typed struct (one pass, no gea_cpp_value tree) instead of boxing. Gated on a
|
|
856
|
+
// JSON.parse initializer so only the exact typed-parse case is promoted.
|
|
857
|
+
function jsonParseCastTypeText(declarator, code) {
|
|
858
|
+
if (!declarator.init) return null
|
|
859
|
+
if (t.isTSAsExpression(declarator.init) && isJsonParseCallNode(declarator.init.expression)) {
|
|
860
|
+
const typeNode = declarator.init.typeAnnotation
|
|
861
|
+
if (typeNode?.start != null && typeNode?.end != null) return code.slice(typeNode.start, typeNode.end).trim()
|
|
862
|
+
}
|
|
863
|
+
if (isJsonParseCallNode(declarator.init)) {
|
|
864
|
+
const typeNode = declarator.id.typeAnnotation?.typeAnnotation
|
|
865
|
+
if (typeNode?.start != null && typeNode?.end != null) return code.slice(typeNode.start, typeNode.end).trim()
|
|
866
|
+
}
|
|
867
|
+
return null
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
function collectBundledTypeHints(appDir, entry) {
|
|
871
|
+
const declarations = new Map()
|
|
872
|
+
const typeImports = new Map()
|
|
873
|
+
const returnHints = new Map()
|
|
874
|
+
const paramHints = new Map()
|
|
875
|
+
const classHints = new Map()
|
|
876
|
+
// Class field type annotations (`hours: Forecast[]`). Bundling strips them,
|
|
877
|
+
// so we restore them as JSDoc `@type` and pull the referenced interface into
|
|
878
|
+
// the sidecar. Needed for typed store-array storage that reuses geatsc's
|
|
879
|
+
// `__gea_type_<Name>` struct — the element interface (e.g. `Forecast`) is
|
|
880
|
+
// referenced ONLY by class properties, so without this it never reaches the
|
|
881
|
+
// geatsc checker and the struct is never emitted. Keyed by `Class#prop`.
|
|
882
|
+
const propertyHints = new Map()
|
|
883
|
+
// Function-local typed arrays (`const rows: Row[] = []`) also lose their
|
|
884
|
+
// annotations in the bundle. Restore these as local JSDoc so helper
|
|
885
|
+
// functions that build typed store arrays do not box each row through
|
|
886
|
+
// `std::vector<gea_cpp_value>`. Keyed by `Function#local` or
|
|
887
|
+
// `Class#method#local`.
|
|
888
|
+
const localHints = new Map()
|
|
889
|
+
// Param/return annotations on arrow- and function-EXPRESSIONS that are NOT
|
|
890
|
+
// directly assigned to a named binding — returned closures (`return (x:
|
|
891
|
+
// number) => …`), callback arguments (`arr.map((v: Row) => …)`), and the
|
|
892
|
+
// like. The name-keyed paramHints/returnHints above only reach NAMED
|
|
893
|
+
// functions, methods, and `const f = (…) => …` declarators, so these
|
|
894
|
+
// anonymous callables otherwise lose their param types and every parameter
|
|
895
|
+
// widens to `gea_cpp_value` (the bundle is `.js`, so `isParameterOptional`
|
|
896
|
+
// treats every un-hinted param as optional and the emitter erases the slot —
|
|
897
|
+
// boxing each call's args). Restored as INLINE JSDoc on the callable node.
|
|
898
|
+
// Keyed by `${enclosingHintScope}|${paramNamesJoined}`; the value is the
|
|
899
|
+
// ORDERED list of callables sharing that key, consumed positionally on apply
|
|
900
|
+
// (vite preserves intra-scope expression order), so repeated shapes still map
|
|
901
|
+
// 1:1. Matching on the exact param-name tuple makes a wrong-node application
|
|
902
|
+
// essentially impossible — a mismatch just skips (no regression).
|
|
903
|
+
const inlineCallableHints = new Map()
|
|
904
|
+
const collectInlineCallableHint = (astPath, code) => {
|
|
905
|
+
const node = astPath.node
|
|
906
|
+
const parent = astPath.parent
|
|
907
|
+
// `const f = (x) => …` / `const f = function (x) {}` already restore via the
|
|
908
|
+
// name-keyed VariableDeclarator path; skip to avoid redundant JSDoc.
|
|
909
|
+
if (parent && t.isVariableDeclarator(parent) && parent.init === node) return
|
|
910
|
+
// The structural param-name key only matches across bundling when every
|
|
911
|
+
// parameter is a plain identifier; destructuring/rest params break it.
|
|
912
|
+
if (!node.params.every((p) => t.isIdentifier(p))) return
|
|
913
|
+
const params = []
|
|
914
|
+
for (const param of node.params) {
|
|
915
|
+
const typeNode = param.typeAnnotation?.typeAnnotation
|
|
916
|
+
if (!typeNode || typeNode.start == null || typeNode.end == null) continue
|
|
917
|
+
params.push({ name: param.name, type: code.slice(typeNode.start, typeNode.end).trim(), optional: param.optional === true })
|
|
918
|
+
}
|
|
919
|
+
const returnTypeNode = node.returnType?.typeAnnotation
|
|
920
|
+
const returnType =
|
|
921
|
+
returnTypeNode && returnTypeNode.start != null && returnTypeNode.end != null
|
|
922
|
+
? code.slice(returnTypeNode.start, returnTypeNode.end).trim()
|
|
923
|
+
: undefined
|
|
924
|
+
if (params.length === 0 && !returnType) return
|
|
925
|
+
const scope = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
|
|
926
|
+
const key = `${scope}|${node.params.map((p) => p.name).join(',')}`
|
|
927
|
+
const list = inlineCallableHints.get(key) ?? []
|
|
928
|
+
list.push({ params, returnType })
|
|
929
|
+
inlineCallableHints.set(key, list)
|
|
930
|
+
}
|
|
931
|
+
const collectParameterHints = (functionName, params, code) => {
|
|
932
|
+
const hints = []
|
|
933
|
+
for (const param of params) {
|
|
934
|
+
let name = null
|
|
935
|
+
let typeNode = null
|
|
936
|
+
let optional = false
|
|
937
|
+
let rest = false
|
|
938
|
+
if (t.isIdentifier(param)) {
|
|
939
|
+
name = param.name
|
|
940
|
+
typeNode = param.typeAnnotation?.typeAnnotation
|
|
941
|
+
optional = param.optional === true
|
|
942
|
+
} else if (t.isRestElement(param) && t.isIdentifier(param.argument)) {
|
|
943
|
+
name = param.argument.name
|
|
944
|
+
typeNode = param.typeAnnotation?.typeAnnotation ?? param.argument.typeAnnotation?.typeAnnotation
|
|
945
|
+
rest = true
|
|
946
|
+
}
|
|
947
|
+
if (!name || !typeNode) continue
|
|
948
|
+
if (typeNode.start == null || typeNode.end == null) continue
|
|
949
|
+
let typeText = code.slice(typeNode.start, typeNode.end).trim()
|
|
950
|
+
if (rest) {
|
|
951
|
+
const restElementType =
|
|
952
|
+
t.isTSArrayType(typeNode) && typeNode.elementType.start != null && typeNode.elementType.end != null
|
|
953
|
+
? code.slice(typeNode.elementType.start, typeNode.elementType.end).trim()
|
|
954
|
+
: t.isTSTypeReference(typeNode) &&
|
|
955
|
+
t.isIdentifier(typeNode.typeName) &&
|
|
956
|
+
(typeNode.typeName.name === 'Array' || typeNode.typeName.name === 'ReadonlyArray') &&
|
|
957
|
+
typeNode.typeParameters?.params?.length === 1 &&
|
|
958
|
+
typeNode.typeParameters.params[0].start != null &&
|
|
959
|
+
typeNode.typeParameters.params[0].end != null
|
|
960
|
+
? code.slice(typeNode.typeParameters.params[0].start, typeNode.typeParameters.params[0].end).trim()
|
|
961
|
+
: typeText
|
|
962
|
+
typeText = `...${restElementType}`
|
|
963
|
+
}
|
|
964
|
+
// Carry the `?` through to the restored JSDoc (`@param {T} [name]`), so
|
|
965
|
+
// the checker types the bundled param `T | undefined` and geatsc keeps a
|
|
966
|
+
// null-capable native slot (std::optional<record>) instead of widening.
|
|
967
|
+
hints.push({ name, type: typeText, optional })
|
|
968
|
+
}
|
|
969
|
+
if (hints.length > 0) paramHints.set(functionName, hints)
|
|
970
|
+
}
|
|
971
|
+
const classNoRuntimeBridgeDirective = (node) =>
|
|
972
|
+
node.leadingComments?.some((comment) => /@gea-no-runtime-bridge\b/.test(comment.value)) === true
|
|
973
|
+
// The gea-embedded runtime's public API (loadImage, loadImageFile, …) is
|
|
974
|
+
// inlined into the bundle by vite, so its signatures vanish exactly like app
|
|
975
|
+
// code's. Walk the runtime sources FIRST so an app function with the same
|
|
976
|
+
// name overrides the runtime hint.
|
|
977
|
+
const hintSourceFiles = [
|
|
978
|
+
...collectSourceFiles(path.join(coreRoot, 'runtime')),
|
|
979
|
+
...collectReachableSourceFiles(appDir, entry),
|
|
980
|
+
]
|
|
981
|
+
for (const file of hintSourceFiles) {
|
|
982
|
+
const code = fs.readFileSync(file, 'utf8')
|
|
983
|
+
const ast = parseTsx(code)
|
|
984
|
+
traverse(ast, {
|
|
985
|
+
ImportDeclaration(astPath) {
|
|
986
|
+
const node = astPath.node
|
|
987
|
+
const source = node.source?.value
|
|
988
|
+
if (typeof source !== 'string') return
|
|
989
|
+
const declarationTypeOnly = node.importKind === 'type'
|
|
990
|
+
for (const specifier of node.specifiers) {
|
|
991
|
+
if (!t.isImportSpecifier(specifier)) continue
|
|
992
|
+
if (!declarationTypeOnly && specifier.importKind !== 'type') continue
|
|
993
|
+
if (!t.isIdentifier(specifier.local)) continue
|
|
994
|
+
const imported = t.isIdentifier(specifier.imported) ? specifier.imported.name : specifier.imported.value
|
|
995
|
+
typeImports.set(specifier.local.name, { file, source, imported, local: specifier.local.name })
|
|
996
|
+
}
|
|
997
|
+
},
|
|
998
|
+
TSInterfaceDeclaration(astPath) {
|
|
999
|
+
const node = astPath.node
|
|
1000
|
+
if (!node.id?.name || node.start == null || node.end == null) return
|
|
1001
|
+
declarations.set(node.id.name, code.slice(node.start, node.end).replace(/^export\s+/, '').trim())
|
|
1002
|
+
},
|
|
1003
|
+
TSTypeAliasDeclaration(astPath) {
|
|
1004
|
+
const node = astPath.node
|
|
1005
|
+
if (!node.id?.name || node.start == null || node.end == null) return
|
|
1006
|
+
declarations.set(node.id.name, code.slice(node.start, node.end).replace(/^export\s+/, '').trim())
|
|
1007
|
+
},
|
|
1008
|
+
FunctionDeclaration(astPath) {
|
|
1009
|
+
const node = astPath.node
|
|
1010
|
+
if (!node.id?.name) return
|
|
1011
|
+
collectParameterHints(node.id.name, node.params, code)
|
|
1012
|
+
if (!node.returnType?.typeAnnotation) return
|
|
1013
|
+
const typeNode = node.returnType.typeAnnotation
|
|
1014
|
+
if (typeNode.start == null || typeNode.end == null) return
|
|
1015
|
+
returnHints.set(node.id.name, code.slice(typeNode.start, typeNode.end).trim())
|
|
1016
|
+
},
|
|
1017
|
+
ArrowFunctionExpression(astPath) {
|
|
1018
|
+
collectInlineCallableHint(astPath, code)
|
|
1019
|
+
},
|
|
1020
|
+
FunctionExpression(astPath) {
|
|
1021
|
+
collectInlineCallableHint(astPath, code)
|
|
1022
|
+
},
|
|
1023
|
+
Class(astPath) {
|
|
1024
|
+
const node = astPath.node
|
|
1025
|
+
if (!node.id?.name) return
|
|
1026
|
+
if (classNoRuntimeBridgeDirective(node)) classHints.set(node.id.name, '@gea-no-runtime-bridge')
|
|
1027
|
+
},
|
|
1028
|
+
VariableDeclarator(astPath) {
|
|
1029
|
+
const node = astPath.node
|
|
1030
|
+
if (!t.isIdentifier(node.id)) return
|
|
1031
|
+
if (node.init && (t.isArrowFunctionExpression(node.init) || t.isFunctionExpression(node.init))) {
|
|
1032
|
+
collectParameterHints(node.id.name, node.init.params, code)
|
|
1033
|
+
const typeNode = node.init.returnType?.typeAnnotation
|
|
1034
|
+
if (typeNode && typeNode.start != null && typeNode.end != null) {
|
|
1035
|
+
returnHints.set(node.id.name, code.slice(typeNode.start, typeNode.end).trim())
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const jsonParseType = jsonParseCastTypeText(node, code)
|
|
1039
|
+
if (jsonParseType) {
|
|
1040
|
+
const fnName = resolveFunctionHintName(astPath)
|
|
1041
|
+
if (fnName) localHints.set(`${fnName}#${node.id.name}`, jsonParseType)
|
|
1042
|
+
return
|
|
1043
|
+
}
|
|
1044
|
+
const localTypeNode = node.id.typeAnnotation?.typeAnnotation
|
|
1045
|
+
if (!localTypeNode || localTypeNode.start == null || localTypeNode.end == null) return
|
|
1046
|
+
// Hintable arrays AND named type references (`const bytes: Uint8Array =
|
|
1047
|
+
// await res.arrayBuffer()`): bundling erases the annotation, the checker
|
|
1048
|
+
// types the local `any` → gea_cpp_value, and a native-typed initializer
|
|
1049
|
+
// (the fetch body's std::vector<std::uint8_t>) gets BOXED at the
|
|
1050
|
+
// assignment — per-byte, catastrophically. Named refs are the same hint
|
|
1051
|
+
// class ClassProperty already restores.
|
|
1052
|
+
if (!isHintableTypeNode(localTypeNode)) return
|
|
1053
|
+
// Module-level typed arrays (`const cacheImgs: GeaEmbeddedImage[] = []`)
|
|
1054
|
+
// lose their element type the same way function-local ones do, so key
|
|
1055
|
+
// them under a module scope rather than skipping.
|
|
1056
|
+
const functionName = resolveFunctionHintName(astPath) ?? MODULE_HINT_SCOPE
|
|
1057
|
+
localHints.set(`${functionName}#${node.id.name}`, code.slice(localTypeNode.start, localTypeNode.end).trim())
|
|
1058
|
+
},
|
|
1059
|
+
// Class method params carry the same annotations as free functions
|
|
1060
|
+
// (`multiply(other: Quaternion)`), but bundling strips them and the
|
|
1061
|
+
// checker then types `other` as `any` -> `gea_cpp_value`, losing the
|
|
1062
|
+
// concrete fields. Restore them as JSDoc keyed by `Class#method` so the
|
|
1063
|
+
// qualified name avoids collisions when two classes share a method name.
|
|
1064
|
+
ClassMethod(astPath) {
|
|
1065
|
+
const node = astPath.node
|
|
1066
|
+
if ((node.kind !== 'method' && node.kind !== 'constructor') || !t.isIdentifier(node.key)) return
|
|
1067
|
+
const classPath = astPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression())
|
|
1068
|
+
const className = classPath?.node?.id?.name
|
|
1069
|
+
if (!className) return
|
|
1070
|
+
const key = `${className}#${node.kind === 'constructor' ? 'constructor' : node.key.name}`
|
|
1071
|
+
collectParameterHints(key, node.params, code)
|
|
1072
|
+
const typeNode = node.returnType?.typeAnnotation
|
|
1073
|
+
if (typeNode && typeNode.start != null && typeNode.end != null) {
|
|
1074
|
+
returnHints.set(key, code.slice(typeNode.start, typeNode.end).trim())
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
// Class field annotations carry the element type of typed store arrays
|
|
1078
|
+
// (`hours: Forecast[]`). Bundling drops them; capture the annotation text
|
|
1079
|
+
// keyed by `Class#prop` so it can be re-applied as a JSDoc `@type`.
|
|
1080
|
+
//
|
|
1081
|
+
// Restricted to arrays whose element is a NAMED type reference
|
|
1082
|
+
// (`Forecast[]` / `Array<Forecast>`). This is exactly the typed
|
|
1083
|
+
// store-array reuse case the `__gea_type_<Name>` lowering targets.
|
|
1084
|
+
// Restoring annotations for primitive arrays (`number[]`) or scalars is
|
|
1085
|
+
// unnecessary here and would promote fields geatsc otherwise keeps
|
|
1086
|
+
// dynamic — exposing unrelated typed-storage lowering paths.
|
|
1087
|
+
ClassProperty(astPath) {
|
|
1088
|
+
const node = astPath.node
|
|
1089
|
+
if (node.computed || !t.isIdentifier(node.key)) return
|
|
1090
|
+
const typeNode = node.typeAnnotation?.typeAnnotation
|
|
1091
|
+
if (!typeNode || typeNode.start == null || typeNode.end == null) return
|
|
1092
|
+
const classPath = astPath.findParent((p) => p.isClassDeclaration() || p.isClassExpression())
|
|
1093
|
+
const className = classPath?.node?.id?.name
|
|
1094
|
+
if (!className) return
|
|
1095
|
+
// Named-element arrays (`Forecast[]`) and bare named-type fields (a host
|
|
1096
|
+
// handle like `drone: GeaEmbeddedImage`) keep their static type.
|
|
1097
|
+
// Primitive arrays (`number[]`) are restored too: the element-write
|
|
1098
|
+
// lowering gap that once forced them dynamic (`q[i] = q[i+1]` swaps
|
|
1099
|
+
// miscompiling on a typed field) is fixed — geatsc now lowers typed
|
|
1100
|
+
// field element writes through `gea::runtime::array::vector_set`
|
|
1101
|
+
// (pinned by compiler-js-callback-param-typing.test.ts's sibling
|
|
1102
|
+
// class-field cases). Without the restoration, a `fetchQueue:
|
|
1103
|
+
// number[]` store field stays `std::vector<gea_cpp_value>` and every
|
|
1104
|
+
// queue operation boxes.
|
|
1105
|
+
//
|
|
1106
|
+
// `@gea-no-runtime-bridge` marks hand-written native adapter classes.
|
|
1107
|
+
// Their annotated scalar fields are part of the typed ABI boundary
|
|
1108
|
+
// (`__nativeHandle: number`, `canvasWidth: () => number`, etc.), so
|
|
1109
|
+
// restore every explicit field annotation for those classes only.
|
|
1110
|
+
const restoreAllAnnotatedFields = classNoRuntimeBridgeDirective(classPath.node)
|
|
1111
|
+
if (
|
|
1112
|
+
!restoreAllAnnotatedFields &&
|
|
1113
|
+
!isNamedInterfaceArrayTypeNode(typeNode) &&
|
|
1114
|
+
!isNamedTypeReferenceNode(typeNode) &&
|
|
1115
|
+
!isPrimitiveArrayTypeNode(typeNode)
|
|
1116
|
+
) return
|
|
1117
|
+
propertyHints.set(`${className}#${node.key.name}`, code.slice(typeNode.start, typeNode.end).trim())
|
|
1118
|
+
},
|
|
1119
|
+
// A `class X extends ReactiveComponent<T>` (or `Component<T>`) gives
|
|
1120
|
+
// `this.el` the element type `T | null` (Component<RootElement> declares
|
|
1121
|
+
// `el: RootElement | null`). The lean ReactiveComponent transform strips
|
|
1122
|
+
// the base and its inherited `el`; the framework re-injects a plain `el`
|
|
1123
|
+
// field (vendor/gea transform.ts), but bundling leaves it UNTYPED so geatsc
|
|
1124
|
+
// boxes it to `gea_cpp_value` — and a typed canvas element must stay native
|
|
1125
|
+
// (it maps to `gea::embedded::ui::NodeHandle`, so `getContext('2d')` and the
|
|
1126
|
+
// mount's root binding flow with zero boxing). `el` has no source field to
|
|
1127
|
+
// annotate, so recover `T` from the heritage generic and key the hint under
|
|
1128
|
+
// `Class#el`; the ClassProperty restorer then stamps `@type {T | null}` onto
|
|
1129
|
+
// the injected field. Mirrors a non-stripped `Component<T>` whose `el`
|
|
1130
|
+
// already lowers to a NodeHandle via nativeTypes.
|
|
1131
|
+
Class(astPath) {
|
|
1132
|
+
const node = astPath.node
|
|
1133
|
+
if (!t.isIdentifier(node.id) || !t.isIdentifier(node.superClass)) return
|
|
1134
|
+
if (node.superClass.name !== 'ReactiveComponent' && node.superClass.name !== 'Component') return
|
|
1135
|
+
const typeArgs = node.superTypeParameters ?? node.superTypeArguments
|
|
1136
|
+
const typeArg = typeArgs?.params?.[0]
|
|
1137
|
+
if (!typeArg || typeArg.start == null || typeArg.end == null) return
|
|
1138
|
+
propertyHints.set(`${node.id.name}#el`, `${code.slice(typeArg.start, typeArg.end).trim()} | null`)
|
|
1139
|
+
},
|
|
1140
|
+
})
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const neededTypeNames = new Set()
|
|
1144
|
+
for (const returnType of returnHints.values()) {
|
|
1145
|
+
for (const name of declarations.keys()) {
|
|
1146
|
+
if (typeTextReferencesName(returnType, name)) neededTypeNames.add(name)
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
for (const params of paramHints.values()) {
|
|
1150
|
+
for (const param of params) {
|
|
1151
|
+
for (const name of declarations.keys()) {
|
|
1152
|
+
if (typeTextReferencesName(param.type, name)) neededTypeNames.add(name)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
for (const propertyType of propertyHints.values()) {
|
|
1157
|
+
for (const name of declarations.keys()) {
|
|
1158
|
+
if (typeTextReferencesName(propertyType, name)) neededTypeNames.add(name)
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
for (const localType of localHints.values()) {
|
|
1162
|
+
for (const name of declarations.keys()) {
|
|
1163
|
+
if (typeTextReferencesName(localType, name)) neededTypeNames.add(name)
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
// Transitively pull in type names referenced *inside* an already-needed
|
|
1167
|
+
// declaration. `type ControlButton = { id: ControlButtonId }` is named only
|
|
1168
|
+
// by a `@returns {ControlButton[]}`; without following the reference to
|
|
1169
|
+
// `ControlButtonId`, that nested alias is dropped from the sidecar and the
|
|
1170
|
+
// checker types `id` as `any` -> gea_cpp_value (losing static lowering and,
|
|
1171
|
+
// for sky-hop, exposing a heap-corruption crash on the dynamic value's
|
|
1172
|
+
// destructor). Fixpoint over declaration bodies so the whole referenced
|
|
1173
|
+
// closure (nested records, `A & B` intersections, string-literal unions) is
|
|
1174
|
+
// preserved. Self-references terminate because the set only grows and is
|
|
1175
|
+
// bounded by `declarations`.
|
|
1176
|
+
let addedTransitive = true
|
|
1177
|
+
while (addedTransitive) {
|
|
1178
|
+
addedTransitive = false
|
|
1179
|
+
for (const needed of [...neededTypeNames]) {
|
|
1180
|
+
const declText = declarations.get(needed)
|
|
1181
|
+
if (!declText) continue
|
|
1182
|
+
for (const name of declarations.keys()) {
|
|
1183
|
+
if (neededTypeNames.has(name)) continue
|
|
1184
|
+
if (typeTextReferencesName(declText, name)) {
|
|
1185
|
+
neededTypeNames.add(name)
|
|
1186
|
+
addedTransitive = true
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (
|
|
1192
|
+
returnHints.size === 0 &&
|
|
1193
|
+
paramHints.size === 0 &&
|
|
1194
|
+
classHints.size === 0 &&
|
|
1195
|
+
propertyHints.size === 0 &&
|
|
1196
|
+
localHints.size === 0 &&
|
|
1197
|
+
inlineCallableHints.size === 0
|
|
1198
|
+
) {
|
|
1199
|
+
return { imports: [], declarations: [], returnHints, paramHints, classHints, propertyHints, localHints, inlineCallableHints }
|
|
1200
|
+
}
|
|
1201
|
+
const declarationTexts = [...neededTypeNames].map((name) => declarations.get(name)).filter(Boolean)
|
|
1202
|
+
// Names emitted as LOCAL declarations in the sidecar. A name re-emitted as
|
|
1203
|
+
// BOTH a local declaration and a re-imported type collides (two competing
|
|
1204
|
+
// symbols for the same name). Subtract them: a locally-declared interface
|
|
1205
|
+
// (`interface CityState {…}`) trivially matches `/\bCityState\b/` against its
|
|
1206
|
+
// OWN declaration text, so without this guard `typeTextReferencesName` would
|
|
1207
|
+
// wrongly re-emit `import type { CityState }` next to the local declaration.
|
|
1208
|
+
const locallyDeclaredNames = new Set(
|
|
1209
|
+
[...neededTypeNames].filter((name) => declarations.get(name))
|
|
1210
|
+
)
|
|
1211
|
+
// A hint can reference an EXTERNAL type directly (not via a local declaration)
|
|
1212
|
+
// — e.g. `const cacheImgs: GeaEmbeddedImage[] = []` where `GeaEmbeddedImage` is
|
|
1213
|
+
// a type-only import from 'gea-embedded'. Re-import any such type so the
|
|
1214
|
+
// restored `@type {GeaEmbeddedImage[]}` resolves in the sidecar.
|
|
1215
|
+
const hintTypeTexts = [
|
|
1216
|
+
...returnHints.values(),
|
|
1217
|
+
...[...paramHints.values()].flatMap((params) => params.map((param) => param.type)),
|
|
1218
|
+
...propertyHints.values(),
|
|
1219
|
+
...localHints.values(),
|
|
1220
|
+
...[...inlineCallableHints.values()].flatMap((entries) =>
|
|
1221
|
+
entries.flatMap((entry) => [...entry.params.map((param) => param.type), ...(entry.returnType ? [entry.returnType] : [])])
|
|
1222
|
+
),
|
|
1223
|
+
]
|
|
1224
|
+
const referencedImports = [...typeImports].flatMap(([local, info]) =>
|
|
1225
|
+
!locallyDeclaredNames.has(local) &&
|
|
1226
|
+
(declarationTexts.some((declText) => typeTextReferencesName(declText, local)) ||
|
|
1227
|
+
hintTypeTexts.some((hintText) => typeTextReferencesName(hintText, local)))
|
|
1228
|
+
? [info]
|
|
1229
|
+
: []
|
|
1230
|
+
)
|
|
1231
|
+
return {
|
|
1232
|
+
imports: renderTypeImports(referencedImports),
|
|
1233
|
+
declarations: declarationTexts,
|
|
1234
|
+
returnHints,
|
|
1235
|
+
paramHints,
|
|
1236
|
+
classHints,
|
|
1237
|
+
propertyHints,
|
|
1238
|
+
localHints,
|
|
1239
|
+
inlineCallableHints,
|
|
1240
|
+
}
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
function writeAppleNativeViteConfig({ configPath, baseConfigPath, appDir, moduleGraphOutDir }) {
|
|
1244
|
+
const applePackageRoot = resolveApplePackageRoot(appDir)
|
|
1245
|
+
const appleRuntimeSubpath = path.join(applePackageRoot, 'runtime/$1.js')
|
|
1246
|
+
const applePackagePath = path.join(applePackageRoot, 'dist/index.js')
|
|
1247
|
+
const moduleGraphPluginPath = path.join(coreRoot, 'scripts/gea-vite-module-graph-plugin.mjs')
|
|
1248
|
+
const { threeSrcDir, threeUtilsModule, threeWebGLAnimationModule, threeWebXRManagerModule, troikaThreeTextModule } = resolveAppleNativeAliasModules(appDir)
|
|
1249
|
+
const q = (value) => JSON.stringify(value)
|
|
1250
|
+
fs.writeFileSync(
|
|
1251
|
+
configPath,
|
|
1252
|
+
`import baseConfig from ${q(baseConfigPath)}
|
|
1253
|
+
import { geaAppleNativeModuleAliases, geaModuleGraphPlugins } from ${q(moduleGraphPluginPath)}
|
|
1254
|
+
|
|
1255
|
+
const appleAliases = [
|
|
1256
|
+
{ find: /^@geajs\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
|
|
1257
|
+
{ find: /^@geastack\\/apple\\/(.+)$/, replacement: ${q(appleRuntimeSubpath)} },
|
|
1258
|
+
{ find: '@geajs/apple', replacement: ${q(applePackagePath)} },
|
|
1259
|
+
{ find: '@geastack/apple', replacement: ${q(applePackagePath)} },
|
|
1260
|
+
]
|
|
1261
|
+
const geaModuleGraph = geaModuleGraphPlugins({ outDir: ${q(moduleGraphOutDir || '')}, entryReachableOnly: true })
|
|
1262
|
+
const geaAppleNativeAliases = geaAppleNativeModuleAliases({
|
|
1263
|
+
threeSrcDir: ${q(threeSrcDir)},
|
|
1264
|
+
threeUtilsModule: ${q(threeUtilsModule)},
|
|
1265
|
+
threeWebGLAnimationModule: ${q(threeWebGLAnimationModule)},
|
|
1266
|
+
threeWebXRManagerModule: ${q(threeWebXRManagerModule)},
|
|
1267
|
+
troikaThreeTextModule: ${q(troikaThreeTextModule)},
|
|
1268
|
+
})
|
|
1269
|
+
|
|
1270
|
+
function normalizeAlias(alias) {
|
|
1271
|
+
if (!alias) return []
|
|
1272
|
+
if (Array.isArray(alias)) return alias
|
|
1273
|
+
return Object.entries(alias).map(([find, replacement]) => ({ find, replacement }))
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
export default async function geaAppleNativeViteConfig(env) {
|
|
1277
|
+
const resolved = typeof baseConfig === 'function' ? await baseConfig(env) : baseConfig
|
|
1278
|
+
const baseResolve = resolved.resolve ?? {}
|
|
1279
|
+
return {
|
|
1280
|
+
...resolved,
|
|
1281
|
+
resolve: {
|
|
1282
|
+
...baseResolve,
|
|
1283
|
+
alias: [...geaAppleNativeAliases, ...appleAliases, ...normalizeAlias(baseResolve.alias)],
|
|
1284
|
+
},
|
|
1285
|
+
plugins: [...(resolved.plugins ?? []), ...geaModuleGraph],
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
`
|
|
1289
|
+
)
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
// Frameworks that only exist on the *other* Apple platform. The fixture ships
|
|
1293
|
+
// every framework (UIKit + AppKit), but UIKit and AppKit declare members with the
|
|
1294
|
+
// same names (font, textColor, hidden, addSubview, ...). When a native member is
|
|
1295
|
+
// accessed on a dynamic gea_cpp_value receiver the emitter can't tell which class
|
|
1296
|
+
// it is, so it picks the first registered binding — UIKit's `::UILabel`/`::UIFont`
|
|
1297
|
+
// etc., which don't exist in a macOS (AppKit) build and fail to compile.
|
|
1298
|
+
const applePlatformOffFrameworks = {
|
|
1299
|
+
macos: new Set(['UIKit']),
|
|
1300
|
+
ios: new Set(['AppKit']),
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async function writeAppleNativeMetadata(outDir, appDir, platform = '') {
|
|
1304
|
+
const applePackage = path.join(resolveApplePackageRoot(appDir), 'dist/index.js')
|
|
1305
|
+
if (!fs.existsSync(applePackage)) {
|
|
1306
|
+
fail(`missing @geastack/apple build output: ${applePackage}. Run the Apple bindings build first.`)
|
|
1307
|
+
}
|
|
1308
|
+
const { appleSdkFixture, generateAppleBridgeMetadata } = await import(pathToFileURL(applePackage).href)
|
|
1309
|
+
const offPlatform = applePlatformOffFrameworks[platform] ?? new Set()
|
|
1310
|
+
// Keep the off-platform class SHELLS — other frameworks extend them (e.g.
|
|
1311
|
+
// MKMapView/MTKView extend UIKit.UIView) so the wrapper structs must still be
|
|
1312
|
+
// declared — but strip their members, constructors, free functions and
|
|
1313
|
+
// constants. Shared member names then register a binding only for the
|
|
1314
|
+
// on-platform class, so native access on a dynamic receiver resolves to the
|
|
1315
|
+
// platform's class instead of leaking an off-platform ObjC type.
|
|
1316
|
+
const fixture =
|
|
1317
|
+
offPlatform.size > 0
|
|
1318
|
+
? {
|
|
1319
|
+
...appleSdkFixture,
|
|
1320
|
+
frameworks: appleSdkFixture.frameworks.map((framework) =>
|
|
1321
|
+
offPlatform.has(framework.name)
|
|
1322
|
+
? {
|
|
1323
|
+
...framework,
|
|
1324
|
+
classes: (framework.classes ?? []).map((cls) => ({ ...cls, constructors: [], methods: [], properties: [] })),
|
|
1325
|
+
functions: [],
|
|
1326
|
+
constants: [],
|
|
1327
|
+
}
|
|
1328
|
+
: framework
|
|
1329
|
+
),
|
|
1330
|
+
}
|
|
1331
|
+
: appleSdkFixture
|
|
1332
|
+
const metadataPath = path.join(outDir, 'gea-apple-metadata.json')
|
|
1333
|
+
fs.writeFileSync(metadataPath, `${JSON.stringify(generateAppleBridgeMetadata(fixture), null, 2)}\n`)
|
|
1334
|
+
return metadataPath
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
function collectMemberPath(node) {
|
|
1338
|
+
if (t.isIdentifier(node)) return [node.name]
|
|
1339
|
+
if (!t.isMemberExpression(node) || node.computed) return null
|
|
1340
|
+
const objectPath = collectMemberPath(node.object)
|
|
1341
|
+
if (!objectPath || !t.isIdentifier(node.property)) return null
|
|
1342
|
+
objectPath.push(node.property.name)
|
|
1343
|
+
return objectPath
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
function evalStaticLiteral(node) {
|
|
1347
|
+
if (t.isBooleanLiteral(node) || t.isNumericLiteral(node) || t.isStringLiteral(node)) return node.value
|
|
1348
|
+
if (t.isNullLiteral(node)) return null
|
|
1349
|
+
if (t.isUnaryExpression(node, { operator: '-' })) {
|
|
1350
|
+
const value = evalStaticLiteral(node.argument)
|
|
1351
|
+
if (typeof value === 'number') return -value
|
|
1352
|
+
}
|
|
1353
|
+
if (t.isUnaryExpression(node, { operator: '+' })) {
|
|
1354
|
+
const value = evalStaticLiteral(node.argument)
|
|
1355
|
+
if (typeof value === 'number') return value
|
|
1356
|
+
}
|
|
1357
|
+
return undefined
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
function evalStaticObject(node) {
|
|
1361
|
+
if (!t.isObjectExpression(node)) return null
|
|
1362
|
+
const result = {}
|
|
1363
|
+
for (const prop of node.properties) {
|
|
1364
|
+
if (!t.isObjectProperty(prop) || prop.computed) return null
|
|
1365
|
+
const key = t.isIdentifier(prop.key) ? prop.key.name : t.isStringLiteral(prop.key) ? prop.key.value : null
|
|
1366
|
+
if (!key) return null
|
|
1367
|
+
const value = evalStaticLiteral(prop.value)
|
|
1368
|
+
if (value === undefined) return null
|
|
1369
|
+
result[key] = value
|
|
1370
|
+
}
|
|
1371
|
+
return result
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function collectDisplayConfig(appDir, entry) {
|
|
1375
|
+
const pending = [path.resolve(appDir, entry)]
|
|
1376
|
+
const processed = new Set()
|
|
1377
|
+
const displayConfig = {}
|
|
1378
|
+
|
|
1379
|
+
while (pending.length > 0) {
|
|
1380
|
+
const srcId = pending.shift()
|
|
1381
|
+
if (!srcId || processed.has(srcId) || !fs.existsSync(srcId)) continue
|
|
1382
|
+
processed.add(srcId)
|
|
1383
|
+
|
|
1384
|
+
// Only parse JS/TS modules. The import walk follows every local import,
|
|
1385
|
+
// including asset imports (e.g. troika font `import url from './x.woff'`);
|
|
1386
|
+
// feeding a binary asset to the TS parser blows up on its first non-ASCII
|
|
1387
|
+
// byte (a .woff's `wOFF` header → "Unexpected character" at column 4).
|
|
1388
|
+
// Assets carry no Display config, so skip them.
|
|
1389
|
+
if (!/\.(?:mjs|cjs|jsx?|tsx?)$/i.test(srcId)) continue
|
|
1390
|
+
|
|
1391
|
+
const code = fs.readFileSync(srcId, 'utf8')
|
|
1392
|
+
const ast = parseTsx(code)
|
|
1393
|
+
const displayVars = new Set()
|
|
1394
|
+
|
|
1395
|
+
traverse(ast, {
|
|
1396
|
+
ImportDeclaration(astPath) {
|
|
1397
|
+
const source = astPath.node.source.value
|
|
1398
|
+
if (source.includes('@geastack/core')) {
|
|
1399
|
+
for (const spec of astPath.node.specifiers) {
|
|
1400
|
+
if (t.isImportSpecifier(spec) && t.isIdentifier(spec.imported, { name: 'Display' }) && t.isIdentifier(spec.local)) {
|
|
1401
|
+
displayVars.add(spec.local.name)
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
discoverLocalImport(source, srcId, pending)
|
|
1406
|
+
},
|
|
1407
|
+
})
|
|
1408
|
+
|
|
1409
|
+
traverse(ast, {
|
|
1410
|
+
ExpressionStatement(astPath) {
|
|
1411
|
+
if (astPath.parent.type !== 'Program') return
|
|
1412
|
+
const expr = astPath.node.expression
|
|
1413
|
+
if (!t.isCallExpression(expr)) return
|
|
1414
|
+
const memberPath = collectMemberPath(expr.callee)
|
|
1415
|
+
if (!memberPath || memberPath.length !== 2 || !displayVars.has(memberPath[0])) return
|
|
1416
|
+
const config = evalStaticObject(expr.arguments[0])
|
|
1417
|
+
if (!config) return
|
|
1418
|
+
if (memberPath[1] === 'setFlushConfig') {
|
|
1419
|
+
const chunkRows = positiveInteger(config.rows)
|
|
1420
|
+
const queueDepth = positiveInteger(config.depth)
|
|
1421
|
+
if (chunkRows > 0) displayConfig.flushChunkRows = chunkRows
|
|
1422
|
+
if (queueDepth > 0) displayConfig.flushQueueDepth = queueDepth
|
|
1423
|
+
} else if (memberPath[1] === 'setMemoryConfig') {
|
|
1424
|
+
const commandBufferCommands = positiveInteger(config.commandBufferCommands)
|
|
1425
|
+
if (commandBufferCommands > 0) displayConfig.commandBufferCommands = commandBufferCommands
|
|
1426
|
+
if (typeof config.backgroundCache === 'boolean') displayConfig.backgroundCache = config.backgroundCache
|
|
1427
|
+
}
|
|
1428
|
+
},
|
|
1429
|
+
})
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
return displayConfig
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
function positiveInteger(value) {
|
|
1436
|
+
if (typeof value !== 'number') return 0
|
|
1437
|
+
const rounded = Math.floor(value)
|
|
1438
|
+
return rounded > 0 ? rounded : 0
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function writeDisplayConfigHeader(outDir, displayDefaults) {
|
|
1442
|
+
const lines = ['#pragma once']
|
|
1443
|
+
const chunkRows = positiveInteger(displayDefaults.flushChunkRows)
|
|
1444
|
+
const queueDepth = positiveInteger(displayDefaults.flushQueueDepth)
|
|
1445
|
+
const commandBufferCommands = positiveInteger(displayDefaults.commandBufferCommands)
|
|
1446
|
+
if (chunkRows > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_FLUSH_CHUNK_MAX ${chunkRows}`)
|
|
1447
|
+
if (queueDepth > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_FLUSH_QUEUE_DEPTH ${queueDepth}`)
|
|
1448
|
+
if (commandBufferCommands > 0) lines.push(`#define GEA_EMBEDDED_DISPLAY_COMMAND_BUFFER_COMMANDS ${commandBufferCommands}`)
|
|
1449
|
+
if (typeof displayDefaults.backgroundCache === 'boolean') {
|
|
1450
|
+
lines.push(`#define GEA_EMBEDDED_DISPLAY_BACKGROUND_CACHE_IN_SRAM ${displayDefaults.backgroundCache ? 1 : 0}`)
|
|
1451
|
+
}
|
|
1452
|
+
writeFileIfChanged(path.join(outDir, 'gea_embedded_app_config.h'), `${lines.join('\n')}\n`)
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// Parse plain CSS rules (already free of @media) into StyleSheet registration
|
|
1456
|
+
// calls. When `mediaCondition` is non-empty, every emitted rule is gated on it
|
|
1457
|
+
// and re-evaluated against the viewport at runtime.
|
|
1458
|
+
function cssRegisterCall(kind) {
|
|
1459
|
+
if (useStaticCssRules) {
|
|
1460
|
+
if (kind === 'class') return 'registerStaticRule'
|
|
1461
|
+
if (kind === 'element') return 'registerStaticElementRule'
|
|
1462
|
+
if (kind === 'selector') return 'registerStaticSelectorRule'
|
|
1463
|
+
if (kind === 'keyframe') return 'registerStaticKeyframeRule'
|
|
1464
|
+
}
|
|
1465
|
+
if (kind === 'class') return 'registerRule'
|
|
1466
|
+
if (kind === 'element') return 'registerElementRule'
|
|
1467
|
+
if (kind === 'selector') return 'registerSelectorRule'
|
|
1468
|
+
return 'registerKeyframeRule'
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
function staticSelectorKind(kind) {
|
|
1472
|
+
if (kind === 'class') return 'gea::embedded::ui::StaticStyleSelectorKind::Class'
|
|
1473
|
+
if (kind === 'element') return 'gea::embedded::ui::StaticStyleSelectorKind::Element'
|
|
1474
|
+
return 'gea::embedded::ui::StaticStyleSelectorKind::Selector'
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
let staticCssSelectorPlanCache = new Set()
|
|
1478
|
+
let staticCssMediaConditionPlanCache = new Set()
|
|
1479
|
+
|
|
1480
|
+
function stripStaticSelectorPseudoElement(selector) {
|
|
1481
|
+
return selector.replace(/::(?:before|after)\s*$/i, '').trim()
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
function parseStaticSimpleSelectorSpec(rawSimple) {
|
|
1485
|
+
const simple = rawSimple.trim()
|
|
1486
|
+
if (!simple || simple.includes('::')) return null
|
|
1487
|
+
const spec = {
|
|
1488
|
+
tag: null,
|
|
1489
|
+
id: null,
|
|
1490
|
+
classes: [],
|
|
1491
|
+
wantsRoot: false,
|
|
1492
|
+
wantsFirstChild: false,
|
|
1493
|
+
wantsLastChild: false,
|
|
1494
|
+
}
|
|
1495
|
+
let i = 0
|
|
1496
|
+
if (simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') {
|
|
1497
|
+
const start = i
|
|
1498
|
+
while (i < simple.length && simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') i++
|
|
1499
|
+
const tag = simple.slice(start, i).trim().toLowerCase()
|
|
1500
|
+
if (!tag) return null
|
|
1501
|
+
spec.tag = tag
|
|
1502
|
+
}
|
|
1503
|
+
while (i < simple.length) {
|
|
1504
|
+
const marker = simple[i++]
|
|
1505
|
+
const start = i
|
|
1506
|
+
while (i < simple.length && simple[i] !== '.' && simple[i] !== '#' && simple[i] !== ':') i++
|
|
1507
|
+
const token = simple.slice(start, i).trim()
|
|
1508
|
+
if (!token) return null
|
|
1509
|
+
if (marker === '.') {
|
|
1510
|
+
spec.classes.push(token)
|
|
1511
|
+
} else if (marker === '#') {
|
|
1512
|
+
spec.id = token
|
|
1513
|
+
} else if (marker === ':') {
|
|
1514
|
+
if (token === 'root') spec.wantsRoot = true
|
|
1515
|
+
else if (token === 'first-child') spec.wantsFirstChild = true
|
|
1516
|
+
else if (token === 'last-child') spec.wantsLastChild = true
|
|
1517
|
+
else return null
|
|
1518
|
+
} else {
|
|
1519
|
+
return null
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (!spec.tag && !spec.id && spec.classes.length === 0 &&
|
|
1523
|
+
!spec.wantsRoot && !spec.wantsFirstChild && !spec.wantsLastChild) {
|
|
1524
|
+
return null
|
|
1525
|
+
}
|
|
1526
|
+
return spec
|
|
1527
|
+
}
|
|
1528
|
+
|
|
1529
|
+
function parseStaticSelectorPlanSpec(selector) {
|
|
1530
|
+
const text = stripStaticSelectorPseudoElement(selector)
|
|
1531
|
+
if (!text) return null
|
|
1532
|
+
const parts = []
|
|
1533
|
+
let nextDirect = false
|
|
1534
|
+
let i = 0
|
|
1535
|
+
while (i < text.length) {
|
|
1536
|
+
while (i < text.length && text.charCodeAt(i) <= 32) i++
|
|
1537
|
+
if (i >= text.length) break
|
|
1538
|
+
if (text[i] === '>') {
|
|
1539
|
+
nextDirect = true
|
|
1540
|
+
i++
|
|
1541
|
+
continue
|
|
1542
|
+
}
|
|
1543
|
+
const start = i
|
|
1544
|
+
while (i < text.length && text.charCodeAt(i) > 32 && text[i] !== '>') i++
|
|
1545
|
+
if (i <= start) continue
|
|
1546
|
+
const simple = parseStaticSimpleSelectorSpec(text.slice(start, i))
|
|
1547
|
+
if (!simple) return null
|
|
1548
|
+
parts.push({ simple, directParent: nextDirect })
|
|
1549
|
+
nextDirect = false
|
|
1550
|
+
}
|
|
1551
|
+
return parts.length > 0 ? { text, parts } : null
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function staticSimpleSelectorSpecCode(simple) {
|
|
1555
|
+
const tag = simple.tag ? JSON.stringify(simple.tag) : 'nullptr'
|
|
1556
|
+
const id = simple.id ? JSON.stringify(simple.id) : 'nullptr'
|
|
1557
|
+
const classes = simple.classes.map((className) => JSON.stringify(className)).join(', ')
|
|
1558
|
+
return `{${tag}, ${id}, {${classes}}, ${simple.wantsRoot ? 'true' : 'false'}, ${simple.wantsFirstChild ? 'true' : 'false'}, ${simple.wantsLastChild ? 'true' : 'false'}}`
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
function staticSelectorPartSpecCode(part) {
|
|
1562
|
+
return `{${staticSimpleSelectorSpecCode(part.simple)}, ${part.directParent ? 'true' : 'false'}}`
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
function staticSelectorPlanRegistration(selector) {
|
|
1566
|
+
if (!useStaticCssRules) return []
|
|
1567
|
+
const plan = parseStaticSelectorPlanSpec(selector)
|
|
1568
|
+
if (!plan) return []
|
|
1569
|
+
if (staticCssSelectorPlanCache.has(plan.text)) return []
|
|
1570
|
+
staticCssSelectorPlanCache.add(plan.text)
|
|
1571
|
+
const parts = plan.parts.map(staticSelectorPartSpecCode).join(', ')
|
|
1572
|
+
return [
|
|
1573
|
+
`__gea_stylesheet.registerStaticSelectorPlan(${JSON.stringify(selector)}, {${parts}});`,
|
|
1574
|
+
]
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
function parseStaticMediaNumber(raw) {
|
|
1578
|
+
const value = raw.trim()
|
|
1579
|
+
if (!value) return null
|
|
1580
|
+
const slash = value.indexOf('/')
|
|
1581
|
+
if (slash >= 0) {
|
|
1582
|
+
const width = Number.parseFloat(value.slice(0, slash))
|
|
1583
|
+
const height = Number.parseFloat(value.slice(slash + 1))
|
|
1584
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || height === 0) return null
|
|
1585
|
+
return width / height
|
|
1586
|
+
}
|
|
1587
|
+
const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(.*)$/)
|
|
1588
|
+
if (!match) return null
|
|
1589
|
+
const number = Number(match[1])
|
|
1590
|
+
if (!Number.isFinite(number)) return null
|
|
1591
|
+
const unit = match[2].trim().toLowerCase()
|
|
1592
|
+
if (unit === 'dpi') return number / 96
|
|
1593
|
+
return number
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
function staticMediaFeatureForName(name) {
|
|
1597
|
+
const feature = name.trim().toLowerCase()
|
|
1598
|
+
const exact = (kind) => ({ kind, compare: 'Equal' })
|
|
1599
|
+
const min = (kind) => ({ kind, compare: 'Min' })
|
|
1600
|
+
const max = (kind) => ({ kind, compare: 'Max' })
|
|
1601
|
+
switch (feature) {
|
|
1602
|
+
case 'orientation': return exact('Orientation')
|
|
1603
|
+
case 'monochrome': return exact('Monochrome')
|
|
1604
|
+
case 'min-monochrome': return min('Monochrome')
|
|
1605
|
+
case 'max-monochrome': return max('Monochrome')
|
|
1606
|
+
case 'width': return exact('Width')
|
|
1607
|
+
case 'min-width': return min('Width')
|
|
1608
|
+
case 'max-width': return max('Width')
|
|
1609
|
+
case 'height': return exact('Height')
|
|
1610
|
+
case 'min-height': return min('Height')
|
|
1611
|
+
case 'max-height': return max('Height')
|
|
1612
|
+
case 'aspect-ratio': return exact('AspectRatio')
|
|
1613
|
+
case 'min-aspect-ratio': return min('AspectRatio')
|
|
1614
|
+
case 'max-aspect-ratio': return max('AspectRatio')
|
|
1615
|
+
case 'resolution':
|
|
1616
|
+
case 'device-pixel-ratio':
|
|
1617
|
+
return exact('Resolution')
|
|
1618
|
+
case 'min-resolution':
|
|
1619
|
+
case 'min-device-pixel-ratio':
|
|
1620
|
+
return min('Resolution')
|
|
1621
|
+
case 'max-resolution':
|
|
1622
|
+
case 'max-device-pixel-ratio':
|
|
1623
|
+
return max('Resolution')
|
|
1624
|
+
default:
|
|
1625
|
+
return null
|
|
1626
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
function alwaysFalseStaticMediaTerm() {
|
|
1630
|
+
return { kind: 'AlwaysFalse', compare: 'Equal', value: 0, orientation: 0 }
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function compileStaticMediaFeatureTerm(name, rawValue) {
|
|
1634
|
+
const feature = staticMediaFeatureForName(name)
|
|
1635
|
+
if (!feature) return alwaysFalseStaticMediaTerm()
|
|
1636
|
+
if (feature.kind === 'Orientation') {
|
|
1637
|
+
const orientation = rawValue.trim().toLowerCase()
|
|
1638
|
+
if (orientation === 'portrait') return { ...feature, value: 0, orientation: 1 }
|
|
1639
|
+
if (orientation === 'landscape') return { ...feature, value: 0, orientation: 2 }
|
|
1640
|
+
return alwaysFalseStaticMediaTerm()
|
|
1641
|
+
}
|
|
1642
|
+
const value = parseStaticMediaNumber(rawValue)
|
|
1643
|
+
if (value === null) return alwaysFalseStaticMediaTerm()
|
|
1644
|
+
return { ...feature, value, orientation: 0 }
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
function parseStaticMediaQuerySpec(rawQuery) {
|
|
1648
|
+
const query = rawQuery.trim()
|
|
1649
|
+
if (!query) return { valid: false, terms: [] }
|
|
1650
|
+
const terms = []
|
|
1651
|
+
let i = 0
|
|
1652
|
+
while (true) {
|
|
1653
|
+
const open = query.indexOf('(', i)
|
|
1654
|
+
if (open < 0) break
|
|
1655
|
+
const close = query.indexOf(')', open)
|
|
1656
|
+
if (close < 0) return { valid: false, terms: [] }
|
|
1657
|
+
const feature = query.slice(open + 1, close)
|
|
1658
|
+
const colon = feature.indexOf(':')
|
|
1659
|
+
if (colon >= 0) {
|
|
1660
|
+
terms.push(compileStaticMediaFeatureTerm(feature.slice(0, colon), feature.slice(colon + 1)))
|
|
1661
|
+
} else if (feature.trim().toLowerCase() === 'monochrome') {
|
|
1662
|
+
terms.push({ kind: 'Monochrome', compare: 'Boolean', value: 0, orientation: 0 })
|
|
1663
|
+
}
|
|
1664
|
+
i = close + 1
|
|
1665
|
+
}
|
|
1666
|
+
return { valid: true, terms }
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function staticMediaTermSpecCode(term) {
|
|
1670
|
+
return `{gea::embedded::ui::StaticStyleMediaFeatureKind::${term.kind}, gea::embedded::ui::StaticStyleMediaCompare::${term.compare}, ${term.value}, ${term.orientation}}`
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function staticMediaQuerySpecCode(query) {
|
|
1674
|
+
return `{{${query.terms.map(staticMediaTermSpecCode).join(', ')}}, ${query.valid ? 'true' : 'false'}}`
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function staticMediaConditionPlanRegistration(condition) {
|
|
1678
|
+
if (!useStaticCssRules || !condition) return []
|
|
1679
|
+
if (staticCssMediaConditionPlanCache.has(condition)) return []
|
|
1680
|
+
const queries = splitTopLevelCss(condition, ',').map(parseStaticMediaQuerySpec)
|
|
1681
|
+
if (queries.length === 0) return []
|
|
1682
|
+
staticCssMediaConditionPlanCache.add(condition)
|
|
1683
|
+
return [
|
|
1684
|
+
`__gea_stylesheet.registerStaticMediaConditionPlan(${JSON.stringify(condition)}, {${queries.map(staticMediaQuerySpecCode).join(', ')}});`,
|
|
1685
|
+
]
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
function roundCssNumber(value) {
|
|
1689
|
+
return value < 0 ? Math.ceil(value - 0.5) : Math.floor(value + 0.5)
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
function clampInt16(value) {
|
|
1693
|
+
if (value < -32768) return -32768
|
|
1694
|
+
if (value > 32767) return 32767
|
|
1695
|
+
return value
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
function simpleStaticLength(value) {
|
|
1699
|
+
const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(px|%)?$/i)
|
|
1700
|
+
if (!match) return null
|
|
1701
|
+
const number = Number(match[1])
|
|
1702
|
+
if (!Number.isFinite(number)) return null
|
|
1703
|
+
const unit = (match[2] || '').toLowerCase()
|
|
1704
|
+
return { number, unit }
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function parseStaticCssLength(rawValue) {
|
|
1708
|
+
const value = rawValue.trim()
|
|
1709
|
+
const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(px|%|vw|vh|vmin|vmax|dvw|dvh)?$/i)
|
|
1710
|
+
if (!match) return null
|
|
1711
|
+
let number = Number(match[1])
|
|
1712
|
+
if (!Number.isFinite(number)) return null
|
|
1713
|
+
if (Object.is(number, -0)) number = 0
|
|
1714
|
+
const unit = (match[2] || '').toLowerCase()
|
|
1715
|
+
const units = new Map([
|
|
1716
|
+
['', 'Raw'],
|
|
1717
|
+
['px', 'Px'],
|
|
1718
|
+
['%', 'Percent'],
|
|
1719
|
+
['vw', 'Vw'],
|
|
1720
|
+
['vh', 'Vh'],
|
|
1721
|
+
['vmin', 'Vmin'],
|
|
1722
|
+
['vmax', 'Vmax'],
|
|
1723
|
+
['dvw', 'Dvw'],
|
|
1724
|
+
['dvh', 'Dvh'],
|
|
1725
|
+
])
|
|
1726
|
+
return units.has(unit) ? { number, unit: units.get(unit) } : null
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
let staticCssLengthExpressionCounter = 0
|
|
1730
|
+
let staticCssLengthExpressionCache = new Map()
|
|
1731
|
+
|
|
1732
|
+
function cssNumberCode(value) {
|
|
1733
|
+
if (!Number.isFinite(value)) return '0'
|
|
1734
|
+
return String(Object.is(value, -0) ? 0 : value)
|
|
1735
|
+
}
|
|
1736
|
+
|
|
1737
|
+
function staticLengthSpecCode(spec) {
|
|
1738
|
+
return `{gea::embedded::ui::StaticStyleLengthUnit::${spec.unit}, ${spec.value}}`
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
function zeroStaticLengthSpec() {
|
|
1742
|
+
return { unit: 'Raw', value: '0' }
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function staticCssFunctionInner(rawValue, name) {
|
|
1746
|
+
const value = rawValue.trim()
|
|
1747
|
+
const lower = value.toLowerCase()
|
|
1748
|
+
const prefix = `${name}(`
|
|
1749
|
+
if (!lower.startsWith(prefix) || !value.endsWith(')')) return null
|
|
1750
|
+
return value.slice(prefix.length, -1)
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
function findTopLevelCssOperator(value, operators) {
|
|
1754
|
+
let depth = 0
|
|
1755
|
+
for (let i = 0; i < value.length; i++) {
|
|
1756
|
+
const char = value[i]
|
|
1757
|
+
if (char === '(') {
|
|
1758
|
+
depth++
|
|
1759
|
+
continue
|
|
1760
|
+
}
|
|
1761
|
+
if (char === ')') {
|
|
1762
|
+
if (depth > 0) depth--
|
|
1763
|
+
continue
|
|
1764
|
+
}
|
|
1765
|
+
if (depth !== 0 || !operators.includes(char) || i === 0) continue
|
|
1766
|
+
if ((char === '+' || char === '-') && /[eE]/.test(value[i - 1] ?? '')) continue
|
|
1767
|
+
return i
|
|
1768
|
+
}
|
|
1769
|
+
return -1
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1772
|
+
function emitStaticLengthExpression(kind, specs, options = {}) {
|
|
1773
|
+
const key = options.key ?? `${kind}:${specs.map((spec) => `${spec.unit}:${spec.value}`).join('|')}:${options.scalar ?? 0}:${options.name ?? ''}:${options.hasFallback ? 1 : 0}`
|
|
1774
|
+
const cached = staticCssLengthExpressionCache.get(key)
|
|
1775
|
+
if (cached) return { setup: [], spec: { unit: 'Expression', value: `static_cast<float>(${cached})` } }
|
|
1776
|
+
const name = `gea_css_len_expr_${staticCssLengthExpressionCounter++}`
|
|
1777
|
+
staticCssLengthExpressionCache.set(key, name)
|
|
1778
|
+
const empty = zeroStaticLengthSpec()
|
|
1779
|
+
const a = specs[0] ?? empty
|
|
1780
|
+
const b = specs[1] ?? empty
|
|
1781
|
+
const c = specs[2] ?? empty
|
|
1782
|
+
const expressionName = options.name ? JSON.stringify(options.name) : 'nullptr'
|
|
1783
|
+
const scalar = cssNumberCode(options.scalar ?? 0)
|
|
1784
|
+
const hasFallback = options.hasFallback ? 'true' : 'false'
|
|
1785
|
+
const call = `const std::uint16_t ${name} = __gea_stylesheet.registerStaticLengthExpression(gea::embedded::ui::StaticStyleLengthExpressionKind::${kind}, ${staticLengthSpecCode(a)}, ${staticLengthSpecCode(b)}, ${staticLengthSpecCode(c)}, ${scalar}, ${expressionName}, ${hasFallback});`
|
|
1786
|
+
return { setup: [call], spec: { unit: 'Expression', value: `static_cast<float>(${name})` } }
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1789
|
+
function parseStaticCssLengthSpec(rawValue) {
|
|
1790
|
+
const value = rawValue.trim()
|
|
1791
|
+
const simple = parseStaticCssLength(value)
|
|
1792
|
+
if (simple) return { setup: [], spec: { unit: simple.unit, value: cssNumberCode(simple.number) } }
|
|
1793
|
+
|
|
1794
|
+
const varInner = staticCssFunctionInner(value, 'var')
|
|
1795
|
+
if (varInner !== null) {
|
|
1796
|
+
const parts = splitTopLevelCss(varInner, ',')
|
|
1797
|
+
if (parts.length < 1 || parts.length > 2) return null
|
|
1798
|
+
const name = parts[0].trim()
|
|
1799
|
+
if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
|
|
1800
|
+
const fallback = parts.length === 2 ? parseStaticCssLengthSpec(parts[1]) : null
|
|
1801
|
+
if (parts.length === 2 && !fallback) return null
|
|
1802
|
+
const emitted = emitStaticLengthExpression('Var',
|
|
1803
|
+
[fallback?.spec ?? zeroStaticLengthSpec()],
|
|
1804
|
+
{ name, hasFallback: Boolean(fallback), key: `var:${name}:${fallback ? `${fallback.spec.unit}:${fallback.spec.value}` : ''}` })
|
|
1805
|
+
return { setup: [...(fallback?.setup ?? []), ...emitted.setup], spec: emitted.spec }
|
|
1806
|
+
}
|
|
1807
|
+
|
|
1808
|
+
const clampInner = staticCssFunctionInner(value, 'clamp')
|
|
1809
|
+
if (clampInner !== null) {
|
|
1810
|
+
const parts = splitTopLevelCss(clampInner, ',')
|
|
1811
|
+
if (parts.length !== 3) return null
|
|
1812
|
+
const parsed = parts.map(parseStaticCssLengthSpec)
|
|
1813
|
+
if (parsed.some((entry) => !entry)) return null
|
|
1814
|
+
const emitted = emitStaticLengthExpression('Clamp',
|
|
1815
|
+
parsed.map((entry) => entry.spec),
|
|
1816
|
+
{ key: `clamp:${parts.map((part) => part.trim()).join('|')}` })
|
|
1817
|
+
return { setup: [...parsed.flatMap((entry) => entry.setup), ...emitted.setup], spec: emitted.spec }
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
for (const [functionName, kind] of [['min', 'Min'], ['max', 'Max']]) {
|
|
1821
|
+
const inner = staticCssFunctionInner(value, functionName)
|
|
1822
|
+
if (inner === null) continue
|
|
1823
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
1824
|
+
if (parts.length < 1) return null
|
|
1825
|
+
const parsed = parts.map(parseStaticCssLengthSpec)
|
|
1826
|
+
if (parsed.some((entry) => !entry)) return null
|
|
1827
|
+
let setup = parsed[0].setup.slice()
|
|
1828
|
+
let current = parsed[0].spec
|
|
1829
|
+
for (let i = 1; i < parsed.length; i++) {
|
|
1830
|
+
setup.push(...parsed[i].setup)
|
|
1831
|
+
const emitted = emitStaticLengthExpression(kind,
|
|
1832
|
+
[current, parsed[i].spec],
|
|
1833
|
+
{ key: `${functionName}:${parts.slice(0, i + 1).map((part) => part.trim()).join('|')}` })
|
|
1834
|
+
setup.push(...emitted.setup)
|
|
1835
|
+
current = emitted.spec
|
|
1836
|
+
}
|
|
1837
|
+
return { setup, spec: current }
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
const calcInner = staticCssFunctionInner(value, 'calc')
|
|
1841
|
+
if (calcInner !== null) {
|
|
1842
|
+
const text = calcInner.trim()
|
|
1843
|
+
for (const [operators, kinds] of [
|
|
1844
|
+
['/*', { '/': 'Divide', '*': 'Multiply' }],
|
|
1845
|
+
['+-', { '+': 'Add', '-': 'Subtract' }],
|
|
1846
|
+
]) {
|
|
1847
|
+
const index = findTopLevelCssOperator(text, operators)
|
|
1848
|
+
if (index <= 0) continue
|
|
1849
|
+
const op = text[index]
|
|
1850
|
+
const left = parseStaticCssLengthSpec(text.slice(0, index))
|
|
1851
|
+
if (!left) return null
|
|
1852
|
+
if (op === '/' || op === '*') {
|
|
1853
|
+
const scalar = Number(text.slice(index + 1).trim())
|
|
1854
|
+
if (!Number.isFinite(scalar)) return null
|
|
1855
|
+
const emitted = emitStaticLengthExpression(kinds[op],
|
|
1856
|
+
[left.spec],
|
|
1857
|
+
{ scalar, key: `calc:${text}` })
|
|
1858
|
+
return { setup: [...left.setup, ...emitted.setup], spec: emitted.spec }
|
|
1859
|
+
}
|
|
1860
|
+
const right = parseStaticCssLengthSpec(text.slice(index + 1))
|
|
1861
|
+
if (!right) return null
|
|
1862
|
+
const emitted = emitStaticLengthExpression(kinds[op],
|
|
1863
|
+
[left.spec, right.spec],
|
|
1864
|
+
{ key: `calc:${text}` })
|
|
1865
|
+
return { setup: [...left.setup, ...right.setup, ...emitted.setup], spec: emitted.spec }
|
|
1866
|
+
}
|
|
1867
|
+
return parseStaticCssLengthSpec(text)
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
return null
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
function cssAlignKeywordValue(value) {
|
|
1874
|
+
const align = new Map([
|
|
1875
|
+
['flex-start', 0],
|
|
1876
|
+
['start', 0],
|
|
1877
|
+
['stretch', 0],
|
|
1878
|
+
['normal', 0],
|
|
1879
|
+
['center', 1],
|
|
1880
|
+
['flex-end', 2],
|
|
1881
|
+
['end', 2],
|
|
1882
|
+
['space-between', 3],
|
|
1883
|
+
['space-around', 4],
|
|
1884
|
+
['baseline', 5],
|
|
1885
|
+
])
|
|
1886
|
+
return align.has(value) ? align.get(value) : null
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function clampCssByte(value) {
|
|
1890
|
+
if (value < 0) return 0
|
|
1891
|
+
if (value > 255) return 255
|
|
1892
|
+
return value
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1895
|
+
function parseCssAlpha(value) {
|
|
1896
|
+
let alpha = Number(value)
|
|
1897
|
+
if (!Number.isFinite(alpha)) return null
|
|
1898
|
+
if (alpha <= 1) alpha *= 255
|
|
1899
|
+
return clampCssByte(Math.floor(alpha + 0.5))
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function parseStaticCssColor(rawValue) {
|
|
1903
|
+
const value = rawValue.trim()
|
|
1904
|
+
if (value.toLowerCase() === 'transparent') return { r: 0, g: 0, b: 0, a: 0 }
|
|
1905
|
+
const hex = value.match(/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/)
|
|
1906
|
+
if (hex) {
|
|
1907
|
+
const text = hex[1]
|
|
1908
|
+
if (text.length === 3 || text.length === 4) {
|
|
1909
|
+
const r = parseInt(text[0] + text[0], 16)
|
|
1910
|
+
const g = parseInt(text[1] + text[1], 16)
|
|
1911
|
+
const b = parseInt(text[2] + text[2], 16)
|
|
1912
|
+
const a = text.length === 4 ? parseInt(text[3] + text[3], 16) : 255
|
|
1913
|
+
return { r, g, b, a }
|
|
1914
|
+
}
|
|
1915
|
+
const r = parseInt(text.slice(0, 2), 16)
|
|
1916
|
+
const g = parseInt(text.slice(2, 4), 16)
|
|
1917
|
+
const b = parseInt(text.slice(4, 6), 16)
|
|
1918
|
+
const a = text.length === 8 ? parseInt(text.slice(6, 8), 16) : 255
|
|
1919
|
+
return { r, g, b, a }
|
|
1920
|
+
}
|
|
1921
|
+
const rgb = value.match(/^rgba?\((.*)\)$/i)
|
|
1922
|
+
if (!rgb) return null
|
|
1923
|
+
const parts = splitTopLevelCss(rgb[1], ',')
|
|
1924
|
+
if (parts.length < 3 || parts.length > 4) return null
|
|
1925
|
+
const channels = parts.slice(0, 3).map((part) => Number(part.trim()))
|
|
1926
|
+
if (channels.some((channel) => !Number.isFinite(channel))) return null
|
|
1927
|
+
const alpha = parts.length === 4 ? parseCssAlpha(parts[3].trim()) : 255
|
|
1928
|
+
if (alpha === null) return null
|
|
1929
|
+
return {
|
|
1930
|
+
r: clampCssByte(Math.trunc(channels[0])),
|
|
1931
|
+
g: clampCssByte(Math.trunc(channels[1])),
|
|
1932
|
+
b: clampCssByte(Math.trunc(channels[2])),
|
|
1933
|
+
a: alpha,
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
function cssStaticColorTarget(property) {
|
|
1938
|
+
switch (property) {
|
|
1939
|
+
case 'color':
|
|
1940
|
+
return 'Color'
|
|
1941
|
+
case 'background':
|
|
1942
|
+
case 'background-color':
|
|
1943
|
+
return 'Background'
|
|
1944
|
+
case 'active-background':
|
|
1945
|
+
case 'active-background-color':
|
|
1946
|
+
return 'ActiveBackground'
|
|
1947
|
+
case 'border-color':
|
|
1948
|
+
return 'Border'
|
|
1949
|
+
case 'border-top-color':
|
|
1950
|
+
return 'BorderTop'
|
|
1951
|
+
case 'border-right-color':
|
|
1952
|
+
return 'BorderRight'
|
|
1953
|
+
case 'border-bottom-color':
|
|
1954
|
+
return 'BorderBottom'
|
|
1955
|
+
case 'border-left-color':
|
|
1956
|
+
return 'BorderLeft'
|
|
1957
|
+
default:
|
|
1958
|
+
return null
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
function cssStaticColorValue(property, rawValue) {
|
|
1963
|
+
const target = cssStaticColorTarget(property)
|
|
1964
|
+
if (!target) return null
|
|
1965
|
+
const color = parseStaticCssColor(rawValue)
|
|
1966
|
+
return color ? { target, color } : null
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
function cssStaticColorVarValue(property, rawValue) {
|
|
1970
|
+
const target = cssStaticColorTarget(property)
|
|
1971
|
+
if (!target) return null
|
|
1972
|
+
const inner = staticCssFunctionInner(rawValue.trim(), 'var')
|
|
1973
|
+
if (inner === null) return null
|
|
1974
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
1975
|
+
if (parts.length < 1 || parts.length > 2) return null
|
|
1976
|
+
const name = parts[0].trim()
|
|
1977
|
+
if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
|
|
1978
|
+
const fallback = parts.length === 2 ? parseStaticCssColor(parts[1]) : null
|
|
1979
|
+
if (parts.length === 2 && !fallback) return null
|
|
1980
|
+
return { target, name, hasFallback: Boolean(fallback), color: fallback ?? { r: 0, g: 0, b: 0, a: 255 } }
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
function parseStaticCssColorRef(rawValue) {
|
|
1984
|
+
const color = parseStaticCssColor(rawValue)
|
|
1985
|
+
if (color) return { name: null, hasFallback: true, color }
|
|
1986
|
+
const inner = staticCssFunctionInner(rawValue.trim(), 'var')
|
|
1987
|
+
if (inner === null) return null
|
|
1988
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
1989
|
+
if (parts.length < 1 || parts.length > 2) return null
|
|
1990
|
+
const name = parts[0].trim()
|
|
1991
|
+
if (!/^--[A-Za-z0-9_-]+$/.test(name)) return null
|
|
1992
|
+
const fallback = parts.length === 2 ? parseStaticCssColor(parts[1]) : null
|
|
1993
|
+
if (parts.length === 2 && !fallback) return null
|
|
1994
|
+
return { name, hasFallback: Boolean(fallback), color: fallback ?? { r: 0, g: 0, b: 0, a: 255 } }
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
function cssIgnoredProperty(property) {
|
|
1998
|
+
return new Set([
|
|
1999
|
+
'color-scheme',
|
|
2000
|
+
'background-position',
|
|
2001
|
+
'box-sizing',
|
|
2002
|
+
'font',
|
|
2003
|
+
'grid-column',
|
|
2004
|
+
'isolation',
|
|
2005
|
+
'letter-spacing',
|
|
2006
|
+
'outline',
|
|
2007
|
+
'scroll-snap-align',
|
|
2008
|
+
'scroll-snap-type',
|
|
2009
|
+
'scrollbar-width',
|
|
2010
|
+
'text-shadow',
|
|
2011
|
+
'transform-style',
|
|
2012
|
+
'transition',
|
|
2013
|
+
'cursor',
|
|
2014
|
+
'-webkit-tap-highlight-color',
|
|
2015
|
+
]).has(property)
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
function cssStaticFontWeightValue(rawValue) {
|
|
2019
|
+
const value = rawValue.trim().toLowerCase()
|
|
2020
|
+
if (!value || value === 'normal') return 400
|
|
2021
|
+
if (value === 'bold' || value === 'bolder') return 700
|
|
2022
|
+
if (value === 'lighter') return 300
|
|
2023
|
+
if (!/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/.test(value)) return null
|
|
2024
|
+
const number = roundCssNumber(Number(value))
|
|
2025
|
+
return Math.max(1, Math.min(1000, number))
|
|
2026
|
+
}
|
|
2027
|
+
|
|
2028
|
+
function cssStaticPropertyValue(property, rawValue) {
|
|
2029
|
+
const value = rawValue.trim()
|
|
2030
|
+
const lower = value.toLowerCase()
|
|
2031
|
+
const direct = (prop, v) => ({ property: `gea::embedded::ui::Property::${prop}`, value: v })
|
|
2032
|
+
const directLength = (prop, length) => {
|
|
2033
|
+
if (!length) return null
|
|
2034
|
+
if (length.unit === '') return direct(prop, roundCssNumber(length.number))
|
|
2035
|
+
if (length.unit === 'px' && length.number === 0) return direct(prop, 0)
|
|
2036
|
+
return null
|
|
2037
|
+
}
|
|
2038
|
+
const directSize = (lengthProp, percentProp, length) => {
|
|
2039
|
+
if (!length) return null
|
|
2040
|
+
if (length.unit === '%') return direct(percentProp, clampInt16(roundCssNumber(length.number * 10)))
|
|
2041
|
+
return directLength(lengthProp, length)
|
|
2042
|
+
}
|
|
2043
|
+
const numeric = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/
|
|
2044
|
+
const length = simpleStaticLength(value)
|
|
2045
|
+
switch (property) {
|
|
2046
|
+
case 'display': {
|
|
2047
|
+
const values = new Map([['block', 0], ['none', 1], ['grid', 2], ['inline-grid', 2], ['flex', 3], ['inline-flex', 3]])
|
|
2048
|
+
return values.has(value) ? direct('Display', values.get(value)) : null
|
|
2049
|
+
}
|
|
2050
|
+
case 'flex-direction':
|
|
2051
|
+
if (value === 'column') return direct('FlexDirection', 0)
|
|
2052
|
+
if (value === 'row') return direct('FlexDirection', 1)
|
|
2053
|
+
return null
|
|
2054
|
+
case 'flex-wrap':
|
|
2055
|
+
if (value === 'nowrap') return direct('FlexWrap', 0)
|
|
2056
|
+
if (value === 'wrap') return direct('FlexWrap', 1)
|
|
2057
|
+
return null
|
|
2058
|
+
case 'justify-content':
|
|
2059
|
+
return cssAlignKeywordValue(value) !== null ? direct('JustifyContent', cssAlignKeywordValue(value)) : null
|
|
2060
|
+
case 'align-items':
|
|
2061
|
+
return cssAlignKeywordValue(value) !== null ? direct('AlignItems', cssAlignKeywordValue(value)) : null
|
|
2062
|
+
case 'justify-items':
|
|
2063
|
+
return cssAlignKeywordValue(value) !== null ? direct('JustifyItems', cssAlignKeywordValue(value)) : null
|
|
2064
|
+
case 'align-content':
|
|
2065
|
+
return cssAlignKeywordValue(value) !== null ? direct('AlignContent', cssAlignKeywordValue(value)) : null
|
|
2066
|
+
case 'align-self':
|
|
2067
|
+
if (value === 'auto') return direct('AlignSelf', -1)
|
|
2068
|
+
return cssAlignKeywordValue(value) !== null ? direct('AlignSelf', cssAlignKeywordValue(value)) : null
|
|
2069
|
+
case 'position':
|
|
2070
|
+
if (value === 'static') return direct('Position', 0)
|
|
2071
|
+
if (value === 'absolute') return direct('Position', 1)
|
|
2072
|
+
if (value === 'relative') return direct('Position', 2)
|
|
2073
|
+
return null
|
|
2074
|
+
case 'gap':
|
|
2075
|
+
return directLength('Gap', length)
|
|
2076
|
+
case 'width':
|
|
2077
|
+
return directSize('Width', 'WidthPercent', length)
|
|
2078
|
+
case 'height':
|
|
2079
|
+
return directSize('Height', 'HeightPercent', length)
|
|
2080
|
+
case 'min-width':
|
|
2081
|
+
return directLength('MinWidth', length)
|
|
2082
|
+
case 'min-height':
|
|
2083
|
+
return directLength('MinHeight', length)
|
|
2084
|
+
case 'max-width':
|
|
2085
|
+
return directLength('MaxWidth', length)
|
|
2086
|
+
case 'max-height':
|
|
2087
|
+
return directLength('MaxHeight', length)
|
|
2088
|
+
case 'flex-basis':
|
|
2089
|
+
return directLength('FlexBasis', length)
|
|
2090
|
+
case 'flex-shrink':
|
|
2091
|
+
return numeric.test(value) ? direct('FlexShrink', roundCssNumber(Number(value))) : null
|
|
2092
|
+
case 'font-weight': {
|
|
2093
|
+
const weight = cssStaticFontWeightValue(value)
|
|
2094
|
+
return weight === null ? null : direct('FontWeight', weight)
|
|
2095
|
+
}
|
|
2096
|
+
case 'padding-top':
|
|
2097
|
+
return directLength('PaddingTop', length)
|
|
2098
|
+
case 'padding-right':
|
|
2099
|
+
return directLength('PaddingRight', length)
|
|
2100
|
+
case 'padding-bottom':
|
|
2101
|
+
return directLength('PaddingBottom', length)
|
|
2102
|
+
case 'padding-left':
|
|
2103
|
+
return directLength('PaddingLeft', length)
|
|
2104
|
+
case 'margin-top':
|
|
2105
|
+
return directLength('MarginTop', length)
|
|
2106
|
+
case 'margin-right':
|
|
2107
|
+
return directLength('MarginRight', length)
|
|
2108
|
+
case 'margin-bottom':
|
|
2109
|
+
return directLength('MarginBottom', length)
|
|
2110
|
+
case 'margin-left':
|
|
2111
|
+
return directLength('MarginLeft', length)
|
|
2112
|
+
case 'top':
|
|
2113
|
+
return directSize('Top', 'TopPercent', length)
|
|
2114
|
+
case 'right':
|
|
2115
|
+
return directSize('Right', 'RightPercent', length)
|
|
2116
|
+
case 'bottom':
|
|
2117
|
+
return directSize('Bottom', 'BottomPercent', length)
|
|
2118
|
+
case 'left':
|
|
2119
|
+
return directSize('Left', 'LeftPercent', length)
|
|
2120
|
+
case 'z-index':
|
|
2121
|
+
return numeric.test(value) ? direct('ZIndex', roundCssNumber(Number(value))) : null
|
|
2122
|
+
case 'opacity': {
|
|
2123
|
+
if (!numeric.test(value)) return null
|
|
2124
|
+
let opacity = Number(value)
|
|
2125
|
+
if (opacity <= 1) opacity *= 255
|
|
2126
|
+
if (opacity < 0) opacity = 0
|
|
2127
|
+
if (opacity > 255) opacity = 255
|
|
2128
|
+
return direct('Opacity', Math.floor(opacity + 0.5))
|
|
2129
|
+
}
|
|
2130
|
+
case 'object-fit':
|
|
2131
|
+
if (lower === 'fill') return direct('ImageFit', 0)
|
|
2132
|
+
if (lower === 'contain') return direct('ImageFit', 1)
|
|
2133
|
+
if (lower === 'cover') return direct('ImageFit', 2)
|
|
2134
|
+
if (lower === 'none') return direct('ImageFit', 3)
|
|
2135
|
+
if (lower === 'scale-down') return direct('ImageFit', 4)
|
|
2136
|
+
return null
|
|
2137
|
+
case 'rotate': {
|
|
2138
|
+
const angle = parseStaticAngleTenths(value)
|
|
2139
|
+
return angle === null ? null : direct('TransformRotate', angle)
|
|
2140
|
+
}
|
|
2141
|
+
case 'text-align':
|
|
2142
|
+
if (value === 'left' || value === 'start') return direct('TextAlign', 0)
|
|
2143
|
+
if (value === 'center') return direct('TextAlign', 1)
|
|
2144
|
+
if (value === 'right' || value === 'end') return direct('TextAlign', 2)
|
|
2145
|
+
return null
|
|
2146
|
+
case 'text-decoration':
|
|
2147
|
+
case 'text-decoration-line':
|
|
2148
|
+
if (value === 'none') return direct('TextDecoration', 0)
|
|
2149
|
+
if (value === 'underline') return direct('TextDecoration', 1)
|
|
2150
|
+
if (value === 'line-through' || value === 'strikethrough') return direct('TextDecoration', 2)
|
|
2151
|
+
return null
|
|
2152
|
+
case 'text-transform':
|
|
2153
|
+
if (lower === 'none') return direct('TextTransform', 0)
|
|
2154
|
+
if (lower === 'uppercase') return direct('TextTransform', 1)
|
|
2155
|
+
if (lower === 'lowercase') return direct('TextTransform', 2)
|
|
2156
|
+
if (lower === 'capitalize') return direct('TextTransform', 3)
|
|
2157
|
+
return null
|
|
2158
|
+
case 'white-space':
|
|
2159
|
+
if (lower === 'normal' || lower === 'pre-line') return direct('WhiteSpace', 0)
|
|
2160
|
+
if (lower === 'nowrap' || lower === 'pre') return direct('WhiteSpace', 1)
|
|
2161
|
+
return null
|
|
2162
|
+
case 'text-overflow':
|
|
2163
|
+
if (lower === 'clip') return direct('TextOverflow', 0)
|
|
2164
|
+
if (lower === 'ellipsis') return direct('TextOverflow', 1)
|
|
2165
|
+
return null
|
|
2166
|
+
case 'backface-visibility':
|
|
2167
|
+
if (lower === 'visible') return direct('Backface', 0)
|
|
2168
|
+
if (lower === 'hidden') return direct('Backface', 1)
|
|
2169
|
+
return null
|
|
2170
|
+
case 'pointer-events':
|
|
2171
|
+
if (lower === 'auto') return direct('PointerEvents', 0)
|
|
2172
|
+
if (lower === 'none') return direct('PointerEvents', 1)
|
|
2173
|
+
return null
|
|
2174
|
+
case 'overflow':
|
|
2175
|
+
if (value === 'visible') return direct('Overflow', 0)
|
|
2176
|
+
if (value === 'hidden') return direct('Overflow', 1)
|
|
2177
|
+
if (value === 'scroll' || value === 'auto') return direct('Overflow', 2)
|
|
2178
|
+
return null
|
|
2179
|
+
case 'overflow-x':
|
|
2180
|
+
if (value === 'visible') return direct('OverflowX', 0)
|
|
2181
|
+
if (value === 'hidden') return direct('OverflowX', 1)
|
|
2182
|
+
if (value === 'scroll' || value === 'auto') return direct('OverflowX', 2)
|
|
2183
|
+
return null
|
|
2184
|
+
case 'overflow-y':
|
|
2185
|
+
if (value === 'visible') return direct('OverflowY', 0)
|
|
2186
|
+
if (value === 'hidden') return direct('OverflowY', 1)
|
|
2187
|
+
if (value === 'scroll' || value === 'auto') return direct('OverflowY', 2)
|
|
2188
|
+
return null
|
|
2189
|
+
case 'flex-grow':
|
|
2190
|
+
return numeric.test(value) ? direct('Flex', roundCssNumber(Number(value))) : null
|
|
2191
|
+
default:
|
|
2192
|
+
return null
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function cssStaticPropertyValues(property, rawValue) {
|
|
2197
|
+
const single = cssStaticPropertyValue(property, rawValue)
|
|
2198
|
+
if (single) return [single]
|
|
2199
|
+
|
|
2200
|
+
const value = rawValue.trim()
|
|
2201
|
+
const length = simpleStaticLength(value)
|
|
2202
|
+
const direct = (prop, v) => ({ property: `gea::embedded::ui::Property::${prop}`, value: v })
|
|
2203
|
+
const directLengthValue = (candidate) => {
|
|
2204
|
+
if (!candidate) return null
|
|
2205
|
+
if (candidate.unit === '') return roundCssNumber(candidate.number)
|
|
2206
|
+
if (candidate.unit === 'px' && candidate.number === 0) return 0
|
|
2207
|
+
return null
|
|
2208
|
+
}
|
|
2209
|
+
const boxLength = directLengthValue(length)
|
|
2210
|
+
if (boxLength !== null) {
|
|
2211
|
+
if (property === 'border') return [direct('BorderWidth', boxLength)]
|
|
2212
|
+
if (property === 'border-top') return [direct('BorderTopWidth', boxLength)]
|
|
2213
|
+
if (property === 'border-right') return [direct('BorderRightWidth', boxLength)]
|
|
2214
|
+
if (property === 'border-bottom') return [direct('BorderBottomWidth', boxLength)]
|
|
2215
|
+
if (property === 'border-left') return [direct('BorderLeftWidth', boxLength)]
|
|
2216
|
+
if (property === 'margin') {
|
|
2217
|
+
return [
|
|
2218
|
+
direct('MarginTop', boxLength),
|
|
2219
|
+
direct('MarginRight', boxLength),
|
|
2220
|
+
direct('MarginBottom', boxLength),
|
|
2221
|
+
direct('MarginLeft', boxLength),
|
|
2222
|
+
]
|
|
2223
|
+
}
|
|
2224
|
+
if (property === 'padding') {
|
|
2225
|
+
return [
|
|
2226
|
+
direct('PaddingTop', boxLength),
|
|
2227
|
+
direct('PaddingRight', boxLength),
|
|
2228
|
+
direct('PaddingBottom', boxLength),
|
|
2229
|
+
direct('PaddingLeft', boxLength),
|
|
2230
|
+
]
|
|
2231
|
+
}
|
|
2232
|
+
if (property === 'inset') {
|
|
2233
|
+
return [
|
|
2234
|
+
direct('Top', boxLength),
|
|
2235
|
+
direct('Right', boxLength),
|
|
2236
|
+
direct('Bottom', boxLength),
|
|
2237
|
+
direct('Left', boxLength),
|
|
2238
|
+
]
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
|
|
2242
|
+
if (property === 'place-items') {
|
|
2243
|
+
const align = cssAlignKeywordValue(value)
|
|
2244
|
+
if (align !== null) {
|
|
2245
|
+
return [
|
|
2246
|
+
direct('AlignItems', align),
|
|
2247
|
+
direct('JustifyItems', align),
|
|
2248
|
+
direct('JustifyContent', align),
|
|
2249
|
+
]
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
|
|
2253
|
+
if (property === 'scale') {
|
|
2254
|
+
const scale = parseStaticScalePermille(value)
|
|
2255
|
+
if (scale !== null) {
|
|
2256
|
+
return [
|
|
2257
|
+
direct('TransformScaleX', scale),
|
|
2258
|
+
direct('TransformScaleY', scale),
|
|
2259
|
+
]
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
|
|
2263
|
+
return []
|
|
2264
|
+
}
|
|
2265
|
+
|
|
2266
|
+
function cssStaticPrimaryFontFamily(rawValue) {
|
|
2267
|
+
let text = rawValue.trim()
|
|
2268
|
+
if (!text) return null
|
|
2269
|
+
const quote = text[0]
|
|
2270
|
+
if (quote === '\'' || quote === '"') {
|
|
2271
|
+
const end = text.indexOf(quote, 1)
|
|
2272
|
+
return end < 0 ? null : text.slice(1, end)
|
|
2273
|
+
}
|
|
2274
|
+
const comma = text.indexOf(',')
|
|
2275
|
+
if (comma >= 0) text = text.slice(0, comma)
|
|
2276
|
+
const family = text.trim()
|
|
2277
|
+
return family || null
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
function cssStaticFontFamilyValue(property, rawValue) {
|
|
2281
|
+
if (property !== 'font-family') return null
|
|
2282
|
+
const family = cssStaticPrimaryFontFamily(rawValue)
|
|
2283
|
+
return family ? { family } : null
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
function cssStaticLineHeightValue(property, rawValue) {
|
|
2287
|
+
if (property !== 'line-height') return null
|
|
2288
|
+
const value = rawValue.trim()
|
|
2289
|
+
const lower = value.toLowerCase()
|
|
2290
|
+
if (lower === 'normal') {
|
|
2291
|
+
return { setup: [], kind: 'Normal', spec: zeroStaticLengthSpec() }
|
|
2292
|
+
}
|
|
2293
|
+
const numberPattern = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/
|
|
2294
|
+
if (numberPattern.test(value)) {
|
|
2295
|
+
return { setup: [], kind: 'Scalar', spec: { unit: 'Raw', value: cssNumberCode(Number(value)) } }
|
|
2296
|
+
}
|
|
2297
|
+
const percent = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
|
|
2298
|
+
if (percent) {
|
|
2299
|
+
return { setup: [], kind: 'Percent', spec: { unit: 'Percent', value: cssNumberCode(Number(percent[1])) } }
|
|
2300
|
+
}
|
|
2301
|
+
const parsed = parseStaticCssLengthSpec(value)
|
|
2302
|
+
return parsed ? { ...parsed, kind: 'Length' } : null
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
function cssStaticFlexValue(property, rawValue) {
|
|
2306
|
+
if (property !== 'flex') return null
|
|
2307
|
+
let grow = 0
|
|
2308
|
+
let numberIndex = 0
|
|
2309
|
+
let basis = zeroStaticLengthSpec()
|
|
2310
|
+
let hasBasis = false
|
|
2311
|
+
const setup = []
|
|
2312
|
+
const words = splitCssWords(rawValue.trim())
|
|
2313
|
+
if (words.length === 0) return null
|
|
2314
|
+
for (const token of words) {
|
|
2315
|
+
const lower = token.toLowerCase()
|
|
2316
|
+
if (lower === 'none') {
|
|
2317
|
+
grow = 0
|
|
2318
|
+
hasBasis = false
|
|
2319
|
+
continue
|
|
2320
|
+
}
|
|
2321
|
+
if (
|
|
2322
|
+
lower === 'auto' ||
|
|
2323
|
+
lower === 'content' ||
|
|
2324
|
+
lower === 'max-content' ||
|
|
2325
|
+
lower === 'min-content' ||
|
|
2326
|
+
lower === 'fit-content' ||
|
|
2327
|
+
lower.includes('%')
|
|
2328
|
+
) {
|
|
2329
|
+
hasBasis = false
|
|
2330
|
+
continue
|
|
2331
|
+
}
|
|
2332
|
+
const number = Number(token)
|
|
2333
|
+
if (Number.isFinite(number) && token.match(/^[+-]?(?:\d+(?:\.\d*)?|\.\d+)$/)) {
|
|
2334
|
+
if (numberIndex === 0) grow = roundCssNumber(number)
|
|
2335
|
+
numberIndex++
|
|
2336
|
+
continue
|
|
2337
|
+
}
|
|
2338
|
+
const parsed = parseStaticCssLengthSpec(token)
|
|
2339
|
+
if (!parsed) return null
|
|
2340
|
+
setup.push(...parsed.setup)
|
|
2341
|
+
basis = parsed.spec
|
|
2342
|
+
hasBasis = true
|
|
2343
|
+
}
|
|
2344
|
+
return { setup, grow, basis, hasBasis }
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
function cssStaticBoxShorthandValue(property, rawValue) {
|
|
2348
|
+
const targets = {
|
|
2349
|
+
padding: ['PaddingTop', 'PaddingRight', 'PaddingBottom', 'PaddingLeft'],
|
|
2350
|
+
margin: ['MarginTop', 'MarginRight', 'MarginBottom', 'MarginLeft'],
|
|
2351
|
+
inset: ['Top', 'Right', 'Bottom', 'Left'],
|
|
2352
|
+
}[property]
|
|
2353
|
+
if (!targets) return null
|
|
2354
|
+
const words = splitCssWords(rawValue.trim())
|
|
2355
|
+
if (words.length < 1 || words.length > 4) return null
|
|
2356
|
+
const parsed = words.map(parseStaticCssLengthSpec)
|
|
2357
|
+
if (parsed.some((entry) => !entry)) return null
|
|
2358
|
+
const specs =
|
|
2359
|
+
parsed.length === 1 ? [parsed[0], parsed[0], parsed[0], parsed[0]] :
|
|
2360
|
+
parsed.length === 2 ? [parsed[0], parsed[1], parsed[0], parsed[1]] :
|
|
2361
|
+
parsed.length === 3 ? [parsed[0], parsed[1], parsed[2], parsed[1]] :
|
|
2362
|
+
[parsed[0], parsed[1], parsed[2], parsed[3]]
|
|
2363
|
+
return {
|
|
2364
|
+
setup: parsed.flatMap((entry) => entry.setup),
|
|
2365
|
+
entries: targets.map((target, index) => ({ target, spec: specs[index].spec })),
|
|
2366
|
+
}
|
|
2367
|
+
}
|
|
2368
|
+
|
|
2369
|
+
const staticBorderRadiusCorners = new Map([
|
|
2370
|
+
['border-top-left-radius', 'TopLeft'],
|
|
2371
|
+
['border-top-right-radius', 'TopRight'],
|
|
2372
|
+
['border-bottom-right-radius', 'BottomRight'],
|
|
2373
|
+
['border-bottom-left-radius', 'BottomLeft'],
|
|
2374
|
+
])
|
|
2375
|
+
|
|
2376
|
+
function cssStaticBorderRadiusValue(property, rawValue) {
|
|
2377
|
+
if (staticBorderRadiusCorners.has(property)) {
|
|
2378
|
+
const parsed = parseStaticCssLengthSpec(rawValue)
|
|
2379
|
+
return parsed
|
|
2380
|
+
? { setup: parsed.setup, corner: staticBorderRadiusCorners.get(property), spec: parsed.spec }
|
|
2381
|
+
: null
|
|
2382
|
+
}
|
|
2383
|
+
if (property !== 'border-radius') return null
|
|
2384
|
+
const axes = splitTopLevelCss(rawValue, '/')
|
|
2385
|
+
const parts = splitCssWords(axes.length > 0 ? axes[0] : rawValue)
|
|
2386
|
+
if (parts.length < 1 || parts.length > 4) return null
|
|
2387
|
+
const parsed = parts.map(parseStaticCssLengthSpec)
|
|
2388
|
+
if (parsed.some((entry) => !entry)) return null
|
|
2389
|
+
const specs =
|
|
2390
|
+
parsed.length === 1 ? [parsed[0], parsed[0], parsed[0], parsed[0]] :
|
|
2391
|
+
parsed.length === 2 ? [parsed[0], parsed[1], parsed[0], parsed[1]] :
|
|
2392
|
+
parsed.length === 3 ? [parsed[0], parsed[1], parsed[2], parsed[1]] :
|
|
2393
|
+
[parsed[0], parsed[1], parsed[2], parsed[3]]
|
|
2394
|
+
return {
|
|
2395
|
+
setup: parsed.flatMap((entry) => entry.setup),
|
|
2396
|
+
specs: specs.map((entry) => entry.spec),
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
|
|
2400
|
+
function cssStaticBorderValue(property, rawValue) {
|
|
2401
|
+
if (property !== 'border') return null
|
|
2402
|
+
const parts = splitCssWords(rawValue.trim())
|
|
2403
|
+
if (parts.length === 0) return null
|
|
2404
|
+
const width = parseStaticCssLengthSpec(parts[0])
|
|
2405
|
+
if (!width) return null
|
|
2406
|
+
let color = null
|
|
2407
|
+
for (const part of parts) {
|
|
2408
|
+
const lower = part.toLowerCase()
|
|
2409
|
+
if (!part.startsWith('#') && !lower.includes('rgb')) continue
|
|
2410
|
+
color = parseStaticCssColor(part)
|
|
2411
|
+
break
|
|
2412
|
+
}
|
|
2413
|
+
return color ? { setup: width.setup, width: width.spec, color } : null
|
|
2414
|
+
}
|
|
2415
|
+
|
|
2416
|
+
function cssStaticBorderShorthandValue(property, rawValue) {
|
|
2417
|
+
const targets = {
|
|
2418
|
+
border: { width: 'BorderWidth', color: 'Border', colorProperty: 'border-color', side: false },
|
|
2419
|
+
'border-top': { width: 'BorderTopWidth', color: 'BorderTop', colorProperty: 'border-top-color', side: true },
|
|
2420
|
+
'border-right': { width: 'BorderRightWidth', color: 'BorderRight', colorProperty: 'border-right-color', side: true },
|
|
2421
|
+
'border-bottom': { width: 'BorderBottomWidth', color: 'BorderBottom', colorProperty: 'border-bottom-color', side: true },
|
|
2422
|
+
'border-left': { width: 'BorderLeftWidth', color: 'BorderLeft', colorProperty: 'border-left-color', side: true },
|
|
2423
|
+
}[property]
|
|
2424
|
+
if (!targets) return null
|
|
2425
|
+
const parts = splitCssWords(rawValue.trim())
|
|
2426
|
+
if (parts.length === 0) return null
|
|
2427
|
+
const width = parseStaticCssLengthSpec(parts[0])
|
|
2428
|
+
if (!width) return null
|
|
2429
|
+
let color = null
|
|
2430
|
+
let colorVar = null
|
|
2431
|
+
for (const part of parts) {
|
|
2432
|
+
const literal = parseStaticCssColor(part)
|
|
2433
|
+
if (literal) {
|
|
2434
|
+
color = { target: targets.color, color: literal }
|
|
2435
|
+
break
|
|
2436
|
+
}
|
|
2437
|
+
const variable = cssStaticColorVarValue(targets.colorProperty, part)
|
|
2438
|
+
if (variable) {
|
|
2439
|
+
colorVar = variable
|
|
2440
|
+
break
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
if (!targets.side && color && !colorVar) return null
|
|
2444
|
+
return { setup: width.setup, widthTarget: targets.width, width: width.spec, color, colorVar }
|
|
2445
|
+
}
|
|
2446
|
+
|
|
2447
|
+
function staticColorCode(color) {
|
|
2448
|
+
return `{${color.r}, ${color.g}, ${color.b}, ${color.a}}`
|
|
2449
|
+
}
|
|
2450
|
+
|
|
2451
|
+
function staticColorRefCode(ref) {
|
|
2452
|
+
const name = ref.name ? JSON.stringify(ref.name) : 'nullptr'
|
|
2453
|
+
return `{${name}, ${staticColorCode(ref.color)}, ${ref.hasFallback ? 'true' : 'false'}}`
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2456
|
+
function staticLinearGradientCode(gradient) {
|
|
2457
|
+
return `{${gradient.angleTenths}, ${staticColorCode(gradient.from)}, ${staticColorCode(gradient.mid)}, ${staticColorCode(gradient.to)}, ${gradient.midStopPermille}, ${gradient.toStopPermille}, ${gradient.hasMid ? 'true' : 'false'}}`
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2460
|
+
function staticLinearGradientRefCode(gradient) {
|
|
2461
|
+
return `{${gradient.angleTenths}, ${staticColorRefCode(gradient.from)}, ${staticColorRefCode(gradient.mid)}, ${staticColorRefCode(gradient.to)}, ${gradient.midStopPermille}, ${gradient.toStopPermille}, ${gradient.hasMid ? 'true' : 'false'}}`
|
|
2462
|
+
}
|
|
2463
|
+
|
|
2464
|
+
function staticRadialGradientRefCode(gradient) {
|
|
2465
|
+
return `{${staticColorRefCode(gradient.from)}, ${staticColorRefCode(gradient.to)}, ${gradient.stopPermille}, ${gradient.cxPermille}, ${gradient.cyPermille}, ${gradient.rxPermille}, ${gradient.ryPermille}, ${gradient.enabled ? 'true' : 'false'}}`
|
|
2466
|
+
}
|
|
2467
|
+
|
|
2468
|
+
function zeroStaticLinearGradient() {
|
|
2469
|
+
const transparent = { r: 0, g: 0, b: 0, a: 0 }
|
|
2470
|
+
return {
|
|
2471
|
+
angleTenths: 1800,
|
|
2472
|
+
from: transparent,
|
|
2473
|
+
mid: transparent,
|
|
2474
|
+
to: transparent,
|
|
2475
|
+
midStopPermille: 500,
|
|
2476
|
+
toStopPermille: 1000,
|
|
2477
|
+
hasMid: false,
|
|
2478
|
+
}
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
function zeroStaticLinearGradientRef() {
|
|
2482
|
+
const transparentRef = { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }
|
|
2483
|
+
return {
|
|
2484
|
+
angleTenths: 1800,
|
|
2485
|
+
from: transparentRef,
|
|
2486
|
+
mid: transparentRef,
|
|
2487
|
+
to: transparentRef,
|
|
2488
|
+
midStopPermille: 500,
|
|
2489
|
+
toStopPermille: 1000,
|
|
2490
|
+
hasMid: false,
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
|
|
2494
|
+
function zeroStaticRadialGradientRef() {
|
|
2495
|
+
const transparentRef = { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }
|
|
2496
|
+
return {
|
|
2497
|
+
from: transparentRef,
|
|
2498
|
+
to: transparentRef,
|
|
2499
|
+
stopPermille: 1000,
|
|
2500
|
+
cxPermille: 500,
|
|
2501
|
+
cyPermille: 500,
|
|
2502
|
+
rxPermille: 1000,
|
|
2503
|
+
ryPermille: 1000,
|
|
2504
|
+
enabled: false,
|
|
2505
|
+
}
|
|
2506
|
+
}
|
|
2507
|
+
|
|
2508
|
+
function staticGridLineCode(line) {
|
|
2509
|
+
if (!line) return `{false, ${staticLengthSpecCode(zeroStaticLengthSpec())}, {0, 0, 0, 255}}`
|
|
2510
|
+
return `{true, ${staticLengthSpecCode(line.width)}, ${staticColorCode(line.color)}}`
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
function staticGridTemplateTrackCode(track) {
|
|
2514
|
+
return `{${track.type}, ${track.value}, ${staticLengthSpecCode(track.length)}}`
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
function cssHasDynamicValue(value) {
|
|
2518
|
+
const lower = value.toLowerCase()
|
|
2519
|
+
return lower.includes('var(') ||
|
|
2520
|
+
lower.includes('calc(') ||
|
|
2521
|
+
lower.includes('min(') ||
|
|
2522
|
+
lower.includes('max(') ||
|
|
2523
|
+
lower.includes('clamp(')
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
function staticColorEquals(a, b) {
|
|
2527
|
+
return a && b && a.r === b.r && a.g === b.g && a.b === b.b && a.a === b.a
|
|
2528
|
+
}
|
|
2529
|
+
|
|
2530
|
+
function staticLengthSpecEquals(a, b) {
|
|
2531
|
+
return a && b && a.unit === b.unit && a.value === b.value
|
|
2532
|
+
}
|
|
2533
|
+
|
|
2534
|
+
function staticLengthSpecIsSafeGridLine(spec) {
|
|
2535
|
+
if (!spec || (spec.unit !== 'Raw' && spec.unit !== 'Px')) return false
|
|
2536
|
+
const value = Number(spec.value)
|
|
2537
|
+
return Number.isFinite(value) && value >= 1
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2540
|
+
function parseStaticGradientStop(rawStop) {
|
|
2541
|
+
const words = splitCssWords(rawStop.trim())
|
|
2542
|
+
if (words.length === 0) return null
|
|
2543
|
+
const color = parseStaticCssColor(words[0])
|
|
2544
|
+
if (!color) return null
|
|
2545
|
+
let stopPermille = null
|
|
2546
|
+
let length = null
|
|
2547
|
+
if (words.length > 1) {
|
|
2548
|
+
const stop = words[1].trim()
|
|
2549
|
+
const percent = stop.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
|
|
2550
|
+
if (percent) {
|
|
2551
|
+
stopPermille = roundCssNumber(Number(percent[1]) * 10)
|
|
2552
|
+
} else {
|
|
2553
|
+
const parsedLength = parseStaticCssLengthSpec(stop)
|
|
2554
|
+
if (parsedLength) length = parsedLength
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
return { color, stopPermille, length }
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
function parseStaticGradientStopRef(rawStop) {
|
|
2561
|
+
const words = splitCssWords(rawStop.trim())
|
|
2562
|
+
if (words.length === 0) return null
|
|
2563
|
+
const color = parseStaticCssColorRef(words[0])
|
|
2564
|
+
if (!color) return null
|
|
2565
|
+
let stopPermille = null
|
|
2566
|
+
if (words.length > 1) {
|
|
2567
|
+
const stop = words[1].trim()
|
|
2568
|
+
const percent = stop.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))%$/)
|
|
2569
|
+
if (percent) stopPermille = roundCssNumber(Number(percent[1]) * 10)
|
|
2570
|
+
}
|
|
2571
|
+
return { color, stopPermille }
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
function parseStaticGradientHeader(parts) {
|
|
2575
|
+
if (parts.length < 2) return null
|
|
2576
|
+
const first = parts[0].trim()
|
|
2577
|
+
const lower = first.toLowerCase()
|
|
2578
|
+
if (first.includes('deg') || first.includes('turn') || first.includes('rad')) {
|
|
2579
|
+
const angle = parseStaticAngleTenths(first)
|
|
2580
|
+
return angle === null ? null : { angleTenths: angle, colorStart: 1 }
|
|
2581
|
+
}
|
|
2582
|
+
if (lower.startsWith('to ')) {
|
|
2583
|
+
let angleTenths = 1800
|
|
2584
|
+
if (lower.includes('right')) angleTenths = 900
|
|
2585
|
+
else if (lower.includes('left')) angleTenths = 2700
|
|
2586
|
+
else if (lower.includes('top')) angleTenths = 0
|
|
2587
|
+
return { angleTenths, colorStart: 1 }
|
|
2588
|
+
}
|
|
2589
|
+
return { angleTenths: 1800, colorStart: 0 }
|
|
2590
|
+
}
|
|
2591
|
+
|
|
2592
|
+
function parseStaticLinearGradientRefLayer(rawLayer) {
|
|
2593
|
+
const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
|
|
2594
|
+
if (inner === null) return null
|
|
2595
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2596
|
+
const header = parseStaticGradientHeader(parts)
|
|
2597
|
+
if (!header || header.colorStart >= parts.length) return null
|
|
2598
|
+
|
|
2599
|
+
const from = parseStaticGradientStopRef(parts[header.colorStart])
|
|
2600
|
+
const to = parseStaticGradientStopRef(parts[parts.length - 1])
|
|
2601
|
+
if (!from || !to) return null
|
|
2602
|
+
|
|
2603
|
+
let mid = { color: { name: null, hasFallback: true, color: { r: 0, g: 0, b: 0, a: 0 } }, stopPermille: 500 }
|
|
2604
|
+
let hasMid = false
|
|
2605
|
+
if (parts.length === header.colorStart + 2) {
|
|
2606
|
+
if (from.stopPermille !== null && from.stopPermille > 0 && from.stopPermille < 1000) {
|
|
2607
|
+
mid = { color: from.color, stopPermille: from.stopPermille }
|
|
2608
|
+
hasMid = true
|
|
2609
|
+
}
|
|
2610
|
+
} else if (parts.length > header.colorStart + 2) {
|
|
2611
|
+
const parsedMid = parseStaticGradientStopRef(parts[header.colorStart + 1])
|
|
2612
|
+
if (!parsedMid) return null
|
|
2613
|
+
mid = {
|
|
2614
|
+
color: parsedMid.color,
|
|
2615
|
+
stopPermille: parsedMid.stopPermille === null ? 500 : Math.max(0, Math.min(1000, parsedMid.stopPermille)),
|
|
2616
|
+
}
|
|
2617
|
+
hasMid = true
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2620
|
+
let toStopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(60000, to.stopPermille))
|
|
2621
|
+
if (toStopPermille <= 0) toStopPermille = 1
|
|
2622
|
+
if (hasMid && toStopPermille <= mid.stopPermille) toStopPermille = mid.stopPermille + 1
|
|
2623
|
+
|
|
2624
|
+
return {
|
|
2625
|
+
angleTenths: header.angleTenths,
|
|
2626
|
+
from: from.color,
|
|
2627
|
+
mid: mid.color,
|
|
2628
|
+
to: to.color,
|
|
2629
|
+
midStopPermille: mid.stopPermille,
|
|
2630
|
+
toStopPermille,
|
|
2631
|
+
hasMid,
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
|
|
2635
|
+
function parseStaticPercentPermille(rawPart, fallback) {
|
|
2636
|
+
const length = parseStaticCssLength(rawPart.trim())
|
|
2637
|
+
if (!length || length.unit !== 'Percent') return fallback
|
|
2638
|
+
return roundCssNumber(length.number * 10)
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
function parseStaticRadialGradientRefLayer(rawLayer) {
|
|
2642
|
+
const inner = staticCssFunctionInner(rawLayer.trim(), 'radial-gradient')
|
|
2643
|
+
if (inner === null) return null
|
|
2644
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2645
|
+
if (parts.length < 2) return null
|
|
2646
|
+
|
|
2647
|
+
let colorStart = 0
|
|
2648
|
+
let cxPermille = 500
|
|
2649
|
+
let cyPermille = 500
|
|
2650
|
+
let rxPermille = 1000
|
|
2651
|
+
let ryPermille = 1000
|
|
2652
|
+
if (!parseStaticGradientStopRef(parts[0])) {
|
|
2653
|
+
colorStart = 1
|
|
2654
|
+
const words = splitCssWords(parts[0])
|
|
2655
|
+
let atIndex = words.length
|
|
2656
|
+
for (let i = 0; i < words.length; i++) {
|
|
2657
|
+
if (words[i].toLowerCase() === 'at') {
|
|
2658
|
+
atIndex = i
|
|
2659
|
+
break
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
const sizeWords = []
|
|
2663
|
+
for (let i = 0; i < atIndex; i++) {
|
|
2664
|
+
const lower = words[i].toLowerCase()
|
|
2665
|
+
if (lower === 'circle' || lower === 'ellipse' || lower === 'closest-side' ||
|
|
2666
|
+
lower === 'closest-corner' || lower === 'farthest-side' || lower === 'farthest-corner')
|
|
2667
|
+
continue
|
|
2668
|
+
sizeWords.push(words[i])
|
|
2669
|
+
}
|
|
2670
|
+
if (sizeWords.length > 0) rxPermille = parseStaticPercentPermille(sizeWords[0], rxPermille)
|
|
2671
|
+
if (sizeWords.length > 1) ryPermille = parseStaticPercentPermille(sizeWords[1], ryPermille)
|
|
2672
|
+
else if (sizeWords.length > 0) ryPermille = rxPermille
|
|
2673
|
+
|
|
2674
|
+
if (atIndex < words.length) {
|
|
2675
|
+
const centerWords = words.slice(atIndex + 1)
|
|
2676
|
+
if (centerWords.length > 0) {
|
|
2677
|
+
const x = parseStaticOriginPart(centerWords[0], cxPermille)
|
|
2678
|
+
if (x === null) return null
|
|
2679
|
+
cxPermille = x
|
|
2680
|
+
}
|
|
2681
|
+
if (centerWords.length > 1) {
|
|
2682
|
+
const y = parseStaticOriginPart(centerWords[1], cyPermille)
|
|
2683
|
+
if (y === null) return null
|
|
2684
|
+
cyPermille = y
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
if (colorStart + 1 >= parts.length) return null
|
|
2689
|
+
const from = parseStaticGradientStopRef(parts[colorStart])
|
|
2690
|
+
const to = parseStaticGradientStopRef(parts[parts.length - 1])
|
|
2691
|
+
if (!from || !to) return null
|
|
2692
|
+
let stopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(1000, to.stopPermille))
|
|
2693
|
+
if (stopPermille <= 0) stopPermille = 1
|
|
2694
|
+
return {
|
|
2695
|
+
from: from.color,
|
|
2696
|
+
to: to.color,
|
|
2697
|
+
stopPermille,
|
|
2698
|
+
cxPermille,
|
|
2699
|
+
cyPermille,
|
|
2700
|
+
rxPermille,
|
|
2701
|
+
ryPermille,
|
|
2702
|
+
enabled: true,
|
|
2703
|
+
}
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
function parseStaticLinearGradientLayer(rawLayer) {
|
|
2707
|
+
const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
|
|
2708
|
+
if (inner === null) return null
|
|
2709
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2710
|
+
const header = parseStaticGradientHeader(parts)
|
|
2711
|
+
if (!header || header.colorStart >= parts.length) return null
|
|
2712
|
+
|
|
2713
|
+
const from = parseStaticGradientStop(parts[header.colorStart])
|
|
2714
|
+
const to = parseStaticGradientStop(parts[parts.length - 1])
|
|
2715
|
+
if (!from || !to) return null
|
|
2716
|
+
|
|
2717
|
+
let mid = { color: { r: 0, g: 0, b: 0, a: 0 }, stopPermille: 500 }
|
|
2718
|
+
let hasMid = false
|
|
2719
|
+
if (parts.length === header.colorStart + 2) {
|
|
2720
|
+
if (from.stopPermille !== null && from.stopPermille > 0 && from.stopPermille < 1000) {
|
|
2721
|
+
mid = { color: from.color, stopPermille: from.stopPermille }
|
|
2722
|
+
hasMid = true
|
|
2723
|
+
}
|
|
2724
|
+
} else if (parts.length > header.colorStart + 2) {
|
|
2725
|
+
const parsedMid = parseStaticGradientStop(parts[header.colorStart + 1])
|
|
2726
|
+
if (!parsedMid) return null
|
|
2727
|
+
mid = {
|
|
2728
|
+
color: parsedMid.color,
|
|
2729
|
+
stopPermille: parsedMid.stopPermille === null ? 500 : Math.max(0, Math.min(1000, parsedMid.stopPermille)),
|
|
2730
|
+
}
|
|
2731
|
+
hasMid = true
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
let toStopPermille = to.stopPermille === null ? 1000 : Math.max(0, Math.min(60000, to.stopPermille))
|
|
2735
|
+
if (toStopPermille <= 0) toStopPermille = 1
|
|
2736
|
+
if (hasMid && toStopPermille <= mid.stopPermille) toStopPermille = mid.stopPermille + 1
|
|
2737
|
+
|
|
2738
|
+
return {
|
|
2739
|
+
angleTenths: header.angleTenths,
|
|
2740
|
+
from: from.color,
|
|
2741
|
+
mid: mid.color,
|
|
2742
|
+
to: to.color,
|
|
2743
|
+
midStopPermille: mid.stopPermille,
|
|
2744
|
+
toStopPermille,
|
|
2745
|
+
hasMid,
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
|
|
2749
|
+
function staticGradientLineIsVertical(angleTenths) {
|
|
2750
|
+
const angleRadians = angleTenths * Math.PI / 1800
|
|
2751
|
+
const dx = Math.sin(angleRadians)
|
|
2752
|
+
const dy = -Math.cos(angleRadians)
|
|
2753
|
+
return Math.abs(dx) >= Math.abs(dy)
|
|
2754
|
+
}
|
|
2755
|
+
|
|
2756
|
+
function parseStaticGradientLineLayer(rawLayer) {
|
|
2757
|
+
const inner = staticCssFunctionInner(rawLayer.trim(), 'linear-gradient')
|
|
2758
|
+
if (inner === null) return { matched: false, line: null, setup: [] }
|
|
2759
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2760
|
+
const header = parseStaticGradientHeader(parts)
|
|
2761
|
+
if (!header || header.colorStart + 1 >= parts.length) return { matched: false, line: null, setup: [] }
|
|
2762
|
+
|
|
2763
|
+
const first = parseStaticGradientStop(parts[header.colorStart])
|
|
2764
|
+
const second = parseStaticGradientStop(parts[header.colorStart + 1])
|
|
2765
|
+
if (!first || !second || !first.length || second.color.a !== 0) return { matched: false, line: null, setup: [] }
|
|
2766
|
+
if (first.color.a === 0 || !staticLengthSpecIsSafeGridLine(first.length.spec))
|
|
2767
|
+
return { matched: true, line: null, setup: [] }
|
|
2768
|
+
if (second.length && !staticLengthSpecEquals(first.length.spec, second.length.spec))
|
|
2769
|
+
return { matched: true, line: null, setup: [] }
|
|
2770
|
+
|
|
2771
|
+
const axis = staticGradientLineIsVertical(header.angleTenths) ? 'x' : 'y'
|
|
2772
|
+
return {
|
|
2773
|
+
matched: true,
|
|
2774
|
+
setup: first.length.setup,
|
|
2775
|
+
line: { axis, width: first.length.spec, color: first.color },
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
|
|
2779
|
+
function cssStaticBackgroundValue(property, rawValue) {
|
|
2780
|
+
if (property !== 'background' && property !== 'background-image') return null
|
|
2781
|
+
if (cssHasDynamicValue(rawValue)) return null
|
|
2782
|
+
const layers = splitTopLevelCss(rawValue, ',')
|
|
2783
|
+
if (layers.length === 0) return null
|
|
2784
|
+
|
|
2785
|
+
const out = {
|
|
2786
|
+
setup: [],
|
|
2787
|
+
gradient: null,
|
|
2788
|
+
overlayGradient: zeroStaticLinearGradient(),
|
|
2789
|
+
hasOverlayGradient: false,
|
|
2790
|
+
gridX: null,
|
|
2791
|
+
gridY: null,
|
|
2792
|
+
gridOrder: [],
|
|
2793
|
+
}
|
|
2794
|
+
|
|
2795
|
+
for (const layer of layers) {
|
|
2796
|
+
const line = parseStaticGradientLineLayer(layer)
|
|
2797
|
+
if (line.matched) {
|
|
2798
|
+
if (!line.line) return null
|
|
2799
|
+
out.setup.push(...line.setup)
|
|
2800
|
+
if (line.line.axis === 'x') out.gridX = line.line
|
|
2801
|
+
else out.gridY = line.line
|
|
2802
|
+
out.gridOrder.push(line.line.axis)
|
|
2803
|
+
continue
|
|
2804
|
+
}
|
|
2805
|
+
|
|
2806
|
+
const gradient = parseStaticLinearGradientLayer(layer)
|
|
2807
|
+
if (gradient) {
|
|
2808
|
+
if (out.gradient) {
|
|
2809
|
+
out.overlayGradient = out.gradient
|
|
2810
|
+
out.hasOverlayGradient = true
|
|
2811
|
+
}
|
|
2812
|
+
out.gradient = gradient
|
|
2813
|
+
continue
|
|
2814
|
+
}
|
|
2815
|
+
if (layer.toLowerCase().includes('var(')) return null
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
if (!out.gradient) return null
|
|
2819
|
+
if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
|
|
2820
|
+
return out
|
|
2821
|
+
}
|
|
2822
|
+
|
|
2823
|
+
function colorRefUsesVar(ref) {
|
|
2824
|
+
return Boolean(ref && ref.name)
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
function linearGradientRefUsesVar(gradient) {
|
|
2828
|
+
return colorRefUsesVar(gradient.from) ||
|
|
2829
|
+
colorRefUsesVar(gradient.mid) ||
|
|
2830
|
+
colorRefUsesVar(gradient.to)
|
|
2831
|
+
}
|
|
2832
|
+
|
|
2833
|
+
function cssStaticBackgroundFullValue(property, rawValue) {
|
|
2834
|
+
if (property !== 'background' && property !== 'background-image') return null
|
|
2835
|
+
const lower = rawValue.toLowerCase()
|
|
2836
|
+
if (lower.includes('calc(') || lower.includes('min(') || lower.includes('max(') || lower.includes('clamp('))
|
|
2837
|
+
return null
|
|
2838
|
+
const layers = splitTopLevelCss(rawValue, ',')
|
|
2839
|
+
if (layers.length === 0) return null
|
|
2840
|
+
|
|
2841
|
+
const out = {
|
|
2842
|
+
setup: [],
|
|
2843
|
+
gradient: null,
|
|
2844
|
+
overlayGradient: zeroStaticLinearGradientRef(),
|
|
2845
|
+
hasOverlayGradient: false,
|
|
2846
|
+
radialGradient: zeroStaticRadialGradientRef(),
|
|
2847
|
+
gridX: null,
|
|
2848
|
+
gridY: null,
|
|
2849
|
+
gridOrder: [],
|
|
2850
|
+
needsFullApi: false,
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
for (const layer of layers) {
|
|
2854
|
+
const line = parseStaticGradientLineLayer(layer)
|
|
2855
|
+
if (line.matched) {
|
|
2856
|
+
if (!line.line) return null
|
|
2857
|
+
out.setup.push(...line.setup)
|
|
2858
|
+
if (line.line.axis === 'x') out.gridX = line.line
|
|
2859
|
+
else out.gridY = line.line
|
|
2860
|
+
out.gridOrder.push(line.line.axis)
|
|
2861
|
+
continue
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
const radialGradient = parseStaticRadialGradientRefLayer(layer)
|
|
2865
|
+
if (radialGradient) {
|
|
2866
|
+
out.radialGradient = radialGradient
|
|
2867
|
+
out.needsFullApi = true
|
|
2868
|
+
continue
|
|
2869
|
+
}
|
|
2870
|
+
|
|
2871
|
+
const gradient = parseStaticLinearGradientRefLayer(layer)
|
|
2872
|
+
if (gradient) {
|
|
2873
|
+
if (out.gradient) {
|
|
2874
|
+
out.overlayGradient = out.gradient
|
|
2875
|
+
out.hasOverlayGradient = true
|
|
2876
|
+
}
|
|
2877
|
+
out.gradient = gradient
|
|
2878
|
+
if (linearGradientRefUsesVar(gradient)) out.needsFullApi = true
|
|
2879
|
+
continue
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
if (layer.toLowerCase().includes('var(')) return null
|
|
2883
|
+
}
|
|
2884
|
+
|
|
2885
|
+
if (!out.gradient) return null
|
|
2886
|
+
if (out.gridX && out.gridY && !staticColorEquals(out.gridX.color, out.gridY.color) && out.gridOrder.join('') !== 'xy') return null
|
|
2887
|
+
return out.needsFullApi ? out : null
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
function cssStaticBackgroundSizeValue(property, rawValue) {
|
|
2891
|
+
if (property !== 'background-size') return null
|
|
2892
|
+
if (cssHasDynamicValue(rawValue)) return null
|
|
2893
|
+
const layers = splitTopLevelCss(rawValue, ',')
|
|
2894
|
+
if (layers.length === 0) return null
|
|
2895
|
+
const parts = splitCssWords(layers[0])
|
|
2896
|
+
if (parts.length === 0) return null
|
|
2897
|
+
const x = parseStaticCssLengthSpec(parts[0])
|
|
2898
|
+
if (!x) return null
|
|
2899
|
+
const y = parts.length > 1 ? parseStaticCssLengthSpec(parts[1]) : x
|
|
2900
|
+
if (!y) return null
|
|
2901
|
+
return { setup: [...x.setup, ...(y === x ? [] : y.setup)], stepX: x.spec, stepY: y.spec }
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
function parseStaticGridTemplateTrack(rawToken) {
|
|
2905
|
+
const token = rawToken.trim()
|
|
2906
|
+
const lower = token.toLowerCase()
|
|
2907
|
+
if (!token) return null
|
|
2908
|
+
if (lower === 'auto') return { type: 0, value: 0, length: zeroStaticLengthSpec() }
|
|
2909
|
+
if (lower.startsWith('minmax(')) {
|
|
2910
|
+
const inner = staticCssFunctionInner(token, 'minmax')
|
|
2911
|
+
if (inner === null) return null
|
|
2912
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2913
|
+
if (parts.length === 0) return null
|
|
2914
|
+
return parseStaticGridTemplateTrack(parts.length > 1 ? parts[1] : parts[0])
|
|
2915
|
+
}
|
|
2916
|
+
if (lower.endsWith('fr')) {
|
|
2917
|
+
const number = Number(lower.slice(0, -2).trim())
|
|
2918
|
+
if (!Number.isFinite(number)) return null
|
|
2919
|
+
return { type: 2, value: Math.max(1, roundCssNumber(number)), length: zeroStaticLengthSpec() }
|
|
2920
|
+
}
|
|
2921
|
+
const length = parseStaticCssLengthSpec(token)
|
|
2922
|
+
if (!length) return null
|
|
2923
|
+
return { type: 1, value: 0, length: length.spec, setup: length.setup }
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
function cssStaticGridTemplateValue(property, rawValue) {
|
|
2927
|
+
let target = null
|
|
2928
|
+
if (property === 'grid-template-columns') target = 'Columns'
|
|
2929
|
+
else if (property === 'grid-template-rows') target = 'Rows'
|
|
2930
|
+
const lowerValue = rawValue.toLowerCase()
|
|
2931
|
+
if (!target || lowerValue.includes('var(') || lowerValue.includes('calc(') || lowerValue.includes('clamp('))
|
|
2932
|
+
return null
|
|
2933
|
+
|
|
2934
|
+
const tracks = []
|
|
2935
|
+
const setup = []
|
|
2936
|
+
for (const token of splitCssWords(rawValue.trim())) {
|
|
2937
|
+
const lower = token.toLowerCase()
|
|
2938
|
+
if (lower.startsWith('repeat(')) {
|
|
2939
|
+
const inner = staticCssFunctionInner(token, 'repeat')
|
|
2940
|
+
if (inner === null) return null
|
|
2941
|
+
const parts = splitTopLevelCss(inner, ',')
|
|
2942
|
+
if (parts.length < 2) return null
|
|
2943
|
+
let repeatCount = roundCssNumber(Number(parts[0].trim()))
|
|
2944
|
+
if (!Number.isFinite(repeatCount)) return null
|
|
2945
|
+
repeatCount = Math.max(0, Math.min(8, repeatCount))
|
|
2946
|
+
const track = parseStaticGridTemplateTrack(parts[1])
|
|
2947
|
+
if (!track) return null
|
|
2948
|
+
if (track.setup) setup.push(...track.setup)
|
|
2949
|
+
for (let i = 0; i < repeatCount && tracks.length < 8; i++) tracks.push(track)
|
|
2950
|
+
continue
|
|
2951
|
+
}
|
|
2952
|
+
const track = parseStaticGridTemplateTrack(token)
|
|
2953
|
+
if (!track) return null
|
|
2954
|
+
if (track.setup) setup.push(...track.setup)
|
|
2955
|
+
if (tracks.length < 8) tracks.push(track)
|
|
2956
|
+
}
|
|
2957
|
+
return { target, tracks, setup }
|
|
2958
|
+
}
|
|
2959
|
+
|
|
2960
|
+
function parseStaticTimeMs(rawToken) {
|
|
2961
|
+
const match = rawToken.trim().toLowerCase().match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(ms|s)$/)
|
|
2962
|
+
if (!match) return null
|
|
2963
|
+
const amount = Number(match[1])
|
|
2964
|
+
if (!Number.isFinite(amount)) return null
|
|
2965
|
+
const ms = match[2] === 's' ? amount * 1000 : amount
|
|
2966
|
+
return Math.max(0, roundCssNumber(ms))
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
function parseStaticAnimationEasing(rawToken) {
|
|
2970
|
+
const token = rawToken.trim()
|
|
2971
|
+
const lower = token.toLowerCase()
|
|
2972
|
+
const keywords = new Map([
|
|
2973
|
+
['linear', 'Linear'],
|
|
2974
|
+
['ease', 'Ease'],
|
|
2975
|
+
['ease-in', 'EaseIn'],
|
|
2976
|
+
['ease-out', 'EaseOut'],
|
|
2977
|
+
['ease-in-out', 'EaseInOut'],
|
|
2978
|
+
])
|
|
2979
|
+
if (keywords.has(lower)) {
|
|
2980
|
+
return { kind: keywords.get(lower), x1: 0, y1: 0, x2: 1, y2: 1, steps: 1 }
|
|
2981
|
+
}
|
|
2982
|
+
const cubic = staticCssFunctionInner(lower, 'cubic-bezier')
|
|
2983
|
+
if (cubic !== null) {
|
|
2984
|
+
const parts = splitTopLevelCss(cubic, ',').map((part) => Number(part.trim()))
|
|
2985
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isFinite(part))) return null
|
|
2986
|
+
return { kind: 'CubicBezier', x1: parts[0], y1: parts[1], x2: parts[2], y2: parts[3], steps: 1 }
|
|
2987
|
+
}
|
|
2988
|
+
const steps = staticCssFunctionInner(lower, 'steps')
|
|
2989
|
+
if (steps !== null) {
|
|
2990
|
+
const count = Number.parseInt(steps.trim(), 10)
|
|
2991
|
+
return { kind: 'Steps', x1: 0, y1: 0, x2: 1, y2: 1, steps: count > 0 ? count : 1 }
|
|
2992
|
+
}
|
|
2993
|
+
return null
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
function staticAnimationEasingCode(easing) {
|
|
2997
|
+
return `{gea::embedded::ui::StaticStyleAnimationEasingKind::${easing.kind}, ${cssNumberCode(easing.x1)}, ${cssNumberCode(easing.y1)}, ${cssNumberCode(easing.x2)}, ${cssNumberCode(easing.y2)}, ${easing.steps}}`
|
|
2998
|
+
}
|
|
2999
|
+
|
|
3000
|
+
function cssStaticAnimationValue(property, rawValue) {
|
|
3001
|
+
if (property !== 'animation') return null
|
|
3002
|
+
const spec = {
|
|
3003
|
+
name: null,
|
|
3004
|
+
durationMs: 0,
|
|
3005
|
+
delayMs: 0,
|
|
3006
|
+
iterations: 1,
|
|
3007
|
+
direction: 'Normal',
|
|
3008
|
+
fill: 'None',
|
|
3009
|
+
easing: { kind: 'Ease', x1: 0, y1: 0, x2: 1, y2: 1, steps: 1 },
|
|
3010
|
+
}
|
|
3011
|
+
let sawDuration = false
|
|
3012
|
+
let sawName = false
|
|
3013
|
+
for (const rawToken of splitCssWords(rawValue)) {
|
|
3014
|
+
const token = rawToken.trim()
|
|
3015
|
+
const lower = token.toLowerCase()
|
|
3016
|
+
const timeMs = parseStaticTimeMs(lower)
|
|
3017
|
+
if (timeMs !== null) {
|
|
3018
|
+
if (!sawDuration) {
|
|
3019
|
+
spec.durationMs = timeMs
|
|
3020
|
+
sawDuration = true
|
|
3021
|
+
} else {
|
|
3022
|
+
spec.delayMs = timeMs
|
|
3023
|
+
}
|
|
3024
|
+
continue
|
|
3025
|
+
}
|
|
3026
|
+
if (lower === 'infinite') {
|
|
3027
|
+
spec.iterations = -1
|
|
3028
|
+
continue
|
|
3029
|
+
}
|
|
3030
|
+
if (/^\d+$/.test(lower)) {
|
|
3031
|
+
spec.iterations = Number.parseInt(lower, 10)
|
|
3032
|
+
continue
|
|
3033
|
+
}
|
|
3034
|
+
if (lower === 'reverse') {
|
|
3035
|
+
spec.direction = 'Reverse'
|
|
3036
|
+
continue
|
|
3037
|
+
}
|
|
3038
|
+
if (lower === 'alternate') {
|
|
3039
|
+
spec.direction = 'Alternate'
|
|
3040
|
+
continue
|
|
3041
|
+
}
|
|
3042
|
+
if (lower === 'alternate-reverse') {
|
|
3043
|
+
spec.direction = 'AlternateReverse'
|
|
3044
|
+
continue
|
|
3045
|
+
}
|
|
3046
|
+
if (lower === 'forwards') {
|
|
3047
|
+
spec.fill = 'Forwards'
|
|
3048
|
+
continue
|
|
3049
|
+
}
|
|
3050
|
+
if (lower === 'backwards') {
|
|
3051
|
+
spec.fill = 'Backwards'
|
|
3052
|
+
continue
|
|
3053
|
+
}
|
|
3054
|
+
if (lower === 'both') {
|
|
3055
|
+
spec.fill = 'Both'
|
|
3056
|
+
continue
|
|
3057
|
+
}
|
|
3058
|
+
const easing = parseStaticAnimationEasing(token)
|
|
3059
|
+
if (easing) {
|
|
3060
|
+
spec.easing = easing
|
|
3061
|
+
continue
|
|
3062
|
+
}
|
|
3063
|
+
if (lower === 'normal' || lower === 'running' || lower === 'none') continue
|
|
3064
|
+
if (!sawName) {
|
|
3065
|
+
spec.name = token
|
|
3066
|
+
sawName = true
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
return spec.name && spec.durationMs > 0 ? spec : null
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
function cssStaticCustomLengthValue(property, rawValue) {
|
|
3073
|
+
if (!property.startsWith('--')) return null
|
|
3074
|
+
const parsed = parseStaticCssLengthSpec(rawValue)
|
|
3075
|
+
return parsed ? { name: property, ...parsed } : null
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
function cssStaticCustomColorValue(property, rawValue) {
|
|
3079
|
+
if (!property.startsWith('--')) return null
|
|
3080
|
+
const color = parseStaticCssColor(rawValue)
|
|
3081
|
+
return color ? { name: property, color } : null
|
|
3082
|
+
}
|
|
3083
|
+
|
|
3084
|
+
function cssStaticLengthPropertyTarget(property) {
|
|
3085
|
+
const map = new Map([
|
|
3086
|
+
['gap', 'Gap'],
|
|
3087
|
+
['width', 'Width'],
|
|
3088
|
+
['height', 'Height'],
|
|
3089
|
+
['min-width', 'MinWidth'],
|
|
3090
|
+
['min-height', 'MinHeight'],
|
|
3091
|
+
['max-width', 'MaxWidth'],
|
|
3092
|
+
['max-height', 'MaxHeight'],
|
|
3093
|
+
['flex-basis', 'FlexBasis'],
|
|
3094
|
+
['padding-top', 'PaddingTop'],
|
|
3095
|
+
['padding-right', 'PaddingRight'],
|
|
3096
|
+
['padding-bottom', 'PaddingBottom'],
|
|
3097
|
+
['padding-left', 'PaddingLeft'],
|
|
3098
|
+
['margin-top', 'MarginTop'],
|
|
3099
|
+
['margin-right', 'MarginRight'],
|
|
3100
|
+
['margin-bottom', 'MarginBottom'],
|
|
3101
|
+
['margin-left', 'MarginLeft'],
|
|
3102
|
+
['border-width', 'BorderWidth'],
|
|
3103
|
+
['border-top-width', 'BorderTopWidth'],
|
|
3104
|
+
['border-right-width', 'BorderRightWidth'],
|
|
3105
|
+
['border-bottom-width', 'BorderBottomWidth'],
|
|
3106
|
+
['border-left-width', 'BorderLeftWidth'],
|
|
3107
|
+
['font-size', 'FontSize'],
|
|
3108
|
+
['perspective', 'Perspective'],
|
|
3109
|
+
['top', 'Top'],
|
|
3110
|
+
['right', 'Right'],
|
|
3111
|
+
['bottom', 'Bottom'],
|
|
3112
|
+
['left', 'Left'],
|
|
3113
|
+
])
|
|
3114
|
+
return map.get(property) ?? null
|
|
3115
|
+
}
|
|
3116
|
+
|
|
3117
|
+
function cssStaticLengthValue(property, rawValue) {
|
|
3118
|
+
const target = cssStaticLengthPropertyTarget(property)
|
|
3119
|
+
if (!target) return null
|
|
3120
|
+
if (rawValue.trim().toLowerCase() === 'auto' && (target === 'Width' || target === 'Height')) {
|
|
3121
|
+
return { target, length: { unit: 'Auto', number: 0 } }
|
|
3122
|
+
}
|
|
3123
|
+
const length = parseStaticCssLength(rawValue)
|
|
3124
|
+
if (!length) return null
|
|
3125
|
+
return { target, length }
|
|
3126
|
+
}
|
|
3127
|
+
|
|
3128
|
+
function cssStaticLengthExpressionValue(property, rawValue) {
|
|
3129
|
+
const target = cssStaticLengthPropertyTarget(property)
|
|
3130
|
+
if (!target) return null
|
|
3131
|
+
const parsed = parseStaticCssLengthSpec(rawValue)
|
|
3132
|
+
if (!parsed || parsed.spec.unit !== 'Expression') return null
|
|
3133
|
+
return { target, ...parsed }
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
function cssStaticMaskImageValue(property, rawValue) {
|
|
3137
|
+
if (property !== 'mask-image' && property !== '-webkit-mask-image') return null
|
|
3138
|
+
const value = rawValue.trim()
|
|
3139
|
+
const lower = value.toLowerCase()
|
|
3140
|
+
if (!value || lower === 'none') return { setup: [], spec: zeroStaticLengthSpec() }
|
|
3141
|
+
if (!lower.includes('linear-gradient') || !lower.includes('to right') || !lower.includes('transparent'))
|
|
3142
|
+
return null
|
|
3143
|
+
const match = value.match(/calc\(\s*100%\s*-\s*([^)]+?)\s*\)/i)
|
|
3144
|
+
if (!match) return null
|
|
3145
|
+
const parsed = parseStaticCssLengthSpec(match[1])
|
|
3146
|
+
if (!parsed) return null
|
|
3147
|
+
return parsed
|
|
3148
|
+
}
|
|
3149
|
+
|
|
3150
|
+
function cssStaticBoxShadowNoneValue(property, rawValue) {
|
|
3151
|
+
if (property !== 'box-shadow') return null
|
|
3152
|
+
const value = rawValue.trim()
|
|
3153
|
+
if (!value || value.toLowerCase() === 'none') return {}
|
|
3154
|
+
for (const layer of splitTopLevelCss(value, ',')) {
|
|
3155
|
+
for (const token of splitCssWords(layer)) {
|
|
3156
|
+
if (token.trim().toLowerCase() === 'inset') return null
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
return {}
|
|
3160
|
+
}
|
|
3161
|
+
|
|
3162
|
+
function parseStaticOriginPart(rawPart, fallback) {
|
|
3163
|
+
const part = rawPart.trim().toLowerCase()
|
|
3164
|
+
if (!part) return fallback
|
|
3165
|
+
if (part === 'left' || part === 'top') return 0
|
|
3166
|
+
if (part === 'center') return 500
|
|
3167
|
+
if (part === 'right' || part === 'bottom') return 1000
|
|
3168
|
+
const length = parseStaticCssLength(part)
|
|
3169
|
+
if (!length || length.unit !== 'Percent') return null
|
|
3170
|
+
return roundCssNumber(length.number * 10)
|
|
3171
|
+
}
|
|
3172
|
+
|
|
3173
|
+
function cssStaticOriginValue(property, rawValue) {
|
|
3174
|
+
let target = null
|
|
3175
|
+
if (property === 'transform-origin') target = 'TransformOrigin'
|
|
3176
|
+
else if (property === 'perspective-origin') target = 'PerspectiveOrigin'
|
|
3177
|
+
if (!target) return null
|
|
3178
|
+
const parts = rawValue.trim().split(/\s+/).filter(Boolean)
|
|
3179
|
+
if (parts.length > 2) return null
|
|
3180
|
+
const x = parseStaticOriginPart(parts[0] ?? '', 500)
|
|
3181
|
+
const y = parseStaticOriginPart(parts[1] ?? '', 500)
|
|
3182
|
+
if (x === null || y === null) return null
|
|
3183
|
+
return { target, x, y }
|
|
3184
|
+
}
|
|
3185
|
+
|
|
3186
|
+
const staticTransformFlags = {
|
|
3187
|
+
rotateX: 1 << 0,
|
|
3188
|
+
rotateY: 1 << 1,
|
|
3189
|
+
rotateZ: 1 << 2,
|
|
3190
|
+
translateX: 1 << 3,
|
|
3191
|
+
translateY: 1 << 4,
|
|
3192
|
+
translateZ: 1 << 5,
|
|
3193
|
+
scaleX: 1 << 8,
|
|
3194
|
+
scaleY: 1 << 9,
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
function parseStaticAngleTenths(rawValue) {
|
|
3198
|
+
const value = rawValue.trim()
|
|
3199
|
+
const match = value.match(/^([+-]?(?:\d+(?:\.\d*)?|\.\d+))(deg|rad|turn)?$/i)
|
|
3200
|
+
if (!match) return null
|
|
3201
|
+
let degrees = Number(match[1])
|
|
3202
|
+
if (!Number.isFinite(degrees)) return null
|
|
3203
|
+
const unit = (match[2] || 'deg').toLowerCase()
|
|
3204
|
+
if (unit === 'rad') degrees = degrees * 180 / Math.PI
|
|
3205
|
+
else if (unit === 'turn') degrees *= 360
|
|
3206
|
+
return roundCssNumber(degrees * 10)
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function parseStaticScalePermille(rawValue) {
|
|
3210
|
+
const value = Number(rawValue.trim())
|
|
3211
|
+
if (!Number.isFinite(value)) return null
|
|
3212
|
+
return roundCssNumber(value * 1000)
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
function parseStaticTransformValue(rawValue) {
|
|
3216
|
+
const value = rawValue.trim()
|
|
3217
|
+
const out = {
|
|
3218
|
+
setup: [],
|
|
3219
|
+
flags: 0,
|
|
3220
|
+
rotateX: 0,
|
|
3221
|
+
rotateY: 0,
|
|
3222
|
+
rotateZ: 0,
|
|
3223
|
+
translateX: zeroStaticLengthSpec(),
|
|
3224
|
+
translateY: zeroStaticLengthSpec(),
|
|
3225
|
+
translateZ: zeroStaticLengthSpec(),
|
|
3226
|
+
scaleX: 1000,
|
|
3227
|
+
scaleY: 1000,
|
|
3228
|
+
}
|
|
3229
|
+
let sawTransform = false
|
|
3230
|
+
let i = 0
|
|
3231
|
+
while (i < value.length) {
|
|
3232
|
+
while (i < value.length && /\s/.test(value[i])) i++
|
|
3233
|
+
if (i >= value.length) break
|
|
3234
|
+
const nameStart = i
|
|
3235
|
+
while (i < value.length && /[A-Za-z3]/.test(value[i])) i++
|
|
3236
|
+
if (i === nameStart) return null
|
|
3237
|
+
while (i < value.length && /\s/.test(value[i])) i++
|
|
3238
|
+
if (i >= value.length || value[i] !== '(') return null
|
|
3239
|
+
const name = value.slice(nameStart, i).toLowerCase()
|
|
3240
|
+
const argStart = ++i
|
|
3241
|
+
let depth = 1
|
|
3242
|
+
while (i < value.length && depth > 0) {
|
|
3243
|
+
if (value[i] === '(') depth++
|
|
3244
|
+
else if (value[i] === ')') depth--
|
|
3245
|
+
i++
|
|
3246
|
+
}
|
|
3247
|
+
if (depth !== 0) return null
|
|
3248
|
+
const arg = value.slice(argStart, i - 1)
|
|
3249
|
+
const args = splitTopLevelCss(arg, ',')
|
|
3250
|
+
sawTransform = true
|
|
3251
|
+
|
|
3252
|
+
const setLength = (field, flag, raw) => {
|
|
3253
|
+
const parsed = parseStaticCssLengthSpec(raw)
|
|
3254
|
+
if (!parsed) return false
|
|
3255
|
+
out.setup.push(...parsed.setup)
|
|
3256
|
+
out[field] = parsed.spec
|
|
3257
|
+
out.flags |= flag
|
|
3258
|
+
return true
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3261
|
+
if (name === 'rotate' || name === 'rotatez') {
|
|
3262
|
+
const angle = parseStaticAngleTenths(arg)
|
|
3263
|
+
if (angle === null) return null
|
|
3264
|
+
out.rotateZ = angle
|
|
3265
|
+
out.flags |= staticTransformFlags.rotateZ
|
|
3266
|
+
} else if (name === 'rotatex') {
|
|
3267
|
+
const angle = parseStaticAngleTenths(arg)
|
|
3268
|
+
if (angle === null) return null
|
|
3269
|
+
out.rotateX = angle
|
|
3270
|
+
out.flags |= staticTransformFlags.rotateX
|
|
3271
|
+
} else if (name === 'rotatey') {
|
|
3272
|
+
const angle = parseStaticAngleTenths(arg)
|
|
3273
|
+
if (angle === null) return null
|
|
3274
|
+
out.rotateY = angle
|
|
3275
|
+
out.flags |= staticTransformFlags.rotateY
|
|
3276
|
+
} else if (name === 'translatex') {
|
|
3277
|
+
if (!setLength('translateX', staticTransformFlags.translateX, arg)) return null
|
|
3278
|
+
} else if (name === 'translatey') {
|
|
3279
|
+
if (!setLength('translateY', staticTransformFlags.translateY, arg)) return null
|
|
3280
|
+
} else if (name === 'translatez') {
|
|
3281
|
+
if (!setLength('translateZ', staticTransformFlags.translateZ, arg)) return null
|
|
3282
|
+
} else if (name === 'translate' || name === 'translate3d') {
|
|
3283
|
+
if (args.length < 1) return null
|
|
3284
|
+
if (!setLength('translateX', staticTransformFlags.translateX, args[0])) return null
|
|
3285
|
+
if (args.length > 1 && !setLength('translateY', staticTransformFlags.translateY, args[1])) return null
|
|
3286
|
+
if (args.length > 2 && !setLength('translateZ', staticTransformFlags.translateZ, args[2])) return null
|
|
3287
|
+
} else if (name === 'scale') {
|
|
3288
|
+
const sx = parseStaticScalePermille(args[0] ?? arg)
|
|
3289
|
+
const sy = parseStaticScalePermille(args[1] ?? args[0] ?? arg)
|
|
3290
|
+
if (sx === null || sy === null) return null
|
|
3291
|
+
out.scaleX = sx
|
|
3292
|
+
out.scaleY = sy
|
|
3293
|
+
out.flags |= staticTransformFlags.scaleX | staticTransformFlags.scaleY
|
|
3294
|
+
} else if (name === 'scalex') {
|
|
3295
|
+
const scale = parseStaticScalePermille(arg)
|
|
3296
|
+
if (scale === null) return null
|
|
3297
|
+
out.scaleX = scale
|
|
3298
|
+
out.flags |= staticTransformFlags.scaleX
|
|
3299
|
+
} else if (name === 'scaley') {
|
|
3300
|
+
const scale = parseStaticScalePermille(arg)
|
|
3301
|
+
if (scale === null) return null
|
|
3302
|
+
out.scaleY = scale
|
|
3303
|
+
out.flags |= staticTransformFlags.scaleY
|
|
3304
|
+
} else {
|
|
3305
|
+
return null
|
|
3306
|
+
}
|
|
3307
|
+
}
|
|
3308
|
+
return sawTransform ? out : null
|
|
3309
|
+
}
|
|
3310
|
+
|
|
3311
|
+
function cssStaticTransformValue(property, rawValue) {
|
|
3312
|
+
if (property !== 'transform') return null
|
|
3313
|
+
return parseStaticTransformValue(rawValue)
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function staticTransformArgs(transform) {
|
|
3317
|
+
return `${transform.flags}, ${transform.rotateX}, ${transform.rotateY}, ${transform.rotateZ}, ${staticLengthSpecCode(transform.translateX)}, ${staticLengthSpecCode(transform.translateY)}, ${staticLengthSpecCode(transform.translateZ)}, ${transform.scaleX}, ${transform.scaleY}`
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
const staticAnimatableLengthTargets = new Set(['Width', 'Height', 'Top', 'Left'])
|
|
3321
|
+
const staticAnimatableColorTargets = new Set(['Color', 'Background'])
|
|
3322
|
+
const staticAnimatableDirectProperties = new Set([
|
|
3323
|
+
'gea::embedded::ui::Property::Opacity',
|
|
3324
|
+
'gea::embedded::ui::Property::TransformRotate',
|
|
3325
|
+
'gea::embedded::ui::Property::TransformScaleX',
|
|
3326
|
+
'gea::embedded::ui::Property::TransformScaleY',
|
|
3327
|
+
])
|
|
3328
|
+
|
|
3329
|
+
function cssStaticFilterBlurValue(property, rawValue) {
|
|
3330
|
+
if (property !== 'filter') return null
|
|
3331
|
+
const value = rawValue.trim()
|
|
3332
|
+
const lower = value.toLowerCase()
|
|
3333
|
+
if (!value || lower === 'none') return { setup: [], spec: zeroStaticLengthSpec() }
|
|
3334
|
+
const blur = lower.indexOf('blur(')
|
|
3335
|
+
if (blur < 0) return { setup: [], spec: zeroStaticLengthSpec() }
|
|
3336
|
+
let i = blur + 5
|
|
3337
|
+
let depth = 1
|
|
3338
|
+
const argStart = i
|
|
3339
|
+
while (i < value.length && depth > 0) {
|
|
3340
|
+
if (value[i] === '(') depth++
|
|
3341
|
+
else if (value[i] === ')') depth--
|
|
3342
|
+
i++
|
|
3343
|
+
}
|
|
3344
|
+
if (depth !== 0 || i <= argStart) return null
|
|
3345
|
+
return parseStaticCssLengthSpec(value.slice(argStart, i - 1))
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
function emitStaticCssKeyframeRegistrations(blockName, offset, name, value) {
|
|
3349
|
+
if (!useStaticCssRules) return null
|
|
3350
|
+
if (cssIgnoredProperty(name)) return []
|
|
3351
|
+
|
|
3352
|
+
if (name === 'transform') {
|
|
3353
|
+
const transform = parseStaticTransformValue(value)
|
|
3354
|
+
if (transform) {
|
|
3355
|
+
return [
|
|
3356
|
+
...transform.setup,
|
|
3357
|
+
`__gea_stylesheet.registerStaticTransformKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${staticTransformArgs(transform)});`,
|
|
3358
|
+
]
|
|
3359
|
+
}
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const directProperties = cssStaticPropertyValues(name, value)
|
|
3363
|
+
if (directProperties.length > 0 && directProperties.every((direct) => staticAnimatableDirectProperties.has(direct.property))) {
|
|
3364
|
+
return directProperties.map((direct) =>
|
|
3365
|
+
`__gea_stylesheet.registerStaticPropertyKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${direct.property}, ${direct.value});`
|
|
3366
|
+
)
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
const color = cssStaticColorValue(name, value)
|
|
3370
|
+
if (color && staticAnimatableColorTargets.has(color.target)) {
|
|
3371
|
+
return [
|
|
3372
|
+
`__gea_stylesheet.registerStaticColorKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a});`,
|
|
3373
|
+
]
|
|
3374
|
+
}
|
|
3375
|
+
|
|
3376
|
+
const colorVar = cssStaticColorVarValue(name, value)
|
|
3377
|
+
if (colorVar && staticAnimatableColorTargets.has(colorVar.target)) {
|
|
3378
|
+
return [
|
|
3379
|
+
`__gea_stylesheet.registerStaticColorVarKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a});`,
|
|
3380
|
+
]
|
|
3381
|
+
}
|
|
3382
|
+
|
|
3383
|
+
const lengthExpression = cssStaticLengthExpressionValue(name, value)
|
|
3384
|
+
if (lengthExpression && staticAnimatableLengthTargets.has(lengthExpression.target)) {
|
|
3385
|
+
return [
|
|
3386
|
+
...lengthExpression.setup,
|
|
3387
|
+
`__gea_stylesheet.registerStaticLengthSpecKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleLengthProperty::${lengthExpression.target}, ${staticLengthSpecCode(lengthExpression.spec)});`,
|
|
3388
|
+
]
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
const length = cssStaticLengthValue(name, value)
|
|
3392
|
+
if (length && staticAnimatableLengthTargets.has(length.target)) {
|
|
3393
|
+
return [
|
|
3394
|
+
`__gea_stylesheet.registerStaticLengthKeyframeRule(${JSON.stringify(blockName)}, ${offset}, gea::embedded::ui::StaticStyleLengthProperty::${length.target}, gea::embedded::ui::StaticStyleLengthUnit::${length.length.unit}, ${length.length.number});`,
|
|
3395
|
+
]
|
|
3396
|
+
}
|
|
3397
|
+
|
|
3398
|
+
const filterBlur = cssStaticFilterBlurValue(name, value)
|
|
3399
|
+
if (filterBlur) {
|
|
3400
|
+
return [
|
|
3401
|
+
...filterBlur.setup,
|
|
3402
|
+
`__gea_stylesheet.registerStaticFilterBlurKeyframeRule(${JSON.stringify(blockName)}, ${offset}, ${staticLengthSpecCode(filterBlur.spec)});`,
|
|
3403
|
+
]
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
return null
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
function emitCssRuleRegistrations(kind, selector, name, value, mediaCondition) {
|
|
3410
|
+
const mediaArg = mediaCondition ? `, ${JSON.stringify(mediaCondition)}` : ''
|
|
3411
|
+
if (useStaticCssRules) {
|
|
3412
|
+
if (cssIgnoredProperty(name)) return []
|
|
3413
|
+
const customLength = cssStaticCustomLengthValue(name, value)
|
|
3414
|
+
if (customLength) {
|
|
3415
|
+
return [
|
|
3416
|
+
...customLength.setup,
|
|
3417
|
+
`__gea_stylesheet.registerStaticCustomLengthRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(customLength.name)}, ${staticLengthSpecCode(customLength.spec)}${mediaArg});`,
|
|
3418
|
+
]
|
|
3419
|
+
}
|
|
3420
|
+
const customColor = cssStaticCustomColorValue(name, value)
|
|
3421
|
+
if (customColor) {
|
|
3422
|
+
return [
|
|
3423
|
+
`__gea_stylesheet.registerStaticCustomColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(customColor.name)}, ${customColor.color.r}, ${customColor.color.g}, ${customColor.color.b}, ${customColor.color.a}${mediaArg});`,
|
|
3424
|
+
]
|
|
3425
|
+
}
|
|
3426
|
+
const colorVar = cssStaticColorVarValue(name, value)
|
|
3427
|
+
if (colorVar) {
|
|
3428
|
+
return [
|
|
3429
|
+
`__gea_stylesheet.registerStaticColorVarRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a}${mediaArg});`,
|
|
3430
|
+
]
|
|
3431
|
+
}
|
|
3432
|
+
const directProperties = cssStaticPropertyValues(name, value)
|
|
3433
|
+
if (directProperties.length > 0) {
|
|
3434
|
+
if (directProperties.length === 1) {
|
|
3435
|
+
const direct = directProperties[0]
|
|
3436
|
+
return [
|
|
3437
|
+
`__gea_stylesheet.registerStaticPropertyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${direct.property}, ${direct.value}${mediaArg});`,
|
|
3438
|
+
]
|
|
3439
|
+
}
|
|
3440
|
+
const entries = directProperties
|
|
3441
|
+
.map((direct) => `{${direct.property}, ${direct.value}}`)
|
|
3442
|
+
.join(', ')
|
|
3443
|
+
return [
|
|
3444
|
+
`__gea_stylesheet.registerStaticPropertyGroupRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, {${entries}}${mediaArg});`,
|
|
3445
|
+
]
|
|
3446
|
+
}
|
|
3447
|
+
const flex = cssStaticFlexValue(name, value)
|
|
3448
|
+
if (flex) {
|
|
3449
|
+
return [
|
|
3450
|
+
...flex.setup,
|
|
3451
|
+
`__gea_stylesheet.registerStaticFlexRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${flex.grow}, ${staticLengthSpecCode(flex.basis)}, ${flex.hasBasis ? 'true' : 'false'}${mediaArg});`,
|
|
3452
|
+
]
|
|
3453
|
+
}
|
|
3454
|
+
const boxShorthand = cssStaticBoxShorthandValue(name, value)
|
|
3455
|
+
if (boxShorthand) {
|
|
3456
|
+
return [
|
|
3457
|
+
...boxShorthand.setup,
|
|
3458
|
+
...boxShorthand.entries.map((entry) =>
|
|
3459
|
+
`__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${entry.target}, ${staticLengthSpecCode(entry.spec)}${mediaArg});`
|
|
3460
|
+
),
|
|
3461
|
+
]
|
|
3462
|
+
}
|
|
3463
|
+
const fontFamily = cssStaticFontFamilyValue(name, value)
|
|
3464
|
+
if (fontFamily) {
|
|
3465
|
+
return [
|
|
3466
|
+
`__gea_stylesheet.registerStaticFontFamilyRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(fontFamily.family)}${mediaArg});`,
|
|
3467
|
+
]
|
|
3468
|
+
}
|
|
3469
|
+
const lineHeight = cssStaticLineHeightValue(name, value)
|
|
3470
|
+
if (lineHeight) {
|
|
3471
|
+
return [
|
|
3472
|
+
...lineHeight.setup,
|
|
3473
|
+
`__gea_stylesheet.registerStaticLineHeightRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLineHeightKind::${lineHeight.kind}, ${staticLengthSpecCode(lineHeight.spec)}${mediaArg});`,
|
|
3474
|
+
]
|
|
3475
|
+
}
|
|
3476
|
+
const borderShorthand = cssStaticBorderShorthandValue(name, value)
|
|
3477
|
+
if (borderShorthand) {
|
|
3478
|
+
const calls = [
|
|
3479
|
+
...borderShorthand.setup,
|
|
3480
|
+
`__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${borderShorthand.widthTarget}, ${staticLengthSpecCode(borderShorthand.width)}${mediaArg});`,
|
|
3481
|
+
]
|
|
3482
|
+
if (borderShorthand.colorVar) {
|
|
3483
|
+
const colorVar = borderShorthand.colorVar
|
|
3484
|
+
calls.push(`__gea_stylesheet.registerStaticColorVarRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${colorVar.target}, ${JSON.stringify(colorVar.name)}, ${colorVar.hasFallback ? 'true' : 'false'}, ${colorVar.color.r}, ${colorVar.color.g}, ${colorVar.color.b}, ${colorVar.color.a}${mediaArg});`)
|
|
3485
|
+
} else if (borderShorthand.color) {
|
|
3486
|
+
const color = borderShorthand.color
|
|
3487
|
+
calls.push(`__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`)
|
|
3488
|
+
}
|
|
3489
|
+
return calls
|
|
3490
|
+
}
|
|
3491
|
+
const border = cssStaticBorderValue(name, value)
|
|
3492
|
+
if (border) {
|
|
3493
|
+
return [
|
|
3494
|
+
...border.setup,
|
|
3495
|
+
`__gea_stylesheet.registerStaticBorderRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(border.width)}, ${border.color.r}, ${border.color.g}, ${border.color.b}, ${border.color.a}${mediaArg});`,
|
|
3496
|
+
]
|
|
3497
|
+
}
|
|
3498
|
+
const borderRadius = cssStaticBorderRadiusValue(name, value)
|
|
3499
|
+
if (borderRadius) {
|
|
3500
|
+
if (borderRadius.corner) {
|
|
3501
|
+
return [
|
|
3502
|
+
...borderRadius.setup,
|
|
3503
|
+
`__gea_stylesheet.registerStaticBorderRadiusCornerRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleBorderRadiusCorner::${borderRadius.corner}, ${staticLengthSpecCode(borderRadius.spec)}${mediaArg});`,
|
|
3504
|
+
]
|
|
3505
|
+
}
|
|
3506
|
+
return [
|
|
3507
|
+
...borderRadius.setup,
|
|
3508
|
+
`__gea_stylesheet.registerStaticBorderRadiusRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${borderRadius.specs.map(staticLengthSpecCode).join(', ')}${mediaArg});`,
|
|
3509
|
+
]
|
|
3510
|
+
}
|
|
3511
|
+
const filterBlur = cssStaticFilterBlurValue(name, value)
|
|
3512
|
+
if (filterBlur) {
|
|
3513
|
+
return [
|
|
3514
|
+
...filterBlur.setup,
|
|
3515
|
+
`__gea_stylesheet.registerStaticFilterBlurRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(filterBlur.spec)}${mediaArg});`,
|
|
3516
|
+
]
|
|
3517
|
+
}
|
|
3518
|
+
const fullBackground = cssStaticBackgroundFullValue(name, value)
|
|
3519
|
+
if (fullBackground) {
|
|
3520
|
+
return [
|
|
3521
|
+
...fullBackground.setup,
|
|
3522
|
+
`__gea_stylesheet.registerStaticBackgroundFullRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientRefCode(fullBackground.gradient)}, ${staticLinearGradientRefCode(fullBackground.overlayGradient)}, ${fullBackground.hasOverlayGradient ? 'true' : 'false'}, ${staticRadialGradientRefCode(fullBackground.radialGradient)}, ${staticGridLineCode(fullBackground.gridX)}, ${staticGridLineCode(fullBackground.gridY)}${mediaArg});`,
|
|
3523
|
+
]
|
|
3524
|
+
}
|
|
3525
|
+
const background = cssStaticBackgroundValue(name, value)
|
|
3526
|
+
if (background) {
|
|
3527
|
+
return [
|
|
3528
|
+
...background.setup,
|
|
3529
|
+
`__gea_stylesheet.registerStaticBackgroundRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLinearGradientCode(background.gradient)}, ${staticLinearGradientCode(background.overlayGradient)}, ${background.hasOverlayGradient ? 'true' : 'false'}, ${staticGridLineCode(background.gridX)}, ${staticGridLineCode(background.gridY)}${mediaArg});`,
|
|
3530
|
+
]
|
|
3531
|
+
}
|
|
3532
|
+
const backgroundSize = cssStaticBackgroundSizeValue(name, value)
|
|
3533
|
+
if (backgroundSize) {
|
|
3534
|
+
return [
|
|
3535
|
+
...backgroundSize.setup,
|
|
3536
|
+
`__gea_stylesheet.registerStaticBackgroundSizeRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticLengthSpecCode(backgroundSize.stepX)}, ${staticLengthSpecCode(backgroundSize.stepY)}${mediaArg});`,
|
|
3537
|
+
]
|
|
3538
|
+
}
|
|
3539
|
+
const gridTemplate = cssStaticGridTemplateValue(name, value)
|
|
3540
|
+
if (gridTemplate) {
|
|
3541
|
+
return [
|
|
3542
|
+
...gridTemplate.setup,
|
|
3543
|
+
`__gea_stylesheet.registerStaticGridTemplateRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleGridTemplateProperty::${gridTemplate.target}, {${gridTemplate.tracks.map(staticGridTemplateTrackCode).join(', ')}}${mediaArg});`,
|
|
3544
|
+
]
|
|
3545
|
+
}
|
|
3546
|
+
const maskImage = cssStaticMaskImageValue(name, value)
|
|
3547
|
+
if (maskImage) {
|
|
3548
|
+
return [
|
|
3549
|
+
...maskImage.setup,
|
|
3550
|
+
`__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::MaskImage, ${staticLengthSpecCode(maskImage.spec)}${mediaArg});`,
|
|
3551
|
+
]
|
|
3552
|
+
}
|
|
3553
|
+
const boxShadowNone = cssStaticBoxShadowNoneValue(name, value)
|
|
3554
|
+
if (boxShadowNone) {
|
|
3555
|
+
return [
|
|
3556
|
+
`__gea_stylesheet.registerStaticBoxShadowNoneRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}${mediaArg});`,
|
|
3557
|
+
]
|
|
3558
|
+
}
|
|
3559
|
+
const animation = cssStaticAnimationValue(name, value)
|
|
3560
|
+
if (animation) {
|
|
3561
|
+
return [
|
|
3562
|
+
`__gea_stylesheet.registerStaticAnimationRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${JSON.stringify(animation.name)}, ${animation.durationMs}, ${animation.delayMs}, ${animation.iterations}, gea::embedded::ui::StaticStyleAnimationDirection::${animation.direction}, gea::embedded::ui::StaticStyleAnimationFill::${animation.fill}, ${staticAnimationEasingCode(animation.easing)}${mediaArg});`,
|
|
3563
|
+
]
|
|
3564
|
+
}
|
|
3565
|
+
const color = cssStaticColorValue(name, value)
|
|
3566
|
+
if (color) {
|
|
3567
|
+
return [
|
|
3568
|
+
`__gea_stylesheet.registerStaticColorRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleColorProperty::${color.target}, ${color.color.r}, ${color.color.g}, ${color.color.b}, ${color.color.a}${mediaArg});`,
|
|
3569
|
+
]
|
|
3570
|
+
}
|
|
3571
|
+
const length = cssStaticLengthValue(name, value)
|
|
3572
|
+
if (length) {
|
|
3573
|
+
return [
|
|
3574
|
+
`__gea_stylesheet.registerStaticLengthRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${length.target}, gea::embedded::ui::StaticStyleLengthUnit::${length.length.unit}, ${length.length.number}${mediaArg});`,
|
|
3575
|
+
]
|
|
3576
|
+
}
|
|
3577
|
+
const lengthExpression = cssStaticLengthExpressionValue(name, value)
|
|
3578
|
+
if (lengthExpression) {
|
|
3579
|
+
return [
|
|
3580
|
+
...lengthExpression.setup,
|
|
3581
|
+
`__gea_stylesheet.registerStaticLengthSpecRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleLengthProperty::${lengthExpression.target}, ${staticLengthSpecCode(lengthExpression.spec)}${mediaArg});`,
|
|
3582
|
+
]
|
|
3583
|
+
}
|
|
3584
|
+
const origin = cssStaticOriginValue(name, value)
|
|
3585
|
+
if (origin) {
|
|
3586
|
+
return [
|
|
3587
|
+
`__gea_stylesheet.registerStaticOriginRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, gea::embedded::ui::StaticStyleOriginProperty::${origin.target}, ${origin.x}, ${origin.y}${mediaArg});`,
|
|
3588
|
+
]
|
|
3589
|
+
}
|
|
3590
|
+
const transform = cssStaticTransformValue(name, value)
|
|
3591
|
+
if (transform) {
|
|
3592
|
+
return [
|
|
3593
|
+
...transform.setup,
|
|
3594
|
+
`__gea_stylesheet.registerStaticTransformRule(${staticSelectorKind(kind)}, ${JSON.stringify(selector)}, ${staticTransformArgs(transform)}${mediaArg});`,
|
|
3595
|
+
]
|
|
3596
|
+
}
|
|
3597
|
+
}
|
|
3598
|
+
return [
|
|
3599
|
+
`__gea_stylesheet.${cssRegisterCall(kind)}(${JSON.stringify(selector)}, ${JSON.stringify(name)}, ${JSON.stringify(value)}${mediaArg});`,
|
|
3600
|
+
]
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
function emitCssRules(cssText, mediaCondition, calls) {
|
|
3604
|
+
calls.push(...staticMediaConditionPlanRegistration(mediaCondition))
|
|
3605
|
+
const ruleRegex = /([^{}]+)\{([^{}]*)\}/g
|
|
3606
|
+
let ruleMatch
|
|
3607
|
+
while ((ruleMatch = ruleRegex.exec(cssText)) !== null) {
|
|
3608
|
+
const selectors = ruleMatch[1].split(',').map((selector) => selector.trim()).filter(Boolean)
|
|
3609
|
+
// Simple selectors keep their compact legacy registration path.
|
|
3610
|
+
// Everything richer (e.g. :root, .a.b, descendants, child selectors,
|
|
3611
|
+
// first/last-child, and ::before/::after) goes through the runtime
|
|
3612
|
+
// selector matcher.
|
|
3613
|
+
const classNames = []
|
|
3614
|
+
const activeClassNames = []
|
|
3615
|
+
const elementNames = []
|
|
3616
|
+
const selectorRules = []
|
|
3617
|
+
for (const selector of selectors) {
|
|
3618
|
+
const className = selector.match(/^\.([A-Za-z0-9_-]+)$/)?.[1]
|
|
3619
|
+
if (className) {
|
|
3620
|
+
classNames.push(className)
|
|
3621
|
+
continue
|
|
3622
|
+
}
|
|
3623
|
+
const activeClass = selector.match(/^\.([A-Za-z0-9_-]+):active$/)?.[1]
|
|
3624
|
+
if (activeClass) {
|
|
3625
|
+
activeClassNames.push(activeClass)
|
|
3626
|
+
continue
|
|
3627
|
+
}
|
|
3628
|
+
const elementName = selector.match(/^([A-Za-z][A-Za-z0-9-]*)$/)?.[1]
|
|
3629
|
+
if (elementName) {
|
|
3630
|
+
elementNames.push(elementName.toLowerCase())
|
|
3631
|
+
continue
|
|
3632
|
+
}
|
|
3633
|
+
selectorRules.push(selector)
|
|
3634
|
+
}
|
|
3635
|
+
if (classNames.length === 0 && activeClassNames.length === 0 && elementNames.length === 0 && selectorRules.length === 0) continue
|
|
3636
|
+
for (const selector of selectorRules) {
|
|
3637
|
+
calls.push(...staticSelectorPlanRegistration(selector))
|
|
3638
|
+
}
|
|
3639
|
+
const declarations = ruleMatch[2]
|
|
3640
|
+
.split(';')
|
|
3641
|
+
.map((decl) => decl.trim())
|
|
3642
|
+
.filter(Boolean)
|
|
3643
|
+
for (const declaration of declarations) {
|
|
3644
|
+
const colon = declaration.indexOf(':')
|
|
3645
|
+
if (colon <= 0) continue
|
|
3646
|
+
const name = declaration.slice(0, colon).trim()
|
|
3647
|
+
const value = declaration.slice(colon + 1).trim()
|
|
3648
|
+
if (!name || !value) continue
|
|
3649
|
+
for (const className of classNames) {
|
|
3650
|
+
calls.push(...emitCssRuleRegistrations('class', className, name, value, mediaCondition))
|
|
3651
|
+
}
|
|
3652
|
+
for (const className of activeClassNames) {
|
|
3653
|
+
// `background-color` in a :active rule becomes
|
|
3654
|
+
// `active-background-color`, which the framework's CSS parser
|
|
3655
|
+
// routes to Property::ActiveBackgroundColor (and auto-sets
|
|
3656
|
+
// HasActiveBackground). Other properties (color, opacity) get
|
|
3657
|
+
// the same `active-` prefix so they only swap in during a tap.
|
|
3658
|
+
const activeName = name.startsWith('active-') ? name : `active-${name}`
|
|
3659
|
+
calls.push(...emitCssRuleRegistrations('class', className, activeName, value, mediaCondition))
|
|
3660
|
+
}
|
|
3661
|
+
for (const elementName of elementNames) {
|
|
3662
|
+
calls.push(...emitCssRuleRegistrations('element', elementName, name, value, mediaCondition))
|
|
3663
|
+
}
|
|
3664
|
+
for (const selector of selectorRules) {
|
|
3665
|
+
calls.push(...emitCssRuleRegistrations('selector', selector, name, value, mediaCondition))
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
}
|
|
3670
|
+
|
|
3671
|
+
// Split CSS into its top-level rules and its @media blocks. The rule regex
|
|
3672
|
+
// can't handle the nested braces inside @media, so we brace-match each block
|
|
3673
|
+
// out first and hand its body to emitCssRules tagged with the raw condition.
|
|
3674
|
+
function extractMediaBlocks(css) {
|
|
3675
|
+
const media = []
|
|
3676
|
+
let base = ''
|
|
3677
|
+
let i = 0
|
|
3678
|
+
while (i < css.length) {
|
|
3679
|
+
const at = css.indexOf('@media', i)
|
|
3680
|
+
if (at === -1) {
|
|
3681
|
+
base += css.slice(i)
|
|
3682
|
+
break
|
|
3683
|
+
}
|
|
3684
|
+
base += css.slice(i, at)
|
|
3685
|
+
const braceOpen = css.indexOf('{', at)
|
|
3686
|
+
if (braceOpen === -1) {
|
|
3687
|
+
base += css.slice(at)
|
|
3688
|
+
break
|
|
3689
|
+
}
|
|
3690
|
+
const condition = css.slice(at + 6, braceOpen).replace(/\s+/g, ' ').trim()
|
|
3691
|
+
let depth = 1
|
|
3692
|
+
let j = braceOpen + 1
|
|
3693
|
+
for (; j < css.length && depth > 0; j++) {
|
|
3694
|
+
if (css[j] === '{') depth++
|
|
3695
|
+
else if (css[j] === '}') depth--
|
|
3696
|
+
}
|
|
3697
|
+
// j points just past the matched closing brace; body excludes it.
|
|
3698
|
+
media.push({ condition, body: css.slice(braceOpen + 1, depth === 0 ? j - 1 : j) })
|
|
3699
|
+
i = j
|
|
3700
|
+
}
|
|
3701
|
+
return { base, media }
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
function extractKeyframeBlocks(css) {
|
|
3705
|
+
const keyframes = []
|
|
3706
|
+
let base = ''
|
|
3707
|
+
let i = 0
|
|
3708
|
+
while (i < css.length) {
|
|
3709
|
+
const at = css.indexOf('@keyframes', i)
|
|
3710
|
+
if (at === -1) {
|
|
3711
|
+
base += css.slice(i)
|
|
3712
|
+
break
|
|
3713
|
+
}
|
|
3714
|
+
base += css.slice(i, at)
|
|
3715
|
+
const braceOpen = css.indexOf('{', at)
|
|
3716
|
+
if (braceOpen === -1) {
|
|
3717
|
+
base += css.slice(at)
|
|
3718
|
+
break
|
|
3719
|
+
}
|
|
3720
|
+
const name = css.slice(at + '@keyframes'.length, braceOpen).trim()
|
|
3721
|
+
let depth = 1
|
|
3722
|
+
let j = braceOpen + 1
|
|
3723
|
+
for (; j < css.length && depth > 0; j++) {
|
|
3724
|
+
if (css[j] === '{') depth++
|
|
3725
|
+
else if (css[j] === '}') depth--
|
|
3726
|
+
}
|
|
3727
|
+
keyframes.push({ name, body: css.slice(braceOpen + 1, depth === 0 ? j - 1 : j) })
|
|
3728
|
+
i = j
|
|
3729
|
+
}
|
|
3730
|
+
return { base, keyframes }
|
|
3731
|
+
}
|
|
3732
|
+
|
|
3733
|
+
function keyframeOffsetPermille(selector) {
|
|
3734
|
+
const trimmed = selector.trim()
|
|
3735
|
+
if (trimmed === 'from') return 0
|
|
3736
|
+
if (trimmed === 'to') return 1000
|
|
3737
|
+
const percent = trimmed.match(/^(-?\d+(?:\.\d+)?)%$/)
|
|
3738
|
+
if (!percent) return null
|
|
3739
|
+
const value = Math.round(Number(percent[1]) * 10)
|
|
3740
|
+
return Math.max(0, Math.min(1000, value))
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
function emitCssKeyframes(blocks, calls) {
|
|
3744
|
+
for (const block of blocks) {
|
|
3745
|
+
if (!block.name) continue
|
|
3746
|
+
const frameRegex = /([^{}]+)\{([^{}]*)\}/g
|
|
3747
|
+
let frameMatch
|
|
3748
|
+
while ((frameMatch = frameRegex.exec(block.body)) !== null) {
|
|
3749
|
+
const offsets = splitTopLevelCss(frameMatch[1], ',')
|
|
3750
|
+
.map(keyframeOffsetPermille)
|
|
3751
|
+
.filter(offset => offset !== null)
|
|
3752
|
+
if (offsets.length === 0) continue
|
|
3753
|
+
const declarations = frameMatch[2]
|
|
3754
|
+
.split(';')
|
|
3755
|
+
.map(decl => decl.trim())
|
|
3756
|
+
.filter(Boolean)
|
|
3757
|
+
for (const declaration of declarations) {
|
|
3758
|
+
const colon = declaration.indexOf(':')
|
|
3759
|
+
if (colon <= 0) continue
|
|
3760
|
+
const name = declaration.slice(0, colon).trim()
|
|
3761
|
+
const value = declaration.slice(colon + 1).trim()
|
|
3762
|
+
if (!name || !value) continue
|
|
3763
|
+
for (const offset of offsets) {
|
|
3764
|
+
const staticCalls = emitStaticCssKeyframeRegistrations(block.name, offset, name, value)
|
|
3765
|
+
if (staticCalls) {
|
|
3766
|
+
calls.push(...staticCalls)
|
|
3767
|
+
continue
|
|
3768
|
+
}
|
|
3769
|
+
calls.push(`__gea_stylesheet.${cssRegisterCall('keyframe')}(${JSON.stringify(block.name)}, ${offset}, ${JSON.stringify(name)}, ${JSON.stringify(value)});`)
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
}
|
|
3774
|
+
}
|
|
3775
|
+
|
|
3776
|
+
function splitTopLevelCppArgs(value) {
|
|
3777
|
+
const out = []
|
|
3778
|
+
let start = 0
|
|
3779
|
+
let paren = 0
|
|
3780
|
+
let brace = 0
|
|
3781
|
+
let inString = false
|
|
3782
|
+
let escaped = false
|
|
3783
|
+
for (let i = 0; i < value.length; i++) {
|
|
3784
|
+
const char = value[i]
|
|
3785
|
+
if (inString) {
|
|
3786
|
+
if (escaped) escaped = false
|
|
3787
|
+
else if (char === '\\') escaped = true
|
|
3788
|
+
else if (char === '"') inString = false
|
|
3789
|
+
continue
|
|
3790
|
+
}
|
|
3791
|
+
if (char === '"') {
|
|
3792
|
+
inString = true
|
|
3793
|
+
continue
|
|
3794
|
+
}
|
|
3795
|
+
if (char === '(') paren++
|
|
3796
|
+
else if (char === ')' && paren > 0) paren--
|
|
3797
|
+
else if (char === '{') brace++
|
|
3798
|
+
else if (char === '}' && brace > 0) brace--
|
|
3799
|
+
else if (char === ',' && paren === 0 && brace === 0) {
|
|
3800
|
+
out.push(value.slice(start, i).trim())
|
|
3801
|
+
start = i + 1
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
out.push(value.slice(start).trim())
|
|
3805
|
+
return out.filter(Boolean)
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
function staticCssTapeLengthSpecParts(code) {
|
|
3809
|
+
const trimmed = code.trim()
|
|
3810
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) return null
|
|
3811
|
+
const parts = splitTopLevelCppArgs(trimmed.slice(1, -1))
|
|
3812
|
+
if (parts.length !== 2) return null
|
|
3813
|
+
return { unit: parts[0], value: parts[1] }
|
|
3814
|
+
}
|
|
3815
|
+
|
|
3816
|
+
function staticCssTapeEntry(call, stringIndex) {
|
|
3817
|
+
if (!useStaticCssTape || !useStaticCssRules) return null
|
|
3818
|
+
const match = call.match(/^__gea_stylesheet\.(registerStatic[A-Za-z0-9_]+)\(([\s\S]*)\);$/)
|
|
3819
|
+
if (!match) return null
|
|
3820
|
+
const method = match[1]
|
|
3821
|
+
const args = splitTopLevelCppArgs(match[2])
|
|
3822
|
+
const media = (index) => args[index] ?? 'nullptr'
|
|
3823
|
+
const selectorKind = (value) => `static_cast<std::uint8_t>(${value})`
|
|
3824
|
+
const enumSlot = (value) => `static_cast<std::uint16_t>(${value})`
|
|
3825
|
+
const lengthUnit = (value) => `static_cast<std::uint8_t>(${value})`
|
|
3826
|
+
const entry = (kind, code) => ({ kind, code, call })
|
|
3827
|
+
|
|
3828
|
+
switch (method) {
|
|
3829
|
+
case 'registerStaticPropertyRule':
|
|
3830
|
+
if (args.length < 4 || args.length > 5) return null
|
|
3831
|
+
return entry('Property', `{${args[3]}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}}`)
|
|
3832
|
+
case 'registerStaticLengthRule':
|
|
3833
|
+
if (args.length < 5 || args.length > 6) return null
|
|
3834
|
+
return entry('Length', `{${args[4]}, ${stringIndex(args[1])}, ${stringIndex(media(5))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(args[3])}}`)
|
|
3835
|
+
case 'registerStaticLengthSpecRule':
|
|
3836
|
+
if (args.length < 4 || args.length > 5) return null
|
|
3837
|
+
{
|
|
3838
|
+
const length = staticCssTapeLengthSpecParts(args[3])
|
|
3839
|
+
if (!length) return null
|
|
3840
|
+
return entry('LengthSpec', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
|
|
3841
|
+
}
|
|
3842
|
+
case 'registerStaticColorRule':
|
|
3843
|
+
if (args.length < 7 || args.length > 8) return null
|
|
3844
|
+
return entry('Color', `{${stringIndex(args[1])}, ${stringIndex(media(7))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${args[3]}, ${args[4]}, ${args[5]}, ${args[6]}}`)
|
|
3845
|
+
case 'registerStaticColorVarRule':
|
|
3846
|
+
if (args.length < 9 || args.length > 10) return null
|
|
3847
|
+
return entry('ColorVar', `{${stringIndex(args[1])}, ${stringIndex(media(9))}, ${stringIndex(args[3])}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${args[4]}, ${args[5]}, ${args[6]}, ${args[7]}, ${args[8]}}`)
|
|
3848
|
+
case 'registerStaticCustomLengthRule':
|
|
3849
|
+
if (args.length < 4 || args.length > 5) return null
|
|
3850
|
+
{
|
|
3851
|
+
const length = staticCssTapeLengthSpecParts(args[3])
|
|
3852
|
+
if (!length) return null
|
|
3853
|
+
return entry('CustomLength', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
|
|
3854
|
+
}
|
|
3855
|
+
case 'registerStaticCustomColorRule':
|
|
3856
|
+
if (args.length < 7 || args.length > 8) return null
|
|
3857
|
+
return entry('CustomColor', `{${stringIndex(args[1])}, ${stringIndex(media(7))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}, ${args[3]}, ${args[4]}, ${args[5]}, ${args[6]}}`)
|
|
3858
|
+
case 'registerStaticBoxShadowNoneRule':
|
|
3859
|
+
if (args.length < 2 || args.length > 3) return null
|
|
3860
|
+
return entry('BoxShadowNone', `{${stringIndex(args[1])}, ${stringIndex(media(2))}, ${selectorKind(args[0])}}`)
|
|
3861
|
+
case 'registerStaticFontFamilyRule':
|
|
3862
|
+
if (args.length < 3 || args.length > 4) return null
|
|
3863
|
+
return entry('FontFamily', `{${stringIndex(args[1])}, ${stringIndex(media(3))}, ${stringIndex(args[2])}, ${selectorKind(args[0])}}`)
|
|
3864
|
+
case 'registerStaticFilterBlurRule':
|
|
3865
|
+
if (args.length < 3 || args.length > 4) return null
|
|
3866
|
+
{
|
|
3867
|
+
const length = staticCssTapeLengthSpecParts(args[2])
|
|
3868
|
+
if (!length) return null
|
|
3869
|
+
return entry('FilterBlur', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(3))}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
|
|
3870
|
+
}
|
|
3871
|
+
case 'registerStaticLineHeightRule':
|
|
3872
|
+
if (args.length < 4 || args.length > 5) return null
|
|
3873
|
+
{
|
|
3874
|
+
const length = staticCssTapeLengthSpecParts(args[3])
|
|
3875
|
+
if (!length) return null
|
|
3876
|
+
return entry('LineHeight', `{${length.value}, ${stringIndex(args[1])}, ${stringIndex(media(4))}, ${enumSlot(args[2])}, ${selectorKind(args[0])}, ${lengthUnit(length.unit)}}`)
|
|
3877
|
+
}
|
|
3878
|
+
default:
|
|
3879
|
+
return null
|
|
3880
|
+
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
function staticCssTapePrelude(stringLiterals) {
|
|
3884
|
+
const strings = stringLiterals.map((value) => `\t${value}`).join(',\n')
|
|
3885
|
+
return `
|
|
3886
|
+
const char * const __gea_static_css_tape_strings[] = {
|
|
3887
|
+
${strings}
|
|
3888
|
+
};
|
|
3889
|
+
enum class __GeaStaticCssTapeKind : std::uint8_t {
|
|
3890
|
+
\tProperty,
|
|
3891
|
+
\tLength,
|
|
3892
|
+
\tLengthSpec,
|
|
3893
|
+
\tColor,
|
|
3894
|
+
\tColorVar,
|
|
3895
|
+
\tCustomLength,
|
|
3896
|
+
\tCustomColor,
|
|
3897
|
+
\tBoxShadowNone,
|
|
3898
|
+
\tFontFamily,
|
|
3899
|
+
\tFilterBlur,
|
|
3900
|
+
\tLineHeight
|
|
3901
|
+
};
|
|
3902
|
+
struct __GeaStaticCssPropertyOp {
|
|
3903
|
+
\tint value;
|
|
3904
|
+
\tstd::uint16_t selector;
|
|
3905
|
+
\tstd::uint16_t media;
|
|
3906
|
+
\tstd::uint16_t property;
|
|
3907
|
+
\tstd::uint8_t selectorKind;
|
|
3908
|
+
};
|
|
3909
|
+
struct __GeaStaticCssLengthOp {
|
|
3910
|
+
\tfloat value;
|
|
3911
|
+
\tstd::uint16_t selector;
|
|
3912
|
+
\tstd::uint16_t media;
|
|
3913
|
+
\tstd::uint16_t property;
|
|
3914
|
+
\tstd::uint8_t selectorKind;
|
|
3915
|
+
\tstd::uint8_t unit;
|
|
3916
|
+
};
|
|
3917
|
+
struct __GeaStaticCssColorOp {
|
|
3918
|
+
\tstd::uint16_t selector;
|
|
3919
|
+
\tstd::uint16_t media;
|
|
3920
|
+
\tstd::uint16_t property;
|
|
3921
|
+
\tstd::uint8_t selectorKind;
|
|
3922
|
+
\tstd::uint8_t r;
|
|
3923
|
+
\tstd::uint8_t g;
|
|
3924
|
+
\tstd::uint8_t b;
|
|
3925
|
+
\tstd::uint8_t a;
|
|
3926
|
+
};
|
|
3927
|
+
struct __GeaStaticCssColorVarOp {
|
|
3928
|
+
\tstd::uint16_t selector;
|
|
3929
|
+
\tstd::uint16_t media;
|
|
3930
|
+
\tstd::uint16_t name;
|
|
3931
|
+
\tstd::uint16_t property;
|
|
3932
|
+
\tstd::uint8_t selectorKind;
|
|
3933
|
+
\tbool hasFallback;
|
|
3934
|
+
\tstd::uint8_t r;
|
|
3935
|
+
\tstd::uint8_t g;
|
|
3936
|
+
\tstd::uint8_t b;
|
|
3937
|
+
\tstd::uint8_t a;
|
|
3938
|
+
};
|
|
3939
|
+
struct __GeaStaticCssCustomLengthOp {
|
|
3940
|
+
\tfloat value;
|
|
3941
|
+
\tstd::uint16_t selector;
|
|
3942
|
+
\tstd::uint16_t media;
|
|
3943
|
+
\tstd::uint16_t name;
|
|
3944
|
+
\tstd::uint8_t selectorKind;
|
|
3945
|
+
\tstd::uint8_t unit;
|
|
3946
|
+
};
|
|
3947
|
+
struct __GeaStaticCssCustomColorOp {
|
|
3948
|
+
\tstd::uint16_t selector;
|
|
3949
|
+
\tstd::uint16_t media;
|
|
3950
|
+
\tstd::uint16_t name;
|
|
3951
|
+
\tstd::uint8_t selectorKind;
|
|
3952
|
+
\tstd::uint8_t r;
|
|
3953
|
+
\tstd::uint8_t g;
|
|
3954
|
+
\tstd::uint8_t b;
|
|
3955
|
+
\tstd::uint8_t a;
|
|
3956
|
+
};
|
|
3957
|
+
struct __GeaStaticCssSelectorOnlyOp {
|
|
3958
|
+
\tstd::uint16_t selector;
|
|
3959
|
+
\tstd::uint16_t media;
|
|
3960
|
+
\tstd::uint8_t selectorKind;
|
|
3961
|
+
};
|
|
3962
|
+
struct __GeaStaticCssNamedOp {
|
|
3963
|
+
\tstd::uint16_t selector;
|
|
3964
|
+
\tstd::uint16_t media;
|
|
3965
|
+
\tstd::uint16_t name;
|
|
3966
|
+
\tstd::uint8_t selectorKind;
|
|
3967
|
+
};
|
|
3968
|
+
struct __GeaStaticCssLengthOnlyOp {
|
|
3969
|
+
\tfloat value;
|
|
3970
|
+
\tstd::uint16_t selector;
|
|
3971
|
+
\tstd::uint16_t media;
|
|
3972
|
+
\tstd::uint8_t selectorKind;
|
|
3973
|
+
\tstd::uint8_t unit;
|
|
3974
|
+
};
|
|
3975
|
+
struct __GeaStaticCssLineHeightOp {
|
|
3976
|
+
\tfloat value;
|
|
3977
|
+
\tstd::uint16_t selector;
|
|
3978
|
+
\tstd::uint16_t media;
|
|
3979
|
+
\tstd::uint16_t kind;
|
|
3980
|
+
\tstd::uint8_t selectorKind;
|
|
3981
|
+
\tstd::uint8_t unit;
|
|
3982
|
+
};
|
|
3983
|
+
auto __gea_css_tape_string = [&](std::uint16_t index) -> const char * {
|
|
3984
|
+
\treturn __gea_static_css_tape_strings[index];
|
|
3985
|
+
};
|
|
3986
|
+
auto __gea_css_tape_selector_kind = [](std::uint8_t kind) {
|
|
3987
|
+
\treturn static_cast<gea::embedded::ui::StaticStyleSelectorKind>(kind);
|
|
3988
|
+
};
|
|
3989
|
+
auto __gea_css_tape_length = [](std::uint8_t unit, float value) {
|
|
3990
|
+
\treturn gea::embedded::ui::StaticStyleLengthSpec{static_cast<gea::embedded::ui::StaticStyleLengthUnit>(unit), value};
|
|
3991
|
+
};
|
|
3992
|
+
auto __gea_register_static_css_tape = [&](
|
|
3993
|
+
\tstd::initializer_list<__GeaStaticCssTapeKind> order,
|
|
3994
|
+
\tstd::initializer_list<__GeaStaticCssPropertyOp> propertyOps,
|
|
3995
|
+
\tstd::initializer_list<__GeaStaticCssLengthOp> lengthOps,
|
|
3996
|
+
\tstd::initializer_list<__GeaStaticCssLengthOp> lengthSpecOps,
|
|
3997
|
+
\tstd::initializer_list<__GeaStaticCssColorOp> colorOps,
|
|
3998
|
+
\tstd::initializer_list<__GeaStaticCssColorVarOp> colorVarOps,
|
|
3999
|
+
\tstd::initializer_list<__GeaStaticCssCustomLengthOp> customLengthOps,
|
|
4000
|
+
\tstd::initializer_list<__GeaStaticCssCustomColorOp> customColorOps,
|
|
4001
|
+
\tstd::initializer_list<__GeaStaticCssSelectorOnlyOp> boxShadowNoneOps,
|
|
4002
|
+
\tstd::initializer_list<__GeaStaticCssNamedOp> fontFamilyOps,
|
|
4003
|
+
\tstd::initializer_list<__GeaStaticCssLengthOnlyOp> filterBlurOps,
|
|
4004
|
+
\tstd::initializer_list<__GeaStaticCssLineHeightOp> lineHeightOps) {
|
|
4005
|
+
\tauto propertyIt = propertyOps.begin();
|
|
4006
|
+
\tauto lengthIt = lengthOps.begin();
|
|
4007
|
+
\tauto lengthSpecIt = lengthSpecOps.begin();
|
|
4008
|
+
\tauto colorIt = colorOps.begin();
|
|
4009
|
+
\tauto colorVarIt = colorVarOps.begin();
|
|
4010
|
+
\tauto customLengthIt = customLengthOps.begin();
|
|
4011
|
+
\tauto customColorIt = customColorOps.begin();
|
|
4012
|
+
\tauto boxShadowNoneIt = boxShadowNoneOps.begin();
|
|
4013
|
+
\tauto fontFamilyIt = fontFamilyOps.begin();
|
|
4014
|
+
\tauto filterBlurIt = filterBlurOps.begin();
|
|
4015
|
+
\tauto lineHeightIt = lineHeightOps.begin();
|
|
4016
|
+
\tfor (const auto kind : order) {
|
|
4017
|
+
\t\tswitch (kind) {
|
|
4018
|
+
\t\tcase __GeaStaticCssTapeKind::Property: {
|
|
4019
|
+
\t\t\tconst auto &op = *propertyIt++;
|
|
4020
|
+
\t\t\t__gea_stylesheet.registerStaticPropertyRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::Property>(op.property), op.value, __gea_css_tape_string(op.media));
|
|
4021
|
+
\t\t\tbreak;
|
|
4022
|
+
\t\t}
|
|
4023
|
+
\t\tcase __GeaStaticCssTapeKind::Length: {
|
|
4024
|
+
\t\t\tconst auto &op = *lengthIt++;
|
|
4025
|
+
\t\t\t__gea_stylesheet.registerStaticLengthRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLengthProperty>(op.property), static_cast<gea::embedded::ui::StaticStyleLengthUnit>(op.unit), op.value, __gea_css_tape_string(op.media));
|
|
4026
|
+
\t\t\tbreak;
|
|
4027
|
+
\t\t}
|
|
4028
|
+
\t\tcase __GeaStaticCssTapeKind::LengthSpec: {
|
|
4029
|
+
\t\t\tconst auto &op = *lengthSpecIt++;
|
|
4030
|
+
\t\t\t__gea_stylesheet.registerStaticLengthSpecRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLengthProperty>(op.property), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
|
|
4031
|
+
\t\t\tbreak;
|
|
4032
|
+
\t\t}
|
|
4033
|
+
\t\tcase __GeaStaticCssTapeKind::Color: {
|
|
4034
|
+
\t\t\tconst auto &op = *colorIt++;
|
|
4035
|
+
\t\t\t__gea_stylesheet.registerStaticColorRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleColorProperty>(op.property), op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
|
|
4036
|
+
\t\t\tbreak;
|
|
4037
|
+
\t\t}
|
|
4038
|
+
\t\tcase __GeaStaticCssTapeKind::ColorVar: {
|
|
4039
|
+
\t\t\tconst auto &op = *colorVarIt++;
|
|
4040
|
+
\t\t\t__gea_stylesheet.registerStaticColorVarRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleColorProperty>(op.property), __gea_css_tape_string(op.name), op.hasFallback, op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
|
|
4041
|
+
\t\t\tbreak;
|
|
4042
|
+
\t\t}
|
|
4043
|
+
\t\tcase __GeaStaticCssTapeKind::CustomLength: {
|
|
4044
|
+
\t\t\tconst auto &op = *customLengthIt++;
|
|
4045
|
+
\t\t\t__gea_stylesheet.registerStaticCustomLengthRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
|
|
4046
|
+
\t\t\tbreak;
|
|
4047
|
+
\t\t}
|
|
4048
|
+
\t\tcase __GeaStaticCssTapeKind::CustomColor: {
|
|
4049
|
+
\t\t\tconst auto &op = *customColorIt++;
|
|
4050
|
+
\t\t\t__gea_stylesheet.registerStaticCustomColorRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), op.r, op.g, op.b, op.a, __gea_css_tape_string(op.media));
|
|
4051
|
+
\t\t\tbreak;
|
|
4052
|
+
\t\t}
|
|
4053
|
+
\t\tcase __GeaStaticCssTapeKind::BoxShadowNone: {
|
|
4054
|
+
\t\t\tconst auto &op = *boxShadowNoneIt++;
|
|
4055
|
+
\t\t\t__gea_stylesheet.registerStaticBoxShadowNoneRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.media));
|
|
4056
|
+
\t\t\tbreak;
|
|
4057
|
+
\t\t}
|
|
4058
|
+
\t\tcase __GeaStaticCssTapeKind::FontFamily: {
|
|
4059
|
+
\t\t\tconst auto &op = *fontFamilyIt++;
|
|
4060
|
+
\t\t\t__gea_stylesheet.registerStaticFontFamilyRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_string(op.name), __gea_css_tape_string(op.media));
|
|
4061
|
+
\t\t\tbreak;
|
|
4062
|
+
\t\t}
|
|
4063
|
+
\t\tcase __GeaStaticCssTapeKind::FilterBlur: {
|
|
4064
|
+
\t\t\tconst auto &op = *filterBlurIt++;
|
|
4065
|
+
\t\t\t__gea_stylesheet.registerStaticFilterBlurRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
|
|
4066
|
+
\t\t\tbreak;
|
|
4067
|
+
\t\t}
|
|
4068
|
+
\t\tcase __GeaStaticCssTapeKind::LineHeight: {
|
|
4069
|
+
\t\t\tconst auto &op = *lineHeightIt++;
|
|
4070
|
+
\t\t\t__gea_stylesheet.registerStaticLineHeightRule(__gea_css_tape_selector_kind(op.selectorKind), __gea_css_tape_string(op.selector), static_cast<gea::embedded::ui::StaticStyleLineHeightKind>(op.kind), __gea_css_tape_length(op.unit, op.value), __gea_css_tape_string(op.media));
|
|
4071
|
+
\t\t\tbreak;
|
|
4072
|
+
\t\t}
|
|
4073
|
+
\t\t}
|
|
4074
|
+
\t}
|
|
4075
|
+
};
|
|
4076
|
+
`
|
|
4077
|
+
}
|
|
4078
|
+
|
|
4079
|
+
function compactStaticCssTapeCalls(calls) {
|
|
4080
|
+
if (!useStaticCssTape || !useStaticCssRules) return { calls, usedTape: false }
|
|
4081
|
+
const kinds = [
|
|
4082
|
+
'Property',
|
|
4083
|
+
'Length',
|
|
4084
|
+
'LengthSpec',
|
|
4085
|
+
'Color',
|
|
4086
|
+
'ColorVar',
|
|
4087
|
+
'CustomLength',
|
|
4088
|
+
'CustomColor',
|
|
4089
|
+
'BoxShadowNone',
|
|
4090
|
+
'FontFamily',
|
|
4091
|
+
'FilterBlur',
|
|
4092
|
+
'LineHeight',
|
|
4093
|
+
]
|
|
4094
|
+
const out = []
|
|
4095
|
+
const stringLiterals = ['nullptr']
|
|
4096
|
+
const stringIds = new Map([['nullptr', 0]])
|
|
4097
|
+
const stringIndex = (value) => {
|
|
4098
|
+
const literal = value === 'nullptr' ? 'nullptr' : value
|
|
4099
|
+
const cached = stringIds.get(literal)
|
|
4100
|
+
if (cached !== undefined) return cached
|
|
4101
|
+
const id = stringLiterals.length
|
|
4102
|
+
stringIds.set(literal, id)
|
|
4103
|
+
stringLiterals.push(literal)
|
|
4104
|
+
return id
|
|
4105
|
+
}
|
|
4106
|
+
let chunk = []
|
|
4107
|
+
let usedTape = false
|
|
4108
|
+
const flush = () => {
|
|
4109
|
+
if (chunk.length === 0) return
|
|
4110
|
+
if (chunk.length < staticCssTapeMinChunk) {
|
|
4111
|
+
out.push(...chunk.map((entry) => entry.call))
|
|
4112
|
+
chunk = []
|
|
4113
|
+
return
|
|
4114
|
+
}
|
|
4115
|
+
usedTape = true
|
|
4116
|
+
const order = chunk.map((entry) => `__GeaStaticCssTapeKind::${entry.kind}`).join(', ')
|
|
4117
|
+
const listFor = (kind) => {
|
|
4118
|
+
const entries = chunk.filter((entry) => entry.kind === kind).map((entry) => entry.code)
|
|
4119
|
+
if (entries.length === 0) return '{}'
|
|
4120
|
+
return `{\n${entries.map((entry) => `\t\t${entry}`).join(',\n')}\n\t}`
|
|
4121
|
+
}
|
|
4122
|
+
out.push(`__gea_register_static_css_tape(\n\t{${order}},\n\t${kinds.map(listFor).join(',\n\t')});`)
|
|
4123
|
+
chunk = []
|
|
4124
|
+
}
|
|
4125
|
+
for (const call of calls) {
|
|
4126
|
+
const entry = staticCssTapeEntry(call, stringIndex)
|
|
4127
|
+
if (entry) {
|
|
4128
|
+
chunk.push(entry)
|
|
4129
|
+
continue
|
|
4130
|
+
}
|
|
4131
|
+
flush()
|
|
4132
|
+
out.push(call)
|
|
4133
|
+
}
|
|
4134
|
+
flush()
|
|
4135
|
+
return { calls: out, usedTape, stringLiterals }
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4138
|
+
function cssRegistrationCode(viteOutDir) {
|
|
4139
|
+
const calls = []
|
|
4140
|
+
staticCssLengthExpressionCounter = 0
|
|
4141
|
+
staticCssLengthExpressionCache = new Map()
|
|
4142
|
+
staticCssSelectorPlanCache = new Set()
|
|
4143
|
+
staticCssMediaConditionPlanCache = new Set()
|
|
4144
|
+
for (const file of findCssFiles(viteOutDir)) {
|
|
4145
|
+
const css = stripCssComments(fs.readFileSync(file, 'utf8'))
|
|
4146
|
+
const withoutKeyframes = extractKeyframeBlocks(css)
|
|
4147
|
+
emitCssKeyframes(withoutKeyframes.keyframes, calls)
|
|
4148
|
+
const { base, media } = extractMediaBlocks(withoutKeyframes.base)
|
|
4149
|
+
// Base rules first, then @media rules: StyleSheet applies in registration
|
|
4150
|
+
// order (last writer wins), so a matching breakpoint overrides the base.
|
|
4151
|
+
emitCssRules(stripAtBlocks(base), '', calls)
|
|
4152
|
+
for (const block of media) {
|
|
4153
|
+
emitCssRules(stripAtBlocks(block.body), block.condition, calls)
|
|
4154
|
+
}
|
|
4155
|
+
}
|
|
4156
|
+
if (calls.length === 0) return ''
|
|
4157
|
+
const compacted = compactStaticCssTapeCalls(calls)
|
|
4158
|
+
const tapePrelude = compacted.usedTape ? staticCssTapePrelude(compacted.stringLiterals) : ''
|
|
4159
|
+
return `{\nauto &__gea_stylesheet = gea::embedded::ui::StyleSheet::instance();\n${tapePrelude}__gea_stylesheet.beginRuleRegistrationBatch();\n${compacted.calls.join('\n')}\n__gea_stylesheet.endRuleRegistrationBatch();\n}\n`
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
const appDir = path.resolve(readOption('--app-dir') ?? fail('missing --app-dir <dir>'))
|
|
4163
|
+
const outDir = path.resolve(readOption('--out-dir') ?? fail('missing --out-dir <dir>'))
|
|
4164
|
+
acquireOutputPipelineLock(outDir)
|
|
4165
|
+
// Which geatsc to compile with. `--geatsc-bin` wins; `GEA_GEATSC_BIN` is the
|
|
4166
|
+
// same choice expressed as an environment variable, for callers that cannot
|
|
4167
|
+
// add a flag -- a test harness that shells out to several runners, say. It is
|
|
4168
|
+
// the variable `targets/web/build-web.sh` already forwards as `--geatsc-bin`,
|
|
4169
|
+
// so this only lets it reach here directly. Unset, everything resolves exactly
|
|
4170
|
+
// as before: the installed `@geastack/compiler`'s own `dist/cli.js`.
|
|
4171
|
+
//
|
|
4172
|
+
// The point is being able to FREEZE the compiler for the length of a
|
|
4173
|
+
// measurement run. `dist/` is shared and can be rebuilt by anyone at any
|
|
4174
|
+
// moment; a run whose rows were compiled by different compilers is not one
|
|
4175
|
+
// compass, and mtimes alone cannot tell a real change from a no-op rebuild.
|
|
4176
|
+
const geatscBin = path.resolve(readOption('--geatsc-bin') ?? process.env.GEA_GEATSC_BIN ?? path.join(path.dirname(requireFromLib.resolve('@geastack/compiler/package.json')), 'dist/cli.js'))
|
|
4177
|
+
const viteBin = path.resolve(readOption('--vite-bin') ?? path.join(path.dirname(requireFromLib.resolve('vite/package.json')), JSON.parse(fs.readFileSync(requireFromLib.resolve('vite/package.json'),'utf8')).bin.vite))
|
|
4178
|
+
const viteOutDir = path.resolve(readOption('--vite-out-dir') ?? path.join(outDir, 'dist'))
|
|
4179
|
+
const bundlePath = path.resolve(readOption('--bundle-out') ?? path.join(outDir, 'gea-vite-bundle.js'))
|
|
4180
|
+
const geaIrPath = path.resolve(readOption('--gea-ir-out') ?? path.join(viteOutDir, 'gea-ir.json'))
|
|
4181
|
+
const moduleGraphOutOption = readOption('--module-graph-out') ?? process.env.GEA_VITE_MODULE_GRAPH_OUT
|
|
4182
|
+
const moduleGraphOnly = hasFlag('--module-graph-only')
|
|
4183
|
+
const moduleGraphCompileRequested = hasFlag('--compile-module-graph')
|
|
4184
|
+
const moduleGraphCompileDisabled = hasFlag('--no-compile-module-graph')
|
|
4185
|
+
const geaPluginSpecifier = path.resolve(readOption('--geatsc-gea-plugin') ?? requireFromLib.resolve('@geastack/geatsc-plugin-gea'))
|
|
4186
|
+
const extraGeatscPlugins = readAllOptions('--extra-geatsc-plugin').map((plugin) => path.resolve(plugin))
|
|
4187
|
+
const appendJsFiles = readAllOptions('--append-js').map((file) => path.resolve(file))
|
|
4188
|
+
const entry = readOption('--entry') ?? 'index.tsx'
|
|
4189
|
+
const entrySymbol = readOption('--entry-symbol')
|
|
4190
|
+
const cppBoard = readOption('--cpp-board')
|
|
4191
|
+
const cxxStandard = readOption('--cxx-standard')
|
|
4192
|
+
const pixelPanelEndian = readOption('--pixel-panel-endian')
|
|
4193
|
+
const cppPreludeSymbol = readOption('--cpp-prelude-symbol')
|
|
4194
|
+
const fontSymbolPrefix = readOption('--font-symbol-prefix')
|
|
4195
|
+
const fontViewportWidths = readAllOptions('--font-viewport-width')
|
|
4196
|
+
const fontViewportHeights = readAllOptions('--font-viewport-height')
|
|
4197
|
+
const fontDevicePixelRatios = readAllOptions('--font-device-pixel-ratio')
|
|
4198
|
+
const runtimeTtfFonts = hasFlag('--runtime-ttf-fonts')
|
|
4199
|
+
|
|
4200
|
+
// Physical panel resolution per board, mirroring each target's
|
|
4201
|
+
// GEA_EMBEDDED_DISPLAY_WIDTH/HEIGHT (the lib/gea-embedded/include/display.h
|
|
4202
|
+
// default is 410x502; boards override via target_compile_definitions). The
|
|
4203
|
+
// font generator pre-rasterizes viewport-relative font sizes (vw/vh/vmin/vmax)
|
|
4204
|
+
// and needs the real viewport to bake glyphs at the exact pixel size the
|
|
4205
|
+
// runtime's parseLengthForNode will request. Resolved from --cpp-board so the
|
|
4206
|
+
// board CMakeLists need not thread dimensions through; an explicit
|
|
4207
|
+
// --font-viewport-{width,height} still wins for non-board callers.
|
|
4208
|
+
const BOARD_VIEWPORTS = {
|
|
4209
|
+
'esp32-s3-epaper-1.54': { width: 200, height: 200 },
|
|
4210
|
+
'esp32-m5stack-m5paper': { width: 540, height: 960 },
|
|
4211
|
+
'esp32-s3-lilygo-t5-epaper-4.7': { width: 540, height: 960 },
|
|
4212
|
+
'esp32-s3-m5stack-papers3': { width: 540, height: 960 },
|
|
4213
|
+
'esp32-s3-m5stack-sticks3': { width: 135, height: 240 },
|
|
4214
|
+
'esp32-s3-elecrow-rotary-2.1': { width: 480, height: 480 },
|
|
4215
|
+
'esp32-s3-touch-amoled-2.06': { width: 410, height: 502 },
|
|
4216
|
+
'esp32-s3-touch-amoled-1.8': { width: 368, height: 448 },
|
|
4217
|
+
'esp32-s3-lilygo-t-display-s3-long': { width: 180, height: 640 },
|
|
4218
|
+
'esp32-p4-waveshare-touch-lcd-7': { width: 720, height: 1280 },
|
|
4219
|
+
'esp32-p4-waveshare-touch-lcd-3.5': { width: 320, height: 480 },
|
|
4220
|
+
'waveshare-rp2350-amoled-2.41': { width: 450, height: 600 },
|
|
4221
|
+
}
|
|
4222
|
+
const boardViewport = cppBoard ? BOARD_VIEWPORTS[cppBoard] : undefined
|
|
4223
|
+
const effectiveFontViewportWidths = fontViewportWidths.length > 0
|
|
4224
|
+
? fontViewportWidths
|
|
4225
|
+
: boardViewport
|
|
4226
|
+
? [String(boardViewport.width)]
|
|
4227
|
+
: []
|
|
4228
|
+
const effectiveFontViewportHeights = fontViewportHeights.length > 0
|
|
4229
|
+
? fontViewportHeights
|
|
4230
|
+
: boardViewport
|
|
4231
|
+
? [String(boardViewport.height)]
|
|
4232
|
+
: []
|
|
4233
|
+
// Glyph antialiasing per board. 1-bit B/W e-paper reads best with crisp,
|
|
4234
|
+
// non-antialiased glyphs — sample at 1:1 (supersample 1) so every glyph pixel
|
|
4235
|
+
// is fully on/off instead of a thresholded AA edge. Grayscale/color panels keep
|
|
4236
|
+
// the default 4x supersampled AA coverage. An explicit --font-supersample wins.
|
|
4237
|
+
const BOARD_FONT_SUPERSAMPLE = {
|
|
4238
|
+
'esp32-s3-epaper-1.54': 1,
|
|
4239
|
+
}
|
|
4240
|
+
const fontSupersampleOverride = readOption('--font-supersample')
|
|
4241
|
+
const effectiveFontSupersample = fontSupersampleOverride
|
|
4242
|
+
?? (cppBoard && BOARD_FONT_SUPERSAMPLE[cppBoard] !== undefined ? String(BOARD_FONT_SUPERSAMPLE[cppBoard]) : undefined)
|
|
4243
|
+
// Baked glyph atlas depth per board. 8 bits of coverage per pixel is the
|
|
4244
|
+
// default. A panel that can only show four gray levels gains nothing from the
|
|
4245
|
+
// other six bits, and reading the atlas out of XIP flash costs four times the
|
|
4246
|
+
// bandwidth — bake those boards at 2 bits/px instead, quantized offline. An
|
|
4247
|
+
// explicit --font-atlas-bits wins.
|
|
4248
|
+
const BOARD_FONT_ATLAS_BITS = {
|
|
4249
|
+
'esp32-c3-xteink-x3': 2,
|
|
4250
|
+
// PaperS3 has a 16-level framebuffer, but glyph antialiasing only needs the
|
|
4251
|
+
// four offline-quantized coverage levels. This makes the existing nonlinear
|
|
4252
|
+
// edge treatment (48/128/208 thresholds plus stem/speckle fixups) part of the
|
|
4253
|
+
// baked atlas and avoids carrying 8-bit coverage through XIP at runtime.
|
|
4254
|
+
'esp32-s3-m5stack-papers3': 2,
|
|
4255
|
+
}
|
|
4256
|
+
const fontAtlasBitsOverride = readOption('--font-atlas-bits')
|
|
4257
|
+
const effectiveFontAtlasBits = fontAtlasBitsOverride
|
|
4258
|
+
?? (cppBoard && BOARD_FONT_ATLAS_BITS[cppBoard] !== undefined ? String(BOARD_FONT_ATLAS_BITS[cppBoard]) : undefined)
|
|
4259
|
+
const geaEmbeddedCompat = hasFlag('--gea-embedded-compat') || usesGeaEmbeddedCompat(appDir)
|
|
4260
|
+
const appleNative = !hasFlag('--no-apple-native') && (hasFlag('--apple-native') || usesAppleNative(appDir))
|
|
4261
|
+
const moduleGraphCompile = !moduleGraphCompileDisabled && (moduleGraphCompileRequested || geaEmbeddedCompat || appleNative)
|
|
4262
|
+
const moduleGraphEnabled = moduleGraphOnly || moduleGraphCompile || !!moduleGraphOutOption
|
|
4263
|
+
const moduleGraphOutDir = moduleGraphOutOption
|
|
4264
|
+
? path.resolve(moduleGraphOutOption)
|
|
4265
|
+
: moduleGraphEnabled
|
|
4266
|
+
? path.join(outDir, 'module-graph')
|
|
4267
|
+
: ''
|
|
4268
|
+
let viteConfig = geaEmbeddedCompat
|
|
4269
|
+
? path.join(outDir, 'gea-embedded-compat.vite.config.mjs')
|
|
4270
|
+
: path.resolve(readOption('--vite-config') ?? path.join(appDir, 'vite.config.ts'))
|
|
4271
|
+
const appleNativePluginSpecifier = path.resolve(readOption('--geatsc-apple-native-plugin') ?? tryResolve('@geastack/geatsc-plugin-apple-native'))
|
|
4272
|
+
|
|
4273
|
+
if (!geaEmbeddedCompat && !fs.existsSync(viteConfig)) fail(`missing Vite config: ${viteConfig}`)
|
|
4274
|
+
if (!fs.existsSync(viteBin)) fail(`missing Vite CLI: ${viteBin}`)
|
|
4275
|
+
if (!fs.existsSync(geatscBin)) fail(`missing geatsc CLI: ${geatscBin}`)
|
|
4276
|
+
if (appleNative && !fs.existsSync(appleNativePluginSpecifier)) fail(`missing Apple native geatsc plugin: ${appleNativePluginSpecifier}`)
|
|
4277
|
+
for (const plugin of extraGeatscPlugins) {
|
|
4278
|
+
if (!fs.existsSync(plugin)) fail(`missing extra geatsc plugin: ${plugin}`)
|
|
4279
|
+
}
|
|
4280
|
+
|
|
4281
|
+
fs.mkdirSync(outDir, { recursive: true })
|
|
4282
|
+
writeDisplayConfigHeader(outDir, collectDisplayConfig(appDir, entry))
|
|
4283
|
+
|
|
4284
|
+
// Where the entry actually lives once compat staging has copied the sources.
|
|
4285
|
+
// The module graph is rooted at the staged tree, so geatsc must be pointed at
|
|
4286
|
+
// the staged entry too: an --entry naming the original app dir matches no
|
|
4287
|
+
// module in the graph, which silently empties the reachability root set.
|
|
4288
|
+
let stagedEntryFile = null
|
|
4289
|
+
|
|
4290
|
+
// Where the font generator looks for @font-face. It scans a DIRECTORY, so the
|
|
4291
|
+
// app dir only works while every stylesheet lives under it. An app whose entry
|
|
4292
|
+
// imports components from a sibling tree declares its face there, and the app
|
|
4293
|
+
// dir holds no CSS at all -- the atlas then comes out empty and every string
|
|
4294
|
+
// falls back to the built-in bitmap font. Compat staging has already gathered
|
|
4295
|
+
// the real source set (the app dir plus each escaped import dir, mirrored at
|
|
4296
|
+
// their original depth so relative url()s still resolve), so point the scan at
|
|
4297
|
+
// that instead. Identical for an app that is its own staging root.
|
|
4298
|
+
let fontSourceDir = appDir
|
|
4299
|
+
|
|
4300
|
+
if (geaEmbeddedCompat) {
|
|
4301
|
+
const compatSrcDir = path.join(outDir, 'gea-embedded-compat-src')
|
|
4302
|
+
fs.rmSync(compatSrcDir, { recursive: true, force: true })
|
|
4303
|
+
// Relative imports may escape the app dir (examples/shared components).
|
|
4304
|
+
// Mirror the escaped trees at their original depth below the common
|
|
4305
|
+
// ancestor so the staged sources keep resolving each other, and aim the
|
|
4306
|
+
// vite entry at the app's staged location.
|
|
4307
|
+
const escapedDirs = escapedCompatImportDirs(appDir, entry)
|
|
4308
|
+
// App .env vars, baked into the staged sources as process.env.<KEY> literals.
|
|
4309
|
+
const envDefines = dotEnvDefines(appDir)
|
|
4310
|
+
let compatEntry = entry
|
|
4311
|
+
let stagingRoot = appDir
|
|
4312
|
+
if (escapedDirs.length === 0) {
|
|
4313
|
+
copyCompatSourceTree(appDir, compatSrcDir, envDefines)
|
|
4314
|
+
} else {
|
|
4315
|
+
stagingRoot = commonAncestorDir([appDir, ...escapedDirs])
|
|
4316
|
+
copyCompatSourceTree(appDir, path.join(compatSrcDir, path.relative(stagingRoot, appDir)), envDefines)
|
|
4317
|
+
for (const dir of escapedDirs) copyCompatSourceTree(dir, path.join(compatSrcDir, path.relative(stagingRoot, dir)), envDefines)
|
|
4318
|
+
compatEntry = path.join(path.relative(stagingRoot, appDir), entry)
|
|
4319
|
+
}
|
|
4320
|
+
linkCompatNodeModules(appDir, path.join(compatSrcDir, path.dirname(compatEntry)), compatSrcDir)
|
|
4321
|
+
stateRuntimeAliasToTypeScript(path.join(compatSrcDir, path.relative(stagingRoot, appDir)), compatSrcDir, stagingRoot)
|
|
4322
|
+
if (moduleGraphCompile) {
|
|
4323
|
+
compatEntry = writeAppendJsCompatEntry({ compatSrcDir, compatEntry, appendFiles: appendJsFiles })
|
|
4324
|
+
}
|
|
4325
|
+
stagedEntryFile = path.join(compatSrcDir, compatEntry)
|
|
4326
|
+
fontSourceDir = compatSrcDir
|
|
4327
|
+
writeCompatViteConfig({ configPath: viteConfig, compatSrcDir, appDir, entry: compatEntry, viteOutDir, geaIrPath, appleNative, moduleGraphOutDir, envDefines })
|
|
4328
|
+
} else if (appleNative) {
|
|
4329
|
+
const baseViteConfig = viteConfig
|
|
4330
|
+
viteConfig = path.join(outDir, 'gea-apple-native.vite.config.mjs')
|
|
4331
|
+
writeAppleNativeViteConfig({ configPath: viteConfig, baseConfigPath: baseViteConfig, appDir, moduleGraphOutDir })
|
|
4332
|
+
}
|
|
4333
|
+
|
|
4334
|
+
run('node', [
|
|
4335
|
+
viteBin,
|
|
4336
|
+
'build',
|
|
4337
|
+
'--config',
|
|
4338
|
+
viteConfig,
|
|
4339
|
+
'--outDir',
|
|
4340
|
+
viteOutDir,
|
|
4341
|
+
'--emptyOutDir',
|
|
4342
|
+
'--minify',
|
|
4343
|
+
'false',
|
|
4344
|
+
], {
|
|
4345
|
+
env: {
|
|
4346
|
+
...process.env,
|
|
4347
|
+
GEA_IR_OUT: geaIrPath,
|
|
4348
|
+
...(moduleGraphOutDir ? { GEA_VITE_MODULE_GRAPH_OUT: moduleGraphOutDir } : {}),
|
|
4349
|
+
},
|
|
4350
|
+
})
|
|
4351
|
+
if (moduleGraphOnly) {
|
|
4352
|
+
if (!moduleGraphOutDir) fail('--module-graph-only requires --module-graph-out <dir> or GEA_VITE_MODULE_GRAPH_OUT')
|
|
4353
|
+
process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
|
|
4354
|
+
process.stdout.write(`module-graph-sources=${path.join(moduleGraphOutDir, 'sources')}\n`)
|
|
4355
|
+
process.exit(0)
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
const viteBundle = findFirstJsFile(viteOutDir)
|
|
4359
|
+
if (!viteBundle) fail(`Vite produced no JS bundle in ${viteOutDir}`)
|
|
4360
|
+
const cssPrelude = geaEmbeddedCompat ? cssRegistrationCode(viteOutDir) : ''
|
|
4361
|
+
const cssPreludePath = path.join(outDir, 'gea-style-registration.cppfrag')
|
|
4362
|
+
if (cssPrelude || cppPreludeSymbol) {
|
|
4363
|
+
fs.writeFileSync(cssPreludePath, cssPrelude)
|
|
4364
|
+
} else {
|
|
4365
|
+
fs.rmSync(cssPreludePath, { force: true })
|
|
4366
|
+
}
|
|
4367
|
+
run('node', [
|
|
4368
|
+
path.join(coreRoot, 'scripts/generate-gea-embedded-fonts.mjs'),
|
|
4369
|
+
'--app-dir',
|
|
4370
|
+
fontSourceDir,
|
|
4371
|
+
'--css-dir',
|
|
4372
|
+
viteOutDir,
|
|
4373
|
+
'--out-cpp',
|
|
4374
|
+
path.join(outDir, 'gea_embedded_font_generated.cpp'),
|
|
4375
|
+
'--out-h',
|
|
4376
|
+
path.join(outDir, 'gea_embedded_font_generated.h'),
|
|
4377
|
+
...effectiveFontViewportWidths.flatMap((width) => ['--viewport-width', width]),
|
|
4378
|
+
...effectiveFontViewportHeights.flatMap((height) => ['--viewport-height', height]),
|
|
4379
|
+
...fontDevicePixelRatios.flatMap((ratio) => ['--device-pixel-ratio', ratio]),
|
|
4380
|
+
...(effectiveFontSupersample ? ['--supersample', effectiveFontSupersample] : []),
|
|
4381
|
+
...(effectiveFontAtlasBits ? ['--atlas-bits', effectiveFontAtlasBits] : []),
|
|
4382
|
+
...(fontSymbolPrefix ? ['--symbol-prefix', fontSymbolPrefix] : []),
|
|
4383
|
+
...(runtimeTtfFonts ? ['--runtime-ttf-fonts'] : []),
|
|
4384
|
+
])
|
|
4385
|
+
// Embed app assets (images referenced as `<img src="...">`) as `.rodata` linked
|
|
4386
|
+
// into the app binary, registered by path. Like the font TU above, this always
|
|
4387
|
+
// emits a (possibly empty) gea_embedded_assets_generated.cpp so every target's
|
|
4388
|
+
// source list can include it unconditionally. Symbols live in an anonymous
|
|
4389
|
+
// namespace, so several isolated programs' asset TUs can be linked together (a
|
|
4390
|
+
// launcher bundle) without colliding.
|
|
4391
|
+
run('node', [
|
|
4392
|
+
path.join(coreRoot, 'scripts/generate-gea-embedded-assets.mjs'),
|
|
4393
|
+
'--app-dir',
|
|
4394
|
+
appDir,
|
|
4395
|
+
'--out-cpp',
|
|
4396
|
+
path.join(outDir, 'gea_embedded_assets_generated.cpp'),
|
|
4397
|
+
])
|
|
4398
|
+
const typeHints = collectBundledTypeHints(appDir, entry)
|
|
4399
|
+
const typeHintsPath = path.join(path.dirname(bundlePath), 'gea-type-hints.d.ts')
|
|
4400
|
+
let bundledCode = fs.readFileSync(viteBundle, 'utf8')
|
|
4401
|
+
let typeHintsReferencePath = ''
|
|
4402
|
+
const typeHintParts = [...(typeHints.imports ?? []), ...typeHints.declarations]
|
|
4403
|
+
if (typeHintParts.length > 0) {
|
|
4404
|
+
fs.writeFileSync(typeHintsPath, `${typeHintParts.join('\n\n')}\n`)
|
|
4405
|
+
typeHintsReferencePath = `./${path.basename(typeHintsPath)}`
|
|
4406
|
+
} else {
|
|
4407
|
+
fs.rmSync(typeHintsPath, { force: true })
|
|
4408
|
+
}
|
|
4409
|
+
bundledCode = applyBundledTypeHints(bundledCode, typeHints, typeHintsReferencePath)
|
|
4410
|
+
// The vite-plugin's lean ReactiveComponent transform emits side-effectful
|
|
4411
|
+
// `(globalThis.__GEA_IR_KEEP__ ||= []).push(Child, …)` keep-alives so the
|
|
4412
|
+
// bundler can't tree-shake IR-live child classes and the store globals they
|
|
4413
|
+
// reference. The references did their job during bundling — strip the marker
|
|
4414
|
+
// statements so geatsc never compiles a globalThis property write.
|
|
4415
|
+
bundledCode = bundledCode.replace(/^.*__GEA_IR_KEEP__.*$\n?/gm, '')
|
|
4416
|
+
// Compiled-template root caches (`var _tplN_root = null`) hold a cached node
|
|
4417
|
+
// that later gets `.cloneNode(true)` — typing them as Node makes geatsc store
|
|
4418
|
+
// them as a native NodeHandle so cloneNode/firstChild/childNodes dispatch to
|
|
4419
|
+
// the typed tree methods instead of falling onto an opaque boxed object (the
|
|
4420
|
+
// untyped null inferred gea_cpp_value, whose generic boxing has no DOM method
|
|
4421
|
+
// table — the app mounted nothing and the screen stayed black).
|
|
4422
|
+
bundledCode = bundledCode.replace(/\bvar (_tpl\d+_root) = null;/g, '/** @type {Node} */ var $1 = null;')
|
|
4423
|
+
// JS `(cache || (cache = make()))` returns the VALUE; on native NodeHandles
|
|
4424
|
+
// the C++ `||` lowering yields bool — statement-ize the idiom instead.
|
|
4425
|
+
bundledCode = bundledCode.replace(/\((_tpl\d+_root) \|\| \(\1 = (_tpl\d+_create)\(\)\)\)\.cloneNode\(true\)/g,
|
|
4426
|
+
'($1 = $1 || $2(), $1).cloneNode(true)')
|
|
4427
|
+
fs.writeFileSync(bundlePath, bundledCode)
|
|
4428
|
+
if (!moduleGraphCompile) {
|
|
4429
|
+
for (const appendFile of appendJsFiles) {
|
|
4430
|
+
if (!fs.existsSync(appendFile)) fail(`missing appended JS file: ${appendFile}`)
|
|
4431
|
+
fs.appendFileSync(bundlePath, `\n${fs.readFileSync(appendFile, 'utf8')}\n`)
|
|
4432
|
+
}
|
|
4433
|
+
}
|
|
4434
|
+
// Module-graph snapshots are captured pre-type-strip, so they keep their
|
|
4435
|
+
// source annotations — EXCEPT the `el = null` field the lean ReactiveComponent
|
|
4436
|
+
// transform injects untyped after stripping the `Component<T>` heritage.
|
|
4437
|
+
// Re-attach the heritage generic as a real TS annotation (`el: T | null`) so
|
|
4438
|
+
// geatsc lowers `this.el` to the typed native handle instead of a boxed value.
|
|
4439
|
+
if (moduleGraphCompile && moduleGraphOutDir) {
|
|
4440
|
+
restoreModuleGraphElAnnotations(moduleGraphOutDir, typeHints.propertyHints)
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
const applePlatform = readOption('--apple-platform') ?? ''
|
|
4444
|
+
const appleNativeMetadataPath = appleNative ? await writeAppleNativeMetadata(outDir, appDir, applePlatform) : ''
|
|
4445
|
+
const geatscArgs = [
|
|
4446
|
+
geatscBin,
|
|
4447
|
+
moduleGraphCompile ? 'compile-module-graph' : 'compile',
|
|
4448
|
+
moduleGraphCompile ? path.join(moduleGraphOutDir, 'gea-module-graph.json') : bundlePath,
|
|
4449
|
+
'--out-dir',
|
|
4450
|
+
outDir,
|
|
4451
|
+
'--plugin',
|
|
4452
|
+
geaPluginSpecifier,
|
|
4453
|
+
'--plugin-option',
|
|
4454
|
+
`gea.ir=${geaIrPath}`,
|
|
4455
|
+
'--plugin-option',
|
|
4456
|
+
`gea.microtasks-namespace=${microtasksNamespaceForEntry(entrySymbol)}`,
|
|
4457
|
+
]
|
|
4458
|
+
if (!moduleGraphCompile) geatscArgs.push('--target', 'cpp')
|
|
4459
|
+
if (moduleGraphCompile) {
|
|
4460
|
+
geatscArgs.push('--entry', stagedEntryFile ?? path.join(appDir, entry), '--module-graph-stage', 'hybrid')
|
|
4461
|
+
}
|
|
4462
|
+
if (appleNative) {
|
|
4463
|
+
geatscArgs.push('--plugin', appleNativePluginSpecifier, '--plugin-option', `apple.metadata=${appleNativeMetadataPath}`)
|
|
4464
|
+
}
|
|
4465
|
+
for (const plugin of extraGeatscPlugins) {
|
|
4466
|
+
geatscArgs.push('--plugin', plugin)
|
|
4467
|
+
}
|
|
4468
|
+
if (cssPrelude || cppPreludeSymbol) geatscArgs.push('--plugin-option', `gea.cpp-prelude=${cssPreludePath}`)
|
|
4469
|
+
if (cppPreludeSymbol) geatscArgs.push('--plugin-option', `gea.cpp-prelude-symbol=${cppPreludeSymbol}`)
|
|
4470
|
+
if (pixelPanelEndian) geatscArgs.push('--plugin-option', `gea.pixel-panel-endian=${pixelPanelEndian}`)
|
|
4471
|
+
if (cppBoard) geatscArgs.push('--cpp-board', cppBoard)
|
|
4472
|
+
if (cxxStandard) geatscArgs.push('--cxx-standard', cxxStandard)
|
|
4473
|
+
if (hasFlag('--allow-any')) geatscArgs.push('--allow-any')
|
|
4474
|
+
if (entrySymbol) geatscArgs.push('--entry-symbol', entrySymbol)
|
|
4475
|
+
if (hasFlag('--isolate-symbols')) geatscArgs.push('--isolate-symbols')
|
|
4476
|
+
// One C++ unit per source module plus a shared header, instead of one unit for
|
|
4477
|
+
// the whole program. Every target already reads `geatsc-sources.txt` as a LIST,
|
|
4478
|
+
// so nothing downstream changes; see the compiler's docs/TRANSLATION-UNITS.md
|
|
4479
|
+
// for what it buys (parallel and incremental C++ builds) and what it costs.
|
|
4480
|
+
// The env var is honored beside the flag so a target need not plumb one: every
|
|
4481
|
+
// build in the tree goes through this script, and the boards' CMakeLists spell
|
|
4482
|
+
// their geatsc command line by hand, so an env var reaches all of them at once
|
|
4483
|
+
// (`GEA_PER_FILE_UNITS=1 <any build>`). build-macos.sh sets the flag itself and
|
|
4484
|
+
// folds the variable into its freshness signature, because a layout change
|
|
4485
|
+
// invalidates every generated file.
|
|
4486
|
+
if (process.env.GEA_CPP_TRANSLATION_UNITS === 'balanced') geatscArgs.push('--translation-units', 'balanced')
|
|
4487
|
+
else if (hasFlag('--per-file-units') || process.env.GEA_PER_FILE_UNITS === '1') geatscArgs.push('--translation-units', 'per-file')
|
|
4488
|
+
if (hasFlag('--gea-ir-backend') || hasFlag('--gea-replace-renderers')) {
|
|
4489
|
+
geatscArgs.push('--plugin-option', 'gea.replace-renderers=true')
|
|
4490
|
+
}
|
|
4491
|
+
// The whole-program analysis of a three.js app (a 284-module graph)
|
|
4492
|
+
// peaks well above node's default heap cap: on 2026-09-14 this exact spawn
|
|
4493
|
+
// died with `Ineffective mark-compacts near heap limit` after five minutes,
|
|
4494
|
+
// and the build reported "C++ generation failed". The compiler's own
|
|
4495
|
+
// measurement scripts are always run with this
|
|
4496
|
+
// limit; the build has to state the same one, not depend on the caller's
|
|
4497
|
+
// NODE_OPTIONS.
|
|
4498
|
+
run('node', ['--max-old-space-size=32768', ...geatscArgs])
|
|
4499
|
+
|
|
4500
|
+
process.stdout.write(`gea-vite-bundle=${bundlePath}\n`)
|
|
4501
|
+
if (moduleGraphOutDir) process.stdout.write(`module-graph=${path.join(moduleGraphOutDir, 'gea-module-graph.json')}\n`)
|
|
4502
|
+
const geatscSourceListPath = path.join(outDir, 'geatsc-sources.txt')
|
|
4503
|
+
process.stdout.write(`geatsc-sources=${geatscSourceListPath}\n`)
|
|
4504
|
+
if (fs.existsSync(geatscSourceListPath)) {
|
|
4505
|
+
const [entrySource] = fs.readFileSync(geatscSourceListPath, 'utf8').split(/\r?\n/).filter(Boolean)
|
|
4506
|
+
if (entrySource) process.stdout.write(`geatsc-entry-source=${entrySource}\n`)
|
|
4507
|
+
}
|
|
4508
|
+
|
|
4509
|
+
function microtasksNamespaceForEntry(symbol) {
|
|
4510
|
+
if (symbol && symbol.endsWith('_top_level')) {
|
|
4511
|
+
return `gea::framework::app::generated::${symbol.replace(/_top_level$/, '')}`
|
|
4512
|
+
}
|
|
4513
|
+
return 'gea::framework::app::generated'
|
|
4514
|
+
}
|