Skip to content

Commit 684a2cf

Browse files
committed
feat: add basic media query support
1 parent ee5ec0a commit 684a2cf

2 files changed

Lines changed: 102 additions & 72 deletions

File tree

cpp/Helpers.hpp

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,12 @@
44
#include <jsi/JSIDynamic.h>
55
#include <folly/dynamic.h>
66
#include <unordered_set>
7+
#include <algorithm>
8+
#include <cctype>
9+
#include <optional>
10+
#include <string>
11+
#include <type_traits>
12+
#include <vector>
713

814
using namespace facebook;
915

@@ -16,4 +22,36 @@ namespace margelo::nitro::cssnitro::helpers {
1622
throw jsi::JSError(rt, message);
1723
}
1824
}
25+
26+
// Convert supported string-like values to std::string; otherwise std::nullopt.
27+
// Supported: std::string, any type convertible to const char*
28+
template<typename T>
29+
inline std::optional<std::string> toString(const T &v) {
30+
using U = std::decay_t<T>;
31+
if constexpr (std::is_same_v<U, std::string>) {
32+
return v;
33+
} else if constexpr (std::is_convertible_v<U, const char *>) {
34+
return std::string(v);
35+
} else {
36+
return std::nullopt;
37+
}
38+
}
39+
40+
// Convert arithmetic types to double; otherwise std::nullopt.
41+
template<typename T>
42+
inline std::optional<double> toNumber(const T &v) {
43+
using U = std::decay_t<T>;
44+
if constexpr (std::is_arithmetic_v<U>) {
45+
return static_cast<double>(v);
46+
} else {
47+
return std::nullopt;
48+
}
49+
}
50+
51+
// Lower-case a string in-place and return it (ASCII only).
52+
inline std::string lower(std::string s) {
53+
std::transform(s.begin(), s.end(), s.begin(),
54+
[](unsigned char c) { return static_cast<char>(std::tolower(c)); });
55+
return s;
56+
}
1957
}

cpp/Rules.hpp

Lines changed: 64 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
#include <optional>
99
#include <algorithm>
1010
#include <cctype>
11+
#include <cmath>
1112

1213
#include "Effect.hpp"
1314
#include "StyleRule.hpp"
1415
#include "Environment.hpp"
16+
#include "Helpers.hpp"
1517

1618
namespace margelo::nitro::cssnitro {
1719

@@ -46,18 +48,73 @@ namespace margelo::nitro::cssnitro {
4648
template<class L, class R>
4749
static bool testMediaComparison(const std::string &op, const L &left, const R &right,
4850
reactnativecss::Effect::GetProxy &get) {
49-
auto lhs = toNumber(left, get);
50-
auto rhs = toNumber(right, get);
51-
if (!lhs.has_value() || !rhs.has_value()) {
51+
using helpers::lower;
52+
using helpers::toNumber;
53+
using helpers::toString;
54+
55+
// Accessors for environment
56+
auto getWidth = [&]() -> double { return get(reactnativecss::env::windowWidth()); };
57+
auto getHeight = [&]() -> double { return get(reactnativecss::env::windowHeight()); };
58+
59+
// Special cases when left is a string keyword
60+
if (auto leftStr = toString(left)) {
61+
const std::string key = lower(*leftStr);
62+
63+
if (key == "min-width") {
64+
auto v = toNumber(right);
65+
return v.has_value() && getWidth() >= *v;
66+
}
67+
if (key == "max-width") {
68+
auto v = toNumber(right);
69+
return v.has_value() && getWidth() <= *v;
70+
}
71+
if (key == "min-height") {
72+
auto v = toNumber(right);
73+
return v.has_value() && getHeight() >= *v;
74+
}
75+
if (key == "max-height") {
76+
auto v = toNumber(right);
77+
return v.has_value() && getHeight() <= *v;
78+
}
79+
if (key == "orientation") {
80+
auto vStr = toString(right);
81+
const double w = getWidth();
82+
const double h = getHeight();
83+
return vStr.has_value() && lower(*vStr) == "landscape" ? (h < w) : (h >= w);
84+
}
85+
}
86+
87+
// For general comparisons, right must be numeric
88+
auto rightValOpt = toNumber(right);
89+
if (!rightValOpt.has_value()) {
5290
return false;
5391
}
54-
const double a = *lhs;
55-
const double b = *rhs;
92+
93+
// Left can be numeric or a string referring to width/height
94+
std::optional<double> leftValOpt = toNumber(left);
95+
if (!leftValOpt.has_value()) {
96+
if (auto leftStr = toString(left)) {
97+
const std::string key = lower(*leftStr);
98+
if (key == "width") {
99+
leftValOpt = getWidth();
100+
} else if (key == "height") {
101+
leftValOpt = getHeight();
102+
} else {
103+
return false;
104+
}
105+
} else {
106+
return false;
107+
}
108+
}
109+
110+
const double a = *leftValOpt;
111+
const double b = *rightValOpt;
112+
56113
if (op == "=") return nearlyEqual(a, b);
57114
if (op == ">") return a > b;
58-
if (op == ">=") return a >= b || nearlyEqual(a, b);
115+
if (op == ">=") return a > b || nearlyEqual(a, b);
59116
if (op == "<") return a < b;
60-
if (op == "<=") return a <= b || nearlyEqual(a, b);
117+
if (op == "<=") return a < b || nearlyEqual(a, b);
61118
return false;
62119
}
63120

@@ -83,71 +140,6 @@ namespace margelo::nitro::cssnitro {
83140
return std::abs(a - b) <= eps * std::max(1.0, std::max(std::abs(a), std::abs(b)));
84141
}
85142

86-
static std::string lower(std::string s) {
87-
std::transform(s.begin(), s.end(), s.begin(),
88-
[](unsigned char c) { return (char) std::tolower(c); });
89-
return s;
90-
}
91-
92-
static std::optional<double>
93-
tokenToNumberFromEnv(const std::string &tok, reactnativecss::Effect::GetProxy &get) {
94-
const auto t = lower(tok);
95-
if (t == "w" || t == "width" || t == "screenwidth") {
96-
return get(reactnativecss::env::windowWidth());
97-
}
98-
if (t == "h" || t == "height" || t == "screenheight") {
99-
return get(reactnativecss::env::windowHeight());
100-
}
101-
if (t == "scale" || t == "dpr" || t == "pixelratio") {
102-
return get(reactnativecss::env::windowScale());
103-
}
104-
if (t == "fontscale" || t == "fs") {
105-
return get(reactnativecss::env::windowFontScale());
106-
}
107-
return std::nullopt;
108-
}
109-
110-
static std::optional<double> parseNumberString(const std::string &s) {
111-
size_t start = 0, end = s.size();
112-
while (start < end && std::isspace((unsigned char) s[start])) ++start;
113-
while (end > start && std::isspace((unsigned char) s[end - 1])) --end;
114-
if (start >= end) return std::nullopt;
115-
size_t i = start;
116-
if (s[i] == '+' || s[i] == '-') ++i;
117-
bool dot = false;
118-
while (i < end) {
119-
char c = s[i];
120-
if (std::isdigit((unsigned char) c)) ++i;
121-
else if (c == '.' && !dot) {
122-
dot = true;
123-
++i;
124-
}
125-
else break;
126-
}
127-
if (i == start || (i == start + 1 && (s[start] == '+' || s[start] == '-')))
128-
return std::nullopt;
129-
try { return std::stod(std::string{s.begin() + (long) start, s.begin() + (long) i}); }
130-
catch (...) { return std::nullopt; }
131-
}
132-
133-
template<class T>
134-
static std::optional<double>
135-
toNumber(const T &value, reactnativecss::Effect::GetProxy &get) {
136-
using Decayed = std::decay_t<T>;
137-
if constexpr (std::is_arithmetic_v<Decayed>) {
138-
return static_cast<double>(value);
139-
} else if constexpr (std::is_same_v<Decayed, bool>) {
140-
return value ? 1.0 : 0.0;
141-
} else if constexpr (std::is_same_v<Decayed, std::string>) {
142-
if (auto env = tokenToNumberFromEnv(value, get)) return env;
143-
return parseNumberString(value);
144-
} else if constexpr (std::is_same_v<Decayed, const char *>) {
145-
return toNumber(std::string(value), get);
146-
} else {
147-
return std::nullopt;
148-
}
149-
}
150-
151143
template<class Cond>
152144
static bool
153145
testMediaConditionDispatch(const Cond &cond, reactnativecss::Effect::GetProxy &get) {

0 commit comments

Comments
 (0)