react-native-figma-shadow 0.1.0-alpha.1

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.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +152 -0
  3. package/android/CMakeLists.txt +30 -0
  4. package/android/build.gradle +76 -0
  5. package/android/gradle.properties +3 -0
  6. package/android/src/main/AndroidManifest.xml +1 -0
  7. package/android/src/main/java/com/figmashadow/FigmaShadowView.kt +224 -0
  8. package/android/src/main/java/com/figmashadow/FigmaShadowViewManager.kt +71 -0
  9. package/android/src/main/java/com/figmashadow/FigmaShadowViewPackage.kt +30 -0
  10. package/android/src/main/jni/cpp-adapter.cpp +77 -0
  11. package/cpp/figmashadow/Color.cpp +225 -0
  12. package/cpp/figmashadow/Color.h +14 -0
  13. package/cpp/figmashadow/FigmaShadow.cpp +114 -0
  14. package/cpp/figmashadow/FigmaShadow.h +30 -0
  15. package/cpp/figmashadow/Parser.cpp +150 -0
  16. package/cpp/figmashadow/Parser.h +16 -0
  17. package/cpp/figmashadow/Rasterizer.cpp +276 -0
  18. package/cpp/figmashadow/Rasterizer.h +21 -0
  19. package/cpp/figmashadow/Types.h +66 -0
  20. package/docs/ARCHITECTURE.md +67 -0
  21. package/docs/images/fs_drop.png +0 -0
  22. package/docs/images/fs_inset.png +0 -0
  23. package/docs/images/fs_offset.png +0 -0
  24. package/ios/FigmaShadowView.h +9 -0
  25. package/ios/FigmaShadowView.mm +207 -0
  26. package/lib/commonjs/FigmaShadowViewNativeComponent.js +10 -0
  27. package/lib/commonjs/FigmaShadowViewNativeComponent.js.map +1 -0
  28. package/lib/commonjs/index.js +106 -0
  29. package/lib/commonjs/index.js.map +1 -0
  30. package/lib/commonjs/package.json +1 -0
  31. package/lib/commonjs/parseShadow.js +145 -0
  32. package/lib/commonjs/parseShadow.js.map +1 -0
  33. package/lib/module/FigmaShadowViewNativeComponent.js +5 -0
  34. package/lib/module/FigmaShadowViewNativeComponent.js.map +1 -0
  35. package/lib/module/index.js +88 -0
  36. package/lib/module/index.js.map +1 -0
  37. package/lib/module/package.json +1 -0
  38. package/lib/module/parseShadow.js +139 -0
  39. package/lib/module/parseShadow.js.map +1 -0
  40. package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts +24 -0
  41. package/lib/typescript/src/FigmaShadowViewNativeComponent.d.ts.map +1 -0
  42. package/lib/typescript/src/index.d.ts +40 -0
  43. package/lib/typescript/src/index.d.ts.map +1 -0
  44. package/lib/typescript/src/parseShadow.d.ts +22 -0
  45. package/lib/typescript/src/parseShadow.d.ts.map +1 -0
  46. package/package.json +122 -0
  47. package/react-native-figma-shadow.podspec +25 -0
  48. package/react-native.config.js +19 -0
  49. package/src/FigmaShadowViewNativeComponent.ts +30 -0
  50. package/src/index.tsx +134 -0
  51. package/src/parseShadow.ts +134 -0
@@ -0,0 +1,225 @@
1
+ #include "Color.h"
2
+
3
+ #include <algorithm>
4
+ #include <array>
5
+ #include <cctype>
6
+ #include <cmath>
7
+ #include <cstdint>
8
+ #include <cstdlib>
9
+ #include <string>
10
+ #include <vector>
11
+
12
+ namespace figmashadow {
13
+
14
+ namespace {
15
+
16
+ std::string toLower(std::string s) {
17
+ std::transform(s.begin(), s.end(), s.begin(),
18
+ [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
19
+ return s;
20
+ }
21
+
22
+ std::string trim(const std::string& s) {
23
+ size_t a = s.find_first_not_of(" \t\n\r\f\v");
24
+ if (a == std::string::npos) return "";
25
+ size_t b = s.find_last_not_of(" \t\n\r\f\v");
26
+ return s.substr(a, b - a + 1);
27
+ }
28
+
29
+ float clamp01(float v) { return v < 0.0f ? 0.0f : (v > 1.0f ? 1.0f : v); }
30
+
31
+ bool hexNibble(char c, int& out) {
32
+ if (c >= '0' && c <= '9') { out = c - '0'; return true; }
33
+ c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
34
+ if (c >= 'a' && c <= 'f') { out = c - 'a' + 10; return true; }
35
+ return false;
36
+ }
37
+
38
+ bool parseHex(const std::string& tok, Color& out) {
39
+ if (tok.empty() || tok[0] != '#') return false;
40
+ std::string h = tok.substr(1);
41
+ const size_t n = h.size();
42
+ if (n != 3 && n != 4 && n != 6 && n != 8) return false;
43
+
44
+ std::vector<int> nibbles;
45
+ nibbles.reserve(n);
46
+ for (char c : h) {
47
+ int v;
48
+ if (!hexNibble(c, v)) return false;
49
+ nibbles.push_back(v);
50
+ }
51
+
52
+ int r, g, b, a = 255;
53
+ if (n == 3 || n == 4) {
54
+ r = nibbles[0] * 17;
55
+ g = nibbles[1] * 17;
56
+ b = nibbles[2] * 17;
57
+ if (n == 4) a = nibbles[3] * 17;
58
+ } else {
59
+ r = nibbles[0] * 16 + nibbles[1];
60
+ g = nibbles[2] * 16 + nibbles[3];
61
+ b = nibbles[4] * 16 + nibbles[5];
62
+ if (n == 8) a = nibbles[6] * 16 + nibbles[7];
63
+ }
64
+ out.r = r / 255.0f;
65
+ out.g = g / 255.0f;
66
+ out.b = b / 255.0f;
67
+ out.a = a / 255.0f;
68
+ return true;
69
+ }
70
+
71
+ // Splits the inside of `rgb( ... )` on commas and/or whitespace (and an
72
+ // optional `/` alpha separator), tolerating the modern and legacy syntaxes.
73
+ std::vector<std::string> splitArgs(const std::string& inner) {
74
+ std::vector<std::string> parts;
75
+ std::string cur;
76
+ for (char c : inner) {
77
+ if (c == ',' || c == '/' || std::isspace(static_cast<unsigned char>(c))) {
78
+ if (!cur.empty()) { parts.push_back(cur); cur.clear(); }
79
+ } else {
80
+ cur.push_back(c);
81
+ }
82
+ }
83
+ if (!cur.empty()) parts.push_back(cur);
84
+ return parts;
85
+ }
86
+
87
+ // Parses a channel that is either `0..255` or a percentage `0..100%`.
88
+ bool parseChannel(const std::string& s, float& out) {
89
+ if (s.empty()) return false;
90
+ char* end = nullptr;
91
+ double v = std::strtod(s.c_str(), &end);
92
+ if (end == s.c_str()) return false;
93
+ if (*end == '%') {
94
+ out = clamp01(static_cast<float>(v / 100.0));
95
+ } else {
96
+ out = clamp01(static_cast<float>(v / 255.0));
97
+ }
98
+ return true;
99
+ }
100
+
101
+ bool parseAlpha(const std::string& s, float& out) {
102
+ if (s.empty()) return false;
103
+ char* end = nullptr;
104
+ double v = std::strtod(s.c_str(), &end);
105
+ if (end == s.c_str()) return false;
106
+ if (*end == '%') v /= 100.0;
107
+ out = clamp01(static_cast<float>(v));
108
+ return true;
109
+ }
110
+
111
+ void hslToRgb(float h, float s, float l, Color& out) {
112
+ h = std::fmod(std::fmod(h, 360.0f) + 360.0f, 360.0f) / 360.0f;
113
+ auto hue = [](float p, float q, float t) {
114
+ if (t < 0.0f) t += 1.0f;
115
+ if (t > 1.0f) t -= 1.0f;
116
+ if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t;
117
+ if (t < 1.0f / 2.0f) return q;
118
+ if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f;
119
+ return p;
120
+ };
121
+ if (s <= 0.0f) {
122
+ out.r = out.g = out.b = l;
123
+ return;
124
+ }
125
+ float q = l < 0.5f ? l * (1.0f + s) : l + s - l * s;
126
+ float p = 2.0f * l - q;
127
+ out.r = hue(p, q, h + 1.0f / 3.0f);
128
+ out.g = hue(p, q, h);
129
+ out.b = hue(p, q, h - 1.0f / 3.0f);
130
+ }
131
+
132
+ bool parseFunctional(const std::string& tok, Color& out) {
133
+ size_t open = tok.find('(');
134
+ if (open == std::string::npos || tok.back() != ')') return false;
135
+ std::string fn = toLower(tok.substr(0, open));
136
+ std::string inner = tok.substr(open + 1, tok.size() - open - 2);
137
+ auto args = splitArgs(inner);
138
+
139
+ if (fn == "rgb" || fn == "rgba") {
140
+ if (args.size() < 3) return false;
141
+ float r, g, b, a = 1.0f;
142
+ if (!parseChannel(args[0], r) || !parseChannel(args[1], g) ||
143
+ !parseChannel(args[2], b))
144
+ return false;
145
+ if (args.size() >= 4 && !parseAlpha(args[3], a)) return false;
146
+ out = {r, g, b, a};
147
+ return true;
148
+ }
149
+ if (fn == "hsl" || fn == "hsla") {
150
+ if (args.size() < 3) return false;
151
+ char* e = nullptr;
152
+ float h = static_cast<float>(std::strtod(args[0].c_str(), &e));
153
+ if (e == args[0].c_str()) return false;
154
+ auto pct = [](const std::string& s, float& v) {
155
+ char* end = nullptr;
156
+ double d = std::strtod(s.c_str(), &end);
157
+ if (end == s.c_str()) return false;
158
+ v = static_cast<float>(d / 100.0);
159
+ return true;
160
+ };
161
+ float s, l, a = 1.0f;
162
+ if (!pct(args[1], s) || !pct(args[2], l)) return false;
163
+ s = clamp01(s);
164
+ l = clamp01(l);
165
+ if (args.size() >= 4 && !parseAlpha(args[3], a)) return false;
166
+ hslToRgb(h, s, l, out);
167
+ out.a = a;
168
+ return true;
169
+ }
170
+ return false;
171
+ }
172
+
173
+ struct NamedColor {
174
+ const char* name;
175
+ uint32_t rgb; // 0xRRGGBB
176
+ };
177
+
178
+ // The most common CSS named colors. rgb()/rgba()/hex cover the rest and are what
179
+ // Figma and design tools actually emit.
180
+ constexpr std::array<NamedColor, 34> kNamed = {{
181
+ {"black", 0x000000}, {"white", 0xffffff}, {"red", 0xff0000},
182
+ {"green", 0x008000}, {"blue", 0x0000ff}, {"yellow", 0xffff00},
183
+ {"cyan", 0x00ffff}, {"magenta", 0xff00ff}, {"gray", 0x808080},
184
+ {"grey", 0x808080}, {"silver", 0xc0c0c0}, {"maroon", 0x800000},
185
+ {"olive", 0x808000}, {"lime", 0x00ff00}, {"aqua", 0x00ffff},
186
+ {"teal", 0x008080}, {"navy", 0x000080}, {"fuchsia", 0xff00ff},
187
+ {"purple", 0x800080}, {"orange", 0xffa500}, {"pink", 0xffc0cb},
188
+ {"brown", 0xa52a2a}, {"gold", 0xffd700}, {"indigo", 0x4b0082},
189
+ {"violet", 0xee82ee}, {"crimson", 0xdc143c}, {"coral", 0xff7f50},
190
+ {"salmon", 0xfa8072}, {"khaki", 0xf0e68c}, {"lavender", 0xe6e6fa},
191
+ {"plum", 0xdda0dd}, {"orchid", 0xda70d6}, {"tan", 0xd2b48c},
192
+ {"turquoise", 0x40e0d0},
193
+ }};
194
+
195
+ } // namespace
196
+
197
+ bool parseColor(const std::string& raw, Color& out) {
198
+ std::string tok = trim(raw);
199
+ if (tok.empty()) return false;
200
+
201
+ if (tok[0] == '#') return parseHex(tok, out);
202
+ if (tok.find('(') != std::string::npos) return parseFunctional(tok, out);
203
+
204
+ std::string lower = toLower(tok);
205
+ if (lower == "transparent") {
206
+ out = {0.0f, 0.0f, 0.0f, 0.0f};
207
+ return true;
208
+ }
209
+ if (lower == "currentcolor") {
210
+ out = {0.0f, 0.0f, 0.0f, 1.0f};
211
+ return true;
212
+ }
213
+ for (const auto& nc : kNamed) {
214
+ if (lower == nc.name) {
215
+ out.r = ((nc.rgb >> 16) & 0xff) / 255.0f;
216
+ out.g = ((nc.rgb >> 8) & 0xff) / 255.0f;
217
+ out.b = (nc.rgb & 0xff) / 255.0f;
218
+ out.a = 1.0f;
219
+ return true;
220
+ }
221
+ }
222
+ return false;
223
+ }
224
+
225
+ } // namespace figmashadow
@@ -0,0 +1,14 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ #include "Types.h"
6
+
7
+ namespace figmashadow {
8
+
9
+ // Parses a CSS color token: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb()/rgba(),
10
+ // hsl()/hsla(), `transparent`, `currentColor` (treated as opaque black) and the
11
+ // common CSS named colors. Returns false if `token` is not a color.
12
+ bool parseColor(const std::string& token, Color& out);
13
+
14
+ } // namespace figmashadow
@@ -0,0 +1,114 @@
1
+ #include "FigmaShadow.h"
2
+
3
+ #include <cmath>
4
+ #include <cstdint>
5
+ #include <list>
6
+ #include <mutex>
7
+ #include <sstream>
8
+ #include <string>
9
+ #include <unordered_map>
10
+
11
+ #include "Parser.h"
12
+ #include "Rasterizer.h"
13
+
14
+ namespace figmashadow {
15
+
16
+ namespace {
17
+
18
+ // Quantise floats to 1/8 px before hashing so sub-pixel layout jitter still
19
+ // hits the cache.
20
+ long quant(float v) { return std::lround(v * 8.0f); }
21
+
22
+ std::string makeKey(float cw, float ch, float rtl, float rtr, float rbr, float rbl,
23
+ const std::string& boxShadow, float bl, float bt, float br,
24
+ float bb, float scale, bool hq) {
25
+ std::ostringstream os;
26
+ os << quant(cw) << '|' << quant(ch) << '|' << quant(rtl) << '|' << quant(rtr)
27
+ << '|' << quant(rbr) << '|' << quant(rbl) << '|' << quant(bl) << '|'
28
+ << quant(bt) << '|' << quant(br) << '|' << quant(bb) << '|' << quant(scale)
29
+ << '|' << (hq ? '1' : '0') << '|' << boxShadow;
30
+ return os.str();
31
+ }
32
+
33
+ constexpr size_t kMaxCacheBytes = 6 * 1024 * 1024;
34
+
35
+ struct Cache {
36
+ std::mutex mutex;
37
+ std::list<std::pair<std::string, Bitmap>> items; // front = most recent
38
+ std::unordered_map<std::string, decltype(items)::iterator> index;
39
+ size_t bytes = 0;
40
+
41
+ bool get(const std::string& key, Bitmap& out) {
42
+ std::lock_guard<std::mutex> lock(mutex);
43
+ auto it = index.find(key);
44
+ if (it == index.end()) return false;
45
+ items.splice(items.begin(), items, it->second);
46
+ out = it->second->second;
47
+ return true;
48
+ }
49
+
50
+ void put(const std::string& key, const Bitmap& bmp) {
51
+ std::lock_guard<std::mutex> lock(mutex);
52
+ if (index.count(key)) return;
53
+ items.emplace_front(key, bmp);
54
+ index[key] = items.begin();
55
+ bytes += bmp.pixels.size();
56
+ while (bytes > kMaxCacheBytes && items.size() > 1) {
57
+ auto& back = items.back();
58
+ bytes -= back.second.pixels.size();
59
+ index.erase(back.first);
60
+ items.pop_back();
61
+ }
62
+ }
63
+
64
+ void clear() {
65
+ std::lock_guard<std::mutex> lock(mutex);
66
+ items.clear();
67
+ index.clear();
68
+ bytes = 0;
69
+ }
70
+
71
+ size_t size() {
72
+ std::lock_guard<std::mutex> lock(mutex);
73
+ return bytes;
74
+ }
75
+ };
76
+
77
+ Cache& cache() {
78
+ static Cache instance;
79
+ return instance;
80
+ }
81
+
82
+ } // namespace
83
+
84
+ Bitmap render(float contentWidth, float contentHeight, float radiusTopLeft,
85
+ float radiusTopRight, float radiusBottomRight, float radiusBottomLeft,
86
+ const std::string& boxShadow, float bleedLeft, float bleedTop,
87
+ float bleedRight, float bleedBottom, float scale, bool highQuality) {
88
+ const std::string key =
89
+ makeKey(contentWidth, contentHeight, radiusTopLeft, radiusTopRight,
90
+ radiusBottomRight, radiusBottomLeft, boxShadow, bleedLeft, bleedTop,
91
+ bleedRight, bleedBottom, scale, highQuality);
92
+
93
+ Bitmap cached;
94
+ if (cache().get(key, cached)) return cached;
95
+
96
+ RenderRequest req;
97
+ req.contentWidth = contentWidth;
98
+ req.contentHeight = contentHeight;
99
+ req.radii = {radiusTopLeft, radiusTopRight, radiusBottomRight, radiusBottomLeft};
100
+ req.bleed = {bleedLeft, bleedTop, bleedRight, bleedBottom};
101
+ req.scale = scale;
102
+ req.highQuality = highQuality;
103
+ req.layers = parseBoxShadow(boxShadow);
104
+
105
+ Bitmap bmp = renderShadow(req);
106
+ if (!bmp.empty()) cache().put(key, bmp);
107
+ return bmp;
108
+ }
109
+
110
+ void clearCache() { cache().clear(); }
111
+
112
+ size_t cacheSizeBytes() { return cache().size(); }
113
+
114
+ } // namespace figmashadow
@@ -0,0 +1,30 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+
5
+ #include "Types.h"
6
+
7
+ namespace figmashadow {
8
+
9
+ // High-level entry point used by both platform glue layers.
10
+ //
11
+ // Parses `boxShadow`, rasterizes every layer into one premultiplied RGBA8888
12
+ // bitmap sized to (content + bleed) * scale device pixels, and memoizes the
13
+ // result: identical requests (e.g. every row of a list) reuse one bitmap.
14
+ //
15
+ // `bleed*` must be the same values the JS layer used to inflate the native
16
+ // view, so the bitmap lines up 1:1 with the view's pixels.
17
+ Bitmap render(float contentWidth, float contentHeight,
18
+ float radiusTopLeft, float radiusTopRight,
19
+ float radiusBottomRight, float radiusBottomLeft,
20
+ const std::string& boxShadow,
21
+ float bleedLeft, float bleedTop, float bleedRight, float bleedBottom,
22
+ float scale, bool highQuality = false);
23
+
24
+ // Drops every memoized bitmap. Call on a memory warning.
25
+ void clearCache();
26
+
27
+ // Approximate number of bytes held by the cache.
28
+ size_t cacheSizeBytes();
29
+
30
+ } // namespace figmashadow
@@ -0,0 +1,150 @@
1
+ #include "Parser.h"
2
+
3
+ #include <algorithm>
4
+ #include <cctype>
5
+ #include <cstdlib>
6
+
7
+ #include "Color.h"
8
+
9
+ namespace figmashadow {
10
+
11
+ namespace {
12
+
13
+ std::string trim(const std::string& s) {
14
+ size_t a = s.find_first_not_of(" \t\n\r\f\v");
15
+ if (a == std::string::npos) return "";
16
+ size_t b = s.find_last_not_of(" \t\n\r\f\v");
17
+ return s.substr(a, b - a + 1);
18
+ }
19
+
20
+ std::string toLower(std::string s) {
21
+ std::transform(s.begin(), s.end(), s.begin(),
22
+ [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
23
+ return s;
24
+ }
25
+
26
+ // Splits on the given delimiter, ignoring delimiters nested inside parentheses
27
+ // (so `rgba(0, 0, 0, .5)` survives a comma split).
28
+ std::vector<std::string> splitTopLevel(const std::string& s, char delim) {
29
+ std::vector<std::string> out;
30
+ std::string cur;
31
+ int depth = 0;
32
+ for (char c : s) {
33
+ if (c == '(') depth++;
34
+ else if (c == ')') depth = std::max(0, depth - 1);
35
+
36
+ if (c == delim && depth == 0) {
37
+ out.push_back(cur);
38
+ cur.clear();
39
+ } else {
40
+ cur.push_back(c);
41
+ }
42
+ }
43
+ out.push_back(cur);
44
+ return out;
45
+ }
46
+
47
+ // Whitespace tokenizer that keeps `fn( ... )` groups intact.
48
+ std::vector<std::string> tokenize(const std::string& s) {
49
+ std::vector<std::string> out;
50
+ std::string cur;
51
+ int depth = 0;
52
+ for (char c : s) {
53
+ if (c == '(') depth++;
54
+ else if (c == ')') depth = std::max(0, depth - 1);
55
+
56
+ if (std::isspace(static_cast<unsigned char>(c)) && depth == 0) {
57
+ if (!cur.empty()) { out.push_back(cur); cur.clear(); }
58
+ } else {
59
+ cur.push_back(c);
60
+ }
61
+ }
62
+ if (!cur.empty()) out.push_back(cur);
63
+ return out;
64
+ }
65
+
66
+ // Parses a CSS length. Accepts a bare number or one suffixed with `px`/`dp`/`pt`
67
+ // (all treated as logical px). Rejects other units so they don't get mistaken
68
+ // for offsets.
69
+ bool parseLength(const std::string& tok, float& out) {
70
+ if (tok.empty()) return false;
71
+ char* end = nullptr;
72
+ double v = std::strtod(tok.c_str(), &end);
73
+ if (end == tok.c_str()) return false;
74
+ std::string unit = toLower(trim(std::string(end)));
75
+ if (unit.empty() || unit == "px" || unit == "dp" || unit == "dip" || unit == "pt") {
76
+ out = static_cast<float>(v);
77
+ return true;
78
+ }
79
+ return false;
80
+ }
81
+
82
+ ShadowLayer parseLayer(const std::string& raw, bool& ok) {
83
+ ShadowLayer layer;
84
+ ok = false;
85
+
86
+ std::vector<float> lengths;
87
+ bool hasColor = false;
88
+
89
+ for (const auto& tok : tokenize(raw)) {
90
+ std::string lower = toLower(tok);
91
+ if (lower == "inset") {
92
+ layer.inset = true;
93
+ continue;
94
+ }
95
+ float len;
96
+ Color color;
97
+ // A bare number is always a length; try length before color so tokens like
98
+ // `0` aren't swallowed by a lenient color parser.
99
+ if (parseLength(tok, len)) {
100
+ lengths.push_back(len);
101
+ } else if (!hasColor && parseColor(tok, color)) {
102
+ layer.color = color;
103
+ hasColor = true;
104
+ } else {
105
+ // Unknown token: ignore it rather than dropping the whole layer.
106
+ }
107
+ }
108
+
109
+ if (lengths.size() < 2) return layer;
110
+
111
+ layer.offsetX = lengths[0];
112
+ layer.offsetY = lengths[1];
113
+ if (lengths.size() >= 3) layer.blur = std::max(0.0f, lengths[2]);
114
+ if (lengths.size() >= 4) layer.spread = lengths[3];
115
+ if (!hasColor) layer.color = {0.0f, 0.0f, 0.0f, 1.0f}; // CSS default is currentColor
116
+
117
+ ok = true;
118
+ return layer;
119
+ }
120
+
121
+ } // namespace
122
+
123
+ std::vector<ShadowLayer> parseBoxShadow(const std::string& input) {
124
+ std::string s = trim(input);
125
+
126
+ // Strip an optional `box-shadow:` / `boxShadow:` prefix.
127
+ size_t colon = s.find(':');
128
+ if (colon != std::string::npos) {
129
+ std::string head = toLower(trim(s.substr(0, colon)));
130
+ if (head == "box-shadow" || head == "boxshadow") {
131
+ s = trim(s.substr(colon + 1));
132
+ }
133
+ }
134
+ if (!s.empty() && s.back() == ';') s.pop_back();
135
+ s = trim(s);
136
+
137
+ std::vector<ShadowLayer> layers;
138
+ if (s.empty() || toLower(s) == "none") return layers;
139
+
140
+ for (const auto& part : splitTopLevel(s, ',')) {
141
+ std::string layerStr = trim(part);
142
+ if (layerStr.empty()) continue;
143
+ bool ok = false;
144
+ ShadowLayer layer = parseLayer(layerStr, ok);
145
+ if (ok) layers.push_back(layer);
146
+ }
147
+ return layers;
148
+ }
149
+
150
+ } // namespace figmashadow
@@ -0,0 +1,16 @@
1
+ #pragma once
2
+
3
+ #include <string>
4
+ #include <vector>
5
+
6
+ #include "Types.h"
7
+
8
+ namespace figmashadow {
9
+
10
+ // Parses a CSS `box-shadow` value into layers, in source order (the first layer
11
+ // paints on top). Tolerates a leading `box-shadow:` and a trailing `;`, the
12
+ // `inset` keyword in any position, 2-4 lengths, and a color token anywhere in
13
+ // the declaration. Unparseable layers are skipped.
14
+ std::vector<ShadowLayer> parseBoxShadow(const std::string& input);
15
+
16
+ } // namespace figmashadow