using System.Globalization;
using System.Text;
namespace XFESystem.Browser;
/// <summary>Shared, bounded screen-media subset for CSS and MediaQueryList.</summary>
internal static class BrowserMediaQuery
{
internal static bool Matches(string query, BrowserViewport viewport)
{
if (query.Length > 2048) return false;
foreach (string branch in query.ToLowerInvariant().Split(','))
if (MatchBranch(branch.Trim(), viewport)) return true;
return false;
}
private static bool MatchBranch(string query, BrowserViewport viewport)
{
var features = new List<string>(); var outside = new StringBuilder();
for (int index = 0; index < query.Length; index++)
{
if (query[index] == ')') return false;
if (query[index] != '(') { outside.Append(query[index]); continue; }
int end = query.IndexOf(')', index + 1);
if (end < 0 || features.Count == 32 || query.AsSpan(index + 1, end - index - 1).Contains('(')) return false;
features.Add(query[(index + 1)..end].Trim()); outside.Append(" # "); index = end;
}
var tokens = outside.ToString().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
int cursor = 0, feature = 0; bool negate = false, only = false, matched = true, previous = false;
if (cursor < tokens.Length && tokens[cursor] is "not" or "only") { negate = tokens[cursor] == "not"; only = tokens[cursor++] == "only"; }
if (cursor < tokens.Length && tokens[cursor] != "#")
{
string medium = tokens[cursor++];
if (!medium.All(character => char.IsAsciiLetter(character) || character == '-')) return false;
matched = medium is "screen" or "all"; previous = true;
}
else if (only) return false;
while (cursor < tokens.Length)
{
if (previous && tokens[cursor++] != "and") return false;
if (cursor >= tokens.Length || tokens[cursor++] != "#" || feature >= features.Count) return false;
matched &= MatchFeature(features[feature++], viewport); previous = true;
}
if (!previous || feature != features.Count) return false;
return negate ? !matched : matched;
}
private static bool MatchFeature(string text, BrowserViewport viewport)
{
string[] pair = text.Split(':', 2, StringSplitOptions.TrimEntries);
if (pair.Length == 1) return pair[0] switch { "width" => viewport.Width > 0, "height" => viewport.Height > 0, "color" or "hover" => true, _ => false };
string name = pair[0], value = pair[1];
bool minimum = name.StartsWith("min-", StringComparison.Ordinal), maximum = name.StartsWith("max-", StringComparison.Ordinal);
string axis = minimum || maximum ? name[4..] : name;
bool Compare(double actual, double expected) => minimum ? actual >= expected : maximum ? actual <= expected : Math.Abs(actual - expected) < .000001;
if (axis is "width" or "height" or "device-width" or "device-height")
{
double actual = axis.EndsWith("height", StringComparison.Ordinal) ? viewport.Height : viewport.Width;
return CssValue.Length(value, actual, 16, viewport, 16) is { } expected && Compare(actual, expected);
}
if (axis is "aspect-ratio" or "device-aspect-ratio")
{
var ratio = value.Split('/', StringSplitOptions.TrimEntries);
return ratio.Length == 2 && double.TryParse(ratio[0], CultureInfo.InvariantCulture, out double w) &&
double.TryParse(ratio[1], CultureInfo.InvariantCulture, out double h) && w >= 0 && h > 0 && Compare(viewport.Width / viewport.Height, w / h);
}
if (axis == "resolution")
{
double expected = value.EndsWith("dppx") ? CssValue.Number(value[..^4], double.NaN) :
value.EndsWith("dpi") ? CssValue.Number(value[..^3], double.NaN) / 96 :
value.EndsWith("dpcm") ? CssValue.Number(value[..^4], double.NaN) * 2.54 / 96 : double.NaN;
return double.IsFinite(expected) && expected > 0 && Compare(1, expected);
}
if (axis is "color" or "color-index" or "monochrome" or "device-pixel-ratio" or "-webkit-device-pixel-ratio")
return double.TryParse(value, CultureInfo.InvariantCulture, out double number) && Compare(axis == "color" ? 8 : axis.EndsWith("pixel-ratio") ? 1 : 0, number);
return name switch
{
"orientation" => value == (viewport.Width >= viewport.Height ? "landscape" : "portrait"),
"prefers-reduced-motion" => value == "reduce",
"prefers-color-scheme" => value == "light",
"forced-colors" => value == "none",
"hover" or "any-hover" => value == "hover",
"pointer" or "any-pointer" => value == "fine",
"display-mode" => value == "browser",
"scripting" => value == "enabled",
_ => false
};
}
}
using System.Globalization;
using System.Text;
namespace XFESystem.Browser;
/// <summary>Shared, bounded screen-media subset for CSS and MediaQueryList.</summary>
internal static class BrowserMediaQuery
{
internal static bool Matches(string query, BrowserViewport viewport)
{
if (query.Length > 2048) return false;
foreach (string branch in query.ToLowerInvariant().Split(','))
if (MatchBranch(branch.Trim(), viewport)) return true;
return false;
}
private static bool MatchBranch(string query, BrowserViewport viewport)
{
var features = new List<string>(); var outside = new StringBuilder();
for (int index = 0; index < query.Length; index++)
{
if (query[index] == ')') return false;
if (query[index] != '(') { outside.Append(query[index]); continue; }
int end = query.IndexOf(')', index + 1);
if (end < 0 || features.Count == 32 || query.AsSpan(index + 1, end - index - 1).Contains('(')) return false;
features.Add(query[(index + 1)..end].Trim()); outside.Append(" # "); index = end;
}
var tokens = outside.ToString().Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
int cursor = 0, feature = 0; bool negate = false, only = false, matched = true, previous = false;
if (cursor < tokens.Length && tokens[cursor] is "not" or "only") { negate = tokens[cursor] == "not"; only = tokens[cursor++] == "only"; }
if (cursor < tokens.Length && tokens[cursor] != "#")
{
string medium = tokens[cursor++];
if (!medium.All(character => char.IsAsciiLetter(character) || character == '-')) return false;
matched = medium is "screen" or "all"; previous = true;
}
else if (only) return false;
while (cursor < tokens.Length)
{
if (previous && tokens[cursor++] != "and") return false;
if (cursor >= tokens.Length || tokens[cursor++] != "#" || feature >= features.Count) return false;
matched &= MatchFeature(features[feature++], viewport); previous = true;
}
if (!previous || feature != features.Count) return false;
return negate ? !matched : matched;
}
private static bool MatchFeature(string text, BrowserViewport viewport)
{
string[] pair = text.Split(':', 2, StringSplitOptions.TrimEntries);
if (pair.Length == 1) return pair[0] switch { "width" => viewport.Width > 0, "height" => viewport.Height > 0, "color" or "hover" => true, _ => false };
string name = pair[0], value = pair[1];
bool minimum = name.StartsWith("min-", StringComparison.Ordinal), maximum = name.StartsWith("max-", StringComparison.Ordinal);
string axis = minimum || maximum ? name[4..] : name;
bool Compare(double actual, double expected) => minimum ? actual >= expected : maximum ? actual <= expected : Math.Abs(actual - expected) < .000001;
if (axis is "width" or "height" or "device-width" or "device-height")
{
double actual = axis.EndsWith("height", StringComparison.Ordinal) ? viewport.Height : viewport.Width;
return CssValue.Length(value, actual, 16, viewport, 16) is { } expected && Compare(actual, expected);
}
if (axis is "aspect-ratio" or "device-aspect-ratio")
{
var ratio = value.Split('/', StringSplitOptions.TrimEntries);
return ratio.Length == 2 && double.TryParse(ratio[0], CultureInfo.InvariantCulture, out double w) &&
double.TryParse(ratio[1], CultureInfo.InvariantCulture, out double h) && w >= 0 && h > 0 && Compare(viewport.Width / viewport.Height, w / h);
}
if (axis == "resolution")
{
double expected = value.EndsWith("dppx") ? CssValue.Number(value[..^4], double.NaN) :
value.EndsWith("dpi") ? CssValue.Number(value[..^3], double.NaN) / 96 :
value.EndsWith("dpcm") ? CssValue.Number(value[..^4], double.NaN) * 2.54 / 96 : double.NaN;
return double.IsFinite(expected) && expected > 0 && Compare(1, expected);
}
if (axis is "color" or "color-index" or "monochrome" or "device-pixel-ratio" or "-webkit-device-pixel-ratio")
return double.TryParse(value, CultureInfo.InvariantCulture, out double number) && Compare(axis == "color" ? 8 : axis.EndsWith("pixel-ratio") ? 1 : 0, number);
return name switch
{
"orientation" => value == (viewport.Width >= viewport.Height ? "landscape" : "portrait"),
"prefers-reduced-motion" => value == "reduce",
"prefers-color-scheme" => value == "light",
"forced-colors" => value == "none",
"hover" or "any-hover" => value == "hover",
"pointer" or "any-pointer" => value == "fine",
"display-mode" => value == "browser",
"scripting" => value == "enabled",
_ => false
};
}
}