@symbiote-native/css-parser 0.2.3 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -26
- package/build/generate-dts/index.js +3 -8
- package/build/generate-dts-cli.js +5 -1
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte +25 -0
- package/build/golden-corpus/fixtures/ScopedGlobalDemo.svelte.d.ts +3 -0
- package/build/index.d.ts +8 -7
- package/build/index.js +4 -4
- package/build/lightning/declarations.d.ts +46 -0
- package/build/lightning/declarations.js +885 -0
- package/build/lightning/rules.d.ts +52 -0
- package/build/lightning/rules.js +157 -0
- package/build/lightning/selectors.d.ts +21 -0
- package/build/lightning/selectors.js +372 -0
- package/build/metro-css-module/index.d.ts +30 -2
- package/build/metro-css-module/index.js +142 -36
- package/build/metro-transformer/index.js +14 -22
- package/build/preprocessors/index.js +4 -3
- package/build/properties.d.ts +5 -6
- package/build/properties.js +29 -17
- package/build/scoped-classes.d.ts +23 -0
- package/build/scoped-classes.js +35 -0
- package/build/values.d.ts +3 -7
- package/build/values.js +3 -60
- package/package.json +2 -3
- package/typescript-plugin.cjs +32 -25
- package/build/global-selectors.d.ts +0 -1
- package/build/global-selectors.js +0 -22
- package/build/parser/index.d.ts +0 -22
- package/build/parser/index.js +0 -221
package/typescript-plugin.cjs
CHANGED
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
// separate, on-disk source of truth for `tsc`/`vue-tsc` CLI runs — plugins never load there.
|
|
9
9
|
//
|
|
10
10
|
// Core mechanism: override getScriptSnapshot + resolveModuleNameLiterals to synthesize a virtual
|
|
11
|
-
// .d.ts for the import. Two things worth calling out: (1) the class extractor
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
11
|
+
// .d.ts for the import. Two things worth calling out: (1) the class extractor keeps the AUTHORED
|
|
12
|
+
// spelling, because that is the key the runtime export map carries — see
|
|
13
|
+
// src/generate-dts/index.ts's classNamesToDtsSource, which this plugin's dts shape mirrors, and
|
|
14
|
+
// which quotes a non-identifier key for the same reason; (2) the dts
|
|
15
15
|
// cache is keyed on the file's mtime, so autocomplete doesn't go stale after editing the CSS file
|
|
16
16
|
// and wait for the IDE to restart tsserver.
|
|
17
17
|
//
|
|
@@ -21,22 +21,20 @@
|
|
|
21
21
|
// a correct, non-approximated preprocessor pipeline can't run here today. Those files still get
|
|
22
22
|
// basic (non-literal) type coverage from the project's ambient `.css` fallback declaration and
|
|
23
23
|
// from `css-dts`'s on-disk generation at pretypecheck time — just without live per-class
|
|
24
|
-
// completion in the plugin.
|
|
24
|
+
// completion in the plugin.
|
|
25
25
|
//
|
|
26
26
|
// SCOPE, second cut: only a SIMPLE `.foo { ... }` class selector is recognized correctly — a
|
|
27
|
-
// compound (`.btn.primary`) or descendant (`.card .title`) selector
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
// limitation of the regex-based approach — complex selectors may not be detected correctly.
|
|
27
|
+
// compound (`.btn.primary`) or descendant (`.card .title`) selector is extracted here as TWO
|
|
28
|
+
// separate keys, where the real compiler keeps the tokens together on one rule and exports only
|
|
29
|
+
// the names the author actually wrote.
|
|
31
30
|
//
|
|
32
31
|
// Hand-written plain CommonJS, NOT compiled from a `.ts`/`.cts` source — same convention already
|
|
33
32
|
// used for each adapter's metro-css-parser.cjs shim. tsserver loads a plugin via a synchronous
|
|
34
33
|
// `require()`, which cannot load this package's own ESM build output; a `.cts` source was tried
|
|
35
|
-
// first and rejected because
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
// machinery than a ~150-line, dependency-free plugin warrants.
|
|
34
|
+
// first and rejected because this package's shared tsconfig (`moduleResolution: "Bundler"`,
|
|
35
|
+
// needed for the rest of the package) doesn't apply the classic .cts→CJS format-forcing
|
|
36
|
+
// TypeScript otherwise gives Node16/NodeNext projects — carving out a second tsconfig/project
|
|
37
|
+
// reference just for one file was more machinery than a ~150-line, dependency-free plugin warrants.
|
|
40
38
|
'use strict';
|
|
41
39
|
|
|
42
40
|
const fs = require('node:fs');
|
|
@@ -48,17 +46,19 @@ function isCssModuleFile(fileName) {
|
|
|
48
46
|
return CSS_MODULE_RE.test(fileName);
|
|
49
47
|
}
|
|
50
48
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
49
|
+
// Names come out AS AUTHORED — `.section-tight` stays `section-tight`, read as
|
|
50
|
+
// `styles['section-tight']`. This must track `compileCssModule`'s export map exactly: the editor
|
|
51
|
+
// offering a key the runtime does not carry is the one failure this plugin can produce, and it
|
|
52
|
+
// fails in both directions at once (a suggested `sectionTight` is `undefined` at runtime, while
|
|
53
|
+
// the real `styles['section-tight']` reads as a TS2339). `generateDts` quotes a key that is not a
|
|
54
|
+
// valid identifier, which is what makes the kebab spelling usable.
|
|
55
55
|
function extractClassNames(css) {
|
|
56
56
|
const withoutComments = css.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
57
57
|
const names = new Set();
|
|
58
58
|
const classRe = /\.([a-zA-Z_][\w-]*)/g;
|
|
59
59
|
let match;
|
|
60
60
|
while ((match = classRe.exec(withoutComments))) {
|
|
61
|
-
names.add(
|
|
61
|
+
names.add(match[1]);
|
|
62
62
|
}
|
|
63
63
|
return [...names];
|
|
64
64
|
}
|
|
@@ -70,7 +70,7 @@ function generateDts(classNames) {
|
|
|
70
70
|
|
|
71
71
|
const fields = [...classNames]
|
|
72
72
|
.sort()
|
|
73
|
-
.map(
|
|
73
|
+
.map(name => {
|
|
74
74
|
const key = IDENTIFIER_RE.test(name) ? name : JSON.stringify(name);
|
|
75
75
|
return ` readonly ${key}: string;`;
|
|
76
76
|
})
|
|
@@ -114,16 +114,20 @@ function init(modules) {
|
|
|
114
114
|
return dts;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
const originalGetScriptKind = host.getScriptKind
|
|
117
|
+
const originalGetScriptKind = host.getScriptKind
|
|
118
|
+
? host.getScriptKind.bind(host)
|
|
119
|
+
: undefined;
|
|
118
120
|
const originalGetScriptSnapshot = host.getScriptSnapshot.bind(host);
|
|
119
121
|
const originalResolveModuleNameLiterals = host.resolveModuleNameLiterals;
|
|
120
122
|
|
|
121
|
-
host.getScriptKind =
|
|
123
|
+
host.getScriptKind = fileName =>
|
|
122
124
|
isCssModuleFile(fileName)
|
|
123
125
|
? typescript.ScriptKind.TS
|
|
124
|
-
:
|
|
126
|
+
: originalGetScriptKind
|
|
127
|
+
? originalGetScriptKind(fileName)
|
|
128
|
+
: typescript.ScriptKind.Unknown;
|
|
125
129
|
|
|
126
|
-
host.getScriptSnapshot =
|
|
130
|
+
host.getScriptSnapshot = fileName =>
|
|
127
131
|
isCssModuleFile(fileName)
|
|
128
132
|
? typescript.ScriptSnapshot.fromString(getDtsForCssFile(fileName))
|
|
129
133
|
: originalGetScriptSnapshot(fileName);
|
|
@@ -150,7 +154,10 @@ function init(modules) {
|
|
|
150
154
|
return literals.map((literal, index) => {
|
|
151
155
|
const moduleName = literal.text;
|
|
152
156
|
if (isCssModuleFile(moduleName) && moduleName.startsWith('.')) {
|
|
153
|
-
const resolvedPath = resolveRelativePath(
|
|
157
|
+
const resolvedPath = resolveRelativePath(
|
|
158
|
+
moduleName,
|
|
159
|
+
containingFile,
|
|
160
|
+
);
|
|
154
161
|
if (host.fileExists && host.fileExists(resolvedPath)) {
|
|
155
162
|
return {
|
|
156
163
|
resolvedModule: {
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function globalClassNamesIn(css: string): Set<string>;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
// A light, independent scan for `:global(...)`-wrapped selectors inside a scoped CSS block's
|
|
2
|
-
// raw text. parseCSS's extractClassName already UNWRAPS :global(...) (so `:global(.reset)`
|
|
3
|
-
// parses to the same `reset` key a plain `.reset` selector would), but its output has no marker
|
|
4
|
-
// for "this key came from inside :global()" — parseCSS's return shape is deliberately just
|
|
5
|
-
// `{ className: style }`, no per-key metadata. A caller doing its own scope-suffixing (Vue's
|
|
6
|
-
// <style scoped>/<style module>, or a standalone .module.css file) needs that distinction to
|
|
7
|
-
// exempt these names, so it re-derives it here with its own minimal regex, independent of the
|
|
8
|
-
// full CSS-to-style pipeline. Only the single-class form (`:global(.name)`) is recognized,
|
|
9
|
-
// matching extractClassName's own documented narrower gap for partial/nested :global() wrapping.
|
|
10
|
-
import { kebabToCamel } from "./parser/index.js";
|
|
11
|
-
const GLOBAL_SELECTOR_PATTERN = /:global\(\s*\.([a-zA-Z0-9_-]+)\s*\)/g;
|
|
12
|
-
export function globalClassNamesIn(css) {
|
|
13
|
-
const names = new Set();
|
|
14
|
-
let match;
|
|
15
|
-
while ((match = GLOBAL_SELECTOR_PATTERN.exec(css)) !== null) {
|
|
16
|
-
// Normalize kebab->camel: parseCSS's output is always camelCase-keyed, but the regex above
|
|
17
|
-
// captures the CSS text verbatim — a kebab selector like `:global(.reset-btn)` would
|
|
18
|
-
// otherwise never match its own `resetBtn` key.
|
|
19
|
-
names.add(kebabToCamel(match[1]));
|
|
20
|
-
}
|
|
21
|
-
return names;
|
|
22
|
-
}
|
package/build/parser/index.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type ICssParserOptions = {
|
|
2
|
-
filename?: string;
|
|
3
|
-
};
|
|
4
|
-
export declare function kebabToCamel(value: string): string;
|
|
5
|
-
/**
|
|
6
|
-
* Extract a camelCase class name from a CSS selector, or `null` if the selector has no RN
|
|
7
|
-
* equivalent (pseudo-classes/-elements, bare element selectors, the universal selector — RN has
|
|
8
|
-
* no element-selector concept, so those would just pollute the output).
|
|
9
|
-
*
|
|
10
|
-
* - `.card` → `'card'`
|
|
11
|
-
* - `#header` → `'header'`
|
|
12
|
-
* - `.btn.primary` → `'btnPrimary'` (compound)
|
|
13
|
-
* - `.card .title` / `.card > .title` → `'cardTitle'` (descendant/child, flattened)
|
|
14
|
-
* - `[data-theme]` → `'dataTheme'` (attribute)
|
|
15
|
-
* - `.my-class-name` → `'myClassName'` (kebab → camel)
|
|
16
|
-
*/
|
|
17
|
-
export declare function extractClassName(selector: string): string | null;
|
|
18
|
-
/**
|
|
19
|
-
* Parse a plain CSS string into a `{ className: RNStyleObject }` map. Build-time only — never
|
|
20
|
-
* ship this in the app's native JS bundle; it is meant to run inside a Metro transformer.
|
|
21
|
-
*/
|
|
22
|
-
export declare function parseCSS(css: string, options?: ICssParserOptions): Record<string, Record<string, unknown>>;
|
package/build/parser/index.js
DELETED
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
// CSS → React Native style-object compiler. `extractClassName` and the CSS-custom-property/
|
|
2
|
-
// `var()` resolution machinery are framework/target-agnostic. `evaluateCalc` treats `px` as
|
|
3
|
-
// identity (RN has no cell grid to scale against) and `rem`/`em` as scaled by the same
|
|
4
|
-
// {@link REM_TO_PX} constant as a bare value (see values.ts). `mapCSSProperty` (properties.ts)
|
|
5
|
-
// targets RN's `ViewStyle`/`TextStyle`.
|
|
6
|
-
import postcss from 'postcss';
|
|
7
|
-
import valueParser from 'postcss-value-parser';
|
|
8
|
-
import { mapCSSProperty } from "../properties.js";
|
|
9
|
-
import { REM_TO_PX } from "../values.js";
|
|
10
|
-
//#region Selector utilities
|
|
11
|
-
// Exported: the SFC style compiler (metro-vue-transformer.js) reuses this exact conversion to
|
|
12
|
-
// normalize a template's kebab-case class="section-label" authoring to the camelCase key this
|
|
13
|
-
// module already registers CSS selectors under, so both spellings resolve to the same style.
|
|
14
|
-
export function kebabToCamel(value) {
|
|
15
|
-
return value.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
16
|
-
}
|
|
17
|
-
function capitalize(value) {
|
|
18
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
19
|
-
}
|
|
20
|
-
function unescapeIdentifier(value) {
|
|
21
|
-
return value.replace(/\\(.)/g, '$1');
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Extract a camelCase class name from a CSS selector, or `null` if the selector has no RN
|
|
25
|
-
* equivalent (pseudo-classes/-elements, bare element selectors, the universal selector — RN has
|
|
26
|
-
* no element-selector concept, so those would just pollute the output).
|
|
27
|
-
*
|
|
28
|
-
* - `.card` → `'card'`
|
|
29
|
-
* - `#header` → `'header'`
|
|
30
|
-
* - `.btn.primary` → `'btnPrimary'` (compound)
|
|
31
|
-
* - `.card .title` / `.card > .title` → `'cardTitle'` (descendant/child, flattened)
|
|
32
|
-
* - `[data-theme]` → `'dataTheme'` (attribute)
|
|
33
|
-
* - `.my-class-name` → `'myClassName'` (kebab → camel)
|
|
34
|
-
*/
|
|
35
|
-
export function extractClassName(selector) {
|
|
36
|
-
const trimmed = selector.trim();
|
|
37
|
-
if (/^[a-z]+$/i.test(trimmed))
|
|
38
|
-
return null;
|
|
39
|
-
if (trimmed === '*')
|
|
40
|
-
return null;
|
|
41
|
-
// `:global(...)` (Vue `<style scoped>` escape hatch) opts a selector out of scope-suffixing —
|
|
42
|
-
// a caller concern outside this package. Here it just needs unwrapping: when the WHOLE trimmed
|
|
43
|
-
// selector is one `:global(...)` wrapper, recurse on its inner text and return whatever that
|
|
44
|
-
// resolves to, reusing every selector shape below instead of duplicating it. Checked before the
|
|
45
|
-
// "starts with :" / "any colon anywhere" guards, since `:global(...)` legitimately contains a
|
|
46
|
-
// colon that must not trigger them. Known gap: a `:global(...)` wrapping only PART of a larger
|
|
47
|
-
// compound/descendant selector (e.g. `.card :global(.reset)`) is NOT unwrapped by this check.
|
|
48
|
-
const globalMatch = trimmed.match(/^:global\(\s*(.+?)\s*\)$/);
|
|
49
|
-
if (globalMatch?.[1])
|
|
50
|
-
return extractClassName(globalMatch[1]);
|
|
51
|
-
if (trimmed.startsWith(':'))
|
|
52
|
-
return null;
|
|
53
|
-
// A pseudo-class/-element trailing a class/id selector (`.card:hover`, `.card::before`) has
|
|
54
|
-
// no RN equivalent — RN has no hover/focus/nth-child style variants — so the WHOLE rule is
|
|
55
|
-
// dropped, same as a bare `:hover`. Stripping just the pseudo suffix and keeping `.card`'s
|
|
56
|
-
// other declarations would be wrong: it'd silently merge hover-only styles into the
|
|
57
|
-
// always-applied base style (a real gap an earlier version of this fix had — found by
|
|
58
|
-
// manually running the parser on `.card:hover { opacity: 0.5 }` and seeing `opacity` leak
|
|
59
|
-
// into `card`'s permanent style). `[...]` is excluded first since an attribute selector's
|
|
60
|
-
// value may legitimately contain a colon (`[data-x="a:b"]`).
|
|
61
|
-
if (trimmed.replace(/\[[^\]]*\]/g, '').includes(':'))
|
|
62
|
-
return null;
|
|
63
|
-
// Compound selector (`.btn.primary`, `div.card`) — split on unescaped dots.
|
|
64
|
-
if (trimmed.includes('.') && !trimmed.includes(' ') && !trimmed.includes('>')) {
|
|
65
|
-
const parts = trimmed.split(/(?<!\\)\./).filter(Boolean);
|
|
66
|
-
if (parts.length > 0) {
|
|
67
|
-
const startsWithElement = !trimmed.startsWith('.');
|
|
68
|
-
const startIndex = startsWithElement ? 1 : 0;
|
|
69
|
-
if (startIndex >= parts.length)
|
|
70
|
-
return null;
|
|
71
|
-
return parts
|
|
72
|
-
.slice(startIndex)
|
|
73
|
-
.map((part, i) => {
|
|
74
|
-
const camelPart = kebabToCamel(unescapeIdentifier(part));
|
|
75
|
-
return i === 0 ? camelPart : capitalize(camelPart);
|
|
76
|
-
})
|
|
77
|
-
.join('');
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
// Descendant/child selector (`.card .title`, `.card > .title`) — flattened into one name.
|
|
81
|
-
if (trimmed.includes(' ')) {
|
|
82
|
-
const parts = trimmed.split(/\s+(?:>\s*)?/).filter(Boolean);
|
|
83
|
-
const classNames = [];
|
|
84
|
-
for (const part of parts) {
|
|
85
|
-
const classMatch = part.match(/\.((?:[a-zA-Z0-9_-]|\\.)+)/);
|
|
86
|
-
if (classMatch?.[1]) {
|
|
87
|
-
classNames.push(unescapeIdentifier(classMatch[1]));
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const idMatch = part.match(/#((?:[a-zA-Z0-9_-]|\\.)+)/);
|
|
91
|
-
if (idMatch?.[1])
|
|
92
|
-
classNames.push(unescapeIdentifier(idMatch[1]));
|
|
93
|
-
}
|
|
94
|
-
if (classNames.length === 0)
|
|
95
|
-
return null;
|
|
96
|
-
return classNames
|
|
97
|
-
.map((name, i) => {
|
|
98
|
-
const camelName = kebabToCamel(name);
|
|
99
|
-
return i === 0 ? camelName : capitalize(camelName);
|
|
100
|
-
})
|
|
101
|
-
.join('');
|
|
102
|
-
}
|
|
103
|
-
// Single class selector (`.card`).
|
|
104
|
-
const classMatch = trimmed.match(/^\.((?:[a-zA-Z0-9_-]|\\.)+)/);
|
|
105
|
-
if (classMatch?.[1])
|
|
106
|
-
return kebabToCamel(unescapeIdentifier(classMatch[1]));
|
|
107
|
-
// ID selector (`#header`).
|
|
108
|
-
const idMatch = trimmed.match(/^#((?:[a-zA-Z0-9_-]|\\.)+)/);
|
|
109
|
-
if (idMatch?.[1])
|
|
110
|
-
return kebabToCamel(unescapeIdentifier(idMatch[1]));
|
|
111
|
-
// Attribute selector (`[data-theme]`).
|
|
112
|
-
const attrMatch = trimmed.match(/^\[([a-zA-Z0-9_-]+)(?:=[^\]]+)?\]/);
|
|
113
|
-
if (attrMatch?.[1])
|
|
114
|
-
return kebabToCamel(attrMatch[1]);
|
|
115
|
-
return null;
|
|
116
|
-
}
|
|
117
|
-
//#endregion Selector utilities
|
|
118
|
-
//#region var() resolution
|
|
119
|
-
function resolveVariables(value, variables) {
|
|
120
|
-
if (!value.includes('var('))
|
|
121
|
-
return value;
|
|
122
|
-
const parsed = valueParser(value);
|
|
123
|
-
parsed.walk((node, index, nodes) => {
|
|
124
|
-
if (node.type !== 'function' || node.value !== 'var' || node.nodes.length === 0)
|
|
125
|
-
return;
|
|
126
|
-
const varName = node.nodes[0]?.value;
|
|
127
|
-
if (!varName)
|
|
128
|
-
return;
|
|
129
|
-
const fallbackNode = node.nodes.length > 2 ? node.nodes[2] : undefined;
|
|
130
|
-
const resolved = variables.get(varName) ?? fallbackNode?.value ?? '';
|
|
131
|
-
if (!resolved)
|
|
132
|
-
return;
|
|
133
|
-
// Replace the `var(...)` function node in its containing array with a plain word node
|
|
134
|
-
// holding the resolved text, instead of mutating `node`'s discriminated `type` in place.
|
|
135
|
-
nodes[index] = {
|
|
136
|
-
type: 'word',
|
|
137
|
-
value: resolveVariables(resolved, variables),
|
|
138
|
-
sourceIndex: node.sourceIndex,
|
|
139
|
-
sourceEndIndex: node.sourceEndIndex,
|
|
140
|
-
};
|
|
141
|
-
});
|
|
142
|
-
return parsed.toString();
|
|
143
|
-
}
|
|
144
|
-
//#endregion var() resolution
|
|
145
|
-
//#region calc() evaluation
|
|
146
|
-
const CALC_TERM_PATTERN = /calc\(([^)]+)\)/g;
|
|
147
|
-
const NUMBER_WITH_UNIT_PATTERN = /(-?\d+(?:\.\d+)?)(rem|em|px)?/g;
|
|
148
|
-
/**
|
|
149
|
-
* Evaluates a narrow shape of `calc()`: a single multiplication, or the first numeric term
|
|
150
|
-
* as a fallback. `px` is identity; `rem`/`em` scale by {@link REM_TO_PX}, matching a bare
|
|
151
|
-
* dimension value.
|
|
152
|
-
*/
|
|
153
|
-
function evaluateCalc(value) {
|
|
154
|
-
if (!value.includes('calc('))
|
|
155
|
-
return value;
|
|
156
|
-
return value.replace(CALC_TERM_PATTERN, (_, expr) => {
|
|
157
|
-
const matches = expr.match(NUMBER_WITH_UNIT_PATTERN) ?? [];
|
|
158
|
-
const values = [];
|
|
159
|
-
for (const term of matches) {
|
|
160
|
-
const numMatch = term.match(/(-?\d+(?:\.\d+)?)(rem|em|px)?/);
|
|
161
|
-
if (!numMatch)
|
|
162
|
-
continue;
|
|
163
|
-
const amount = parseFloat(numMatch[1]);
|
|
164
|
-
const unit = numMatch[2];
|
|
165
|
-
values.push(unit === 'rem' || unit === 'em' ? amount * REM_TO_PX : amount);
|
|
166
|
-
}
|
|
167
|
-
if (expr.includes('*')) {
|
|
168
|
-
const parts = expr.split('*').map(part => part.trim());
|
|
169
|
-
if (parts.length === 2) {
|
|
170
|
-
const a = values[0] ?? parseFloat(parts[0]) ?? 0;
|
|
171
|
-
const b = parseFloat(parts[1]) || 1;
|
|
172
|
-
return String(Math.round(a * b));
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
return String(Math.round(values[0] ?? 0));
|
|
176
|
-
});
|
|
177
|
-
}
|
|
178
|
-
//#endregion calc() evaluation
|
|
179
|
-
/**
|
|
180
|
-
* Parse a plain CSS string into a `{ className: RNStyleObject }` map. Build-time only — never
|
|
181
|
-
* ship this in the app's native JS bundle; it is meant to run inside a Metro transformer.
|
|
182
|
-
*/
|
|
183
|
-
export function parseCSS(css, options) {
|
|
184
|
-
if (!css || typeof css !== 'string')
|
|
185
|
-
return {};
|
|
186
|
-
const root = postcss.parse(css, { from: options?.filename });
|
|
187
|
-
const styles = {};
|
|
188
|
-
const warnedProperties = new Set();
|
|
189
|
-
// `@media` (and any other at-rule) is unsupported; drop it before the rule walk below so its
|
|
190
|
-
// nested rules never leak into the output.
|
|
191
|
-
root.walkAtRules(atRule => {
|
|
192
|
-
console.warn(`[@symbiote-native/css-parser] "@${atRule.name}" at-rules are not supported, "@${atRule.name} ${atRule.params}" skipped`);
|
|
193
|
-
atRule.remove();
|
|
194
|
-
});
|
|
195
|
-
const variables = new Map();
|
|
196
|
-
root.walkDecls(decl => {
|
|
197
|
-
if (decl.prop.startsWith('--'))
|
|
198
|
-
variables.set(decl.prop, decl.value);
|
|
199
|
-
});
|
|
200
|
-
root.walkRules(rule => {
|
|
201
|
-
const selectors = rule.selector.split(',').map(selector => selector.trim());
|
|
202
|
-
for (const selector of selectors) {
|
|
203
|
-
const className = extractClassName(selector);
|
|
204
|
-
if (!className)
|
|
205
|
-
continue;
|
|
206
|
-
const style = {};
|
|
207
|
-
rule.walkDecls(decl => {
|
|
208
|
-
if (decl.prop.startsWith('--'))
|
|
209
|
-
return;
|
|
210
|
-
const resolvedValue = evaluateCalc(resolveVariables(decl.value, variables));
|
|
211
|
-
const mapped = mapCSSProperty(decl.prop.toLowerCase(), resolvedValue, warnedProperties);
|
|
212
|
-
if (mapped)
|
|
213
|
-
Object.assign(style, mapped);
|
|
214
|
-
});
|
|
215
|
-
if (Object.keys(style).length === 0)
|
|
216
|
-
continue;
|
|
217
|
-
styles[className] = { ...styles[className], ...style };
|
|
218
|
-
}
|
|
219
|
-
});
|
|
220
|
-
return styles;
|
|
221
|
-
}
|