diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs
index 7996cb334..343bdf8cb 100644
--- a/Source/HtmlRenderer/Core/CssData.cs
+++ b/Source/HtmlRenderer/Core/CssData.cs
@@ -348,7 +348,7 @@ private static void CollectIndexKeys(ISelector selector, List<(SelectorBucketKin
// re-derived at match time (see DoesSelectorMatch), including for synthesized
// pseudo-element boxes whose HtmlTag is null - the tag/class/id index can't
// safely represent that, so fall back to a full scan for these.
- if (compoundSelector.Any(s => s is PseudoElementSelector || s is FirstChildSelector))
+ if (compoundSelector.Any(s => s is PseudoElementSelector || IsChildIndexedPseudoClass(s)))
{
keys.Add((SelectorBucketKind.Universal, string.Empty));
return;
@@ -629,6 +629,14 @@ private static bool DoesSelectorMatch(AttrAvailableSelector attrAvailableSelecto
return false;
}
+ // Selectors 4 §6.3, the s flag compares the value case-sensitively
+ private static StringComparison ValueComparison(AttrSelectorBase selector)
+ {
+ return selector.CaseSensitivity == AttrCaseSensitivity.Sensitive
+ ? StringComparison.Ordinal
+ : StringComparison.InvariantCultureIgnoreCase;
+ }
+
private static bool DoesSelectorMatch(AttrMatchSelector attrMatchSelector, CssBox box)
{
if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null)
@@ -640,7 +648,7 @@ private static bool DoesSelectorMatch(AttrMatchSelector attrMatchSelector, CssBo
{
if (attribute.Key.Equals(attrMatchSelector.Attribute, StringComparison.InvariantCultureIgnoreCase))
{
- return attribute.Value.Equals(attrMatchSelector.Value, StringComparison.InvariantCultureIgnoreCase);
+ return attribute.Value.Equals(attrMatchSelector.Value, ValueComparison(attrMatchSelector));
}
}
@@ -660,7 +668,7 @@ private static bool DoesSelectorMatch(AttrListSelector attrListSelector, CssBox
{
var attributeValues = attribute.Value.Split(' ').Where(x => x.Length > 0).ToArray();
- return attributeValues.Any(value => value.Equals(attrListSelector.Value, StringComparison.InvariantCultureIgnoreCase));
+ return attributeValues.Any(value => value.Equals(attrListSelector.Value, ValueComparison(attrListSelector)));
}
}
@@ -678,7 +686,7 @@ private static bool DoesSelectorMatch(AttrContainsSelector attrContainsSelector,
{
if (attribute.Key.Equals(attrContainsSelector.Attribute, StringComparison.InvariantCultureIgnoreCase))
{
- return attribute.Value.IndexOf(attrContainsSelector.Value, StringComparison.InvariantCultureIgnoreCase) >= 0;
+ return attribute.Value.IndexOf(attrContainsSelector.Value, ValueComparison(attrContainsSelector)) >= 0;
}
}
@@ -690,7 +698,7 @@ private static bool DoesSelectorMatch(AttrBeginsSelector s, CssBox box)
if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false;
foreach (var attr in box.HtmlTag.Attributes)
if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase))
- return attr.Value.StartsWith(s.Value, StringComparison.InvariantCultureIgnoreCase);
+ return attr.Value.StartsWith(s.Value, ValueComparison(s));
return false;
}
@@ -699,7 +707,7 @@ private static bool DoesSelectorMatch(AttrEndsSelector s, CssBox box)
if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false;
foreach (var attr in box.HtmlTag.Attributes)
if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase))
- return attr.Value.EndsWith(s.Value, StringComparison.InvariantCultureIgnoreCase);
+ return attr.Value.EndsWith(s.Value, ValueComparison(s));
return false;
}
@@ -708,8 +716,8 @@ private static bool DoesSelectorMatch(AttrHyphenSelector s, CssBox box)
if (box == null || box.HtmlTag == null || box.HtmlTag.Attributes == null) return false;
foreach (var attr in box.HtmlTag.Attributes)
if (attr.Key.Equals(s.Attribute, StringComparison.InvariantCultureIgnoreCase))
- return attr.Value.Equals(s.Value, StringComparison.InvariantCultureIgnoreCase)
- || attr.Value.StartsWith(s.Value + "-", StringComparison.InvariantCultureIgnoreCase);
+ return attr.Value.Equals(s.Value, ValueComparison(s))
+ || attr.Value.StartsWith(s.Value + "-", ValueComparison(s));
return false;
}
@@ -724,6 +732,15 @@ private static bool DoesSelectorMatch(PseudoClassSelector pseudoClassSelector, C
if (pseudoClassSelector.Class == "root")
return box.HtmlTag != null && box.HtmlTag.Name.Equals("html", StringComparison.InvariantCultureIgnoreCase);
+ // Selectors 4 §13.3 and §13.4, the bare forms are their nth-*() function with An+B = 1
+ switch (pseudoClassSelector.Class)
+ {
+ case "first-child": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), false, false);
+ case "last-child": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), false, true);
+ case "first-of-type": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), true, false);
+ case "last-of-type": return MatchesChildPosition(box, 0, 1, AllSelector.Create(), true, true);
+ }
+
// CSS 2.1 §5.11.2. There's no history, every link is unvisited and :visited never matches.
return pseudoClassSelector.Class == "link" && box.IsClickable;
}
@@ -740,10 +757,15 @@ private static bool DoesSelectorMatch(PseudoElementSelector pseudoElementSelecto
return false;
}
- ///
- /// The child-indexed pseudo-classes, Selectors 4 §13.3 and §13.4, including the bare forms like
- /// :first-child, which PseudoClassSelectorFactory creates with Step 0 and Offset 1.
- ///
+ private static bool IsChildIndexedPseudoClass(ISelector selector)
+ {
+ if (selector is ChildSelector) return true;
+ if (!(selector is PseudoClassSelector pseudoClass)) return false;
+ return pseudoClass.Class == "first-child" || pseudoClass.Class == "last-child"
+ || pseudoClass.Class == "first-of-type" || pseudoClass.Class == "last-of-type";
+ }
+
+ /// The child-indexed pseudo-classes, Selectors 4 §13.3 and §13.4.
private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
{
if (box == null || box.HtmlTag == null)
@@ -756,6 +778,18 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
return DoesColumnSelectorMatch(childSelector, box, childSelector is LastColumnSelector);
}
+ var sameTypeOnly = childSelector is FirstTypeSelector || childSelector is LastTypeSelector;
+ var fromEnd = childSelector is LastChildSelector || childSelector is LastTypeSelector;
+ return MatchesChildPosition(box, childSelector.Step, childSelector.Offset, childSelector.Kind, sameTypeOnly, fromEnd);
+ }
+
+ private static bool MatchesChildPosition(CssBox box, int step, int offset, ISelector of, bool sameTypeOnly, bool fromEnd)
+ {
+ if (box == null || box.HtmlTag == null)
+ {
+ return false;
+ }
+
var parentBox = DomUtils.GetNearestParentElementBox(box);
if (parentBox == null)
{
@@ -764,7 +798,6 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
IEnumerable siblingBoxes = parentBox.Boxes.Where(b => b.HtmlTag != null);
- var sameTypeOnly = childSelector is FirstTypeSelector || childSelector is LastTypeSelector;
if (sameTypeOnly)
{
siblingBoxes = siblingBoxes.Where(b =>
@@ -772,7 +805,7 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
}
// The "of S" filter of :nth-child()/:nth-last-child(). Without it Kind is AllSelector.
- siblingBoxes = siblingBoxes.Where(b => DoesSelectorMatch(childSelector.Kind, b));
+ siblingBoxes = siblingBoxes.Where(b => DoesSelectorMatch(of, b));
var siblings = siblingBoxes.ToList();
var index = siblings.IndexOf(box);
@@ -781,10 +814,9 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
return false;
}
- var fromEndPosition = childSelector is LastChildSelector || childSelector is LastTypeSelector;
- var position = fromEndPosition ? siblings.Count - index : index + 1;
+ var position = fromEnd ? siblings.Count - index : index + 1;
- return MatchesAnPlusB(position, childSelector.Step, childSelector.Offset);
+ return MatchesAnPlusB(position, step, offset);
}
///
diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs
index 109022872..aa5efd949 100644
--- a/Source/HtmlRenderer/Core/CssDefaults.cs
+++ b/Source/HtmlRenderer/Core/CssDefaults.cs
@@ -163,6 +163,7 @@ @media print {
{ "font-size", "medium" },
{ "font-style", "normal" },
{ "font-variant", "normal" },
+ { "font-variant-caps", "normal" },
{ "font-weight", "normal" },
{ "font-stretch", "normal" },
{ "float", "none" },
@@ -196,6 +197,7 @@ @media print {
{ "orphans", "2" },
{ "page", "auto" },
{ "text-align", "" },
+ { "text-align-all", "" },
{ "text-decoration-line", "" },
{ "text-indent", "0" },
{ "top", "auto" },
@@ -207,6 +209,8 @@ @media print {
{ "word-break", "normal" },
{ "visibility", "visible" },
{ "white-space", "normal" },
+ { "white-space-collapse", "collapse" },
+ { "text-wrap-mode", "wrap" },
};
///
@@ -219,10 +223,10 @@ @media print {
"border-spacing", "border-collapse",
"color",
"empty-cells",
- "white-space",
+ "white-space", "white-space-collapse", "text-wrap-mode",
"visibility",
- "text-indent", "text-align",
- "font-family", "font-size", "font-style", "font-variant", "font-weight", "font-stretch",
+ "text-indent", "text-align", "text-align-all",
+ "font-family", "font-size", "font-style", "font-variant", "font-variant-caps", "font-weight", "font-stretch",
"list-style-image", "list-style-position", "list-style-type", "list-style",
"line-height",
"word-break",
@@ -237,7 +241,7 @@ @media print {
internal static string GetInitialValue(string propertyName)
{
string value;
- return InitialValues.TryGetValue(propertyName, out value) ? value : null;
+ return InitialValues.TryGetValue(Utils.CssUtils.ToPhysicalPropertyName(propertyName), out value) ? value : null;
}
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs
deleted file mode 100644
index 54e2e7907..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs
+++ /dev/null
@@ -1,97 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// The aspect-ratio grammar (CSS Sizing 4 §4.1) and the <ratio> type (CSS Values 4 §5.7).
- ///
- internal static class AspectRatioGrammar
- {
- ///
- /// is width / height, or null for a bare auto or a degenerate ratio.
- ///
- internal static bool TryParse(IReadOnlyList tokens, out double? ratio) =>
- TryParse(tokens, out ratio, out _);
-
- ///
- /// Like , tells
- /// auto <ratio>, where a replaced element's natural ratio wins, from a bare <ratio>.
- ///
- internal static bool TryParse(IReadOnlyList tokens, out double? ratio, out bool hasAuto)
- {
- ratio = null;
- hasAuto = false;
-
- Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
- if (toks.Length == 0) return false;
-
- var i = 0;
-
- if (IsAuto(toks[i])) { hasAuto = true; i++; }
-
- if (i == toks.Length) return hasAuto; // bare `auto`
-
- // = [ / ]?
- if (!TryRatioCore(toks, ref i, out ratio)) return false;
-
- // An optional trailing `auto` (the `||` allows either order: ` auto`).
- if (i < toks.Length && !hasAuto && IsAuto(toks[i])) { hasAuto = true; i++; }
-
- if (i != toks.Length) return false; // trailing junk
-
- return true;
- }
-
- ///
- /// A bare <ratio> without auto, for the @property syntax matcher.
- ///
- internal static bool TryParseRatio(IReadOnlyList tokens, out double? ratio)
- {
- ratio = null;
-
- Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
-
- var i = 0;
- if (!TryRatioCore(toks, ref i, out ratio)) return false;
- return i == toks.Length; // no trailing junk (and, since we never consume `auto`, no `auto`)
- }
-
- private static bool TryRatioCore(Token[] toks, ref int i, out double? ratio)
- {
- ratio = null;
-
- // [ / ]?
- if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out var width)) return false;
- double height = 1;
- i++;
-
- if (i < toks.Length && IsSlash(toks[i]))
- {
- i++;
- if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out height)) return false;
- i++;
- }
-
- // A zero term degenerates to "no preferred aspect ratio".
- ratio = width <= 0 || height <= 0 ? (double?)null : width / height;
- return true;
- }
-
- private static bool IsAuto(Token token) => token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto);
-
- private static bool IsSlash(Token token) => token.Type == TokenType.Delim && token.Data == "/";
-
- private static bool TryNonNegativeNumber(Token token, out double value)
- {
- if (token is NumberToken number && number.Value >= 0f)
- {
- value = number.Value;
- return true;
- }
-
- value = 0;
- return false;
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
deleted file mode 100644
index b3e52ec8f..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
+++ /dev/null
@@ -1,186 +0,0 @@
-using System.Collections.Generic;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// Grammar of a background-position value, CSS Backgrounds 3 §2.6: the 1 to 4 token forms, including edge offsets
- /// like "right 20px bottom 10px". Used by and
- /// to validate and serialize the value.
- ///
- internal static class BackgroundPositionGrammar
- {
- internal enum AxisKeyword { None, Left, Right, Top, Bottom, Center }
-
- internal readonly struct Component
- {
- public AxisKeyword Keyword { get; }
-
- /// The length/percentage/calc() token for this component, or null when there is
- /// none (a keyword-only component, or the implicit "center" filled in for a 1-token value).
- public Token Offset { get; }
-
- public Component(AxisKeyword keyword, Token offset)
- {
- Keyword = keyword;
- Offset = offset;
- }
- }
-
- internal sealed class ParsedPosition
- {
- public Component X { get; }
- public Component Y { get; }
-
- /// The 1-2 authored components in original left-to-right order, needed to
- /// re-serialize in the exact order/form the author wrote (CSS round-trips verbatim).
- public IReadOnlyList AuthoredOrder { get; }
-
- public ParsedPosition(Component x, Component y, IReadOnlyList authoredOrder)
- {
- X = x;
- Y = y;
- AuthoredOrder = authoredOrder;
- }
- }
-
- internal static ParsedPosition TryParse(IReadOnlyList tokens)
- {
- switch (tokens.Count)
- {
- case 1:
- return ParseSingle(tokens[0]);
- case 2:
- return ParseDouble(tokens[0], tokens[1]);
- case 3:
- return ParseTriple(tokens[0], tokens[1], tokens[2]);
- case 4:
- return ParseQuadruple(tokens[0], tokens[1], tokens[2], tokens[3]);
- default:
- return null;
- }
- }
-
- private static ParsedPosition ParseSingle(Token token)
- {
- var keyword = KeywordOf(token);
- if (keyword == AxisKeyword.None)
- {
- if (!IsLengthOrPercent(token)) return null;
-
- var bare = new Component(AxisKeyword.None, token);
- var center = new Component(AxisKeyword.Center, null);
- return new ParsedPosition(bare, center, new[] { bare });
- }
-
- if (IsVerticalOnly(keyword))
- {
- var y = new Component(keyword, null);
- var x = new Component(AxisKeyword.Center, null);
- return new ParsedPosition(x, y, new[] { y });
- }
-
- var xComp = new Component(keyword, null);
- var yComp = new Component(AxisKeyword.Center, null);
- return new ParsedPosition(xComp, yComp, new[] { xComp });
- }
-
- private static ParsedPosition ParseDouble(Token t0, Token t1)
- {
- // "h then v" (horizontal-ish first): left/right/center/bare-length, then top/bottom/center/bare-length.
- if (IsHSlotValid(t0) && IsVSlotValid(t1))
- {
- var x = ToComponent(t0);
- var y = ToComponent(t1);
- return new ParsedPosition(x, y, new[] { x, y });
- }
-
- // "v then h" (vertical-ish first, e.g. "bottom center", "top left").
- if (IsVSlotValid(t0) && IsHSlotValid(t1))
- {
- var y = ToComponent(t0);
- var x = ToComponent(t1);
- return new ParsedPosition(x, y, new[] { y, x });
- }
-
- return null;
- }
-
- private static ParsedPosition ParseTriple(Token t0, Token t1, Token t2)
- {
- // hi, vi, offset (offset applies to the vertical group): e.g. "right bottom 20px".
- if (IsEdgeKeywordSlot(t0, horizontal: true) && IsEdgeKeywordSlot(t1, horizontal: false) && IsLengthOrPercent(t2))
- {
- var x = new Component(KeywordOf(t0), null);
- var y = new Component(KeywordOf(t1), t2);
- return new ParsedPosition(x, y, new[] { x, y });
- }
-
- // hi, offset, vi (offset applies to the horizontal group): e.g. "right 20px bottom".
- if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) && IsEdgeKeywordSlot(t2, horizontal: false))
- {
- var x = new Component(KeywordOf(t0), t1);
- var y = new Component(KeywordOf(t2), null);
- return new ParsedPosition(x, y, new[] { x, y });
- }
-
- return null;
- }
-
- private static ParsedPosition ParseQuadruple(Token t0, Token t1, Token t2, Token t3)
- {
- // hi, offset, vi, offset: e.g. "right 20px bottom 10px".
- if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) &&
- IsEdgeKeywordSlot(t2, horizontal: false) && IsLengthOrPercent(t3))
- {
- var x = new Component(KeywordOf(t0), t1);
- var y = new Component(KeywordOf(t2), t3);
- return new ParsedPosition(x, y, new[] { x, y });
- }
-
- return null;
- }
-
- private static Component ToComponent(Token token) =>
- new Component(KeywordOf(token), KeywordOf(token) == AxisKeyword.None ? token : null);
-
- private static AxisKeyword KeywordOf(Token token)
- {
- if (token.Type != TokenType.Ident) return AxisKeyword.None;
- if (token.Data.Isi(Keywords.Left)) return AxisKeyword.Left;
- if (token.Data.Isi(Keywords.Right)) return AxisKeyword.Right;
- if (token.Data.Isi(Keywords.Top)) return AxisKeyword.Top;
- if (token.Data.Isi(Keywords.Bottom)) return AxisKeyword.Bottom;
- if (token.Data.Isi(Keywords.Center)) return AxisKeyword.Center;
- return AxisKeyword.None;
- }
-
- private static bool IsHorizontalOnly(AxisKeyword k) => k == AxisKeyword.Left || k == AxisKeyword.Right;
- private static bool IsVerticalOnly(AxisKeyword k) => k == AxisKeyword.Top || k == AxisKeyword.Bottom;
-
- private static bool IsLengthOrPercent(Token token) =>
- Converters.LengthOrPercentConverter.Convert(new[] { token }) != null;
-
- /// Valid for the 2-value "h" slot: left/right/center keywords, or a bare length/percent - not top/bottom.
- private static bool IsHSlotValid(Token token)
- {
- var k = KeywordOf(token);
- return k != AxisKeyword.None ? !IsVerticalOnly(k) : IsLengthOrPercent(token);
- }
-
- /// Valid for the 2-value "v" slot: top/bottom/center keywords, or a bare length/percent - not left/right.
- private static bool IsVSlotValid(Token token)
- {
- var k = KeywordOf(token);
- return k != AxisKeyword.None ? !IsHorizontalOnly(k) : IsLengthOrPercent(token);
- }
-
- /// Valid for the 3/4-value "hi"/"vi" edge-keyword slots: keyword only, no bare-length fallback.
- private static bool IsEdgeKeywordSlot(Token token, bool horizontal)
- {
- var k = KeywordOf(token);
- if (k == AxisKeyword.Center) return true;
- if (k == AxisKeyword.None) return false;
- return horizontal ? IsHorizontalOnly(k) : IsVerticalOnly(k);
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
deleted file mode 100644
index 9d56175fd..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
+++ /dev/null
@@ -1,86 +0,0 @@
-using System.Collections.Generic;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// Grammar of a background-size value, CSS Backgrounds 3 §2.9: auto, cover, contain or
- /// 1 to 2 length, percentage or auto tokens. Used by
- /// to validate and serialize the value.
- ///
- internal static class BackgroundSizeGrammar
- {
- internal readonly struct Component
- {
- public bool IsAuto { get; }
-
- /// The length/percentage/calc() token for this component, null when .
- public Token Value { get; }
-
- public Component(bool isAuto, Token value)
- {
- IsAuto = isAuto;
- Value = value;
- }
- }
-
- internal sealed class ParsedSize
- {
- public bool IsCover { get; }
- public bool IsContain { get; }
- public Component Width { get; }
- public Component Height { get; }
-
- /// False for the 1-token auto/length/percent form (height defaults to auto for
- /// resolution purposes, but must NOT be re-serialized - "2em" stays "2em", not "2em auto").
- public bool HasExplicitHeight { get; }
-
- private ParsedSize(bool isCover, bool isContain, Component width, Component height, bool hasExplicitHeight)
- {
- IsCover = isCover;
- IsContain = isContain;
- Width = width;
- Height = height;
- HasExplicitHeight = hasExplicitHeight;
- }
-
- public static ParsedSize Cover() => new ParsedSize(true, false, default, default, false);
- public static ParsedSize Contain() => new ParsedSize(false, true, default, default, false);
- public static ParsedSize SingleValue(Component width) => new ParsedSize(false, false, width, AutoComponent, false);
- public static ParsedSize TwoValue(Component width, Component height) => new ParsedSize(false, false, width, height, true);
- }
-
- private static readonly Component AutoComponent = new Component(true, null);
-
- internal static ParsedSize TryParse(IReadOnlyList tokens)
- {
- if (tokens.Count == 1)
- {
- var t = tokens[0];
- if (IsKeyword(t, Keywords.Cover)) return ParsedSize.Cover();
- if (IsKeyword(t, Keywords.Contain)) return ParsedSize.Contain();
-
- var w = ToComponent(t);
- return w is null ? null : ParsedSize.SingleValue(w.Value);
- }
-
- if (tokens.Count == 2)
- {
- var w = ToComponent(tokens[0]);
- var h = ToComponent(tokens[1]);
- return w is null || h is null ? null : ParsedSize.TwoValue(w.Value, h.Value);
- }
-
- return null;
- }
-
- private static Component? ToComponent(Token token)
- {
- if (IsKeyword(token, Keywords.Auto)) return AutoComponent;
- if (Converters.LengthOrPercentConverter.Convert(new[] { token }) != null) return new Component(false, token);
- return null;
- }
-
- private static bool IsKeyword(Token token, string keyword) =>
- token.Type == TokenType.Ident && token.Data.Isi(keyword);
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs
deleted file mode 100644
index 451ee9959..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs
+++ /dev/null
@@ -1,386 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// The <basic-shape> functions polygon(), inset(), circle() and ellipse()
- /// (CSS Shapes 1 §3), parsed into unresolved component strings and enums.
- ///
- internal static class BasicShapeGrammar
- {
- internal enum BasicShapeKind { Polygon, Inset, Circle, Ellipse }
-
- internal enum FillRule { Nonzero, Evenodd }
-
- internal enum ShapeRadiusKind { LengthPercentage, ClosestSide, FarthestSide }
-
- /// A <shape-radius>: either an explicit length-percentage (component string
- /// in ) or one of the closest-side/farthest-side keywords.
- internal readonly struct ShapeRadius
- {
- public ShapeRadiusKind Kind { get; }
-
- /// The length-percentage component string when is
- /// ; otherwise null.
- public string Length { get; }
-
- private ShapeRadius(ShapeRadiusKind kind, string length)
- {
- Kind = kind;
- Length = length;
- }
-
- public static readonly ShapeRadius ClosestSide = new ShapeRadius(ShapeRadiusKind.ClosestSide, null);
- public static readonly ShapeRadius FarthestSide = new ShapeRadius(ShapeRadiusKind.FarthestSide, null);
- public static ShapeRadius FromLength(string length) => new ShapeRadius(ShapeRadiusKind.LengthPercentage, length);
- }
-
- /// A single <length-percentage> <length-percentage> polygon vertex,
- /// stored as the two authored component strings.
- internal readonly struct Point
- {
- public string X { get; }
- public string Y { get; }
-
- public Point(string x, string y)
- {
- X = x;
- Y = y;
- }
- }
-
- internal sealed class ParsedBasicShape
- {
- public BasicShapeKind Kind { get; private set; }
-
- // --- polygon() ---
- public FillRule PolygonFillRule { get; private set; }
- public IReadOnlyList PolygonPoints { get; private set; }
-
- // --- inset() ---
- /// The four inset offsets in [top, right, bottom, left] order (CSS shorthand-filled),
- /// each an authored length-percentage component string.
- public IReadOnlyList InsetEdges { get; private set; }
-
- /// Whether an inset(... round <border-radius>) corner radius was present.
- public bool InsetHasRound { get; private set; }
- public IReadOnlyList InsetRoundRadius { get; private set; }
-
- // --- circle() / ellipse() ---
- /// circle: the single radius. ellipse: the x-radius.
- public ShapeRadius RadiusX { get; private set; }
- /// ellipse: the y-radius. Unused for circle.
- public ShapeRadius RadiusY { get; private set; }
- /// Center x as an authored length-percentage component string (position keywords
- /// already resolved to 0%/50%/100%). Default 50%.
- public string CenterX { get; private set; }
- /// Center y, same convention as .
- public string CenterY { get; private set; }
-
- internal static ParsedBasicShape Polygon(FillRule fillRule, IReadOnlyList points) => new ParsedBasicShape
- {
- Kind = BasicShapeKind.Polygon,
- PolygonFillRule = fillRule,
- PolygonPoints = points,
- };
-
- internal static ParsedBasicShape Inset(IReadOnlyList edges, bool hasRound, IReadOnlyList roundRadius) => new ParsedBasicShape
- {
- Kind = BasicShapeKind.Inset,
- InsetEdges = edges,
- InsetHasRound = hasRound,
- InsetRoundRadius = roundRadius,
- };
-
- internal static ParsedBasicShape Circle(ShapeRadius radius, string centerX, string centerY) => new ParsedBasicShape
- {
- Kind = BasicShapeKind.Circle,
- RadiusX = radius,
- CenterX = centerX,
- CenterY = centerY,
- };
-
- internal static ParsedBasicShape Ellipse(ShapeRadius radiusX, ShapeRadius radiusY, string centerX, string centerY) => new ParsedBasicShape
- {
- Kind = BasicShapeKind.Ellipse,
- RadiusX = radiusX,
- RadiusY = radiusY,
- CenterX = centerX,
- CenterY = centerY,
- };
- }
-
- ///
- /// Null for anything that isn't a basic shape, none included.
- ///
- internal static ParsedBasicShape TryParse(IReadOnlyList tokens)
- {
- var significant = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
-
- if (significant.Length != 1) return null;
- var function = significant[0] as FunctionToken;
- if (function == null) return null;
-
- var args = function.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
-
- if (function.Data.Isi(FunctionNames.Polygon)) return ParsePolygon(args);
- if (function.Data.Isi(FunctionNames.Inset)) return ParseInset(args);
- if (function.Data.Isi(FunctionNames.Circle)) return ParseCircle(args);
- if (function.Data.Isi(FunctionNames.Ellipse)) return ParseEllipse(args);
-
- return null;
- }
-
- private static ParsedBasicShape ParsePolygon(IReadOnlyList args)
- {
- var groups = SplitByComma(args);
- if (groups.Count == 0) return null;
-
- var fillRule = FillRule.Nonzero;
- var firstGroup = 0;
-
- // An optional leading fill-rule ident is its own comma-separated group: "polygon(evenodd, x y, ...)".
- if (groups[0].Count == 1 && TryFillRule(groups[0][0], out var parsedRule))
- {
- fillRule = parsedRule;
- firstGroup = 1;
- }
-
- var points = new List();
-
- for (var i = firstGroup; i < groups.Count; i++)
- {
- var group = groups[i];
- if (group.Count != 2) return null;
- if (!IsLengthPercentage(group[0]) || !IsLengthPercentage(group[1])) return null;
- points.Add(new Point(group[0].ToValue(), group[1].ToValue()));
- }
-
- return points.Count == 0 ? null : ParsedBasicShape.Polygon(fillRule, points);
- }
-
- private static ParsedBasicShape ParseInset(IReadOnlyList args)
- {
- if (args.Count == 0) return null;
-
- var roundIndex = -1;
- for (var i = 0; i < args.Count; i++)
- {
- if (args[i].Type == TokenType.Ident && args[i].Data.Isi(Keywords.Round))
- {
- roundIndex = i;
- break;
- }
- }
-
- var lengthTokens = roundIndex >= 0 ? args.Take(roundIndex).ToArray() : args.ToArray();
- Token[] roundTokens = roundIndex >= 0 ? args.Skip(roundIndex + 1).ToArray() : new Token[0];
-
- if (lengthTokens.Length < 1 || lengthTokens.Length > 4) return null;
- if (lengthTokens.Any(t => !IsLengthPercentage(t))) return null;
-
- // "round" with no radius following it is malformed.
- if (roundIndex >= 0 && roundTokens.Length == 0) return null;
-
- var values = lengthTokens.Select(t => t.ToValue()).ToArray();
- var (top, right, bottom, left) = ExpandBox(values);
-
- return ParsedBasicShape.Inset(new[] { top, right, bottom, left }, roundIndex >= 0, roundTokens);
- }
-
- private static ParsedBasicShape ParseCircle(IReadOnlyList args)
- {
- if (!SplitAtKeyword(args, Keywords.At, out var radiusTokens, out var hasAt, out var positionTokens))
- return null;
-
- ShapeRadius radius;
- switch (radiusTokens.Count)
- {
- case 0:
- radius = ShapeRadius.ClosestSide;
- break;
- case 1:
- if (!TryShapeRadius(radiusTokens[0], out radius)) return null;
- break;
- default:
- return null;
- }
-
- if (!ResolveCenter(hasAt, positionTokens, out var centerX, out var centerY)) return null;
-
- return ParsedBasicShape.Circle(radius, centerX, centerY);
- }
-
- private static ParsedBasicShape ParseEllipse(IReadOnlyList args)
- {
- if (!SplitAtKeyword(args, Keywords.At, out var radiusTokens, out var hasAt, out var positionTokens))
- return null;
-
- ShapeRadius rx, ry;
- switch (radiusTokens.Count)
- {
- case 0:
- rx = ShapeRadius.ClosestSide;
- ry = ShapeRadius.ClosestSide;
- break;
- case 2:
- if (!TryShapeRadius(radiusTokens[0], out rx)) return null;
- if (!TryShapeRadius(radiusTokens[1], out ry)) return null;
- break;
- default:
- return null;
- }
-
- if (!ResolveCenter(hasAt, positionTokens, out var centerX, out var centerY)) return null;
-
- return ParsedBasicShape.Ellipse(rx, ry, centerX, centerY);
- }
-
- /// Resolves the optional at <position> tail (via the shared
- /// ) to center-x/center-y component strings; defaults to
- /// 50% 50% when absent.
- private static bool ResolveCenter(bool hasAt, IReadOnlyList positionTokens, out string centerX, out string centerY)
- {
- centerX = "50%";
- centerY = "50%";
-
- if (!hasAt) return true;
-
- var parsed = BackgroundPositionGrammar.TryParse(positionTokens);
- if (parsed is null) return false;
-
- centerX = ComponentToLength(parsed.X, horizontal: true);
- centerY = ComponentToLength(parsed.Y, horizontal: false);
- return true;
- }
-
- private static string ComponentToLength(BackgroundPositionGrammar.Component c, bool horizontal)
- {
- switch (c.Keyword)
- {
- case BackgroundPositionGrammar.AxisKeyword.Center:
- return "50%";
- case BackgroundPositionGrammar.AxisKeyword.Left:
- case BackgroundPositionGrammar.AxisKeyword.Top:
- // "left"/"top" == 0% edge; "left 20px" == 20px in from that edge.
- return c.Offset != null ? c.Offset.ToValue() : "0%";
- case BackgroundPositionGrammar.AxisKeyword.Right:
- case BackgroundPositionGrammar.AxisKeyword.Bottom:
- // "right"/"bottom" == 100% edge; "right 20px" == 20px in from the far edge.
- return c.Offset != null ? $"calc(100% - {c.Offset.ToValue()})" : "100%";
- default: // a bare length-percentage
- return c.Offset.ToValue();
- }
- }
-
- /// Splits tokens at the first standalone ident equal to
- /// (e.g. at). Fails if the keyword appears with nothing after it.
- private static bool SplitAtKeyword(IReadOnlyList tokens, string keyword,
- out IReadOnlyList before, out bool found, out IReadOnlyList after)
- {
- for (var i = 0; i < tokens.Count; i++)
- {
- if (tokens[i].Type == TokenType.Ident && tokens[i].Data.Isi(keyword))
- {
- before = tokens.Take(i).ToArray();
- after = tokens.Skip(i + 1).ToArray();
- found = true;
- return after.Count > 0;
- }
- }
-
- before = tokens.ToArray();
- after = new Token[0];
- found = false;
- return true;
- }
-
- private static List> SplitByComma(IReadOnlyList tokens)
- {
- var groups = new List>();
- var current = new List();
-
- foreach (var token in tokens)
- {
- if (token.Type == TokenType.Comma)
- {
- groups.Add(current);
- current = new List();
- }
- else
- {
- current.Add(token);
- }
- }
-
- groups.Add(current);
-
- // A trailing/leading/doubled comma yields an empty group, which is always malformed here.
- return groups.Any(g => g.Count == 0) && tokens.Count > 0 ? new List>() : groups;
- }
-
- private static (string top, string right, string bottom, string left) ExpandBox(IReadOnlyList values)
- {
- switch (values.Count)
- {
- case 1: return (values[0], values[0], values[0], values[0]);
- case 2: return (values[0], values[1], values[0], values[1]);
- case 3: return (values[0], values[1], values[2], values[1]);
- default: return (values[0], values[1], values[2], values[3]);
- }
- }
-
- private static bool TryFillRule(Token token, out FillRule fillRule)
- {
- if (token.Type == TokenType.Ident)
- {
- if (token.Data.Isi(Keywords.Nonzero)) { fillRule = FillRule.Nonzero; return true; }
- if (token.Data.Isi(Keywords.Evenodd)) { fillRule = FillRule.Evenodd; return true; }
- }
-
- fillRule = FillRule.Nonzero;
- return false;
- }
-
- private static bool TryShapeRadius(Token token, out ShapeRadius radius)
- {
- if (token.Type == TokenType.Ident)
- {
- if (token.Data.Isi(Keywords.ClosestSide)) { radius = ShapeRadius.ClosestSide; return true; }
- if (token.Data.Isi(Keywords.FarthestSide)) { radius = ShapeRadius.FarthestSide; return true; }
- radius = default;
- return false;
- }
-
- if (IsLengthPercentage(token))
- {
- // , CSS Shapes 1 §3.1
- var unitToken = token as UnitToken;
- var numberToken = token as NumberToken;
- if ((unitToken != null && unitToken.Value < 0f) || (numberToken != null && numberToken.Value < 0f))
- {
- radius = default(ShapeRadius);
- return false;
- }
-
- radius = ShapeRadius.FromLength(token.ToValue());
- return true;
- }
-
- radius = default;
- return false;
- }
-
- private static bool IsLengthPercentage(Token token)
- {
- if (token.Type == TokenType.Dimension || token.Type == TokenType.Percentage) return true;
- // calc() stays unevaluated, ToValue() keeps its text
- var function = token as FunctionToken;
- if (function != null && CalcParser.IsCalcFamily(function.Data)) return true;
- // Unitless zero is a valid length.
- var number = token as NumberToken;
- return number != null && number.Value == 0f;
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs
deleted file mode 100644
index 02661e877..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs
+++ /dev/null
@@ -1,198 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// Grammar of a box-shadow value, CSS Backgrounds 3 §6.1. Keeps the components as authored
- /// strings, em/rem lengths can only be resolved against the box.
- ///
- internal static class BoxShadowGrammar
- {
- ///
- /// One shadow layer. and default to "0", a null
- /// means currentcolor.
- ///
- internal sealed class ShadowLayer
- {
- public bool Inset { get; set; }
- public string OffsetX { get; set; }
- public string OffsetY { get; set; }
- public string Blur { get; set; }
- public string Spread { get; set; }
- public string Color { get; set; }
- }
-
- private static readonly IValueConverter ColorValidator = Converters.ColorConverter.WithCurrentColor();
-
- ///
- /// The layers in declaration order, an empty list for none, or null if the value is invalid.
- ///
- internal static List TryParse(IReadOnlyList tokens)
- {
- var significant = NormalizeHexColorTokens(tokens.Where(t => t.Type != TokenType.Whitespace).ToArray());
-
- if (significant.Count == 0) return null;
-
- if (significant.Count == 1 && significant[0].Type == TokenType.Ident && significant[0].Data.Isi(Keywords.None))
- return new List();
-
- var layers = new List();
-
- foreach (var group in SplitByComma(significant))
- {
- if (group.Count == 0) return null; // leading/trailing/doubled comma
-
- var layer = ParseLayer(group);
- if (layer is null) return null;
-
- layers.Add(layer);
- }
-
- return layers;
- }
-
- private static ShadowLayer ParseLayer(IReadOnlyList group)
- {
- var insetSeen = false;
- var lengths = new List();
- var colorTokens = new List();
- var lengthsClosed = false; // set once a non-length token appears after lengths have begun
-
- foreach (var token in group)
- {
- if (IsInset(token))
- {
- if (insetSeen) return null; // "inset" at most once
- insetSeen = true;
- if (lengths.Count > 0) lengthsClosed = true;
- }
- else if (IsLength(token))
- {
- if (lengthsClosed) return null; // {2,4} must be one contiguous run
- lengths.Add(token);
- }
- else
- {
- if (lengths.Count > 0) lengthsClosed = true;
- colorTokens.Add(token);
- }
- }
-
- // Two to four lengths: offset-x, offset-y, [blur], [spread].
- if (lengths.Count < 2 || lengths.Count > 4) return null;
-
- // Blur radius (the third length) must be non-negative.
- if (lengths.Count >= 3 && LengthValue(lengths[2]) < 0) return null;
-
- // Any color present must be a single valid .
- string color = null;
- if (colorTokens.Count > 0)
- {
- if (!IsValidColor(colorTokens)) return null;
- color = string.Concat(colorTokens.Select(t => t.ToValue()));
- }
-
- return new ShadowLayer
- {
- Inset = insetSeen,
- OffsetX = lengths[0].ToValue(),
- OffsetY = lengths[1].ToValue(),
- Blur = lengths.Count >= 3 ? lengths[2].ToValue() : "0",
- Spread = lengths.Count >= 4 ? lengths[3].ToValue() : "0",
- Color = color,
- };
- }
-
- ///
- /// Outside of a declaration value the tokenizer gives a hex color as a bare
- /// token instead of a , which doesn't accept.
- ///
- private static bool IsValidColor(IReadOnlyList colorTokens)
- {
- if (colorTokens.Count == 1 && colorTokens[0].Type == TokenType.Hash)
- return IsHexColor(colorTokens[0].Data);
-
- return ColorValidator.Convert(colorTokens) != null;
- }
-
- private static bool IsHexColor(string data)
- {
- var length = data.Length;
- return (length == 3 || length == 4 || length == 6 || length == 8) && data.All(Uri.IsHexDigit);
- }
-
- ///
- /// Outside of a declaration value the tokenizer splits a hex color starting with a digit (#08f)
- /// into a # delimiter and a number or dimension, this merges them back into one hash token.
- ///
- private static List NormalizeHexColorTokens(IReadOnlyList tokens)
- {
- var result = new List(tokens.Count);
-
- for (var i = 0; i < tokens.Count; i++)
- {
- var token = tokens[i];
-
- if (token.Type == TokenType.Delim && token.Data == "#" && i + 1 < tokens.Count
- && (tokens[i + 1].Type == TokenType.Number || tokens[i + 1].Type == TokenType.Dimension)
- && IsHexColor(tokens[i + 1].ToValue()))
- {
- result.Add(new KeywordToken(TokenType.Hash, tokens[i + 1].ToValue(), token.Position));
- i++;
- continue;
- }
-
- result.Add(token);
- }
-
- return result;
- }
-
- private static bool IsInset(Token token) =>
- token.Type == TokenType.Ident && token.Data.Isi(Keywords.Inset);
-
- /// A box-shadow offset/blur/spread is a <length> (not a length-percentage):
- /// a dimension, or the unitless zero. Percentages are not valid here.
- private static bool IsLength(Token token)
- {
- if (token.Type == TokenType.Dimension) return true;
- var number = token as NumberToken;
- return number != null && number.Value == 0f;
- }
-
- private static float LengthValue(Token token)
- {
- var unit = token as UnitToken;
- if (unit != null) return unit.Value;
-
- var number = token as NumberToken;
- if (number != null) return number.Value;
-
- return 0f;
- }
-
- private static List> SplitByComma(IReadOnlyList tokens)
- {
- var groups = new List>();
- var current = new List();
-
- foreach (var token in tokens)
- {
- if (token.Type == TokenType.Comma)
- {
- groups.Add(current);
- current = new List();
- }
- else
- {
- current.Add(token);
- }
- }
-
- groups.Add(current);
- return groups;
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
index 7c327ee19..eb8c080cf 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
@@ -3,8 +3,9 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The type a calc() expression resolves to (CSS Values 4 §10.9). Flags, so a sum of a length and
- /// a percentage is Length | Percentage.
+ /// The CSS calc() type-checking category a (sub-)expression resolves to. A flags enum so that
+ /// Length | Percentage can represent the combined <length-percentage> type once a
+ /// length and a percentage have been added or subtracted together.
///
[Flags]
internal enum CalcCategory
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs
index 0198713fd..60e72cc66 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs
@@ -3,7 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// A node of a parsed calc(), min(), max() or clamp() expression (CSS Values 4 §10.10).
+ /// AST node for a parsed calc()/min()/max()/clamp() expression (CSS Values 4 §10).
///
internal abstract class CalcNode
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs
index 3b3b6d591..825fa62cd 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs
@@ -3,12 +3,17 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Parses calc(), min(), max() and clamp() (CSS Values 4 §10.8), both when a declaration is
- /// converted and again at layout time from the folded text. Returns null on a syntax error.
+ /// Recursive-descent parser for calc()/min()/max()/clamp() expressions (CSS Values 4 §10). Grammar:
+ /// <calc-sum> := <calc-product> (('+'|'-') <calc-product>)*,
+ /// <calc-product> := <calc-value> (('*'|'/') <calc-value>)*,
+ /// <calc-value> := <number> | <dimension> | <percentage> | '(' <calc-sum> ')' | <nested-call> | <unary-sign> <calc-value>.
+ /// Binary +/- require whitespace on both sides; *// do not. Returns null on
+ /// any grammar violation. An optional set of variables binds identifiers to numbers (the channel keywords
+ /// of a relative color, CSS Color 5 section 4): a bound identifier is a <calc-value> that stands for its number.
///
internal static class CalcParser
{
- public static CalcNode Parse(FunctionToken function)
+ public static CalcNode Parse(FunctionToken function, IReadOnlyDictionary variables = null)
{
var name = function.Data;
@@ -16,8 +21,8 @@ public static CalcNode Parse(FunctionToken function)
{
var tokens = new List(function.ArgumentTokens);
var pos = 0;
- var node = ParseSum(tokens, ref pos);
- if (node is null) return null;
+ var node = ParseSum(tokens, ref pos, variables);
+ if (node == null) return null;
SkipWhitespace(tokens, ref pos);
return pos == tokens.Count ? node : null;
@@ -25,7 +30,7 @@ public static CalcNode Parse(FunctionToken function)
if (name.Isi(FunctionNames.Min) || name.Isi(FunctionNames.Max))
{
- var args = ParseCommaSeparatedSums(function, out var groupCount);
+ var args = ParseCommaSeparatedSums(function, variables, out var groupCount);
return groupCount >= 1 && args != null
? new CallCalcNode(name.Isi(FunctionNames.Min) ? FunctionNames.Min : FunctionNames.Max, args)
: null;
@@ -33,26 +38,33 @@ public static CalcNode Parse(FunctionToken function)
if (name.Isi(FunctionNames.Clamp))
{
- var args = ParseCommaSeparatedSums(function, out var groupCount);
+ var args = ParseCommaSeparatedSums(function, variables, out var groupCount);
return groupCount == 3 && args != null ? new CallCalcNode(FunctionNames.Clamp, args) : null;
}
return null;
}
- private static List ParseCommaSeparatedSums(FunctionToken function, out int groupCount)
+ public static bool IsCalcFamily(string name)
+ {
+ return name.Isi(FunctionNames.Calc) || name.Isi(FunctionNames.Min) ||
+ name.Isi(FunctionNames.Max) || name.Isi(FunctionNames.Clamp);
+ }
+
+ private static List ParseCommaSeparatedSums(FunctionToken function, IReadOnlyDictionary variables, out int groupCount)
{
var groups = function.ArgumentTokens.ToList();
groupCount = groups.Count;
var args = new List(groups.Count);
+
foreach (var group in groups)
{
if (group.Count == 0) return null;
var pos = 0;
- var node = ParseSum(group, ref pos);
- if (node is null) return null;
+ var node = ParseSum(group, ref pos, variables);
+ if (node == null) return null;
SkipWhitespace(group, ref pos);
if (pos != group.Count) return null;
@@ -63,10 +75,10 @@ private static List ParseCommaSeparatedSums(FunctionToken function, ou
return args;
}
- private static CalcNode ParseSum(List tokens, ref int pos)
+ private static CalcNode ParseSum(List tokens, ref int pos, IReadOnlyDictionary variables)
{
- var left = ParseProduct(tokens, ref pos);
- if (left is null) return null;
+ var left = ParseProduct(tokens, ref pos, variables);
+ if (left == null) return null;
while (true)
{
@@ -81,7 +93,8 @@ private static CalcNode ParseSum(List tokens, ref int pos)
break;
}
- // A sign without whitespace before it was already merged into the number token
+ // Binary +/- must have whitespace on both sides; without it this can't be a valid binary
+ // operator here (an attached sign is already merged into the adjacent numeric token).
if (!sawWhitespaceBefore) return null;
var op = tokens[pos].Data[0];
@@ -91,8 +104,8 @@ private static CalcNode ParseSum(List tokens, ref int pos)
SkipWhitespace(tokens, ref pos);
if (pos == afterOp) return null;
- var right = ParseProduct(tokens, ref pos);
- if (right is null) return null;
+ var right = ParseProduct(tokens, ref pos, variables);
+ if (right == null) return null;
left = new BinaryCalcNode(op, left, right);
}
@@ -100,10 +113,10 @@ private static CalcNode ParseSum(List tokens, ref int pos)
return left;
}
- private static CalcNode ParseProduct(List tokens, ref int pos)
+ private static CalcNode ParseProduct(List tokens, ref int pos, IReadOnlyDictionary variables)
{
- var left = ParseValue(tokens, ref pos);
- if (left is null) return null;
+ var left = ParseValue(tokens, ref pos, variables);
+ if (left == null) return null;
while (true)
{
@@ -121,8 +134,8 @@ private static CalcNode ParseProduct(List tokens, ref int pos)
pos++;
SkipWhitespace(tokens, ref pos);
- var right = ParseValue(tokens, ref pos);
- if (right is null) return null;
+ var right = ParseValue(tokens, ref pos, variables);
+ if (right == null) return null;
left = new BinaryCalcNode(op, left, right);
}
@@ -130,7 +143,7 @@ private static CalcNode ParseProduct(List tokens, ref int pos)
return left;
}
- private static CalcNode ParseValue(List tokens, ref int pos)
+ private static CalcNode ParseValue(List tokens, ref int pos, IReadOnlyDictionary variables)
{
SkipWhitespace(tokens, ref pos);
if (pos >= tokens.Count) return null;
@@ -141,51 +154,25 @@ private static CalcNode ParseValue(List tokens, ref int pos)
{
case TokenType.Number:
pos++;
- return new NumberCalcNode(((NumberToken)token).Value);
+ return new NumberCalcNode(((NumberToken) token).Value);
case TokenType.Percentage:
pos++;
- return new PercentageCalcNode(((UnitToken)token).Value);
+ return new PercentageCalcNode(((UnitToken) token).Value);
- case TokenType.Dimension:
- {
- var unitToken = (UnitToken)token;
- var lengthUnit = Length.GetUnit(unitToken.Unit);
- if (IsSupportedLengthUnit(lengthUnit))
- {
- pos++;
- return new DimensionCalcNode(unitToken.Value, lengthUnit);
- }
-
- var angleUnit = Angle.GetUnit(unitToken.Unit);
- if (angleUnit != Angle.Unit.None)
- {
- pos++;
- return new AngleCalcNode(unitToken.Value, angleUnit);
- }
-
- var timeUnit = Time.GetUnit(unitToken.Unit);
- if (timeUnit != Time.Unit.None)
- {
- pos++;
- return new TimeCalcNode(unitToken.Value, timeUnit);
- }
-
- var resolutionUnit = Resolution.GetUnit(unitToken.Unit);
- if (resolutionUnit != Resolution.Unit.None)
- {
- pos++;
- return new ResolutionCalcNode(unitToken.Value, resolutionUnit);
- }
+ // An identifier bound by the caller stands for its number.
+ case TokenType.Ident when variables != null && variables.TryGetValue(token.Data.ToLowerInvariant(), out var bound):
+ pos++;
+ return new NumberCalcNode(bound);
- return null;
- }
+ case TokenType.Dimension:
+ return ParseDimension((UnitToken) token, tokens, ref pos);
case TokenType.RoundBracketOpen:
{
pos++;
- var inner = ParseSum(tokens, ref pos);
- if (inner is null) return null;
+ var inner = ParseSum(tokens, ref pos, variables);
+ if (inner == null) return null;
SkipWhitespace(tokens, ref pos);
if (pos >= tokens.Count || tokens[pos].Type != TokenType.RoundBracketClose) return null;
@@ -198,23 +185,24 @@ private static CalcNode ParseValue(List tokens, ref int pos)
var negative = token.Data == "-";
pos++;
- // Only reachable before ( or a function, a signed number is a single token
+ // A unary sign must be immediately followed by a parenthesized group or a nested
+ // function call (a signed number/dimension is already a single token from the lexer).
if (pos >= tokens.Count ||
(tokens[pos].Type != TokenType.RoundBracketOpen && tokens[pos].Type != TokenType.Function))
{
return null;
}
- var operand = ParseValue(tokens, ref pos);
- return operand is null ? null : new UnaryCalcNode(negative, operand);
+ var operand = ParseValue(tokens, ref pos, variables);
+ return operand == null ? null : new UnaryCalcNode(negative, operand);
}
case TokenType.Function:
{
- var nested = (FunctionToken)token;
+ var nested = (FunctionToken) token;
if (!IsCalcFamily(nested.Data)) return null;
pos++;
- return Parse(nested);
+ return Parse(nested, variables);
}
default:
@@ -222,17 +210,96 @@ private static CalcNode ParseValue(List tokens, ref int pos)
}
}
- private static bool IsSupportedLengthUnit(Length.Unit unit)
+ private static CalcNode ParseDimension(UnitToken token, List tokens, ref int pos)
{
- return unit == Length.Unit.Em || unit == Length.Unit.Rem || unit == Length.Unit.Ex || unit == Length.Unit.Px ||
- unit == Length.Unit.Mm || unit == Length.Unit.Cm || unit == Length.Unit.In || unit == Length.Unit.Pt || unit == Length.Unit.Pc;
+ var lengthUnit = Length.GetUnit(token.Unit);
+ if (IsSupportedLengthUnit(lengthUnit))
+ {
+ pos++;
+ return new DimensionCalcNode(token.Value, lengthUnit);
+ }
+
+ var angleUnit = Angle.GetUnit(token.Unit);
+ if (angleUnit != Angle.Unit.None)
+ {
+ pos++;
+ return new AngleCalcNode(token.Value, angleUnit);
+ }
+
+ var timeUnit = Time.GetUnit(token.Unit);
+ if (timeUnit != Time.Unit.None)
+ {
+ pos++;
+ return new TimeCalcNode(token.Value, timeUnit);
+ }
+
+ var resolutionUnit = Resolution.GetUnit(token.Unit);
+ if (resolutionUnit != Resolution.Unit.None)
+ {
+ pos++;
+ return new ResolutionCalcNode(token.Value, resolutionUnit);
+ }
+
+ return null;
}
- /// Whether is calc, min, max or clamp, ignoring case.
- public static bool IsCalcFamily(string name)
+ private static bool IsSupportedLengthUnit(Length.Unit unit)
{
- return name.Isi(FunctionNames.Calc) || name.Isi(FunctionNames.Min) ||
- name.Isi(FunctionNames.Max) || name.Isi(FunctionNames.Clamp);
+ switch (unit)
+ {
+ case Length.Unit.Em:
+ case Length.Unit.Rem:
+ case Length.Unit.Ex:
+ case Length.Unit.Ch:
+ case Length.Unit.Vw:
+ case Length.Unit.Vh:
+ case Length.Unit.Vmin:
+ case Length.Unit.Vmax:
+ case Length.Unit.Px:
+ case Length.Unit.Mm:
+ case Length.Unit.Cm:
+ case Length.Unit.In:
+ case Length.Unit.Pt:
+ case Length.Unit.Pc:
+ case Length.Unit.Q:
+ case Length.Unit.Cap:
+ case Length.Unit.Ic:
+ case Length.Unit.Lh:
+ case Length.Unit.Rlh:
+ case Length.Unit.Rex:
+ case Length.Unit.Rch:
+ case Length.Unit.Rcap:
+ case Length.Unit.Ric:
+ case Length.Unit.Vi:
+ case Length.Unit.Vb:
+ case Length.Unit.Svw:
+ case Length.Unit.Svh:
+ case Length.Unit.Svi:
+ case Length.Unit.Svb:
+ case Length.Unit.Svmin:
+ case Length.Unit.Svmax:
+ case Length.Unit.Lvw:
+ case Length.Unit.Lvh:
+ case Length.Unit.Lvi:
+ case Length.Unit.Lvb:
+ case Length.Unit.Lvmin:
+ case Length.Unit.Lvmax:
+ case Length.Unit.Dvw:
+ case Length.Unit.Dvh:
+ case Length.Unit.Dvi:
+ case Length.Unit.Dvb:
+ case Length.Unit.Dvmin:
+ case Length.Unit.Dvmax:
+ case Length.Unit.Cqw:
+ case Length.Unit.Cqh:
+ case Length.Unit.Cqi:
+ case Length.Unit.Cqb:
+ case Length.Unit.Cqmin:
+ case Length.Unit.Cqmax:
+ return true;
+ default:
+ return false;
+ }
}
private static void SkipWhitespace(List tokens, ref int pos)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
index 70110181c..c680398ac 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs
@@ -1,11 +1,16 @@
+using System;
using System.Collections.Generic;
using System.Globalization;
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The text of a type-checked calc() (CSS Values 4 §10.10.1): numbers, angles and absolute lengths
- /// fold to a single value, anything with em, rem, ex or % stays an expression for layout.
+ /// Produces the canonical CSS text for a validated calc()/min()/max()/clamp() tree. An expression made
+ /// only of numbers folds to a plain number; one made only of angles folds to a plain angle in
+ /// deg; one made only of absolute lengths (px, cm, mm, Q, in, pt, pc) folds to a plain
+ /// px length. Anything that needs layout context (em, rem, %, viewport units, ...) is
+ /// re-serialized to normalized calc()/min()/max()/clamp() text, with spaces around binary operators
+ /// and only the parentheses that precedence requires.
///
internal static class CalcSerializer
{
@@ -13,44 +18,58 @@ public static string Serialize(CalcNode node, CalcCategory category)
{
if (category == CalcCategory.Number)
{
- var value = CalcTypeChecker.FoldNumber(node) ?? 0d;
- return ((float)value).ToString(null, CultureInfo.InvariantCulture);
+ var number = CalcTypeChecker.FoldNumber(node);
+ if (number != null) return FormatNumber(number.Value);
}
-
- if (category == CalcCategory.Angle)
+ else if (category == CalcCategory.Angle)
{
- var radians = FoldAngle(node) ?? 0d;
- var degrees = radians * (180.0 / System.Math.PI);
- return new Angle((float)degrees, Angle.Unit.Deg).ToString(null, CultureInfo.InvariantCulture);
+ // Every angle unit converts by a fixed constant, so a valid angle expression always folds.
+ var degrees = FoldAngle(node);
+ if (degrees != null) return new Angle((float) degrees.Value, Angle.Unit.Deg).ToString(null, CultureInfo.InvariantCulture);
}
-
- if (TryFoldPixels(node, out var pixels))
+ else if (category == CalcCategory.Length && TryFoldPixels(node, out var pixels))
{
- return new Length((float)pixels, Length.Unit.Px).ToString(null, CultureInfo.InvariantCulture);
+ return new Length((float) pixels, Length.Unit.Px).ToString(null, CultureInfo.InvariantCulture);
}
return SerializeExpression(node);
}
- /// Folds an Angle-category subtree to radians; always succeeds for a validated tree.
+ ///
+ /// Evaluates a pure-number expression and returns the folded value, or null when the tree is not
+ /// purely numeric. This is the constant-folding used for <integer>/<number>
+ /// contexts; it never needs layout context.
+ ///
+ public static double? EvaluateNumber(CalcNode node)
+ {
+ return CalcTypeChecker.FoldNumber(node);
+ }
+
+ private static string FormatNumber(double value)
+ {
+ return ((float) value).ToString(null, CultureInfo.InvariantCulture);
+ }
+
+ // ---- Angle folding (degrees) ----
+
private static double? FoldAngle(CalcNode node)
{
switch (node)
{
case AngleCalcNode angle:
- return new Angle((float)angle.Value, angle.Unit).ToRadian();
+ return ToDegrees(angle.Value, angle.Unit);
case UnaryCalcNode unary:
{
var value = FoldAngle(unary.Operand);
- return value is null ? null : (double?)(unary.Negative ? -value.Value : value.Value);
+ return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value;
}
case BinaryCalcNode binary when binary.Operator == '+' || binary.Operator == '-':
{
var left = FoldAngle(binary.Left);
var right = FoldAngle(binary.Right);
- if (left is null || right is null) return null;
+ if (left == null || right == null) return null;
return binary.Operator == '+' ? left.Value + right.Value : left.Value - right.Value;
}
@@ -60,89 +79,179 @@ public static string Serialize(CalcNode node, CalcCategory category)
if (leftAngle != null)
{
var scalar = CalcTypeChecker.FoldNumber(binary.Right);
- return scalar is null ? (double?)null : leftAngle.Value * scalar.Value;
+ return scalar == null ? (double?) null : leftAngle.Value * scalar.Value;
}
var rightAngle = FoldAngle(binary.Right);
- if (rightAngle != null)
- {
- var scalar = CalcTypeChecker.FoldNumber(binary.Left);
- return scalar is null ? (double?)null : rightAngle.Value * scalar.Value;
- }
-
- return null;
+ if (rightAngle == null) return null;
+ var factor = CalcTypeChecker.FoldNumber(binary.Left);
+ return factor == null ? (double?) null : rightAngle.Value * factor.Value;
}
case BinaryCalcNode binary when binary.Operator == '/':
{
var left = FoldAngle(binary.Left);
- if (left is null) return null;
var divisor = CalcTypeChecker.FoldNumber(binary.Right);
- return divisor is null || divisor == 0d ? (double?)null : left.Value / divisor.Value;
+ if (left == null || divisor == null || divisor.Value == 0d) return null;
+ return left.Value / divisor.Value;
}
case CallCalcNode call:
+ return FoldCall(call, FoldAngle);
+
+ default:
+ return null;
+ }
+ }
+
+ private static double ToDegrees(double value, Angle.Unit unit)
+ {
+ switch (unit)
+ {
+ case Angle.Unit.Grad: return value * 0.9d;
+ case Angle.Unit.Rad: return value * (180d / Math.PI);
+ case Angle.Unit.Turn: return value * 360d;
+ default: return value;
+ }
+ }
+
+ // ---- Absolute length folding (CSS pixels) ----
+
+ private static bool TryFoldPixels(CalcNode node, out double pixels)
+ {
+ var value = FoldPixels(node);
+ pixels = value ?? 0d;
+ return value != null;
+ }
+
+ private static double? FoldPixels(CalcNode node)
+ {
+ switch (node)
+ {
+ // A unitless number is only ever a scalar operand of '*' or '/' in a Length expression.
+ case NumberCalcNode number:
+ return number.Value;
+
+ case DimensionCalcNode dimension:
{
- var values = new List(call.Arguments.Count);
- foreach (var argument in call.Arguments)
- {
- var value = FoldAngle(argument);
- if (value is null) return null;
- values.Add(value.Value);
- }
+ var length = new Length((float) dimension.Value, dimension.Unit);
+ return length.IsAbsolute ? length.ToPixel() : (double?) null;
+ }
+
+ case UnaryCalcNode unary:
+ {
+ var value = FoldPixels(unary.Operand);
+ return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value;
+ }
- if (call.Name.Isi(FunctionNames.Min)) return Min(values);
- if (call.Name.Isi(FunctionNames.Max)) return Max(values);
- if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3)
- return values[0] > values[2] ? values[2] : System.Math.Min(System.Math.Max(values[1], values[0]), values[2]);
+ case BinaryCalcNode binary:
+ {
+ var left = FoldPixels(binary.Left);
+ var right = FoldPixels(binary.Right);
+ if (left == null || right == null) return null;
- return null;
+ switch (binary.Operator)
+ {
+ case '+': return left.Value + right.Value;
+ case '-': return left.Value - right.Value;
+ case '*': return left.Value * right.Value;
+ case '/': return right.Value != 0d ? left.Value / right.Value : (double?) null;
+ default: return null;
+ }
}
+ case CallCalcNode call:
+ return FoldCall(call, FoldPixels);
+
default:
return null;
}
}
+ private static double? FoldCall(CallCalcNode call, Func fold)
+ {
+ var values = new List(call.Arguments.Count);
+ foreach (var argument in call.Arguments)
+ {
+ var value = fold(argument);
+ if (value == null) return null;
+ values.Add(value.Value);
+ }
+
+ if (call.Name.Isi(FunctionNames.Min))
+ {
+ var result = values[0];
+ for (var i = 1; i < values.Count; i++) if (values[i] < result) result = values[i];
+ return result;
+ }
+
+ if (call.Name.Isi(FunctionNames.Max))
+ {
+ var result = values[0];
+ for (var i = 1; i < values.Count; i++) if (values[i] > result) result = values[i];
+ return result;
+ }
+
+ if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3)
+ {
+ // clamp(min, val, max) == max(min, min(val, max)); if min > max, min wins.
+ var upper = values[1] < values[2] ? values[1] : values[2];
+ return upper < values[0] ? values[0] : upper;
+ }
+
+ return null;
+ }
+
+ // ---- Normalized expression text ----
+
private static string SerializeExpression(CalcNode node)
{
if (node is CallCalcNode call)
{
var args = new List(call.Arguments.Count);
foreach (var argument in call.Arguments) args.Add(SerializeOperand(argument, false, 0));
- return $"{call.Name}({string.Join(", ", args)})";
+ return call.Name + "(" + string.Join(", ", args) + ")";
}
- return $"calc({SerializeOperand(node, false, 0)})";
+ return "calc(" + SerializeOperand(node, false, 0) + ")";
}
- ///
- /// Serializes an operand of a parent with the given precedence (0 none, 1 + and -, 2 * and /),
- /// in parentheses where re-parsing would group it differently.
- ///
+ // parentPrecedence: 0 = none, 1 = sum-level + and -, 2 = product-level * and /, 3 = unary operand.
+ // Parentheses are added whenever omitting them would change the meaning on re-parse: a
+ // lower-precedence child under a higher-precedence parent, or a same-precedence child on the right
+ // (the operators are left-associative).
private static string SerializeOperand(CalcNode node, bool isRightOperand, int parentPrecedence)
{
switch (node)
{
case NumberCalcNode number:
- return ((float)number.Value).ToString(null, CultureInfo.InvariantCulture);
+ return FormatNumber(number.Value);
+ // Units are always written here, unlike Length.ToString which drops the unit of a zero
+ // length - a unitless 0 inside calc() would be a number and break type checking.
case DimensionCalcNode dimension:
- return new Length((float)dimension.Value, dimension.Unit).ToString(null, CultureInfo.InvariantCulture);
+ return FormatNumber(dimension.Value) + new Length(0f, dimension.Unit).UnitString;
case PercentageCalcNode percentage:
- return new Length((float)percentage.Value, Length.Unit.Percent).ToString(null, CultureInfo.InvariantCulture);
+ return FormatNumber(percentage.Value) + "%";
+
+ case AngleCalcNode angle:
+ return FormatNumber(angle.Value) + new Angle(0f, angle.Unit).UnitString;
case TimeCalcNode time:
- return new Time((float)time.Value, time.Unit).ToString(null, CultureInfo.InvariantCulture);
+ return new Time((float) time.Value, time.Unit).ToString(null, CultureInfo.InvariantCulture);
case ResolutionCalcNode resolution:
- return new Resolution((float)resolution.Value, resolution.Unit).ToString(null, CultureInfo.InvariantCulture);
+ return new Resolution((float) resolution.Value, resolution.Unit)
+ .ToString(null, CultureInfo.InvariantCulture);
case UnaryCalcNode unary:
{
var operand = SerializeOperand(unary.Operand, false, 3);
- return unary.Negative ? $"-{operand}" : operand;
+ if (!unary.Negative) return operand;
+
+ // "--x" would not re-parse as a double negation.
+ return operand.StartsWith("-", StringComparison.Ordinal) ? "-(" + operand + ")" : "-" + operand;
}
case CallCalcNode call:
@@ -151,107 +260,16 @@ private static string SerializeOperand(CalcNode node, bool isRightOperand, int p
case BinaryCalcNode binary:
{
var precedence = binary.Operator == '+' || binary.Operator == '-' ? 1 : 2;
- var text =
- $"{SerializeOperand(binary.Left, false, precedence)} {binary.Operator} {SerializeOperand(binary.Right, true, precedence)}";
- var needsParens = precedence < parentPrecedence || (precedence == parentPrecedence && isRightOperand);
- return needsParens ? $"({text})" : text;
+ var text = SerializeOperand(binary.Left, false, precedence) + " " + binary.Operator + " " +
+ SerializeOperand(binary.Right, true, precedence);
+ var needsParentheses = precedence < parentPrecedence ||
+ (precedence == parentPrecedence && isRightOperand);
+ return needsParentheses ? "(" + text + ")" : text;
}
default:
return string.Empty;
}
}
-
- private static bool TryFoldPixels(CalcNode node, out double pixels)
- {
- switch (node)
- {
- case NumberCalcNode number:
- pixels = number.Value;
- return true;
-
- case DimensionCalcNode dimension when dimension.Unit != Length.Unit.Em && dimension.Unit != Length.Unit.Rem && dimension.Unit != Length.Unit.Ex:
- pixels = new Length((float)dimension.Value, dimension.Unit).ToPixels(0, 0, 0);
- return true;
-
- case UnaryCalcNode unary when TryFoldPixels(unary.Operand, out var operand):
- pixels = unary.Negative ? -operand : operand;
- return true;
-
- case BinaryCalcNode binary when TryFoldPixels(binary.Left, out var left) && TryFoldPixels(binary.Right, out var right):
- switch (binary.Operator)
- {
- case '+':
- pixels = left + right;
- return true;
- case '-':
- pixels = left - right;
- return true;
- case '*':
- pixels = left * right;
- return true;
- case '/' when right != 0d:
- pixels = left / right;
- return true;
- default:
- pixels = 0;
- return false;
- }
-
- case CallCalcNode call:
- {
- var values = new List(call.Arguments.Count);
- foreach (var argument in call.Arguments)
- {
- if (!TryFoldPixels(argument, out var value))
- {
- pixels = 0;
- return false;
- }
-
- values.Add(value);
- }
-
- if (call.Name.Isi(FunctionNames.Min))
- {
- pixels = Min(values);
- return true;
- }
-
- if (call.Name.Isi(FunctionNames.Max))
- {
- pixels = Max(values);
- return true;
- }
-
- if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3)
- {
- pixels = values[0] > values[2] ? values[2] : System.Math.Min(System.Math.Max(values[1], values[0]), values[2]);
- return true;
- }
-
- pixels = 0;
- return false;
- }
-
- default:
- pixels = 0;
- return false;
- }
- }
-
- private static double Min(List values)
- {
- var result = values[0];
- for (var i = 1; i < values.Count; i++) if (values[i] < result) result = values[i];
- return result;
- }
-
- private static double Max(List values)
- {
- var result = values[0];
- for (var i = 1; i < values.Count; i++) if (values[i] > result) result = values[i];
- return result;
- }
}
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs
index 3bee69a49..5c503ed04 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs
@@ -1,12 +1,14 @@
-using System;
using System.Collections.Generic;
using System.Linq;
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Type checking of a calc() tree (CSS Values 4 §10.9), null when the types don't combine. Only a
- /// number may be multiplied with or divide another type, so a division by zero is found here.
+ /// Implements CSS calc()'s type-checking rules (CSS Values 4 §10.10) over a parsed
+ /// tree, returning the resolved category or null when the expression is
+ /// dimensionally inconsistent (e.g. adding a length to a time, or dividing by something that isn't a
+ /// number). Also folds pure-number subtrees, which is what makes a constant divide-by-zero divisor
+ /// detectable here.
///
internal static class CalcTypeChecker
{
@@ -36,17 +38,13 @@ internal static class CalcTypeChecker
return Check(unary.Operand);
case BinaryCalcNode binary when binary.Operator == '+' || binary.Operator == '-':
- {
- var left = Check(binary.Left);
- var right = Check(binary.Right);
- return Combine(left, right);
- }
+ return Combine(Check(binary.Left), Check(binary.Right));
case BinaryCalcNode binary when binary.Operator == '*':
{
var left = Check(binary.Left);
var right = Check(binary.Right);
- if (left is null || right is null) return null;
+ if (left == null || right == null) return null;
if (left == CalcCategory.Number) return right;
if (right == CalcCategory.Number) return left;
return null;
@@ -56,10 +54,12 @@ internal static class CalcTypeChecker
{
var left = Check(binary.Left);
var right = Check(binary.Right);
- if (left is null || right != CalcCategory.Number) return null;
+ if (left == null || right != CalcCategory.Number) return null;
+ // A Number-category subtree is built entirely from number leaves, so it always folds -
+ // this is what lets a constant divide-by-zero be rejected here rather than deferred.
var divisor = FoldNumber(binary.Right);
- return divisor == null || divisor == 0d ? null : left;
+ return divisor == null || divisor.Value == 0d ? (CalcCategory?) null : left;
}
case CallCalcNode call:
@@ -68,9 +68,9 @@ internal static class CalcTypeChecker
foreach (var argument in call.Arguments)
{
var category = Check(argument);
- if (category is null) return null;
- combined = combined is null ? category : Combine(combined, category);
- if (combined is null) return null;
+ if (category == null) return null;
+ combined = combined == null ? category : Combine(combined, category);
+ if (combined == null) return null;
}
return combined;
@@ -81,7 +81,7 @@ internal static class CalcTypeChecker
}
}
- /// Folds a pure-Number subtree to a concrete value; null if it isn't purely numeric.
+ // Folds a pure-number subtree to a concrete value; null if it isn't purely numeric.
public static double? FoldNumber(CalcNode node)
{
switch (node)
@@ -92,27 +92,22 @@ internal static class CalcTypeChecker
case UnaryCalcNode unary:
{
var value = FoldNumber(unary.Operand);
- return value is null ? null : (double?)(unary.Negative ? -value.Value : value.Value);
+ return value == null ? (double?) null : unary.Negative ? -value.Value : value.Value;
}
case BinaryCalcNode binary:
{
var left = FoldNumber(binary.Left);
var right = FoldNumber(binary.Right);
- if (left is null || right is null) return null;
+ if (left == null || right == null) return null;
switch (binary.Operator)
{
- case '+':
- return left.Value + right.Value;
- case '-':
- return left.Value - right.Value;
- case '*':
- return left.Value * right.Value;
- case '/':
- return right.Value != 0d ? (double?)(left.Value / right.Value) : null;
- default:
- return null;
+ case '+': return left.Value + right.Value;
+ case '-': return left.Value - right.Value;
+ case '*': return left.Value * right.Value;
+ case '/': return right.Value != 0d ? left.Value / right.Value : (double?) null;
+ default: return null;
}
}
@@ -122,26 +117,34 @@ internal static class CalcTypeChecker
foreach (var argument in call.Arguments)
{
var value = FoldNumber(argument);
- if (value is null) return null;
+ if (value == null) return null;
values.Add(value.Value);
}
if (call.Name.Isi(FunctionNames.Min)) return values.Min();
if (call.Name.Isi(FunctionNames.Max)) return values.Max();
if (call.Name.Isi(FunctionNames.Clamp) && values.Count == 3)
- return values[0] > values[2] ? values[2] : Math.Max(values[0], Math.Min(values[2], values[1]));
+ {
+ var min = values[0];
+ var val = values[1];
+ var max = values[2];
+ // clamp(min, val, max) == max(min, min(val, max)); if min > max, min wins.
+ var upper = val < max ? val : max;
+ return upper < min ? min : upper;
+ }
return null;
}
default:
+ // Dimension/percentage leaves are never purely numeric.
return null;
}
}
private static CalcCategory? Combine(CalcCategory? left, CalcCategory? right)
{
- if (left is null || right is null) return null;
+ if (left == null || right == null) return null;
if (left == CalcCategory.Number && right == CalcCategory.Number) return CalcCategory.Number;
if (left.Value.HasFlag(CalcCategory.Number) || right.Value.HasFlag(CalcCategory.Number)) return null;
return left.Value | right.Value;
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs
index 928e2d5ce..5012a212d 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcValue.cs
@@ -3,7 +3,10 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// An accepted calc(), min(), max() or clamp() value, is the folded text.
+ /// The value produced by for an accepted calc()/min()/max()/clamp()
+ /// expression. Keeps the parsed and its type-checked ;
+ /// is the canonical text from (folded to a plain
+ /// value where the expression needs no layout context), and the raw tokens.
///
internal sealed class CalcValue : IPropertyValue
{
@@ -15,9 +18,9 @@ public CalcValue(CalcNode node, CalcCategory category, IEnumerable tokens
Original = new TokenValue(tokens);
}
- internal CalcNode Node { get; }
+ public CalcNode Node { get; }
- internal CalcCategory Category { get; }
+ public CalcCategory Category { get; }
public string CssText { get; }
diff --git a/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs
deleted file mode 100644
index 4cbfdfe5a..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/ColorInterpolationMethodGrammar.cs
+++ /dev/null
@@ -1,165 +0,0 @@
-using System;
-using System.Collections.Generic;
-
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- ///
- /// The <color-interpolation-method> of a gradient (CSS Color 4 §13.3). Only the color
- /// spaces implements are accepted, so a98-rgb, prophoto-rgb, rec2020
- /// and display-p3-linear are rejected.
- ///
- internal static class ColorInterpolationMethodGrammar
- {
- private const string In = "in";
- private const string Hue = "hue";
-
- private static readonly HashSet ColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "srgb", "srgb-linear", "display-p3", "lab", "oklab", "xyz", "xyz-d65", "xyz-d50",
- "hsl", "hwb", "lch", "oklch",
- };
-
- private static readonly HashSet PolarColorSpaces = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "hsl", "hwb", "lch", "oklch",
- };
-
- private static readonly HashSet HueDirections = new HashSet(StringComparer.OrdinalIgnoreCase)
- {
- "shorter", "longer", "increasing", "decreasing",
- };
-
- public static bool IsColorSpace(string name)
- {
- return name != null && ColorSpaces.Contains(name);
- }
-
- public static bool IsPolarColorSpace(string name)
- {
- return name != null && PolarColorSpaces.Contains(name);
- }
-
- public static bool IsHueDirection(string name)
- {
- return name != null && HueDirections.Contains(name);
- }
-
- ///
- /// If (one gradient comma-group's tokens) contains an in
- /// interpolation-method prelude, validates it and returns the group with the contiguous
- /// in … slice removed as (for the caller to validate as the
- /// gradient's own direction/shape/position, since the two combine with || in either order).
- /// Returns when an in is present but the method is malformed
- /// (unknown/unsupported space, a hue-interpolation method on a rectangular space or without the
- /// trailing hue, or in with nothing after it) — the caller drops the declaration.
- /// When no in is present, is
- /// , is the group unchanged, and it returns
- /// .
- ///
- public static bool TryExtractInterpolationMethod(
- IEnumerable group, out List remainder, out bool hasInterpolationMethod)
- {
- var tokens = new List(group);
-
- int inIndex = IndexOfIdent(tokens, 0, In);
- if (inIndex < 0)
- {
- remainder = tokens;
- hasInterpolationMethod = false;
- return true;
- }
-
- hasInterpolationMethod = true;
-
- // in
- int spaceIndex = NextNonWhitespace(tokens, inIndex + 1);
- string spaceName;
- if (spaceIndex < 0 || !IsIdent(tokens[spaceIndex], out spaceName) || !IsColorSpace(spaceName))
- {
- remainder = null;
- return false;
- }
-
- int lastMethodIndex = spaceIndex;
-
- // A hue-interpolation method ( hue) is only valid after a polar space, and only complete
- // with the trailing "hue" keyword.
- if (IsPolarColorSpace(spaceName))
- {
- int dirIndex = NextNonWhitespace(tokens, spaceIndex + 1);
- string dir;
- if (dirIndex >= 0 && IsIdent(tokens[dirIndex], out dir) && IsHueDirection(dir))
- {
- int hueIndex = NextNonWhitespace(tokens, dirIndex + 1);
- string hueWord;
- if (hueIndex < 0 || !IsIdent(tokens[hueIndex], out hueWord) ||
- !string.Equals(hueWord, Hue, StringComparison.OrdinalIgnoreCase))
- {
- remainder = null;
- return false;
- }
-
- lastMethodIndex = hueIndex;
- }
- }
-
- remainder = BuildRemainder(tokens, inIndex, lastMethodIndex);
- return true;
- }
-
- // The group with tokens [firstMethodIndex..lastMethodIndex] removed, then whitespace-trimmed at the
- // edges so the leftover direction/shape validates the same as an un-prefixed group would.
- private static List BuildRemainder(List tokens, int firstMethodIndex, int lastMethodIndex)
- {
- var remainder = new List(tokens.Count);
-
- for (int i = 0; i < tokens.Count; i++)
- {
- if (i >= firstMethodIndex && i <= lastMethodIndex) continue;
- remainder.Add(tokens[i]);
- }
-
- while (remainder.Count > 0 && remainder[0].Type == TokenType.Whitespace)
- remainder.RemoveAt(0);
- while (remainder.Count > 0 && remainder[remainder.Count - 1].Type == TokenType.Whitespace)
- remainder.RemoveAt(remainder.Count - 1);
-
- return remainder;
- }
-
- private static int IndexOfIdent(List tokens, int start, string data)
- {
- for (int i = start; i < tokens.Count; i++)
- {
- string value;
- if (IsIdent(tokens[i], out value) &&
- string.Equals(value, data, StringComparison.OrdinalIgnoreCase))
- return i;
- }
-
- return -1;
- }
-
- private static int NextNonWhitespace(List tokens, int start)
- {
- for (int i = start; i < tokens.Count; i++)
- {
- if (tokens[i].Type != TokenType.Whitespace) return i;
- }
-
- return -1;
- }
-
- private static bool IsIdent(Token token, out string data)
- {
- if (token.Type == TokenType.Ident)
- {
- data = token.Data;
- return true;
- }
-
- data = null;
- return false;
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs
new file mode 100644
index 000000000..e5ce27e52
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerCondition.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.IO;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal sealed class ContainerCondition : IContainerCondition
+ {
+ private static readonly IReadOnlyList NoOperands = new IContainerCondition[0];
+
+ private ContainerCondition(ContainerConditionType type, IReadOnlyList operands,
+ string property, string value)
+ {
+ Type = type;
+ Operands = operands;
+ Property = property;
+ Value = value;
+ }
+
+ public ContainerConditionType Type { get; }
+ public IReadOnlyList Operands { get; }
+ public string Property { get; }
+ public string Value { get; }
+
+ public static ContainerCondition Size(string text) =>
+ new ContainerCondition(ContainerConditionType.Size, NoOperands, null, text);
+
+ public static ContainerCondition Declaration(string property, string value) =>
+ new ContainerCondition(ContainerConditionType.Declaration, NoOperands, property, value);
+
+ public static ContainerCondition Style(IContainerCondition query) =>
+ new ContainerCondition(ContainerConditionType.Style, new[] { query }, null, null);
+
+ public static ContainerCondition Not(IContainerCondition operand) =>
+ new ContainerCondition(ContainerConditionType.Not, new[] { operand }, null, null);
+
+ public static ContainerCondition And(List operands) =>
+ new ContainerCondition(ContainerConditionType.And, operands, null, null);
+
+ public static ContainerCondition Or(List operands) =>
+ new ContainerCondition(ContainerConditionType.Or, operands, null, null);
+
+ public void ToCss(TextWriter writer, IStyleFormatter formatter)
+ {
+ switch (Type)
+ {
+ case ContainerConditionType.Size:
+ writer.Write(Value);
+ break;
+ case ContainerConditionType.Declaration:
+ writer.Write(Value == null ? Property : $"{Property}: {Value}");
+ break;
+ case ContainerConditionType.Style:
+ // Directly inside style( ), a lone feature needs no parentheses of its own.
+ writer.Write("style(");
+ Operands[0].ToCss(writer, formatter);
+ writer.Write(")");
+ break;
+ case ContainerConditionType.Not:
+ writer.Write("not ");
+ WriteOperand(Operands[0], writer, formatter);
+ break;
+ default:
+ var connector = Type == ContainerConditionType.And ? " and " : " or ";
+ for (var i = 0; i < Operands.Count; i++)
+ {
+ if (i > 0) writer.Write(connector);
+ WriteOperand(Operands[i], writer, formatter);
+ }
+ break;
+ }
+ }
+
+ // Features and nested combinators are parenthesised inside a combinator; style() and size features carry their own.
+ private static void WriteOperand(IContainerCondition operand, TextWriter writer, IStyleFormatter formatter)
+ {
+ var wrap = operand.Type != ContainerConditionType.Size && operand.Type != ContainerConditionType.Style;
+ if (wrap) writer.Write("(");
+ operand.ToCss(writer, formatter);
+ if (wrap) writer.Write(")");
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs
new file mode 100644
index 000000000..dc849e5f1
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/ContainerConditionType.cs
@@ -0,0 +1,21 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// The kind of a node in a parsed @container condition containing a style() query.
+ ///
+ internal enum ContainerConditionType : byte
+ {
+ /// A size feature such as (min-width: 400px); holds its text.
+ Size,
+ /// A style( ... ) query; the single operand is the query inside the function.
+ Style,
+ /// A style feature --name: value (or the boolean form --name, whose value is null).
+ Declaration,
+ /// Operands joined by and.
+ And,
+ /// Operands joined by or.
+ Or,
+ /// A not negation of the single operand.
+ Not
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs
index 85e4c74e3..35dba9e9b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/DeclarationCondition.cs
@@ -13,10 +13,6 @@ public DeclarationCondition(Property property, TokenValue tokenValue)
_tokenValue = tokenValue;
}
- ///
- /// CSS Conditional 3 §6.1. A value the parser accepts counts as supported, even where layout
- /// ignores it.
- ///
public bool Check()
{
return _property is UnknownProperty == false &&
diff --git a/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs b/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs
new file mode 100644
index 000000000..78275df9d
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Conditions/IContainerCondition.cs
@@ -0,0 +1,25 @@
+using System.Collections.Generic;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// A node of the parsed condition of an @container rule that uses style() queries
+ /// (CSS Containment 3), possibly mixed with size features. The tree is data only; ExCSS does not evaluate it.
+ ///
+ internal interface IContainerCondition : IStyleFormattable
+ {
+ ContainerConditionType Type { get; }
+
+ /// The operands of an And/Or (two or more), Not or Style (exactly one) node; empty for leaves.
+ IReadOnlyList Operands { get; }
+
+ /// The property or custom property name of a Declaration node; otherwise null.
+ string Property { get; }
+
+ ///
+ /// For a Declaration node the raw value text (null for the boolean form style(--flag));
+ /// for a Size node the text of the feature including its parentheses; otherwise null.
+ ///
+ string Value { get; }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs
new file mode 100644
index 000000000..305a1323a
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/ContainGrammar.cs
@@ -0,0 +1,84 @@
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// The grammars of CSS Containment 2 §2 (contain) and CSS Sizing 4 §5.1
+ /// (contain-intrinsic-*). The authored text is preserved by the converters that call these.
+ ///
+ internal static class ContainGrammar
+ {
+ private const int Size = 1;
+ private const int Layout = 2;
+ private const int Style = 4;
+ private const int Paint = 8;
+ private const int InlineSize = 16;
+
+ ///
+ /// none | strict | content | [ [ size | inline-size ] || layout || style || paint ].
+ /// none, strict and content stand alone; the other keywords come in any order,
+ /// each at most once, and size and inline-size not together.
+ ///
+ internal static bool TryParseContain(IEnumerable value)
+ {
+ var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray();
+
+ if (tokens.Length == 0 || tokens.Any(t => t.Type != TokenType.Ident)) return false;
+
+ if (tokens.Length == 1 &&
+ (tokens[0].Data.Isi(Keywords.None) || tokens[0].Data.Isi("strict") || tokens[0].Data.Isi("content")))
+ {
+ return true;
+ }
+
+ var seen = 0;
+
+ foreach (var token in tokens)
+ {
+ var kind = KindOf(token.Data);
+
+ if (kind == 0 || (seen & kind) != 0) return false;
+
+ seen |= kind;
+ }
+
+ return (seen & (Size | InlineSize)) != (Size | InlineSize);
+ }
+
+ private static int KindOf(string keyword) =>
+ keyword.Isi("size") ? Size
+ : keyword.Isi("inline-size") ? InlineSize
+ : keyword.Isi("layout") ? Layout
+ : keyword.Isi("style") ? Style
+ : keyword.Isi("paint") ? Paint
+ : 0;
+
+ /// auto? [ none | <length [0,∞]> ] over the tokens of one value.
+ internal static bool TryParseIntrinsic(IEnumerable value)
+ {
+ var tokens = new List(value.Where(t => t.Type != TokenType.Whitespace));
+
+ if (tokens.Count > 0 && tokens[0].Type == TokenType.Ident && tokens[0].Data.Isi(Keywords.Auto))
+ {
+ tokens.RemoveAt(0);
+ }
+
+ if (tokens.Count != 1) return false;
+
+ var token = tokens[0];
+
+ if (token.Type == TokenType.Ident) return token.Data.Isi(Keywords.None);
+
+ return IsNonNegativeLength(token);
+ }
+
+ /// A <length> (a bare 0 or a calc() included) that is not negative.
+ internal static bool IsNonNegativeLength(Token token)
+ {
+ if (token is UnitToken { Value: < 0f }) return false;
+
+ return Converters.LengthConverter.Convert(new[] { token }) != null;
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs b/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs
deleted file mode 100644
index c47b38093..000000000
--- a/Source/HtmlRenderer/Core/CssEngine/CssProperty.cs
+++ /dev/null
@@ -1,90 +0,0 @@
-namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
-{
- /// The five CSS-wide keywords (CSS Cascade & Inheritance §7.3) a property value can be.
- internal enum CssGlobalKeyword
- {
- Inherit,
- Initial,
- Unset,
- Revert,
- RevertLayer
- }
-
- ///
- /// A typed box-side property value that carries the parsed value through the cascade to the box,
- /// so a layout consumer reads the object rather than re-parsing the authored string. A value is in exactly
- /// one of three states:
- ///
- /// - a CSS-wide keyword ( non-null),
- /// - unresolved — the authored text still contains var() and has not been resolved yet
- /// (; is default), or
- /// - resolved — holds the parsed .
- ///
- /// The authored/keyword text is retained so the string-based getter/snapshot/revert cascade path keeps
- /// working (). Grid templates (T = GridTemplate) are the first adopter; the
- /// mechanism is intended to eventually back every box property.
- ///
- internal sealed class CssProperty
- {
- private readonly string _cssText;
-
- private CssProperty(CssGlobalKeyword? global, bool unresolved, T value, string cssText)
- {
- GlobalValue = global;
- IsUnresolved = unresolved;
- Value = value;
- _cssText = cssText;
- }
-
- /// The CSS-wide keyword, when this value is one; otherwise null.
- public CssGlobalKeyword? GlobalValue { get; }
-
- public bool IsGlobalValue => GlobalValue.HasValue;
-
- /// The authored text still contains an unresolved var() — is not
- /// meaningful until resolution replaces this with a resolved value.
- public bool IsUnresolved { get; }
-
- /// The parsed value — meaningful only when the value is neither global nor unresolved.
- public T Value { get; }
-
- public static CssProperty Global(CssGlobalKeyword keyword) =>
- new CssProperty(keyword, unresolved: false, value: default(T), CssGlobalKeywords.ToText(keyword));
-
- public static CssProperty Unresolved(string rawText) =>
- new CssProperty(global: null, unresolved: true, value: default(T), rawText);
-
- public static CssProperty FromValue(string cssText, T value) =>
- new CssProperty(global: null, unresolved: false, value, cssText);
-
- public override string ToString() => _cssText;
- }
-
- /// Maps between the CSS-wide keyword string literals and .
- internal static class CssGlobalKeywords
- {
- public static bool TryParse(string value, out CssGlobalKeyword keyword)
- {
- if (value.Isi(Keywords.Inherit)) { keyword = CssGlobalKeyword.Inherit; return true; }
- if (value.Isi(Keywords.Initial)) { keyword = CssGlobalKeyword.Initial; return true; }
- if (value.Isi(Keywords.Unset)) { keyword = CssGlobalKeyword.Unset; return true; }
- if (value.Isi(Keywords.Revert)) { keyword = CssGlobalKeyword.Revert; return true; }
- if (value.Isi(Keywords.RevertLayer)) { keyword = CssGlobalKeyword.RevertLayer; return true; }
- keyword = default;
- return false;
- }
-
- public static string ToText(CssGlobalKeyword keyword)
- {
- switch (keyword)
- {
- case CssGlobalKeyword.Inherit: return Keywords.Inherit;
- case CssGlobalKeyword.Initial: return Keywords.Initial;
- case CssGlobalKeyword.Unset: return Keywords.Unset;
- case CssGlobalKeyword.Revert: return Keywords.Revert;
- case CssGlobalKeyword.RevertLayer: return Keywords.RevertLayer;
- default: return Keywords.Initial;
- }
- }
- }
-}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs
new file mode 100644
index 000000000..79c3f48a2
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/AttrCaseSensitivity.cs
@@ -0,0 +1,18 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// The case-sensitivity flag of an attribute selector value (Selectors 4):
+ /// [attr=value i] and [attr=value s].
+ ///
+ internal enum AttrCaseSensitivity
+ {
+ /// No flag; the document language decides how the value is compared.
+ Default,
+
+ /// The i flag: the value is compared ASCII case-insensitively.
+ Insensitive,
+
+ /// The s flag: the value is compared case-sensitively.
+ Sensitive
+ }
+}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs
index dbd7ca9f4..6508e4565 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/BreakMode.cs
@@ -11,6 +11,8 @@ internal enum BreakMode : byte
Column,
AvoidPage,
AvoidColumn,
- AvoidRegion
+ AvoidRegion,
+ Recto,
+ Verso
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs
index b19a8307a..28bcc58bf 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/ClearMode.cs
@@ -5,6 +5,8 @@ internal enum ClearMode : byte
None,
Left,
Right,
- Both
+ Both,
+ InlineStart,
+ InlineEnd
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs
index 5c1ccdeb7..870a28824 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Colors.cs
@@ -7,7 +7,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal static class Colors
{
private static readonly Dictionary NamedColors =
- new Dictionary(StringComparer.OrdinalIgnoreCase)
+ new (StringComparer.OrdinalIgnoreCase)
{
{"aliceblue", new Color(240, 248, 255)},
{"antiquewhite", new Color(250, 235, 215)},
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
index e023138ad..45e491d6e 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Combinators.cs
@@ -2,22 +2,19 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal static class Combinators
{
- // const (not static readonly) so these can be used as constant patterns in switch statements
- // (e.g. AttributeSelectorFactory's reflection-free dispatch). String literals are interned, so
- // reference/value equality behaviour is unchanged.
- public const string Exactly = "=";
- public const string Unlike = "!=";
- public const string InList = "~=";
- public const string InToken = "|=";
- public const string Begins = "^=";
- public const string Ends = "$=";
- public const string InText = "*=";
- public const string Column = "||";
- public const string Pipe = "|";
- public const string Adjacent = "+";
- public const string Descendent = " ";
- public const string Deep = ">>>";
- public const string Child = ">";
- public const string Sibling = "~";
+ public static readonly string Exactly = "=";
+ public static readonly string Unlike = "!=";
+ public static readonly string InList = "~=";
+ public static readonly string InToken = "|=";
+ public static readonly string Begins = "^=";
+ public static readonly string Ends = "$=";
+ public static readonly string InText = "*=";
+ public static readonly string Column = "||";
+ public static readonly string Pipe = "|";
+ public static readonly string Adjacent = "+";
+ public static readonly string Descendent = " ";
+ public static readonly string Deep = ">>>";
+ public static readonly string Child = ">";
+ public static readonly string Sibling = "~";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs
index 5f2739ce8..a1ec8f55c 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/DisplayMode.cs
@@ -20,6 +20,8 @@ internal enum DisplayMode : byte
Flex,
InlineFlex,
Grid,
- InlineGrid
+ InlineGrid,
+ Contents,
+ FlowRoot
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
index 69e71400f..905e3ff8c 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FeatureNames.cs
@@ -51,5 +51,12 @@ internal static class FeatureNames
public static readonly string Update = "update";
public static readonly string AnyHover = "any-hover";
public static readonly string AnyPointer = "any-pointer";
+ public static readonly string ForcedColors = "forced-colors";
+ public static readonly string PrefersReducedData = "prefers-reduced-data";
+ public static readonly string InvertedColors = "inverted-colors";
+ public static readonly string DynamicRange = "dynamic-range";
+ public static readonly string VideoDynamicRange = "video-dynamic-range";
+ public static readonly string OverflowBlock = "overflow-block";
+ public static readonly string OverflowInline = "overflow-inline";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs
index 6ae2fe94b..05fa91470 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Floating.cs
@@ -4,6 +4,8 @@ internal enum Floating : byte
{
None,
Left,
- Right
+ Right,
+ InlineStart,
+ InlineEnd
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
index 94c802298..a7468d429 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/FunctionNames.cs
@@ -23,14 +23,27 @@ internal static class FunctionNames
public static readonly string RadialGradient = "radial-gradient";
public static readonly string RepeatingLinearGradient = "repeating-linear-gradient";
public static readonly string RepeatingRadialGradient = "repeating-radial-gradient";
+ public static readonly string ConicGradient = "conic-gradient";
+ public static readonly string RepeatingConicGradient = "repeating-conic-gradient";
public static readonly string Image = "image";
+ public static readonly string ImageSet = "image-set";
+ public static readonly string CrossFade = "cross-fade";
+ public static readonly string Element = "element";
public static readonly string Counter = "counter";
public static readonly string Counters = "counters";
public static readonly string Content = "content";
+ public static readonly string StringFn = "string";
+ public static readonly string TargetCounter = "target-counter";
+ public static readonly string TargetCounters = "target-counters";
+ public static readonly string TargetText = "target-text";
+ public static readonly string Leader = "leader";
+ public static readonly string Running = "running";
public static readonly string Calc = "calc";
+ public static readonly string Style = "style";
public static readonly string Min = "min";
public static readonly string Max = "max";
public static readonly string Clamp = "clamp";
+ public static readonly string Var = "var";
public static readonly string Toggle = "toggle";
public static readonly string Translate = "translate";
public static readonly string TranslateX = "translateX";
@@ -54,6 +67,7 @@ internal static class FunctionNames
public static readonly string ScaleZ = "scaleZ";
public static readonly string Steps = "steps";
public static readonly string CubicBezier = "cubic-bezier";
+ public static readonly string Linear = "linear";
public static readonly string Perspective = "perspective";
public static readonly string Gray = "gray";
public static readonly string Hwb = "hwb";
@@ -62,11 +76,8 @@ internal static class FunctionNames
public static readonly string Lch = "lch";
public static readonly string Oklch = "oklch";
public static readonly string ColorMix = "color-mix";
- public static readonly string ConicGradient = "conic-gradient";
- public static readonly string RepeatingConicGradient = "repeating-conic-gradient";
- public static readonly string Var = "var";
- public static readonly string ImageSet = "image-set";
- public static readonly string CrossFade = "cross-fade";
- public static readonly string Element = "element";
+ public static readonly string ColorFunction = "color";
+ public static readonly string LightDark = "light-dark";
+ public static readonly string DeviceCmyk = "device-cmyk";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs
index ad5286781..47ca9871d 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/HorizontalAlignment.cs
@@ -5,6 +5,9 @@ internal enum HorizontalAlignment : byte
Left,
Center,
Right,
- Justify
+ Justify,
+ Start,
+ End,
+ MatchParent
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs
new file mode 100644
index 000000000..6c1f67174
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Hyphens.cs
@@ -0,0 +1,9 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum Hyphens : byte
+ {
+ None,
+ Manual,
+ Auto
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
index 232ba506e..8ce50190f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Keywords.cs
@@ -5,9 +5,9 @@ internal static class Keywords
public static readonly string Important = "important";
public static readonly string Inherit = "inherit";
public static readonly string Initial = "initial";
+ public static readonly string Manual = "manual";
public static readonly string None = "none";
public static readonly string Auto = "auto";
- public static readonly string Manual = "manual";
public static readonly string From = "from";
public static readonly string To = "to";
public static readonly string Always = "always";
@@ -22,6 +22,8 @@ internal static class Keywords
public static readonly string AvoidPage = "avoid-page";
public static readonly string AvoidColumn = "avoid-column";
public static readonly string AvoidRegion = "avoid-region";
+ public static readonly string Recto = "recto";
+ public static readonly string Verso = "verso";
public static readonly string Inset = "inset";
public static readonly string Outset = "outset";
public static readonly string Ridge = "ridge";
@@ -29,6 +31,8 @@ internal static class Keywords
public static readonly string Fixed = "fixed";
public static readonly string Local = "local";
public static readonly string Scroll = "scroll";
+ public static readonly string Clip = "clip";
+ public static readonly string Ellipsis = "ellipsis";
public static readonly string NoRepeat = "no-repeat";
public static readonly string Repeat = "repeat";
public static readonly string RepeatX = "repeat-x";
@@ -52,6 +56,8 @@ internal static class Keywords
public static readonly string TableRowGroup = "table-row-group";
public static readonly string Flex = "flex";
public static readonly string InlineFlex = "inline-flex";
+ public static readonly string Contents = "contents";
+ public static readonly string FlowRoot = "flow-root";
public static readonly string Grid = "grid";
public static readonly string InlineGrid = "inline-grid";
public static readonly string Visible = "visible";
@@ -134,6 +140,7 @@ internal static class Keywords
public static readonly string Cell = "cell";
public static readonly string Crosshair = "crosshair";
public static readonly string Text = "text";
+ public static readonly string BorderArea = "border-area";
public static readonly string VerticalText = "vertical-text";
public static readonly string Alias = "alias";
public static readonly string Copy = "copy";
@@ -177,6 +184,10 @@ internal static class Keywords
public static readonly string Linear = "linear";
public static readonly string StepStart = "step-start";
public static readonly string StepEnd = "step-end";
+ public static readonly string JumpStart = "jump-start";
+ public static readonly string JumpEnd = "jump-end";
+ public static readonly string JumpNone = "jump-none";
+ public static readonly string JumpBoth = "jump-both";
public static readonly string Disc = "disc";
public static readonly string Circle = "circle";
public static readonly string Square = "square";
@@ -290,6 +301,10 @@ internal static class Keywords
public static readonly string BreakAll = "break-all";
public static readonly string KeepAll = "keep-all";
public static readonly string BreakWord = "break-word";
+ public static readonly string Anywhere = "anywhere";
+ public static readonly string MatchParent = "match-parent";
+ public static readonly string InlineStart = "inline-start";
+ public static readonly string InlineEnd = "inline-end";
public static readonly string Nonzero = "nonzero";
public static readonly string Evenodd = "evenodd";
public static readonly string Row = "row";
@@ -305,6 +320,7 @@ internal static class Keywords
public static readonly string AutoFlow = "auto-flow";
public static readonly string Subgrid = "subgrid";
public static readonly string Dense = "dense";
+ public static readonly string Span = "span";
public static readonly string Content = "content";
public static readonly string Revert = "revert";
public static readonly string RevertLayer = "revert-layer";
@@ -319,8 +335,12 @@ internal static class Keywords
public static readonly string SelfEnd = "self-end";
public static readonly string Size = "size";
public static readonly string InlineSize = "inline-size";
-
- // The `span` keyword of a grid-placement value (CSS Grid 1 §8.3).
- public static readonly string Span = "span";
+ public static readonly string NoPreference = "no-preference";
+ public static readonly string Reduce = "reduce";
+ public static readonly string Active = "active";
+ public static readonly string Inverted = "inverted";
+ public static readonly string Standard = "standard";
+ public static readonly string High = "high";
+ public static readonly string Paged = "paged";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs
new file mode 100644
index 000000000..f6b3241d2
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/LineBreak.cs
@@ -0,0 +1,11 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum LineBreak : byte
+ {
+ Auto,
+ Loose,
+ Normal,
+ Strict,
+ Anywhere
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs
new file mode 100644
index 000000000..a24afef12
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/MediaFeatureComparison.cs
@@ -0,0 +1,11 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum MediaFeatureComparison : byte
+ {
+ Equal,
+ LessThan,
+ LessThanOrEqual,
+ GreaterThan,
+ GreaterThanOrEqual
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs
index d04300cd4..62e2db540 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/Overflow.cs
@@ -5,6 +5,7 @@ internal enum Overflow : byte
Auto,
Visible,
Hidden,
- Scroll
+ Scroll,
+ Clip
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs
index 921c52c04..5e1580e7e 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/OverflowWrap.cs
@@ -3,6 +3,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal enum OverflowWrap : byte
{
Normal,
- BreakWord
+ BreakWord,
+ Anywhere
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
index c8ffd137f..9e030f397 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PropertyNames.cs
@@ -2,12 +2,15 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal static class PropertyNames
{
+ public static readonly string AccentColor = "accent-color";
+ public static readonly string All = "all";
public static readonly string AlignContent = "align-content";
public static readonly string AlignItems = "align-items";
public static readonly string AlignBaseline = "alignment-baseline";
public static readonly string AlignSelf = "align-self";
public static readonly string Accelerator = "accelerator";
public static readonly string Animation = "animation";
+ public static readonly string AnimationComposition = "animation-composition";
public static readonly string AnimationDelay = "animation-delay";
public static readonly string AnimationDirection = "animation-direction";
public static readonly string AnimationDuration = "animation-duration";
@@ -15,8 +18,14 @@ internal static class PropertyNames
public static readonly string AnimationIterationCount = "animation-iteration-count";
public static readonly string AnimationName = "animation-name";
public static readonly string AnimationPlayState = "animation-play-state";
+ public static readonly string AnimationRange = "animation-range";
+ public static readonly string AnimationRangeEnd = "animation-range-end";
+ public static readonly string AnimationRangeStart = "animation-range-start";
+ public static readonly string AnimationTimeline = "animation-timeline";
public static readonly string AnimationTimingFunction = "animation-timing-function";
+ public static readonly string BackdropFilter = "backdrop-filter";
public static readonly string BackgroundAttachment = "background-attachment";
+ public static readonly string BackgroundBlendMode = "background-blend-mode";
public static readonly string BackgroundColor = "background-color";
public static readonly string BackgroundClip = "background-clip";
public static readonly string BackgroundOrigin = "background-origin";
@@ -29,6 +38,12 @@ internal static class PropertyNames
public static readonly string Background = "background";
public static readonly string Behavior = "behavior";
public static readonly string BaselineShift = "baseline-shift";
+ public static readonly string BlockEllipsis = "block-ellipsis";
+ public static readonly string BlockStep = "block-step";
+ public static readonly string BlockStepAlign = "block-step-align";
+ public static readonly string BlockStepInsert = "block-step-insert";
+ public static readonly string BlockStepRound = "block-step-round";
+ public static readonly string BlockStepSize = "block-step-size";
public static readonly string BorderColor = "border-color";
public static readonly string BorderSpacing = "border-spacing";
public static readonly string BorderCollapse = "border-collapse";
@@ -38,6 +53,18 @@ internal static class PropertyNames
public static readonly string BorderTopRightRadius = "border-top-right-radius";
public static readonly string BorderBottomLeftRadius = "border-bottom-left-radius";
public static readonly string BorderBottomRightRadius = "border-bottom-right-radius";
+ public static readonly string BorderTopRadius = "border-top-radius";
+ public static readonly string BorderRightRadius = "border-right-radius";
+ public static readonly string BorderBottomRadius = "border-bottom-radius";
+ public static readonly string BorderLeftRadius = "border-left-radius";
+ public static readonly string BorderBlockStartRadius = "border-block-start-radius";
+ public static readonly string BorderBlockEndRadius = "border-block-end-radius";
+ public static readonly string BorderInlineStartRadius = "border-inline-start-radius";
+ public static readonly string BorderInlineEndRadius = "border-inline-end-radius";
+ public static readonly string BorderStartStartRadius = "border-start-start-radius";
+ public static readonly string BorderStartEndRadius = "border-start-end-radius";
+ public static readonly string BorderEndStartRadius = "border-end-start-radius";
+ public static readonly string BorderEndEndRadius = "border-end-end-radius";
public static readonly string BoxShadow = "box-shadow";
public static readonly string BoxSizing = "box-sizing";
public static readonly string AspectRatio = "aspect-ratio";
@@ -68,9 +95,47 @@ internal static class PropertyNames
public static readonly string BorderLeftWidth = "border-left-width";
public static readonly string BorderRightWidth = "border-right-width";
public static readonly string BorderBottomWidth = "border-bottom-width";
+ public static readonly string BorderBlockStartWidth = "border-block-start-width";
+ public static readonly string BorderBlockStartStyle = "border-block-start-style";
+ public static readonly string BorderBlockStartColor = "border-block-start-color";
+ public static readonly string BorderBlockEndWidth = "border-block-end-width";
+ public static readonly string BorderBlockEndStyle = "border-block-end-style";
+ public static readonly string BorderBlockEndColor = "border-block-end-color";
+ public static readonly string BorderInlineStartWidth = "border-inline-start-width";
+ public static readonly string BorderInlineStartStyle = "border-inline-start-style";
+ public static readonly string BorderInlineStartColor = "border-inline-start-color";
+ public static readonly string BorderInlineEndWidth = "border-inline-end-width";
+ public static readonly string BorderInlineEndStyle = "border-inline-end-style";
+ public static readonly string BorderInlineEndColor = "border-inline-end-color";
+ public static readonly string BorderBlockWidth = "border-block-width";
+ public static readonly string BorderBlockStyle = "border-block-style";
+ public static readonly string BorderBlockColor = "border-block-color";
+ public static readonly string BorderInlineWidth = "border-inline-width";
+ public static readonly string BorderInlineStyle = "border-inline-style";
+ public static readonly string BorderInlineColor = "border-inline-color";
+ public static readonly string BorderBlock = "border-block";
+ public static readonly string BorderBlockStart = "border-block-start";
+ public static readonly string BorderBlockEnd = "border-block-end";
+ public static readonly string BorderInline = "border-inline";
+ public static readonly string BorderInlineStart = "border-inline-start";
+ public static readonly string BorderInlineEnd = "border-inline-end";
public static readonly string BorderWidth = "border-width";
+ public static readonly string Bleed = "bleed";
+ public static readonly string BookmarkLabel = "bookmark-label";
+ public static readonly string BookmarkLevel = "bookmark-level";
+ public static readonly string BookmarkState = "bookmark-state";
+ public static readonly string BookmarkTarget = "bookmark-target";
public static readonly string Border = "border";
public static readonly string Bottom = "bottom";
+ public static readonly string BufferedRendering = "buffered-rendering";
+ public static readonly string CaretColor = "caret-color";
+ public static readonly string InsetBlockStart = "inset-block-start";
+ public static readonly string InsetBlockEnd = "inset-block-end";
+ public static readonly string InsetInlineStart = "inset-inline-start";
+ public static readonly string InsetInlineEnd = "inset-inline-end";
+ public static readonly string InsetBlock = "inset-block";
+ public static readonly string InsetInline = "inset-inline";
+ public static readonly string Inset = "inset";
public static readonly string Columns = "columns";
public static readonly string ColumnCount = "column-count";
public static readonly string ColumnFill = "column-fill";
@@ -92,12 +157,74 @@ internal static class PropertyNames
public static readonly string ClipRule = "clip-rule";
public static readonly string Color = "color";
public static readonly string ColorInterpolationFilters = "color-interpolation-filters";
- public static readonly string ContainerName = "container-name";
+ public static readonly string ColorScheme = "color-scheme";
+ public static readonly string ColorInterpolation = "color-interpolation";
+ public static readonly string ContainerName= "container-name";
public static readonly string ContainerType = "container-type";
public static readonly string Content = "content";
+ public static readonly string FloatReference = "float-reference";
+ public static readonly string FootnoteDisplay = "footnote-display";
+ public static readonly string FootnotePolicy = "footnote-policy";
+ public static readonly string ForcedColorAdjust = "forced-color-adjust";
+ public static readonly string ImageRendering = "image-rendering";
+ public static readonly string Interactivity = "interactivity";
+ public static readonly string Isolation = "isolation";
+ public static readonly string MathDepth = "math-depth";
+ public static readonly string MathShift = "math-shift";
+ public static readonly string MathStyle = "math-style";
+ public static readonly string MixBlendMode = "mix-blend-mode";
+ public static readonly string PrintColorAdjust = "print-color-adjust";
+ public static readonly string ShapeRendering = "shape-rendering";
+ public static readonly string Contain = "contain";
+ public static readonly string ContainIntrinsicWidth = "contain-intrinsic-width";
+ public static readonly string ContainIntrinsicHeight = "contain-intrinsic-height";
+ public static readonly string ContainIntrinsicInlineSize = "contain-intrinsic-inline-size";
+ public static readonly string ContainIntrinsicBlockSize = "contain-intrinsic-block-size";
+ public static readonly string ContainIntrinsicSize = "contain-intrinsic-size";
+ public static readonly string Container = "container";
+ public static readonly string ContentVisibility = "content-visibility";
+ public static readonly string CornerTopLeftShape = "corner-top-left-shape";
+ public static readonly string CornerTopRightShape = "corner-top-right-shape";
+ public static readonly string CornerBottomRightShape = "corner-bottom-right-shape";
+ public static readonly string CornerBottomLeftShape = "corner-bottom-left-shape";
+ public static readonly string CornerStartStartShape = "corner-start-start-shape";
+ public static readonly string CornerStartEndShape = "corner-start-end-shape";
+ public static readonly string CornerEndStartShape = "corner-end-start-shape";
+ public static readonly string CornerEndEndShape = "corner-end-end-shape";
+ public static readonly string CornerShape = "corner-shape";
+ public static readonly string Corner = "corner";
+ public static readonly string CornerTopShape = "corner-top-shape";
+ public static readonly string CornerTop = "corner-top";
+ public static readonly string CornerRightShape = "corner-right-shape";
+ public static readonly string CornerRight = "corner-right";
+ public static readonly string CornerBottomShape = "corner-bottom-shape";
+ public static readonly string CornerBottom = "corner-bottom";
+ public static readonly string CornerLeftShape = "corner-left-shape";
+ public static readonly string CornerLeft = "corner-left";
+ public static readonly string CornerBlockStartShape = "corner-block-start-shape";
+ public static readonly string CornerBlockStart = "corner-block-start";
+ public static readonly string CornerBlockEndShape = "corner-block-end-shape";
+ public static readonly string CornerBlockEnd = "corner-block-end";
+ public static readonly string CornerInlineStartShape = "corner-inline-start-shape";
+ public static readonly string CornerInlineStart = "corner-inline-start";
+ public static readonly string CornerInlineEndShape = "corner-inline-end-shape";
+ public static readonly string CornerInlineEnd = "corner-inline-end";
+ public static readonly string CornerTopLeft = "corner-top-left";
+ public static readonly string CornerTopRight = "corner-top-right";
+ public static readonly string CornerBottomRight = "corner-bottom-right";
+ public static readonly string CornerBottomLeft = "corner-bottom-left";
+ public static readonly string CornerStartStart = "corner-start-start";
+ public static readonly string CornerStartEnd = "corner-start-end";
+ public static readonly string CornerEndStart = "corner-end-start";
+ public static readonly string CornerEndEnd = "corner-end-end";
+ public static readonly string StringSet = "string-set";
public static readonly string CounterIncrement = "counter-increment";
public static readonly string CounterReset = "counter-reset";
+ public static readonly string CounterSet = "counter-set";
public static readonly string Cursor = "cursor";
+ public static readonly string Cx = "cx";
+ public static readonly string Cy = "cy";
+ public static readonly string D = "d";
public static readonly string Direction = "direction";
public static readonly string Display = "display";
public static readonly string DominantBaseline = "dominant-baseline";
@@ -114,18 +241,37 @@ internal static class PropertyNames
public static readonly string FlexGrow = "flex-grow";
public static readonly string FlexShrink = "flex-shrink";
public static readonly string FlexWrap = "flex-wrap";
+ public static readonly string FloodColor = "flood-color";
+ public static readonly string FloodOpacity = "flood-opacity";
public static readonly string Float = "float";
public static readonly string FontFamily = "font-family";
public static readonly string FontFeatureSettings = "font-feature-settings";
+ public static readonly string FontKerning = "font-kerning";
+ public static readonly string FontLanguageOverride = "font-language-override";
+ public static readonly string FontOpticalSizing = "font-optical-sizing";
public static readonly string FontPalette = "font-palette";
public static readonly string FontSize = "font-size";
public static readonly string FontSizeAdjust = "font-size-adjust";
public static readonly string FontStyle = "font-style";
public static readonly string FontStretch = "font-stretch";
+ public static readonly string FontSynthesis = "font-synthesis";
+ public static readonly string FontSynthesisPosition = "font-synthesis-position";
+ public static readonly string FontSynthesisSmallCaps = "font-synthesis-small-caps";
+ public static readonly string FontSynthesisStyle = "font-synthesis-style";
+ public static readonly string FontSynthesisWeight = "font-synthesis-weight";
public static readonly string FontVariant = "font-variant";
+ public static readonly string FontVariantAlternates = "font-variant-alternates";
+ public static readonly string FontVariantCaps = "font-variant-caps";
+ public static readonly string FontVariantEastAsian = "font-variant-east-asian";
+ public static readonly string FontVariantEmoji = "font-variant-emoji";
+ public static readonly string FontVariantLigatures = "font-variant-ligatures";
+ public static readonly string FontVariantNumeric = "font-variant-numeric";
+ public static readonly string FontVariantPosition = "font-variant-position";
+ public static readonly string FontVariationSettings = "font-variation-settings";
public static readonly string FontWeight = "font-weight";
public static readonly string Font = "font";
public static readonly string Gap = "gap";
+ public static readonly string InlineSize = "inline-size";
public static readonly string Grid = "grid";
public static readonly string GridTemplate = "grid-template";
public static readonly string GridTemplateColumns = "grid-template-columns";
@@ -141,17 +287,26 @@ internal static class PropertyNames
public static readonly string GridRowStart = "grid-row-start";
public static readonly string GridRowEnd = "grid-row-end";
public static readonly string GridArea = "grid-area";
- public static readonly string JustifyItems = "justify-items";
- public static readonly string JustifySelf = "justify-self";
- public static readonly string PlaceItems = "place-items";
- public static readonly string PlaceContent = "place-content";
- public static readonly string PlaceSelf = "place-self";
public static readonly string GlyphOrientationHorizontal = "glyph-orientation-horizontal";
public static readonly string GlyphOrientationVertical = "glyph-orientation-vertical";
+ public static readonly string HangingPunctuation = "hanging-punctuation";
public static readonly string Height = "height";
+ public static readonly string HyphenateCharacter = "hyphenate-character";
+ public static readonly string HyphenateLimitChars = "hyphenate-limit-chars";
+ public static readonly string HyphenateLimitLast = "hyphenate-limit-last";
+ public static readonly string HyphenateLimitLines = "hyphenate-limit-lines";
+ public static readonly string HyphenateLimitZone = "hyphenate-limit-zone";
public static readonly string Hyphens = "hyphens";
+ public static readonly string ImageOrientation = "image-orientation";
+ public static readonly string ImageResolution = "image-resolution";
public static readonly string ImeMode = "ime-mode";
+ public static readonly string InitialLetter = "initial-letter";
public static readonly string JustifyContent = "justify-content";
+ public static readonly string JustifyItems = "justify-items";
+ public static readonly string JustifySelf = "justify-self";
+ public static readonly string PlaceItems = "place-items";
+ public static readonly string PlaceContent = "place-content";
+ public static readonly string PlaceSelf = "place-self";
public static readonly string LayoutGrid = "layout-grid";
public static readonly string LayoutGridChar = "layout-grid-char";
public static readonly string LayoutGridType = "layout-grid-type";
@@ -159,7 +314,11 @@ internal static class PropertyNames
public static readonly string LayoutGridLine = "layout-grid-line";
public static readonly string Left = "left";
public static readonly string LetterSpacing = "letter-spacing";
+ public static readonly string LineBreak = "line-break";
+ public static readonly string LightingColor = "lighting-color";
public static readonly string LineHeight = "line-height";
+ public static readonly string LineClamp = "line-clamp";
+ public static readonly string LineHeightStep = "line-height-step";
public static readonly string ListStyleImage = "list-style-image";
public static readonly string ListStylePosition = "list-style-position";
public static readonly string ListStyleType = "list-style-type";
@@ -168,12 +327,27 @@ internal static class PropertyNames
public static readonly string MarginLeft = "margin-left";
public static readonly string MarginTop = "margin-top";
public static readonly string MarginBottom = "margin-bottom";
+ public static readonly string MarginBlockStart = "margin-block-start";
+ public static readonly string MarginBlockEnd = "margin-block-end";
+ public static readonly string MarginInlineStart = "margin-inline-start";
+ public static readonly string MarginInlineEnd = "margin-inline-end";
+ public static readonly string MarginBlock = "margin-block";
+ public static readonly string MarginInline = "margin-inline";
public static readonly string Margin = "margin";
+ public static readonly string Marks = "marks";
public static readonly string Marker = "marker";
public static readonly string MarkerEnd = "marker-end";
public static readonly string MarkerMid = "marker-mid";
public static readonly string MarkerStart = "marker-start";
public static readonly string Mask = "mask";
+ public static readonly string MaskClip = "mask-clip";
+ public static readonly string MaskComposite = "mask-composite";
+ public static readonly string MaskImage = "mask-image";
+ public static readonly string MaskMode = "mask-mode";
+ public static readonly string MaskOrigin = "mask-origin";
+ public static readonly string MaskPosition = "mask-position";
+ public static readonly string MaskRepeat = "mask-repeat";
+ public static readonly string MaskSize = "mask-size";
public static readonly string MaxHeight = "max-height";
public static readonly string MaxWidth = "max-width";
public static readonly string MinHeight = "min-height";
@@ -182,32 +356,76 @@ internal static class PropertyNames
public static readonly string Order = "order";
public static readonly string Orphans = "orphans";
public static readonly string OutlineColor = "outline-color";
+ public static readonly string OutlineOffset = "outline-offset";
public static readonly string OutlineStyle = "outline-style";
public static readonly string OutlineWidth = "outline-width";
public static readonly string Outline = "outline";
public static readonly string Overflow = "overflow";
public static readonly string OverflowX = "overflow-x";
public static readonly string OverflowY = "overflow-y";
+ public static readonly string OverflowClipMargin = "overflow-clip-margin";
+ public static readonly string OverflowClipMarginTop = "overflow-clip-margin-top";
+ public static readonly string OverflowClipMarginRight = "overflow-clip-margin-right";
+ public static readonly string OverflowClipMarginBottom = "overflow-clip-margin-bottom";
+ public static readonly string OverflowClipMarginLeft = "overflow-clip-margin-left";
+ public static readonly string OverflowClipMarginBlock = "overflow-clip-margin-block";
+ public static readonly string OverflowClipMarginBlockStart = "overflow-clip-margin-block-start";
+ public static readonly string OverflowClipMarginBlockEnd = "overflow-clip-margin-block-end";
+ public static readonly string OverflowClipMarginInline = "overflow-clip-margin-inline";
+ public static readonly string OverflowClipMarginInlineStart = "overflow-clip-margin-inline-start";
+ public static readonly string OverflowClipMarginInlineEnd = "overflow-clip-margin-inline-end";
public static readonly string OverflowWrap = "overflow-wrap";
public static readonly string PaddingTop = "padding-top";
public static readonly string PaddingRight = "padding-right";
public static readonly string PaddingLeft = "padding-left";
public static readonly string PaddingBottom = "padding-bottom";
+ public static readonly string PaddingBlockStart = "padding-block-start";
+ public static readonly string PaddingBlockEnd = "padding-block-end";
+ public static readonly string PaddingInlineStart = "padding-inline-start";
+ public static readonly string PaddingInlineEnd = "padding-inline-end";
+ public static readonly string PaddingBlock = "padding-block";
+ public static readonly string PaddingInline = "padding-inline";
public static readonly string Padding = "padding";
public static readonly string PageBreakAfter = "page-break-after";
public static readonly string PageBreakBefore = "page-break-before";
public static readonly string PageBreakInside = "page-break-inside";
- public static readonly string PageName = "page";
+ public static readonly string PaintOrder = "paint-order";
public static readonly string Perspective = "perspective";
public static readonly string PerspectiveOrigin = "perspective-origin";
public static readonly string PointerEvents = "pointer-events";
public static readonly string Position = "position";
+ public static readonly string OverscrollBehavior = "overscroll-behavior";
+ public static readonly string OverscrollBehaviorX = "overscroll-behavior-x";
+ public static readonly string OverscrollBehaviorY = "overscroll-behavior-y";
public static readonly string Quotes = "quotes";
+ public static readonly string R = "r";
+ public static readonly string Rx = "rx";
+ public static readonly string Ry = "ry";
public static readonly string Right = "right";
public static readonly string RowGap = "row-gap";
public static readonly string RubyAlign = "ruby-align";
public static readonly string RubyOverhang = "ruby-overhang";
public static readonly string RubyPosition = "ruby-position";
+ public static readonly string ScrollBehavior = "scroll-behavior";
+ public static readonly string ScrollMargin = "scroll-margin";
+ public static readonly string ScrollMarginTop = "scroll-margin-top";
+ public static readonly string ScrollMarginRight = "scroll-margin-right";
+ public static readonly string ScrollMarginBottom = "scroll-margin-bottom";
+ public static readonly string ScrollMarginLeft = "scroll-margin-left";
+ public static readonly string ScrollPadding = "scroll-padding";
+ public static readonly string ScrollPaddingTop = "scroll-padding-top";
+ public static readonly string ScrollPaddingRight = "scroll-padding-right";
+ public static readonly string ScrollPaddingBottom = "scroll-padding-bottom";
+ public static readonly string ScrollPaddingLeft = "scroll-padding-left";
+ public static readonly string ScrollSnapAlign = "scroll-snap-align";
+ public static readonly string ScrollSnapStop = "scroll-snap-stop";
+ public static readonly string ScrollSnapType = "scroll-snap-type";
+ public static readonly string ScrollTimeline = "scroll-timeline";
+ public static readonly string ScrollTimelineAxis = "scroll-timeline-axis";
+ public static readonly string ScrollTimelineName = "scroll-timeline-name";
+ public static readonly string ShapeImageThreshold = "shape-image-threshold";
+ public static readonly string ShapeMargin = "shape-margin";
+ public static readonly string ShapeOutside = "shape-outside";
public static readonly string Scrollbar3dLightColor = "scrollbar3d-light-color";
public static readonly string ScrollbarArrowColor = "scrollbar-arrow-color";
public static readonly string ScrollbarDarkShadowColor = "scrollbar-dark-shadow-color";
@@ -215,6 +433,11 @@ internal static class PropertyNames
public static readonly string ScrollbarHighlightColor = "scrollbar-highlight-color";
public static readonly string ScrollbarShadowColor = "scrollbar-shadow-color";
public static readonly string ScrollbarTrackColor = "scrollbar-track-color";
+ public static readonly string ScrollbarColor = "scrollbar-color";
+ public static readonly string ScrollbarGutter = "scrollbar-gutter";
+ public static readonly string ScrollbarWidth = "scrollbar-width";
+ public static readonly string StopColor = "stop-color";
+ public static readonly string StopOpacity = "stop-opacity";
public static readonly string Stroke = "stroke";
public static readonly string StrokeDasharray = "stroke-dasharray";
public static readonly string StrokeDashoffset = "stroke-dashoffset";
@@ -223,103 +446,90 @@ internal static class PropertyNames
public static readonly string StrokeMiterlimit = "stroke-miterlimit";
public static readonly string StrokeOpacity = "stroke-opacity";
public static readonly string StrokeWidth = "stroke-width";
- public static readonly string StringSet = "string-set";
+ public static readonly string TabSize = "tab-size";
public static readonly string TableLayout = "table-layout";
public static readonly string TextAlign = "text-align";
+ public static readonly string TextAlignAll = "text-align-all";
public static readonly string TextAlignLast = "text-align-last";
public static readonly string TextAnchor = "text-anchor";
public static readonly string TextAutospace = "text-autospace";
+ public static readonly string TextCombineUpright = "text-combine-upright";
+ public static readonly string TextBox = "text-box";
+ public static readonly string TextBoxEdge = "text-box-edge";
+ public static readonly string TextBoxTrim = "text-box-trim";
public static readonly string TextDecoration = "text-decoration";
public static readonly string TextDecorationColor = "text-decoration-color";
public static readonly string TextDecorationLine = "text-decoration-line";
+ public static readonly string TextDecorationInset = "text-decoration-inset";
+ public static readonly string TextDecorationSkip = "text-decoration-skip";
+ public static readonly string TextDecorationSkipInk = "text-decoration-skip-ink";
public static readonly string TextDecorationStyle = "text-decoration-style";
+ public static readonly string TextDecorationThickness = "text-decoration-thickness";
+ public static readonly string TextEmphasis = "text-emphasis";
+ public static readonly string TextEmphasisColor = "text-emphasis-color";
+ public static readonly string TextEmphasisPosition = "text-emphasis-position";
+ public static readonly string TextEmphasisStyle = "text-emphasis-style";
public static readonly string TextIndent = "text-indent";
public static readonly string TextJustify = "text-justify";
+ public static readonly string TextOrientation = "text-orientation";
public static readonly string TextOverflow = "text-overflow";
+ public static readonly string TextRendering = "text-rendering";
public static readonly string TextShadow = "text-shadow";
public static readonly string TextTransform = "text-transform";
+ public static readonly string TextWrap = "text-wrap";
+ public static readonly string TextWrapMode = "text-wrap-mode";
+ public static readonly string TextWrapStyle = "text-wrap-style";
+ public static readonly string TextUnderlineOffset = "text-underline-offset";
public static readonly string TextUnderlinePosition = "text-underline-position";
+ public static readonly string TouchAction = "touch-action";
+ public static readonly string TimelineScope = "timeline-scope";
public static readonly string Transform = "transform";
+ public static readonly string TransformBox = "transform-box";
public static readonly string TransformOrigin = "transform-origin";
public static readonly string TransformStyle = "transform-style";
+ public static readonly string Translate = "translate";
+ public static readonly string Rotate = "rotate";
+ public static readonly string Scale = "scale";
public static readonly string Transition = "transition";
+ public static readonly string TransitionBehavior = "transition-behavior";
public static readonly string TransitionDelay = "transition-delay";
public static readonly string TransitionDuration = "transition-duration";
public static readonly string TransitionTimingFunction = "transition-timing-function";
public static readonly string TransitionProperty = "transition-property";
public static readonly string Top = "top";
+ public static readonly string ViewTimeline = "view-timeline";
+ public static readonly string ViewTimelineAxis = "view-timeline-axis";
+ public static readonly string ViewTimelineInset = "view-timeline-inset";
+ public static readonly string ViewTimelineName = "view-timeline-name";
public static readonly string UnicodeBidirectional = "unicode-bidi";
+ public static readonly string UserSelect = "user-select";
+ public static readonly string VectorEffect = "vector-effect";
public static readonly string VerticalAlign = "vertical-align";
public static readonly string Visibility = "visibility";
public static readonly string WhiteSpace = "white-space";
+ public static readonly string WhiteSpaceCollapse = "white-space-collapse";
public static readonly string Widows = "widows";
public static readonly string Width = "width";
public static readonly string WordBreak = "word-break";
public static readonly string WordSpacing = "word-spacing";
public static readonly string WordWrap = "word-wrap";
public static readonly string WritingMode = "writing-mode";
+ public static readonly string X = "x";
+ public static readonly string Y = "y";
public static readonly string ZIndex = "z-index";
public static readonly string Zoom = "zoom";
public static readonly string UnicodeRange = "unicode-range";
public static readonly string Src = "src";
- // @property descriptors (CSS Properties & Values API Level 1 §3)
+ // @property descriptors (CSS Properties and Values API 1 3)
public static readonly string Syntax = "syntax";
public static readonly string InitialValue = "initial-value";
public static readonly string Inherits = "inherits";
public static readonly string BasePalette = "base-palette";
public static readonly string OverrideColors = "override-colors";
public static readonly string ObjectFit = "object-fit";
- public static readonly string ObjectPosition = "object-position";
+ // CSS Paged Media 3: the "page" property assigns a box to a named page; "size" is an @page descriptor.
+ public static readonly string PageName = "page";
public static readonly string Size = "size";
-
- // CSS Logical 1 §4
- public static readonly string MarginBlock = "margin-block";
- public static readonly string MarginBlockStart = "margin-block-start";
- public static readonly string MarginBlockEnd = "margin-block-end";
- public static readonly string MarginInline = "margin-inline";
- public static readonly string MarginInlineStart = "margin-inline-start";
- public static readonly string MarginInlineEnd = "margin-inline-end";
-
- public static readonly string PaddingBlock = "padding-block";
- public static readonly string PaddingBlockStart = "padding-block-start";
- public static readonly string PaddingBlockEnd = "padding-block-end";
- public static readonly string PaddingInline = "padding-inline";
- public static readonly string PaddingInlineStart = "padding-inline-start";
- public static readonly string PaddingInlineEnd = "padding-inline-end";
-
- public static readonly string Inset = "inset";
- public static readonly string InsetBlock = "inset-block";
- public static readonly string InsetBlockStart = "inset-block-start";
- public static readonly string InsetBlockEnd = "inset-block-end";
- public static readonly string InsetInline = "inset-inline";
- public static readonly string InsetInlineStart = "inset-inline-start";
- public static readonly string InsetInlineEnd = "inset-inline-end";
-
- public static readonly string BorderBlock = "border-block";
- public static readonly string BorderBlockStart = "border-block-start";
- public static readonly string BorderBlockEnd = "border-block-end";
- public static readonly string BorderInline = "border-inline";
- public static readonly string BorderInlineStart = "border-inline-start";
- public static readonly string BorderInlineEnd = "border-inline-end";
-
- public static readonly string BorderBlockWidth = "border-block-width";
- public static readonly string BorderBlockStyle = "border-block-style";
- public static readonly string BorderBlockColor = "border-block-color";
- public static readonly string BorderInlineWidth = "border-inline-width";
- public static readonly string BorderInlineStyle = "border-inline-style";
- public static readonly string BorderInlineColor = "border-inline-color";
-
- public static readonly string BorderBlockStartWidth = "border-block-start-width";
- public static readonly string BorderBlockStartStyle = "border-block-start-style";
- public static readonly string BorderBlockStartColor = "border-block-start-color";
- public static readonly string BorderBlockEndWidth = "border-block-end-width";
- public static readonly string BorderBlockEndStyle = "border-block-end-style";
- public static readonly string BorderBlockEndColor = "border-block-end-color";
- public static readonly string BorderInlineStartWidth = "border-inline-start-width";
- public static readonly string BorderInlineStartStyle = "border-inline-start-style";
- public static readonly string BorderInlineStartColor = "border-inline-start-color";
- public static readonly string BorderInlineEndWidth = "border-inline-end-width";
- public static readonly string BorderInlineEndStyle = "border-inline-end-style";
- public static readonly string BorderInlineEndColor = "border-inline-end-color";
+ public static readonly string ObjectPosition = "object-position";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
index e99dec18c..b90878a44 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoClassNames.cs
@@ -41,6 +41,8 @@ internal static class PseudoClassNames
public static readonly string Matches = "matches";
public static readonly string Is = "is";
public static readonly string Where = "where";
+ public static readonly string WebkitAny = "-webkit-any";
+ public static readonly string MozAny = "-moz-any";
public static readonly string NthChild = "nth-child";
public static readonly string NthLastChild = "nth-last-child";
public static readonly string NthOfType = "nth-of-type";
@@ -51,6 +53,29 @@ internal static class PseudoClassNames
public static readonly string Lang = "lang";
public static readonly string Contains = "contains";
public static readonly string HostContext = "host-context";
+ public static readonly string Host = "host";
+ public static readonly string State = "state";
+ public static readonly string Defined = "defined";
+ public static readonly string Modal = "modal";
+ public static readonly string Open = "open";
+ public static readonly string PopoverOpen = "popover-open";
+ public static readonly string Fullscreen = "fullscreen";
+ public static readonly string PictureInPicture = "picture-in-picture";
+ public static readonly string UserValid = "user-valid";
+ public static readonly string UserInvalid = "user-invalid";
+ public static readonly string Autofill = "autofill";
+ public static readonly string TargetWithin = "target-within";
+ public static readonly string LocalLink = "local-link";
+ public static readonly string Current = "current";
+ public static readonly string Past = "past";
+ public static readonly string Future = "future";
+ public static readonly string Playing = "playing";
+ public static readonly string Paused = "paused";
+ public static readonly string Seeking = "seeking";
+ public static readonly string Buffering = "buffering";
+ public static readonly string Stalled = "stalled";
+ public static readonly string Muted = "muted";
+ public static readonly string VolumeLocked = "volume-locked";
public static readonly string Separator = ":";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs
index c78fd111f..42f35c7b6 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/PseudoElementNames.cs
@@ -9,6 +9,23 @@ internal static class PseudoElementNames
public static readonly string FirstLine = "first-line";
public static readonly string FirstLetter = "first-letter";
public static readonly string Content = "content";
+ public static readonly string Placeholder = "placeholder";
+ public static readonly string Backdrop = "backdrop";
+ public static readonly string FileSelectorButton = "file-selector-button";
+ public static readonly string DetailsContent = "details-content";
+ public static readonly string TargetText = "target-text";
+ public static readonly string SpellingError = "spelling-error";
+ public static readonly string GrammarError = "grammar-error";
+ public static readonly string Cue = "cue";
+ public static readonly string CueRegion = "cue-region";
+ public static readonly string ViewTransition = "view-transition";
+ public static readonly string Part = "part";
+ public static readonly string Slotted = "slotted";
+ public static readonly string Highlight = "highlight";
+ public static readonly string ViewTransitionGroup = "view-transition-group";
+ public static readonly string ViewTransitionImagePair = "view-transition-image-pair";
+ public static readonly string ViewTransitionOld = "view-transition-old";
+ public static readonly string ViewTransitionNew = "view-transition-new";
public static readonly string Separator = "::";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs
new file mode 100644
index 000000000..a7818596c
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RotationAxis.cs
@@ -0,0 +1,10 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ /// The axis keywords of the rotate property (CSS Transforms 2).
+ internal enum RotationAxis : byte
+ {
+ X,
+ Y,
+ Z
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
index 6ada85658..54ea94326 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleNames.cs
@@ -16,5 +16,14 @@ internal static class RuleNames
public static readonly string Property = "property";
public static readonly string Layer = "layer";
public static readonly string FontPaletteValues = "font-palette-values";
+ public static readonly string StartingStyle = "starting-style";
+ public static readonly string CounterStyle = "counter-style";
+ public static readonly string FontFeatureValues = "font-feature-values";
+ public static readonly string Styleset = "styleset";
+ public static readonly string CharacterVariant = "character-variant";
+ public static readonly string Swash = "swash";
+ public static readonly string Ornaments = "ornaments";
+ public static readonly string Annotation = "annotation";
+ public static readonly string Stylistic = "stylistic";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
index f13b491b6..b98212b0c 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/RuleType.cs
@@ -23,6 +23,8 @@ internal enum RuleType : byte
Property,
Layer,
LayerStatement,
- FontPaletteValues
+ FontPaletteValues,
+ StartingStyle,
+ FontFeatureValueSet
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs
new file mode 100644
index 000000000..e2e18294d
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignAll.cs
@@ -0,0 +1,13 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum TextAlignAll : byte
+ {
+ Start,
+ End,
+ Left,
+ Right,
+ Center,
+ Justify,
+ MatchParent
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs
index 6c7a72e2e..c7f3fcccd 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextAlignLast.cs
@@ -8,6 +8,7 @@ internal enum TextAlignLast : byte
Left,
Right,
Center,
- Justify
+ Justify,
+ MatchParent
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs
new file mode 100644
index 000000000..caeea58e8
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOrientation.cs
@@ -0,0 +1,9 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum TextOrientation : byte
+ {
+ Mixed,
+ Upright,
+ Sideways
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs
new file mode 100644
index 000000000..88fb3066d
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextOverflow.cs
@@ -0,0 +1,8 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum TextOverflow : byte
+ {
+ Clip,
+ Ellipsis
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs
new file mode 100644
index 000000000..52c0a6dd8
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapMode.cs
@@ -0,0 +1,8 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum TextWrapMode : byte
+ {
+ Wrap,
+ NoWrap
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs
new file mode 100644
index 000000000..3c292caff
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/TextWrapStyle.cs
@@ -0,0 +1,11 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum TextWrapStyle : byte
+ {
+ Auto,
+ Balance,
+ Stable,
+ Pretty,
+ AvoidOrphans
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs
index a1ad6394d..7b5ab0858 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/UnitNames.cs
@@ -27,6 +27,41 @@ internal static class UnitNames
public static readonly string Pt = "pt";
public static readonly string Rem = "rem";
public static readonly string Pc = "pc";
+ public static readonly string Q = "q";
+ public static readonly string Cap = "cap";
+ public static readonly string Ic = "ic";
+ public static readonly string Lh = "lh";
+ public static readonly string Rlh = "rlh";
+ public static readonly string Rex = "rex";
+ public static readonly string Rch = "rch";
+ public static readonly string Rcap = "rcap";
+ public static readonly string Ric = "ric";
+ public static readonly string Vi = "vi";
+ public static readonly string Vb = "vb";
+ public static readonly string Svw = "svw";
+ public static readonly string Svh = "svh";
+ public static readonly string Svi = "svi";
+ public static readonly string Svb = "svb";
+ public static readonly string Svmin = "svmin";
+ public static readonly string Svmax = "svmax";
+ public static readonly string Lvw = "lvw";
+ public static readonly string Lvh = "lvh";
+ public static readonly string Lvi = "lvi";
+ public static readonly string Lvb = "lvb";
+ public static readonly string Lvmin = "lvmin";
+ public static readonly string Lvmax = "lvmax";
+ public static readonly string Dvw = "dvw";
+ public static readonly string Dvh = "dvh";
+ public static readonly string Dvi = "dvi";
+ public static readonly string Dvb = "dvb";
+ public static readonly string Dvmin = "dvmin";
+ public static readonly string Dvmax = "dvmax";
+ public static readonly string Cqw = "cqw";
+ public static readonly string Cqh = "cqh";
+ public static readonly string Cqi = "cqi";
+ public static readonly string Cqb = "cqb";
+ public static readonly string Cqmin = "cqmin";
+ public static readonly string Cqmax = "cqmax";
public static readonly string Percent = "%";
}
}
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs
new file mode 100644
index 000000000..e9d49b720
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WhiteSpaceCollapse.cs
@@ -0,0 +1,11 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum WhiteSpaceCollapse : byte
+ {
+ Collapse,
+ Preserve,
+ PreserveBreaks,
+ PreserveSpaces,
+ BreakSpaces
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs
new file mode 100644
index 000000000..48292f3d5
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Enumerations/WritingMode.cs
@@ -0,0 +1,11 @@
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ internal enum WritingMode : byte
+ {
+ HorizontalTb,
+ VerticalRl,
+ VerticalLr,
+ SidewaysRl,
+ SidewaysLr
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs
index b2054ec40..5754dc5c0 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/CharExtensions.cs
@@ -12,7 +12,7 @@ public static int FromHex(this char c)
public static string ToHex(this char character)
{
- return ((int)character).ToString("x");
+ return ((int) character).ToString("x");
}
public static bool IsInRange(this char c, int lower, int upper)
@@ -39,12 +39,12 @@ public static bool IsNormalPathCharacter(this char c)
public static bool IsUppercaseAscii(this char c)
{
- return c >= Symbols.CapitalA && c <= Symbols.CapitalZ;
+ return c is >= Symbols.CapitalA and <= Symbols.CapitalZ;
}
public static bool IsLowercaseAscii(this char c)
{
- return c >= Symbols.LowerA && c <= Symbols.LowerZ;
+ return c is >= Symbols.LowerA and <= Symbols.LowerZ;
}
public static bool IsAlphanumericAscii(this char c)
@@ -54,8 +54,8 @@ public static bool IsAlphanumericAscii(this char c)
public static bool IsHex(this char c)
{
- return c.IsDigit() || (c >= Symbols.CapitalA && c <= Symbols.CapitalF) ||
- (c >= Symbols.LowerA && c <= Symbols.LowerF);
+ return c.IsDigit() || c is >= Symbols.CapitalA and <= Symbols.CapitalF ||
+ c is >= Symbols.LowerA and <= Symbols.LowerF;
}
public static bool IsNonAscii(this char c)
@@ -65,9 +65,9 @@ public static bool IsNonAscii(this char c)
public static bool IsNonPrintable(this char c)
{
- return /*c >= Symbols.Null &&*/ c <= Symbols.Backspace
- || (c >= Symbols.ShiftOut && c <= Symbols.UnitSeparator)
- || (c >= Symbols.Delete && c < Symbols.NonBreakingSpace);
+ return c is /*>= Symbols.Null and*/ <= Symbols.Backspace
+ or >= Symbols.ShiftOut and <= Symbols.UnitSeparator
+ or >= Symbols.Delete and < Symbols.NonBreakingSpace;
}
public static bool IsLetter(this char c)
@@ -87,17 +87,17 @@ public static bool IsNameStart(this char c)
public static bool IsLineBreak(this char c)
{
- return c == Symbols.LineFeed || c == Symbols.CarriageReturn;
+ return c is Symbols.LineFeed or Symbols.CarriageReturn;
}
public static bool IsSpaceCharacter(this char c)
{
- return c == Symbols.Space || c == Symbols.Tab || c == Symbols.LineFeed || c == Symbols.CarriageReturn || c == Symbols.FormFeed;
+ return c is Symbols.Space or Symbols.Tab or Symbols.LineFeed or Symbols.CarriageReturn or Symbols.FormFeed;
}
public static bool IsDigit(this char c)
{
- return c >= Symbols.Zero && c <= Symbols.Nine;
+ return c is >= Symbols.Zero and <= Symbols.Nine;
}
// HTML forbids the use of Universal Character Set / Unicode code points
@@ -109,7 +109,7 @@ public static bool IsDigit(this char c)
public static bool IsInvalid(this int c)
{
return c == 0 || c > Symbols.MaximumCodepoint ||
- (c >= Symbols.UTF16SurrogateMin && c <= Symbols.UTF16SurrogateMax);
+ c is >= Symbols.UTF16SurrogateMin and <= Symbols.UTF16SurrogateMax;
}
public static bool IsOneOf(this char c, char a, char b)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs
index ca617f4c6..3683f84e7 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/CollectionExtensions.cs
@@ -20,8 +20,7 @@ public static T GetItemByIndex(this IEnumerable items, int index)
var i = 0;
foreach (var item in items)
- {
- if (i++ == index)
+ { if (i++ == index)
{
return item;
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs
new file mode 100644
index 000000000..c3f021dcf
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.Relative.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ // Relative color syntax (CSS Color 5 section 4): rgb(from red r g b), hsl(from ...), hwb(from ...), lab(from ...), lch(from ...), oklab(from ...),
+ // oklch(from ...) and color(from ... ...). The origin color is converted to the space of the function it is written in, its components
+ // become channel keywords (r g b, h s l, l a b, ... and alpha) that stand for numbers, and each component of the result is then a number, a percentage,
+ // none, a keyword, or a calc() over keywords.
+ internal static partial class ColorFunctionExtensions
+ {
+ private static bool IsRelative(IEnumerable args)
+ {
+ foreach (var token in args)
+ {
+ if (token.Type == TokenType.Whitespace) continue;
+ return token.Type == TokenType.Ident && token.Data.Equals("from", StringComparison.OrdinalIgnoreCase);
+ }
+
+ return false;
+ }
+
+ private static Color? ParseRelative(string name, IEnumerable args, bool wantOklab, out double[] oklab)
+ {
+ oklab = null;
+
+ var tokens = new List(args.Where(t => t.Type != TokenType.Whitespace));
+ // from [/ alpha]: no commas, and the origin is a single token (a nested function arrives as one).
+ if (tokens.Count < 5 || tokens.Any(t => t.Type == TokenType.Comma)) return null;
+
+ if (!Relative.TryGetSpec(name.ToLowerInvariant(), out var spec)) return null;
+
+ // Exactly the components, then optionally a slash and one alpha.
+ var rest = new List(tokens.Skip(2));
+ var slash = rest.FindIndex(t => t.Type == TokenType.Delim && t.Data == "/");
+ if (slash < 0 ? rest.Count != spec.ComponentCount : slash != spec.ComponentCount || rest.Count != spec.ComponentCount + 2) return null;
+
+ if (!Extract(rest, spec.ComponentCount, out var comps, out var alphaTok, allowImplicitAlpha: false)) return null;
+ if (spec.SpaceToken && !Relative.TryReadSpace(comps[0], out spec)) return null;
+
+ // color-mix() operands and relative origins nest without bound in the grammar; they share one depth limit.
+ if (_colorMixDepth >= MaxColorMixDepth) return null;
+ Color? origin;
+ double[] originOklab;
+ _colorMixDepth++;
+ try
+ {
+ origin = ResolveColor(new[] { tokens[1] }, wantOklab: true, out originOklab);
+ }
+ finally
+ {
+ _colorMixDepth--;
+ }
+
+ // A device-cmyk() origin has no sRGB value to take channels from.
+ if (!origin.HasValue || origin.Value.IsDeviceCmyk) return null;
+ var originColor = origin.Value;
+
+ var coords = Relative.OriginCoordinates(originColor, originOklab, spec.Space);
+ var variables = new Dictionary(4);
+ for (var i = 0; i < 3; i++) variables[spec.Keywords[i]] = coords[i] * spec.Scale;
+ variables["alpha"] = originColor.A / 255.0;
+
+ var offset = spec.SpaceToken ? 1 : 0;
+ var values = new double[3];
+ for (var i = 0; i < 3; i++)
+ {
+ var token = comps[offset + i];
+ var value = spec.IsHue(i) ? Relative.EvaluateHue(token, variables) : Relative.Evaluate(token, spec.References[i], variables);
+ if (value is null) return null;
+ values[i] = value.Value;
+ }
+
+ float alpha;
+ if (alphaTok != null)
+ {
+ var a = Relative.Evaluate(alphaTok, 1.0, variables);
+ if (a is null) return null;
+ alpha = (float)Clamp(a.Value, 0, 1);
+ }
+ else
+ {
+ alpha = originColor.A / 255f;
+ }
+
+ return Relative.Build(spec, values, alpha, wantOklab, out oklab);
+ }
+
+ // What each function says about its components: the space its result is in, the channel keywords, what 100% of each component is, which one is a hue.
+ private sealed class RelativeSpec
+ {
+ public RelativeSpec(ColorSpaceMath.Space space, string[] keywords, double[] references, int hueSlot, double scale, bool spaceToken, int componentCount, string function)
+ {
+ Space = space;
+ Keywords = keywords;
+ References = references;
+ HueSlot = hueSlot;
+ Scale = scale;
+ SpaceToken = spaceToken;
+ ComponentCount = componentCount;
+ Function = function;
+ }
+
+ public ColorSpaceMath.Space Space { get; }
+ public string[] Keywords { get; }
+ public double[] References { get; }
+ public int HueSlot { get; }
+ public double Scale { get; }
+ public bool SpaceToken { get; }
+ public int ComponentCount { get; }
+ public string Function { get; }
+
+ public bool IsHue(int slot) => slot == HueSlot;
+ }
+
+ private static class Relative
+ {
+ public static bool TryGetSpec(string name, out RelativeSpec spec)
+ {
+ spec = null;
+ switch (name)
+ {
+ case "rgb":
+ case "rgba":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Srgb, new[] { "r", "g", "b" }, new double[] { 255, 255, 255 }, -1, 255, false, 3, "rgb");
+ return true;
+ case "hsl":
+ case "hsla":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Hsl, new[] { "h", "s", "l" }, new double[] { 360, 100, 100 }, 0, 1, false, 3, "hsl");
+ return true;
+ case "hwb":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Hwb, new[] { "h", "w", "b" }, new double[] { 360, 100, 100 }, 0, 1, false, 3, "hwb");
+ return true;
+ case "lab":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Lab, new[] { "l", "a", "b" }, new double[] { 100, 125, 125 }, -1, 1, false, 3, "lab");
+ return true;
+ case "oklab":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Oklab, new[] { "l", "a", "b" }, new[] { 1, 0.4, 0.4 }, -1, 1, false, 3, "oklab");
+ return true;
+ case "lch":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Lch, new[] { "l", "c", "h" }, new double[] { 100, 150, 360 }, 2, 1, false, 3, "lch");
+ return true;
+ case "oklch":
+ spec = new RelativeSpec(ColorSpaceMath.Space.Oklch, new[] { "l", "c", "h" }, new[] { 1, 0.4, 360 }, 2, 1, false, 3, "oklch");
+ return true;
+ case "color":
+ // The space is the first component; its keywords follow from it (TryReadSpace).
+ spec = new RelativeSpec(ColorSpaceMath.Space.Srgb, new[] { "r", "g", "b" }, new double[] { 1, 1, 1 }, -1, 1, true, 4, "color");
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ public static bool TryReadSpace(Token token, out RelativeSpec spec)
+ {
+ spec = null;
+ if (token.Type != TokenType.Ident || !PredefinedSpaces.TryGetValue(token.Data, out var space)) return false;
+
+ var isXyz = space == ColorSpaceMath.Space.XyzD65 || space == ColorSpaceMath.Space.XyzD50;
+ spec = new RelativeSpec(space, isXyz ? new[] { "x", "y", "z" } : new[] { "r", "g", "b" }, new double[] { 1, 1, 1 }, -1, 1, true, 4, "color");
+ return true;
+ }
+
+ // The origin's components in the space of the function it is relative to. A plain sRGB color converts from its own 8-bit value (so rgb(from red r g b) is red
+ // exactly); one that can lie outside the gamut from the coordinates it was written with, unclipped.
+ public static double[] OriginCoordinates(Color origin, double[] oklab, ColorSpaceMath.Space space)
+ {
+ if (oklab != null && space != ColorSpaceMath.Space.Hsl && space != ColorSpaceMath.Space.Hwb)
+ return ColorSpaceMath.ToSpaceFromOklab(oklab, space);
+
+ return ColorSpaceMath.ToSpace(origin.R / 255.0, origin.G / 255.0, origin.B / 255.0, space);
+ }
+
+ // A component: a number (or a percentage of the reference), none (0), a channel keyword, or a calc() over the keywords.
+ public static double? Evaluate(Token token, double reference, Dictionary variables)
+ {
+ if (IsNone(token)) return 0.0;
+
+ if (token.Type == TokenType.Ident)
+ return variables.TryGetValue(token.Data.ToLowerInvariant(), out var keyword) ? keyword : (double?)null;
+
+ if (token is FunctionToken function && CalcParser.IsCalcFamily(function.Data))
+ {
+ var result = Calc(CalcParser.Parse(function, variables));
+ if (result is null) return null;
+ switch (result.Value.Category)
+ {
+ case CalcCategory.Number: return result.Value.Value;
+ case CalcCategory.Percentage: return result.Value.Value / 100.0 * reference;
+ default: return null;
+ }
+ }
+
+ return Component(token, reference);
+ }
+
+ public static double? EvaluateHue(Token token, Dictionary variables)
+ {
+ if (IsNone(token)) return 0.0;
+
+ if (token.Type == TokenType.Ident)
+ return variables.TryGetValue(token.Data.ToLowerInvariant(), out var keyword) ? keyword : (double?)null;
+
+ if (token is FunctionToken function && CalcParser.IsCalcFamily(function.Data))
+ {
+ var result = Calc(CalcParser.Parse(function, variables));
+ if (result is null) return null;
+ return result.Value.Category == CalcCategory.Number || result.Value.Category == CalcCategory.Angle ? result.Value.Value : (double?)null;
+ }
+
+ return HueDegrees(token);
+ }
+
+ // A calc() tree to a number, with the category it type-checks as: numbers, percentages and angles (in degrees), the only kinds a color component takes.
+ // Anything else (lengths, times, ...) is not a color component, so it is null.
+ private static (double Value, CalcCategory Category)? Calc(CalcNode node)
+ {
+ switch (node)
+ {
+ case NumberCalcNode number:
+ return (number.Value, CalcCategory.Number);
+ case PercentageCalcNode percentage:
+ return (percentage.Value, CalcCategory.Percentage);
+ case AngleCalcNode angle:
+ return (ToDegrees(angle), CalcCategory.Angle);
+ case UnaryCalcNode unary:
+ {
+ var operand = Calc(unary.Operand);
+ if (operand is null) return null;
+ return (unary.Negative ? -operand.Value.Value : operand.Value.Value, operand.Value.Category);
+ }
+ case BinaryCalcNode binary:
+ {
+ var left = Calc(binary.Left);
+ var right = Calc(binary.Right);
+ if (left is null || right is null) return null;
+ var l = left.Value;
+ var r = right.Value;
+ switch (binary.Operator)
+ {
+ case '+' when l.Category == r.Category:
+ return (l.Value + r.Value, l.Category);
+ case '-' when l.Category == r.Category:
+ return (l.Value - r.Value, l.Category);
+ case '*' when l.Category == CalcCategory.Number:
+ return (l.Value * r.Value, r.Category);
+ case '*' when r.Category == CalcCategory.Number:
+ return (l.Value * r.Value, l.Category);
+ case '/' when r.Category == CalcCategory.Number && r.Value != 0:
+ return (l.Value / r.Value, l.Category);
+ default:
+ return null;
+ }
+ }
+ case CallCalcNode call:
+ {
+ var values = new List<(double Value, CalcCategory Category)>(call.Arguments.Count);
+ foreach (var argument in call.Arguments)
+ {
+ var value = Calc(argument);
+ if (value is null) return null;
+ if (values.Count > 0 && values[0].Category != value.Value.Category) return null;
+ values.Add(value.Value);
+ }
+
+ if (values.Count == 0) return null;
+ var category = values[0].Category;
+ if (call.Name.Equals(FunctionNames.Min, StringComparison.OrdinalIgnoreCase)) return (values.Min(v => v.Value), category);
+ if (call.Name.Equals(FunctionNames.Max, StringComparison.OrdinalIgnoreCase)) return (values.Max(v => v.Value), category);
+ if (call.Name.Equals(FunctionNames.Clamp, StringComparison.OrdinalIgnoreCase) && values.Count == 3)
+ return (values[0].Value > values[2].Value ? values[2].Value : Clamp(values[1].Value, values[0].Value, values[2].Value), category);
+ return null;
+ }
+ default:
+ return null;
+ }
+ }
+
+ private static double ToDegrees(AngleCalcNode angle)
+ {
+ switch (angle.Unit)
+ {
+ case Angle.Unit.Grad: return angle.Value * 0.9;
+ case Angle.Unit.Turn: return angle.Value * 360.0;
+ case Angle.Unit.Rad: return angle.Value * 180.0 / Math.PI;
+ default: return angle.Value;
+ }
+ }
+
+ public static Color? Build(RelativeSpec spec, double[] v, float alpha, bool wantOklab, out double[] oklab)
+ {
+ oklab = null;
+ switch (spec.Function)
+ {
+ case "rgb":
+ return new Color((byte)Math.Round(Clamp(v[0], 0, 255)), (byte)Math.Round(Clamp(v[1], 0, 255)), (byte)Math.Round(Clamp(v[2], 0, 255)), Clamp01Byte(alpha));
+ case "hsl":
+ return Color.FromHsla((float)(v[0] / 360.0), (float)(v[1] / 100.0), (float)(v[2] / 100.0), alpha);
+ case "hwb":
+ return Color.FromHwba((float)(v[0] / 360.0), (float)(v[1] / 100.0), (float)(v[2] / 100.0), alpha);
+ case "lab":
+ case "oklab":
+ {
+ var coords = new[] { Clamp(v[0], 0, spec.References[0]), v[1], v[2] };
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(coords, spec.Space);
+ return Color.FromSpaceComponents(coords, spec.Space, alpha);
+ }
+ case "lch":
+ case "oklch":
+ {
+ var coords = new[] { Clamp(v[0], 0, spec.References[0]), Math.Max(v[1], 0), v[2] };
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(coords, spec.Space);
+ return Color.FromSpaceComponents(coords, spec.Space, alpha);
+ }
+ default:
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, spec.Space);
+ return Color.FromSpaceComponents(v, spec.Space, alpha);
+ }
+ }
+ }
+ }
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
index ad5c46ac3..4bff4ebf1 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ColorFunctionExtensions.cs
@@ -5,34 +5,101 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Resolves the color functions to an sRGB : rgb(), rgba(), hsl(), hsla(), hwb(),
- /// lab(), lch(), oklab() and oklch() (CSS Color 4), color-mix() (CSS Color 5) and gray().
+ /// Resolves the CSS color function forms from a tokenized value into a concrete
+ /// : the legacy/CSS Color 4 rgb()/rgba()/hsl()/hsla()/hwb()/gray() and the
+ /// CSS Color 4/5 lab()/oklab()/lch()/oklch()/color()/color-mix(). Tokenization is the object model's
+ /// job, so this consumes the already-parsed stream (nested functions
+ /// arrive as a single token) rather than re-scanning the source text.
///
- internal static class ColorFunctionExtensions
+ internal static partial class ColorFunctionExtensions
{
///
- /// Resolves a named, hex or function color to sRGB. Unlike ,
- /// which keeps a function as specified, this always computes the color.
+ /// Fully resolves a tokenized color value to a concrete - named colors and
+ /// hex via , plus every color function
+ /// (rgb/hsl/hwb/gray/lab/oklab/lch/oklch/color()/color-mix/device-cmyk), including the relative
+ /// color syntax of the first ones (rgb(from red r g b)). Unlike ToColor (which the object-model
+ /// cascade uses and which deliberately preserves a function's specified form for serialization),
+ /// this resolver always computes the sRGB value. A component written as none is 0 here (a
+ /// missing alpha is transparent); it only stays missing inside a color-mix().
///
- public static Color? ToResolvedColor(this IEnumerable value)
+ public static Color? ToResolvedColor(this IEnumerable value) => ResolveColor(value, wantOklab: false, out _);
+
+ // The one resolver behind ToResolvedColor and TryResolveOklab, so a nested color-mix() is read once per level, not once per question asked of it.
+ private static Color? ResolveColor(IEnumerable value, bool wantOklab, out double[] oklab)
{
- var basic = value.ToColor();
+ oklab = null;
+ var enumerable = value as Token[] ?? value.ToArray();
+
+ var basic = enumerable.ToColor();
if (basic.HasValue) return basic;
+ var element = enumerable.OnlyOrDefault();
+
+ // A hex color nested inside a function (e.g. a color-mix() operand) tokenizes as a Hash
+ // keyword token rather than a ColorToken, so ToColor misses it - resolve it here.
+ if (element is { Type: TokenType.Hash } hash) return Color.FromHex(hash.Data);
+
+ return element is FunctionToken function ? ParseColorFunction(function, wantOklab, out oklab) : null;
+ }
+
+ ///
+ /// Reads light-dark(<color>, <color>) (CSS Color 5 section 4): the tokens of its light and of its dark arm, in order. Which
+ /// arm applies depends on the color scheme of the element the color is used on, which a parser does not know, so the value is not
+ /// resolved to one ( is null for it); each arm is any color, light-dark() included.
+ /// False if the value is not a light-dark() of exactly two arms.
+ ///
+ public static bool TryGetLightDark(this IEnumerable value, out List light, out List dark)
+ {
+ light = dark = null;
var element = value.OnlyOrDefault();
+ return element is FunctionToken function &&
+ function.Data.Equals(FunctionNames.LightDark, StringComparison.OrdinalIgnoreCase) &&
+ TrySplitColorPair(function.ArgumentTokens, out light, out dark);
+ }
- // Inside a function, e.g. as a color-mix() operand, a hex color is a hash token
- if (element != null && element.Type == TokenType.Hash) return Color.FromHex(element.Data);
+ ///
+ /// Reads a light-dark() value as the two sRGB colors its arms resolve to; an arm that is not a color
+ /// resolves (currentcolor, or a nested light-dark()) is null.
+ ///
+ public static bool TryResolveLightDark(this IEnumerable value, out Color? light, out Color? dark)
+ {
+ light = dark = null;
+ if (!value.TryGetLightDark(out var lightTokens, out var darkTokens)) return false;
+ light = lightTokens.ToResolvedColor();
+ dark = darkTokens.ToResolvedColor();
+ return true;
+ }
- var function = element as FunctionToken;
- return function != null ? ParseColorFunction(function) : null;
+ // Exactly two comma-separated, non-empty arms (whitespace dropped).
+ internal static bool TrySplitColorPair(IEnumerable args, out List first, out List second)
+ {
+ first = second = null;
+ var arms = new List> { new List() };
+ foreach (var token in args)
+ {
+ if (token.Type == TokenType.Comma) arms.Add(new List());
+ else if (token.Type != TokenType.Whitespace) arms[arms.Count - 1].Add(token);
+ }
+
+ if (arms.Count != 2 || arms[0].Count == 0 || arms[1].Count == 0) return false;
+ first = arms[0];
+ second = arms[1];
+ return true;
}
- public static Color? ParseColorFunction(FunctionToken function)
+ public static Color? ParseColorFunction(FunctionToken function) => ParseColorFunction(function, wantOklab: false, out _);
+
+ // oklab: for lab(), lch(), oklab(), oklch(), color() and color-mix() (when wantOklab), the Oklab coordinates the color was written with, which are
+ // outside the sRGB gamut where the returned Color is clipped to it; null for every other color.
+ private static Color? ParseColorFunction(FunctionToken function, bool wantOklab, out double[] oklab)
{
+ oklab = null;
var name = function.Data;
var args = function.ArgumentTokens;
+ // Relative color syntax (rgb(from red r g b) and the like) is read by whichever function it is written in.
+ if (IsRelative(args)) return ParseRelative(name, args, wantOklab, out oklab);
+
if (name.Equals(FunctionNames.Rgb, StringComparison.OrdinalIgnoreCase) ||
name.Equals(FunctionNames.Rgba, StringComparison.OrdinalIgnoreCase))
return ParseRgb(args);
@@ -48,135 +115,301 @@ internal static class ColorFunctionExtensions
return ParseGray(args);
if (name.Equals(FunctionNames.Lab, StringComparison.OrdinalIgnoreCase))
- return ParseRect(args, Color.FromLab, abReference: 125.0, lReference: 100.0);
+ return ParseRect(args, Color.FromLab, abReference: 125.0, lReference: 100.0, ColorSpaceMath.Space.Lab, wantOklab, out oklab);
if (name.Equals(FunctionNames.Oklab, StringComparison.OrdinalIgnoreCase))
- return ParseRect(args, Color.FromOklab, abReference: 0.4, lReference: 1.0);
+ return ParseRect(args, Color.FromOklab, abReference: 0.4, lReference: 1.0, ColorSpaceMath.Space.Oklab, wantOklab, out oklab);
if (name.Equals(FunctionNames.Lch, StringComparison.OrdinalIgnoreCase))
- return ParsePolar(args, Color.FromLch, chromaReference: 150.0, lReference: 100.0);
+ return ParsePolar(args, Color.FromLch, chromaReference: 150.0, lReference: 100.0, ColorSpaceMath.Space.Lch, wantOklab, out oklab);
if (name.Equals(FunctionNames.Oklch, StringComparison.OrdinalIgnoreCase))
- return ParsePolar(args, Color.FromOklch, chromaReference: 0.4, lReference: 1.0);
+ return ParsePolar(args, Color.FromOklch, chromaReference: 0.4, lReference: 1.0, ColorSpaceMath.Space.Oklch, wantOklab, out oklab);
if (name.Equals(FunctionNames.ColorMix, StringComparison.OrdinalIgnoreCase))
- return ParseColorMix(args);
+ return ParseColorMix(args, wantOklab, out oklab);
+
+ if (name.Equals(FunctionNames.ColorFunction, StringComparison.OrdinalIgnoreCase))
+ return ParseColorSpaceFunction(args, wantOklab, out oklab);
+
+ if (name.Equals(FunctionNames.DeviceCmyk, StringComparison.OrdinalIgnoreCase))
+ return ParseDeviceCmyk(args);
return null;
}
- // rgb()/rgba(): 3 components (int or % of 255) + optional alpha
+ // ── device-cmyk(): 4 components (number or %) + optional alpha ───────────
+ // https://www.w3.org/TR/css-color-5/#device-cmyk - alpha is only ever introduced by '/', and there is no relative ("from") form.
+ private static Color? ParseDeviceCmyk(IEnumerable args)
+ {
+ if (!Extract(args, 4, out var comps, out var alphaTok, allowImplicitAlpha: false)) return null;
+ var c = CmykComponent(comps[0]);
+ var m = CmykComponent(comps[1]);
+ var y = CmykComponent(comps[2]);
+ var k = CmykComponent(comps[3]);
+ if (c is null || m is null || y is null || k is null) return null;
+ return Color.FromDeviceCmyk(c.Value, m.Value, y.Value, k.Value, Alpha(alphaTok));
+ }
+
+ // A is a or , each normalized to 0..1; none is 0.
+ private static float? CmykComponent(Token token)
+ {
+ if (IsNone(token)) return 0f;
+ var number = AsTokens(token).ToSingle();
+ if (number.HasValue) return number.Value;
+ return AsTokens(token).ToPercent()?.NormalizedValue;
+ }
+
+ // ── rgb()/rgba(): 3 components (int or % of 255) + optional alpha ─────────
private static Color? ParseRgb(IEnumerable args)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 3, out comps, out alphaTok)) return null;
- var r = AsTokens(comps[0]).ToRgbComponent();
- var g = AsTokens(comps[1]).ToRgbComponent();
- var b = AsTokens(comps[2]).ToRgbComponent();
- if (r == null || g == null || b == null) return null;
- var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return new Color(r.Value, g.Value, b.Value, Clamp01Byte(a));
+ if (!Extract(args, 3, out var comps, out var alphaTok)) return null;
+ // none (a missing component) is only part of the space-separated syntax, not the legacy comma one.
+ if (HasComma(args) && (comps.Any(IsNone) || (alphaTok != null && IsNone(alphaTok)))) return null;
+ var r = RgbComponent(comps[0]);
+ var g = RgbComponent(comps[1]);
+ var b = RgbComponent(comps[2]);
+ if (r is null || g is null || b is null) return null;
+ return new Color(r.Value, g.Value, b.Value, Clamp01Byte(Alpha(alphaTok)));
}
- // hsl()/hsla(): hue angle + saturation% + lightness% + optional alpha
+ // ── hsl()/hsla(): hue angle + saturation% + lightness% + optional alpha ──
private static Color? ParseHsl(IEnumerable args)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ if (!Extract(args, 3, out var comps, out var alphaTok)) return null;
+ if (HasComma(args) && (comps.Any(IsNone) || (alphaTok != null && IsNone(alphaTok)))) return null;
var hue = HueDegrees(comps[0]);
- var sat = AsTokens(comps[1]).ToPercent();
- var light = AsTokens(comps[2]).ToPercent();
- if (hue == null || sat == null || light == null) return null;
- var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return Color.FromHsla((float)(hue.Value / 360.0), sat.Value.NormalizedValue, light.Value.NormalizedValue, a);
+ var sat = PercentValue(comps[1]);
+ var light = PercentValue(comps[2]);
+ if (hue is null || sat is null || light is null) return null;
+ return Color.FromHsla((float)(hue.Value / 360.0), sat.Value, light.Value, Alpha(alphaTok));
}
- // hwb(): hue angle + whiteness% + blackness% + optional alpha
+ // ── hwb(): hue angle + whiteness% + blackness% + optional alpha ──────────
private static Color? ParseHwb(IEnumerable args)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ if (!Extract(args, 3, out var comps, out var alphaTok)) return null;
var hue = HueDegrees(comps[0]);
- var white = AsTokens(comps[1]).ToPercent();
- var black = AsTokens(comps[2]).ToPercent();
- if (hue == null || white == null || black == null) return null;
- var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return Color.FromHwba((float)(hue.Value / 360.0), white.Value.NormalizedValue, black.Value.NormalizedValue, a);
+ var white = PercentValue(comps[1]);
+ var black = PercentValue(comps[2]);
+ if (hue is null || white is null || black is null) return null;
+ return Color.FromHwba((float)(hue.Value / 360.0), white.Value, black.Value, Alpha(alphaTok));
}
- // gray(): a single lightness component + optional alpha
+ // ── gray(): a single lightness component + optional alpha ────────────────
private static Color? ParseGray(IEnumerable args)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 1, out comps, out alphaTok)) return null;
+ if (!Extract(args, 1, out var comps, out var alphaTok)) return null;
var value = AsTokens(comps[0]).ToRgbComponent();
- if (value == null) return null;
- var a = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return Color.FromGray(value.Value, a);
+ if (value is null) return null;
+ return Color.FromGray(value.Value, Alpha(alphaTok));
}
- // lab()/oklab(): L + a + b + optional alpha
- private static Color? ParseRect(IEnumerable args, Func build, double abReference, double lReference)
+ // ── lab()/oklab(): L + a + b + optional alpha ────────────────────────────
+ private static Color? ParseRect(IEnumerable args, Func build, double abReference, double lReference,
+ ColorSpaceMath.Space space, bool wantOklab, out double[] oklab)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ oklab = null;
+ if (!TryRectComponents(args, abReference, lReference, out var v, out var alpha, out _)) return null;
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space);
+ return build(v[0], v[1], v[2], alpha);
+ }
+
+ private static bool TryRectComponents(IEnumerable args, double abReference, double lReference, out double[] v, out float alpha, out int missing)
+ {
+ v = new double[0];
+ alpha = 1f;
+ missing = 0;
+ if (!Extract(args, 3, out var comps, out var alphaTok)) return false;
var l = Component(comps[0], lReference);
var a = Component(comps[1], abReference);
var b = Component(comps[2], abReference);
- if (l == null || a == null || b == null) return null;
- var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return build(l.Value, a.Value, b.Value, alpha);
+ if (l is null || a is null || b is null) return false;
+ alpha = Alpha(alphaTok);
+ missing = MissingMask(comps, alphaTok);
+ // CSS Color 4 section 9: the lightness is clamped to its range when the color is parsed (the reference is 100% of it), before any mix.
+ v = new[] { Clamp(l.Value, 0, lReference), a.Value, b.Value };
+ return true;
}
- // lch()/oklch(): L + C + hue angle + optional alpha
- private static Color? ParsePolar(IEnumerable args, Func build, double chromaReference, double lReference)
+ // ── lch()/oklch(): L + C + hue angle + optional alpha ────────────────────
+ private static Color? ParsePolar(IEnumerable args, Func build, double chromaReference, double lReference,
+ ColorSpaceMath.Space space, bool wantOklab, out double[] oklab)
{
- List comps;
- Token alphaTok;
- if (!Extract(args, 3, out comps, out alphaTok)) return null;
+ oklab = null;
+ if (!TryPolarComponents(args, chromaReference, lReference, out var v, out var alpha, out _)) return null;
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space);
+ return build(v[0], v[1], v[2], alpha);
+ }
+
+ private static bool TryPolarComponents(IEnumerable args, double chromaReference, double lReference, out double[] v, out float alpha, out int missing)
+ {
+ v = new double[0];
+ alpha = 1f;
+ missing = 0;
+ if (!Extract(args, 3, out var comps, out var alphaTok)) return false;
var l = Component(comps[0], lReference);
var c = Component(comps[1], chromaReference);
var h = HueDegrees(comps[2]);
- if (l == null || c == null || h == null) return null;
- var alpha = alphaTok == null ? 1f : AsTokens(alphaTok).ToAlphaValue() ?? 1f;
- return build(l.Value, c.Value, h.Value, alpha);
+ if (l is null || c is null || h is null) return false;
+ alpha = Alpha(alphaTok);
+ missing = MissingMask(comps, alphaTok);
+ // CSS Color 4 section 9: the lightness is clamped to its range and a negative chroma to 0 when the color is parsed.
+ v = new[] { Clamp(l.Value, 0, lReference), Math.Max(c.Value, 0), h.Value };
+ return true;
+ }
+
+ // ── color( c1 c2 c3 [/ alpha]) ────────────────────────────────────
+ // https://www.w3.org/TR/css-color-4/#color-function - the predefined spaces. A colour in a
+ // wide-gamut space is clipped to sRGB, like the other wide-gamut functions.
+ private static readonly Dictionary PredefinedSpaces =
+ new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ ["srgb"] = ColorSpaceMath.Space.Srgb,
+ ["srgb-linear"] = ColorSpaceMath.Space.SrgbLinear,
+ ["display-p3"] = ColorSpaceMath.Space.DisplayP3,
+ ["a98-rgb"] = ColorSpaceMath.Space.A98Rgb,
+ ["prophoto-rgb"] = ColorSpaceMath.Space.ProphotoRgb,
+ ["rec2020"] = ColorSpaceMath.Space.Rec2020,
+ ["xyz"] = ColorSpaceMath.Space.XyzD65,
+ ["xyz-d65"] = ColorSpaceMath.Space.XyzD65,
+ ["xyz-d50"] = ColorSpaceMath.Space.XyzD50
+ };
+
+ private static Color? ParseColorSpaceFunction(IEnumerable args, bool wantOklab, out double[] oklab)
+ {
+ oklab = null;
+ if (!TryColorFunctionComponents(args, out var space, out var v, out var alpha, out _)) return null;
+ if (wantOklab) oklab = ColorSpaceMath.ToOklab(v, space);
+ return Color.FromSpaceComponents(v, space, alpha);
+ }
+
+ private static bool TryColorFunctionComponents(IEnumerable args, out ColorSpaceMath.Space space, out double[] v, out float alpha, out int missing)
+ {
+ space = default;
+ v = new double[0];
+ alpha = 1f;
+ missing = 0;
+ // The alpha is only ever introduced by "/": a fifth bare value is malformed.
+ if (!Extract(args, 4, out var comps, out var alphaTok, allowImplicitAlpha: false)) return false;
+ if (comps[0].Type != TokenType.Ident || !PredefinedSpaces.TryGetValue(comps[0].Data, out space)) return false;
+
+ // The components of every predefined space are a number or a percentage of 1 (the xyz spaces too).
+ var c1 = Component(comps[1], 1.0);
+ var c2 = Component(comps[2], 1.0);
+ var c3 = Component(comps[3], 1.0);
+ if (c1 is null || c2 is null || c3 is null) return false;
+ v = new[] { c1.Value, c2.Value, c3.Value };
+ alpha = Alpha(alphaTok);
+ missing = MissingMask(comps.GetRange(1, 3), alphaTok);
+ return true;
+ }
+
+ // ── color-mix(in [], c1 [p1], c2 [p2]) ───────────────
+ private static Color? ParseColorMix(IEnumerable args, bool wantOklab, out double[] oklab)
+ {
+ oklab = null;
+ if (!TryColorMixOperands(args, out var c1, out var c2, out var t, out var space, out var hue, out var alphaMultiplier,
+ out var oklab1, out var oklab2, out var written1, out var written2))
+ return null;
+
+ // Both operands device-cmyk(): a mix in the C/M/Y/K components themselves (see Color.MixCmyk). One of them with an sRGB color has
+ // no profile-free conversion, so it stays invalid, as does asking for the Oklab coordinates of a CMYK mix.
+ if (c1.IsDeviceCmyk != c2.IsDeviceCmyk) return null;
+ if (c1.IsDeviceCmyk) return Color.MixCmyk(c1, c2, t, alphaMultiplier);
+
+ if (wantOklab)
+ {
+ var mixed = Color.MixComponents(c1, c2, t, space, hue, out _, oklab1, oklab2, written1, written2);
+ oklab = ColorSpaceMath.ToOklab(mixed, space);
+ }
+
+ return Color.Mix(c1, c2, t, space, hue, alphaMultiplier, oklab1, oklab2, written1, written2);
+ }
+
+ ///
+ /// Reads a lab(), lch(), oklab(), oklch(), color() or color-mix() color as the Oklab coordinates it names,
+ /// without the clip to the sRGB gamut that applies: an out-of-gamut color keeps its out-of-range
+ /// coordinates, which is what a browser mixes two colors from (CSS Color 4 section 13). False for any other color (the clipped sRGB value
+ /// is the color then).
+ ///
+ public static bool TryResolveOklab(this IEnumerable value, out double[] oklab)
+ {
+ var color = ResolveColor(value, wantOklab: true, out oklab);
+ if (color.HasValue && oklab != null) return true;
+
+ oklab = new double[0];
+ return false;
+ }
+
+ ///
+ /// Reads any color as an operand of a mix in Oklab (an animation between two colors): its Oklab coordinates as written, without the clip to the sRGB gamut, and
+ /// which of them are missing (bits 0 to 2 for L, a and b; for the alpha) - a component written as none, carried to
+ /// the analogous Oklab component where there is one (CSS Color 4 section 12.4). False for a color that does not resolve.
+ ///
+ public static bool TryResolveMixOperand(this IEnumerable value, out double[] oklab, out int missing)
+ {
+ missing = 0;
+ var tokens = new List(value.Where(t => t.Type != TokenType.Whitespace));
+ var color = ResolveColor(tokens, wantOklab: true, out oklab);
+ if (!(color is { } resolved) || resolved.IsDeviceCmyk)
+ {
+ oklab = new double[0];
+ return false;
+ }
+
+ oklab ??= ColorSpaceMath.ToSpace(resolved.R / 255.0, resolved.G / 255.0, resolved.B / 255.0, ColorSpaceMath.Space.Oklab);
+
+ var written = WrittenComponents(tokens);
+ if (written.HasValue)
+ missing = ColorSpaceMath.MapMissing(written.Value.Space, written.Value.Missing, ColorSpaceMath.Space.Oklab);
+ return true;
}
- // color-mix(in [], c1 [p1], c2 [p2])
- private static Color? ParseColorMix(IEnumerable args)
+ // The operands, weight and space of a color-mix(), after the CSS Color 5 section 3 percentage normalization.
+ private static bool TryColorMixOperands(IEnumerable args, out Color c1, out Color c2, out double t, out ColorSpaceMath.Space space,
+ out ColorSpaceMath.HueMethod hue, out double alphaMultiplier, out double[] oklab1, out double[] oklab2,
+ out ColorSpaceMath.WrittenCoords? written1, out ColorSpaceMath.WrittenCoords? written2)
{
+ c1 = c2 = default;
+ oklab1 = oklab2 = null;
+ written1 = written2 = null;
+ t = alphaMultiplier = 0;
+ space = default;
+ hue = default;
+
var segments = SplitByComma(args);
- if (segments.Count != 3) return null;
+ if (segments.Count != 3) return false;
- var config = segments[0].Where(token => token.Type != TokenType.Whitespace)
- .Select(token => token.Data.ToLowerInvariant()).ToList();
- if (config.Count < 2 || config[0] != "in") return null;
- var space = MapSpace(config[1]);
- var hue = MapHue(config, 2);
+ var config = segments[0].Where(x => x.Type != TokenType.Whitespace)
+ .Select(x => x.Data.ToLowerInvariant()).ToList();
+ if (config.Count < 2 || config[0] != "in") return false;
+ space = MapSpace(config[1]);
+ hue = MapHue(config, 2);
- Color c1, c2;
- double? p1, p2;
- if (!ParseMixOperand(segments[1], out c1, out p1)) return null;
- if (!ParseMixOperand(segments[2], out c2, out p2)) return null;
+ if (!ParseMixOperand(segments[1], out c1, out var p1, out oklab1, out written1)) return false;
+ if (!ParseMixOperand(segments[2], out c2, out var p2, out oklab2, out written2)) return false;
- // CSS Color 5 §3.2
- if (p1 == null && p2 == null) { p1 = 0.5; p2 = 0.5; }
- else if (p1 == null) p1 = Math.Max(0, 1.0 - p2.Value);
- else if (p2 == null) p2 = Math.Max(0, 1.0 - p1.Value);
+ // CSS Color 5 section 3 normalization: a missing percentage is 100% minus the other; percentages
+ // scale to sum 1, and a pre-normalization sum below 100% scales the result alpha down.
+ if (p1 is null && p2 is null) { p1 = 0.5; p2 = 0.5; }
+ else if (p1 is null) p1 = Math.Max(0, 1.0 - p2.Value);
+ else if (p2 is null) p2 = Math.Max(0, 1.0 - p1.Value);
var sum = p1.Value + p2.Value;
- if (sum <= 0) return null;
- var alphaMultiplier = sum < 1.0 ? sum : 1.0;
- var t = p2.Value / sum;
-
- return Color.Mix(c1, c2, t, space, hue, alphaMultiplier);
+ if (sum <= 0) return false;
+ alphaMultiplier = sum < 1.0 ? sum : 1.0;
+ t = p2.Value / sum;
+ return true;
}
- private static bool ParseMixOperand(List segment, out Color color, out double? percent)
+ private const int MaxColorMixDepth = 32;
+
+ [ThreadStatic]
+ private static int _colorMixDepth;
+
+ private static bool ParseMixOperand(List segment, out Color color, out double? percent, out double[] oklab, out ColorSpaceMath.WrittenCoords? written)
{
- color = default(Color);
+ color = default;
+ oklab = null;
+ written = null;
percent = null;
var tokens = new List();
@@ -186,31 +419,116 @@ private static bool ParseMixOperand(List segment, out Color color, out do
}
if (tokens.Count == 0) return false;
- // The percentage may come before or after the color (CSS Color 5 §3)
+ // The optional percentage may appear before or after the color (CSS Color 5 section 3.1), e.g.
+ // both `red 30%` and `30% red` are valid.
if (tokens[tokens.Count - 1].Type == TokenType.Percentage)
{
- var value = AsTokens(tokens[tokens.Count - 1]).ToPercent();
- percent = value.HasValue ? value.Value.NormalizedValue : (double?)null;
+ percent = AsTokens(tokens[tokens.Count - 1]).ToPercent()?.NormalizedValue;
tokens.RemoveAt(tokens.Count - 1);
}
else if (tokens[0].Type == TokenType.Percentage)
{
- var value = AsTokens(tokens[0]).ToPercent();
- percent = value.HasValue ? value.Value.NormalizedValue : (double?)null;
+ percent = AsTokens(tokens[0]).ToPercent()?.NormalizedValue;
tokens.RemoveAt(0);
}
- var resolved = tokens.ToResolvedColor();
- if (resolved == null) return false;
+ // A color-mix operand may itself be any color form (named/hex/rgb/oklch/...), so resolve it
+ // through the full resolver rather than the named/hex-only ToColor. The operand comes back with
+ // the coordinates it was written with too, when it is one that can lie outside the gamut the Color is clipped to.
+ // color-mix() operands nest without bound in the grammar, and each level costs several frames of the machine
+ // stack, so past a depth no stylesheet needs the color is invalid, as any other value that cannot be read.
+ if (_colorMixDepth >= MaxColorMixDepth) return false;
+ Color? resolved;
+ _colorMixDepth++;
+ try
+ {
+ resolved = ResolveColor(tokens, wantOklab: true, out oklab);
+ }
+ finally
+ {
+ _colorMixDepth--;
+ }
+ if (resolved is null) return false;
color = resolved.Value;
+ written = WrittenComponents(tokens);
return true;
}
- // Token helpers
+ // The components a color operand was written with (null for any other color). Those of lab(), lch(), oklab(), oklch() and color() are always
+ // read, for a gray's hue and an out-of-gamut color; the rest only when one of their components or the alpha is missing (none), as that is all a mix needs.
+ private static ColorSpaceMath.WrittenCoords? WrittenComponents(List tokens)
+ {
+ if (tokens.Count != 1 || !(tokens[0] is FunctionToken function)) return null;
+
+ var name = function.Data;
+ var args = function.ArgumentTokens;
+ double[] v;
+ int missing;
+
+ // A relative color's written components are the ones its channel keywords resolved to: a mix takes it from its Oklab coordinates.
+ if (IsRelative(args)) return null;
+
+ if (name.Equals(FunctionNames.Lab, StringComparison.OrdinalIgnoreCase) && TryRectComponents(args, 125.0, 100.0, out v, out _, out missing))
+ return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Lab, v, missing);
+ if (name.Equals(FunctionNames.Oklab, StringComparison.OrdinalIgnoreCase) && TryRectComponents(args, 0.4, 1.0, out v, out _, out missing))
+ return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Oklab, v, missing);
+ if (name.Equals(FunctionNames.Lch, StringComparison.OrdinalIgnoreCase) && TryPolarComponents(args, 150.0, 100.0, out v, out _, out missing))
+ return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Lch, v, missing);
+ if (name.Equals(FunctionNames.Oklch, StringComparison.OrdinalIgnoreCase) && TryPolarComponents(args, 0.4, 1.0, out v, out _, out missing))
+ return new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Oklch, v, missing);
+ if (name.Equals(FunctionNames.ColorFunction, StringComparison.OrdinalIgnoreCase) && TryColorFunctionComponents(args, out var space, out v, out _, out missing))
+ return new ColorSpaceMath.WrittenCoords(space, v, missing);
+
+ if (!Extract(args, 3, out var comps, out var alphaTok) || HasComma(args)) return null;
+ missing = MissingMask(comps, alphaTok);
+ if (missing == 0) return null;
+
+ if (name.Equals(FunctionNames.Rgb, StringComparison.OrdinalIgnoreCase) || name.Equals(FunctionNames.Rgba, StringComparison.OrdinalIgnoreCase))
+ {
+ var r = RgbComponent(comps[0]);
+ var g = RgbComponent(comps[1]);
+ var b = RgbComponent(comps[2]);
+ return r is null || g is null || b is null
+ ? (ColorSpaceMath.WrittenCoords?)null
+ : new ColorSpaceMath.WrittenCoords(ColorSpaceMath.Space.Srgb, new[] { r.Value / 255.0, g.Value / 255.0, b.Value / 255.0 }, missing);
+ }
+
+ var isHsl = name.Equals(FunctionNames.Hsl, StringComparison.OrdinalIgnoreCase) || name.Equals(FunctionNames.Hsla, StringComparison.OrdinalIgnoreCase);
+ if (isHsl || name.Equals(FunctionNames.Hwb, StringComparison.OrdinalIgnoreCase))
+ {
+ var hue = HueDegrees(comps[0]);
+ var second = PercentValue(comps[1]);
+ var third = PercentValue(comps[2]);
+ return hue is null || second is null || third is null
+ ? (ColorSpaceMath.WrittenCoords?)null
+ : new ColorSpaceMath.WrittenCoords(isHsl ? ColorSpaceMath.Space.Hsl : ColorSpaceMath.Space.Hwb,
+ new[] { hue.Value, second.Value * 100.0, third.Value * 100.0 }, missing);
+ }
+
+ return null;
+ }
- // Splits the arguments into `count` components and an optional alpha (CSS Color 4 §4.1). Commas
- // and whitespace both separate, the alpha follows a "/" or is the extra last value.
- private static bool Extract(IEnumerable args, int count, out List components, out Token alpha)
+ // Which of the three components (bits 0 to 2) and the alpha (ColorSpaceMath.AlphaMissing) were written as none.
+ private static int MissingMask(List components, Token alpha)
+ {
+ var mask = 0;
+ for (var i = 0; i < 3 && i < components.Count; i++)
+ {
+ if (IsNone(components[i])) mask |= 1 << i;
+ }
+
+ if (alpha != null && IsNone(alpha)) mask |= ColorSpaceMath.AlphaMissing;
+ return mask;
+ }
+
+ // ── Token helpers ────────────────────────────────────────────────────────
+
+ // Partitions a function's argument tokens into `count` component tokens and an optional alpha
+ // token. Whitespace and commas separate; a "/" delimiter (CSS Color 4) marks the alpha that
+ // follows. Without a slash, a trailing extra value (legacy rgba/hsla comma alpha) is the alpha -
+ // unless allowImplicitAlpha is false (color() has no such legacy form: alpha is only ever
+ // introduced by '/'), in which case an unslashed trailing value is malformed.
+ private static bool Extract(IEnumerable args, int count, out List components, out Token alpha, bool allowImplicitAlpha = true)
{
components = new List();
alpha = null;
@@ -236,6 +554,13 @@ private static bool Extract(IEnumerable args, int count, out List
if (values.Count < count) return false;
for (var i = 0; i < count; i++) components.Add(values[i]);
+ if (!allowImplicitAlpha)
+ {
+ if (slashIndex < 0) return values.Count == count;
+ if (slashIndex < values.Count) alpha = values[slashIndex];
+ return true;
+ }
+
var alphaAt = slashIndex >= 0 ? slashIndex : count;
if (alphaAt < values.Count) alpha = values[alphaAt];
return true;
@@ -261,16 +586,35 @@ private static List> SplitByComma(IEnumerable tokens)
return result;
}
- private static Token[] AsTokens(Token token)
+ private static Token[] AsTokens(Token token) => new[] { token };
+
+ private static bool IsNone(Token token) =>
+ token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase);
+
+ private static bool HasComma(IEnumerable args) => args.Any(t => t.Type == TokenType.Comma);
+
+ private static double Clamp(double value, double min, double max) => value < min ? min : (value > max ? max : value);
+
+ // A missing component (none) is 0 once the color is used for anything but a mix.
+ private static byte? RgbComponent(Token token) => IsNone(token) ? (byte)0 : AsTokens(token).ToRgbComponent();
+
+ private static float? PercentValue(Token token)
+ {
+ if (IsNone(token)) return 0f;
+ return AsTokens(token).ToPercent()?.NormalizedValue;
+ }
+
+ // The alpha of a color: a number or percentage; a missing alpha (none) is transparent once the color is used for anything but a mix.
+ private static float Alpha(Token token)
{
- return new[] { token };
+ if (token is null) return 1f;
+ return IsNone(token) ? 0f : AsTokens(token).ToAlphaValue() ?? 1f;
}
// A number (returned as-is) or a percentage resolved against `reference`; "none" resolves to 0.
private static double? Component(Token token, double reference)
{
- if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase))
- return 0.0;
+ if (IsNone(token)) return 0.0;
var number = AsTokens(token).ToSingle();
if (number.HasValue) return number.Value;
var percent = AsTokens(token).ToPercent();
@@ -279,11 +623,9 @@ private static Token[] AsTokens(Token token)
private static double? HueDegrees(Token token)
{
- if (token.Type == TokenType.Ident && token.Data.Equals("none", StringComparison.OrdinalIgnoreCase))
- return 0.0;
+ if (IsNone(token)) return 0.0;
var angle = AsTokens(token).ToAngleNumber();
if (!angle.HasValue) return null;
-
switch (angle.Value.Type)
{
case Angle.Unit.Grad: return angle.Value.Value * 0.9;
@@ -295,7 +637,8 @@ private static Token[] AsTokens(Token token)
private static byte Clamp01Byte(float alpha)
{
- return (byte)Math.Round(Math.Min(Math.Max(alpha, 0f), 1f) * 255);
+ var clamped = alpha < 0f ? 0f : (alpha > 1f ? 1f : alpha);
+ return (byte)Math.Round(clamped * 255);
}
private static ColorSpaceMath.Space MapSpace(string name)
@@ -304,6 +647,9 @@ private static ColorSpaceMath.Space MapSpace(string name)
{
case "srgb-linear": return ColorSpaceMath.Space.SrgbLinear;
case "display-p3": return ColorSpaceMath.Space.DisplayP3;
+ case "a98-rgb": return ColorSpaceMath.Space.A98Rgb;
+ case "prophoto-rgb": return ColorSpaceMath.Space.ProphotoRgb;
+ case "rec2020": return ColorSpaceMath.Space.Rec2020;
case "lab": return ColorSpaceMath.Space.Lab;
case "oklab": return ColorSpaceMath.Space.Oklab;
case "xyz":
@@ -322,46 +668,15 @@ private static ColorSpaceMath.HueMethod MapHue(List idents, int startInd
for (var i = startIndex; i + 1 < idents.Count; i++)
{
if (idents[i + 1] != "hue") continue;
- return MapHueName(idents[i]);
+ switch (idents[i])
+ {
+ case "longer": return ColorSpaceMath.HueMethod.Longer;
+ case "increasing": return ColorSpaceMath.HueMethod.Increasing;
+ case "decreasing": return ColorSpaceMath.HueMethod.Decreasing;
+ default: return ColorSpaceMath.HueMethod.Shorter;
+ }
}
return ColorSpaceMath.HueMethod.Shorter;
}
-
- private static ColorSpaceMath.HueMethod MapHueName(string name)
- {
- switch (name)
- {
- case "longer": return ColorSpaceMath.HueMethod.Longer;
- case "increasing": return ColorSpaceMath.HueMethod.Increasing;
- case "decreasing": return ColorSpaceMath.HueMethod.Decreasing;
- default: return ColorSpaceMath.HueMethod.Shorter;
- }
- }
-
- ///
- /// Blends two colors per the CSS palette-mix() rules (CSS Fonts 4), reusing color-mix()'s
- /// color-space mapping () and CSS Color 5 §3 percentage normalization so the two
- /// functions interpolate identically. / are 0..100 percentages
- /// (or null); is a hue-interpolation direction (or null for the default).
- ///
- internal static Color MixPaletteColors(Color c1, Color c2, string spaceName, string hueName, double? p1, double? p2)
- {
- var space = MapSpace(spaceName.ToLowerInvariant());
- var hue = MapHueName(hueName.ToLowerInvariant());
-
- double? f1 = p1.HasValue ? Math.Min(Math.Max(p1.Value, 0), 100) / 100.0 : (double?)null;
- double? f2 = p2.HasValue ? Math.Min(Math.Max(p2.Value, 0), 100) / 100.0 : (double?)null;
-
- if (f1 == null && f2 == null) { f1 = 0.5; f2 = 0.5; }
- else if (f1 == null) f1 = 1.0 - f2;
- else if (f2 == null) f2 = 1.0 - f1;
-
- var sum = f1.Value + f2.Value;
- if (sum <= 0) return c1;
-
- var alphaMultiplier = sum < 1.0 ? sum : 1.0;
- var t = f2.Value / sum;
- return Color.Mix(c1, c2, t, space, hue, alphaMultiplier);
- }
}
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs
index c37739b2a..17ab81f8a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/PortableExtensions.cs
@@ -1,3 +1,8 @@
+using System;
+using System.Diagnostics.CodeAnalysis;
+using System.Linq;
+using System.Reflection;
+
#if !NET40 && !SL50
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
@@ -8,7 +13,16 @@ public static string ConvertFromUtf32(this int utf32)
{
return char.ConvertFromUtf32(utf32);
}
+
+ public static PropertyInfo[] GetProperties(
+#if NET6_0_OR_GREATER
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.NonPublicProperties)]
+#endif
+ this Type type)
+ {
+ return type.GetRuntimeProperties().ToArray();
+ }
}
}
-#endif
+#endif
\ No newline at end of file
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs
index 441ef42e4..12ceaa8d1 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueConverterExtensions.cs
@@ -74,7 +74,7 @@ public static IValueConverter FromList(this IValueConverter converter)
return new ListValueConverter(converter);
}
- public static IValueConverter ToConverter(this IReadOnlyDictionary values)
+ public static IValueConverter ToConverter(this Dictionary values)
{
return new DictionaryValueConverter(values);
}
@@ -84,9 +84,10 @@ public static IValueConverter Periodic(this IValueConverter converter, params st
return new PeriodicValueConverter(converter, labels);
}
- public static IValueConverter RequiresEnd(this IValueConverter listConverter, IValueConverter endConverter)
+ public static IValueConverter RequiresEnd(this IValueConverter listConverter, IValueConverter endConverter,
+ IReadOnlyDictionary layerDefaults = null, IReadOnlyDictionary layerFallbacks = null)
{
- return new EndListValueConverter(listConverter, endConverter);
+ return new EndListValueConverter(listConverter, endConverter, layerDefaults, layerFallbacks);
}
public static IValueConverter Required(this IValueConverter converter)
@@ -132,6 +133,8 @@ public static IValueConverter OrNone(this IValueConverter primary)
public static IValueConverter OrDefault(this IValueConverter primary)
{
+ // inherit/initial/revert/revert-layer/unset are CSS-wide keywords, valid on every property
+ // (CSS Cascade 4 7.3, Cascade 5 for revert-layer). OrGlobalValue is the full set.
return primary.OrGlobalValue();
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs
index 65126a5e2..00775786f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Extensions/ValueExtensions.cs
@@ -6,13 +6,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal static class ValueExtensions
{
- private static bool IsWeight(int value)
- {
- return value == 100 || value == 200 || value == 300 || value == 400 ||
- value == 500 || value == 600 || value == 700 || value == 800 ||
- value == 900;
- }
-
public static Token OnlyOrDefault(this IEnumerable value)
{
var result = default(Token);
@@ -42,7 +35,7 @@ public static string ToUri(this IEnumerable value)
{
var element = value.OnlyOrDefault();
- if (element != null && element.Type == TokenType.Url) return element.Data;
+ if (element is { Type: TokenType.Url }) return element.Data;
return null;
}
@@ -86,7 +79,7 @@ public static Length ToLength(this FontSize fontSize)
{
var element = value.OnlyOrDefault();
- if (element != null && element.Type == TokenType.Percentage)
+ if (element is { Type: TokenType.Percentage })
return new Percent(((UnitToken)element).Value);
return null;
@@ -97,13 +90,13 @@ public static Length ToLength(this FontSize fontSize)
var enumerable = value as Token[] ?? value.ToArray();
var percent = ToPercent(enumerable);
- if (percent != null)
+ if (percent is not null)
{
return percent;
}
var element = value.OnlyOrDefault();
- if (!(element is NumberToken token))
+ if (element is not NumberToken token)
{
return null;
}
@@ -125,13 +118,13 @@ public static Length ToLength(this FontSize fontSize)
var enumerable = value as Token[] ?? value.ToArray();
var percent = ToPercent(enumerable);
- if (percent != null)
+ if (percent is not null)
{
return new Number(percent.Value.Value, Number.Unit.Percent);
}
var element = value.OnlyOrDefault();
- if (!(element is NumberToken token))
+ if (element is not NumberToken token)
{
return null;
}
@@ -150,7 +143,7 @@ public static string ToCssString(this IEnumerable value)
{
var element = value.OnlyOrDefault();
- if (element != null && element.Type == TokenType.String) return element.Data;
+ if (element is { Type: TokenType.String }) return element.Data;
return null;
}
@@ -188,6 +181,34 @@ public static string ToIdentifier(this IEnumerable value)
return null;
}
+ ///
+ /// An animation-name: a custom identifier (never a CSS-wide keyword) or a quoted string, which
+ /// names the same animation as the identifier spelled the same way. Case is preserved.
+ ///
+ public static string ToAnimationName(this IEnumerable value)
+ {
+ var element = value.OnlyOrDefault();
+
+ if (element == null) return null;
+
+ if (element.Type == TokenType.Ident)
+ {
+ var data = element.Data;
+
+ return data.Isi(Keywords.Inherit) || data.Isi(Keywords.Initial) || data.Isi(Keywords.Unset) ||
+ data.Isi(Keywords.Revert) || data.Isi(Keywords.RevertLayer)
+ ? null
+ : data;
+ }
+
+ if (element.Type == TokenType.String)
+ {
+ return "\"" + element.Data.Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
+ }
+
+ return null;
+ }
+
public static string ToIdentifierCaseInsensitive(this IEnumerable value)
{
var element = value.OnlyOrDefault();
@@ -214,7 +235,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
{
var element = value.OnlyOrDefault();
- if (element != null && element.Type == TokenType.Number) return ((NumberToken)element).Value;
+ if (element != null && element.Type == TokenType.Number) return ((NumberToken) element).Value;
return null;
}
@@ -235,9 +256,25 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
{
var element = value.OnlyOrDefault();
- if (element != null && element.Type == TokenType.Number && ((NumberToken)element).IsInteger)
+ if (element != null && element.Type == TokenType.Number && ((NumberToken) element).IsInteger)
{
- return ((NumberToken)element).IntegerValue;
+ return ((NumberToken) element).IntegerValue;
+ }
+
+ // CSS Values 4 10: a calc() whose type is is also valid where an is
+ // expected, and its result is rounded to the nearest integer (halves away from zero, 10.9).
+ if (element is FunctionToken function && CalcParser.IsCalcFamily(function.Data))
+ {
+ var node = CalcParser.Parse(function);
+ if (node == null || CalcTypeChecker.Check(node) != CalcCategory.Number) return null;
+
+ var number = CalcSerializer.EvaluateNumber(node);
+ if (number == null) return null;
+
+ var rounded = Math.Round(number.Value, MidpointRounding.AwayFromZero);
+ if (double.IsNaN(rounded) || rounded < int.MinValue || rounded > int.MaxValue) return null;
+
+ return (int) rounded;
}
return null;
@@ -255,10 +292,11 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
return element > 0 ? element : null;
}
- public static int? ToWeightInteger(this IEnumerable value)
+ // CSS Fonts 4: font-weight accepts any , fractions included.
+ public static double? ToWeightNumber(this IEnumerable value)
{
- var element = value.ToPositiveInteger();
- return element.HasValue && IsWeight(element.Value) ? element : null;
+ var element = value.ToSingle();
+ return element >= 1f && element <= 1000f ? element : null;
}
public static int? ToBinary(this IEnumerable value)
@@ -284,13 +322,13 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
var enumerable = value as Token[] ?? value.ToArray();
var element = enumerable.ToNaturalInteger();
- if (element.HasValue) return (byte)Math.Min(element.Value, 255);
+ if (element.HasValue) return (byte) Math.Min(element.Value, 255);
var percent = enumerable.ToPercent();
if (!percent.HasValue) return null;
- return (byte)(255f * percent.Value.NormalizedValue);
+ return (byte) (255f * percent.Value.NormalizedValue);
}
public static Angle? ToAngle(this IEnumerable value)
@@ -299,7 +337,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
if (element == null || element.Type != TokenType.Dimension) return null;
- var token = (UnitToken)element;
+ var token = (UnitToken) element;
var unit = Angle.GetUnit(token.Unit);
if (unit != Angle.Unit.None) return new Angle(token.Value, unit);
@@ -307,6 +345,25 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
return null;
}
+ ///
+ /// [ <angle> | <zero> ] - the argument grammar of the CSS Transforms rotate and skew
+ /// functions, which keep the legacy unitless-zero exception that the general <angle>
+ /// grammar () does not allow.
+ ///
+ public static Angle? ToAngleOrZero(this IEnumerable value)
+ {
+ var tokens = value as Token[] ?? value.ToArray();
+ var angle = tokens.ToAngle();
+
+ if (angle.HasValue) return angle;
+
+ var element = tokens.OnlyOrDefault();
+
+ return element is { Type: TokenType.Number } && ((NumberToken) element).Value == 0f
+ ? new Angle(0f, Angle.Unit.Deg)
+ : null;
+ }
+
public static Angle? ToAngleNumber(this IEnumerable value)
{
var enumerable = value as Token[] ?? value.ToArray();
@@ -330,14 +387,14 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
switch (element.Type)
{
case TokenType.Dimension:
- {
- var token = (UnitToken)element;
- var unit = Length.GetUnit(token.Unit);
-
- if (unit != Length.Unit.None) return new Length(token.Value, unit);
- break;
- }
- case TokenType.Number when ((NumberToken)element).Value == 0f:
+ {
+ var token = (UnitToken) element;
+ var unit = Length.GetUnit(token.Unit);
+
+ if (unit != Length.Unit.None) return new Length(token.Value, unit);
+ break;
+ }
+ case TokenType.Number when ((NumberToken) element).Value == 0f:
return Length.Zero;
}
}
@@ -351,7 +408,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
if (element == null || element.Type != TokenType.Dimension) return null;
- var token = (UnitToken)element;
+ var token = (UnitToken) element;
var unit = Resolution.GetUnit(token.Unit);
if (unit != Resolution.Unit.None) return new Resolution(token.Value, unit);
@@ -365,7 +422,7 @@ public static string ToAnimatableIdentifier(this IEnumerable value)
if (element == null || element.Type != TokenType.Dimension) return null;
- var token = (UnitToken)element;
+ var token = (UnitToken) element;
var unit = Time.GetUnit(token.Unit);
if (unit != Time.Unit.None) return new Time(token.Value, unit);
@@ -483,6 +540,8 @@ public static string ToText(this IEnumerable value)
return string.Join(string.Empty, value.Select(m => m.ToValue()));
}
+ // Whether the value contains a call to the named function anywhere, including nested inside another
+ // function's arguments - e.g. var() inside calc(var(--x) + 1px).
public static bool ContainsFunction(this IEnumerable tokens, string functionName)
{
foreach (var token in tokens)
@@ -503,7 +562,7 @@ public static bool ContainsFunction(this IEnumerable tokens, string funct
if (element != null && element.Type == TokenType.Ident) return Color.FromName(element.Data);
- if (element != null && element.Type == TokenType.Color && !((ColorToken)element).IsValid)
+ if (element != null && element.Type == TokenType.Color && !((ColorToken) element).IsValid)
{
return Color.FromHex(element.Data);
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs
index dc067c397..8d4d0217b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Factories/AttributeSelectorFactory.cs
@@ -4,7 +4,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal sealed class AttributeSelectorFactory
{
- private static readonly Lazy Lazy = new Lazy(() => new AttributeSelectorFactory());
+ private static readonly Lazy Lazy = new(() => new AttributeSelectorFactory());
private AttributeSelectorFactory()
{
@@ -13,6 +13,12 @@ private AttributeSelectorFactory()
internal static AttributeSelectorFactory Instance => Lazy.Value;
public IAttrSelector Create(string combinator, string match, string value, string prefix)
+ {
+ return Create(combinator, match, value, prefix, AttrCaseSensitivity.Default);
+ }
+
+ public IAttrSelector Create(string combinator, string match, string value, string prefix,
+ AttrCaseSensitivity caseSensitivity)
{
var name = match;
@@ -22,20 +28,21 @@ public IAttrSelector Create(string combinator, string match, string value, strin
_ = AttributeSelectorFactory.FormMatch(prefix, match);
}
- // A reflection-free dispatch (no Activator.CreateInstance) so the two-arg Attr*Selector
- // constructors are statically reachable and survive trimming/AOT (IsTrimmable=true) - see
- // upstream ExCSS commit c497ca7. Unknown combinators fall back to a presence selector.
- switch (combinator)
- {
- case Combinators.Exactly: return new AttrMatchSelector(name, value);
- case Combinators.InList: return new AttrListSelector(name, value);
- case Combinators.InToken: return new AttrHyphenSelector(name, value);
- case Combinators.Begins: return new AttrBeginsSelector(name, value);
- case Combinators.Ends: return new AttrEndsSelector(name, value);
- case Combinators.InText: return new AttrContainsSelector(name, value);
- case Combinators.Unlike: return new AttrNotMatchSelector(name, value);
- default: return new AttrAvailableSelector(name, value);
- }
+ if (combinator == Combinators.Exactly)
+ return new AttrMatchSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.InList)
+ return new AttrListSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.InToken)
+ return new AttrHyphenSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.Begins)
+ return new AttrBeginsSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.Ends)
+ return new AttrEndsSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.InText)
+ return new AttrContainsSelector(name, value, caseSensitivity);
+ if (combinator == Combinators.Unlike)
+ return new AttrNotMatchSelector(name, value, caseSensitivity);
+ return new AttrAvailableSelector(name, value);
}
private static string FormFront(string prefix, string match)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs
new file mode 100644
index 000000000..fe0a441f0
--- /dev/null
+++ b/Source/HtmlRenderer/Core/CssEngine/Factories/LegacyGradientSyntax.cs
@@ -0,0 +1,380 @@
+using System;
+using System.Collections.Generic;
+using System.Globalization;
+
+namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
+{
+ ///
+ /// Rewrites the pre-standard vendor gradient functions (-webkit-linear-gradient(),
+ /// -moz-radial-gradient(), and their repeating- forms; -o- and -ms- spellings too)
+ /// into the standard linear-gradient()/radial-gradient() token stream, so the value converters
+ /// only ever see standard syntax. Only applied when is on.
+ ///
+ ///
+ /// Not a pure rename, the prefixed grammar differs from the standard one:
+ ///
+ /// -
+ /// Linear: an angle is measured counter-clockwise from the east axis (standard: clockwise from north),
+ /// so standard = 90deg - legacy; a side/corner keyword names where the line starts
+ /// (top = the standard to bottom), so each keyword is flipped to its opposite; with neither,
+ /// both default to top-to-bottom.
+ ///
+ /// -
+ /// Radial: the center position comes first (standard: after at), the size keywords are
+ /// cover/contain (standard farthest-corner/closest-side).
+ ///
+ ///
+ /// A function whose first group already uses the standard to <side> form passes through unchanged.
+ /// -webkit-gradient() (a different function with a different grammar) is not handled.
+ ///
+ internal static class LegacyGradientSyntax
+ {
+ private static readonly string[] Prefixes = {"-webkit-", "-moz-", "-o-", "-ms-"};
+
+ private static readonly string[] StandardNames =
+ {
+ FunctionNames.LinearGradient, FunctionNames.RepeatingLinearGradient,
+ FunctionNames.RadialGradient, FunctionNames.RepeatingRadialGradient
+ };
+
+ ///
+ /// Returns with every legacy gradient function (at any nesting depth) rewritten
+ /// to standard syntax, or itself when it contains none.
+ ///
+ internal static TokenValue Rewrite(TokenValue value)
+ {
+ if (value == null) return null;
+
+ List tokens = null;
+ for (var i = 0; i < value.Count; i++)
+ {
+ var token = value[i];
+ var rewritten = Rewrite(token);
+
+ if (ReferenceEquals(rewritten, token))
+ {
+ tokens?.Add(token);
+ continue;
+ }
+
+ if (tokens == null)
+ {
+ tokens = new List(value.Count);
+ for (var j = 0; j < i; j++) tokens.Add(value[j]);
+ }
+
+ tokens.Add(rewritten);
+ }
+
+ return tokens == null ? value : new TokenValue(tokens) {StylesheetText = value.StylesheetText};
+ }
+
+ ///
+ /// Returns rewritten to standard syntax if it is (or contains) a legacy gradient
+ /// function; otherwise the same instance.
+ ///
+ internal static Token Rewrite(Token token)
+ {
+ if (!(token is FunctionToken function)) return token;
+
+ // Rewrite nested functions first (a gradient inside image-set(), cross-fade(), ...).
+ var arguments = new List();
+ var changed = false;
+ foreach (var argument in function)
+ {
+ var rewrittenArgument = Rewrite(argument);
+ changed |= !ReferenceEquals(rewrittenArgument, argument);
+ arguments.Add(rewrittenArgument);
+ }
+
+ if (TryGetStandardName(function.Data, out var standardName, out var isLinear))
+ {
+ var groups = SplitGroups(Without(arguments));
+ if (groups.Count > 0) groups = isLinear ? RewriteLinear(groups) : RewriteRadial(groups);
+
+ var rewritten = new FunctionToken(standardName, function.Position);
+ for (var i = 0; i < groups.Count; i++)
+ {
+ if (i > 0)
+ {
+ rewritten.AddArgumentToken(Token.Comma);
+ rewritten.AddArgumentToken(Token.Whitespace);
+ }
+
+ foreach (var t in groups[i]) rewritten.AddArgumentToken(t);
+ }
+
+ rewritten.AddArgumentToken(new Token(TokenType.RoundBracketClose, ")", function.Position));
+ return rewritten;
+ }
+
+ if (!changed) return token;
+
+ var copy = new FunctionToken(function.Data, function.Position);
+ foreach (var argument in arguments) copy.AddArgumentToken(argument);
+ return copy;
+ }
+
+ // The arguments without the function's own closing parenthesis.
+ private static List Without(List arguments)
+ {
+ var result = new List(arguments);
+ if (result.Count > 0 && result[result.Count - 1].Type == TokenType.RoundBracketClose)
+ result.RemoveAt(result.Count - 1);
+ return result;
+ }
+
+ private static bool TryGetStandardName(string name, out string standardName, out bool isLinear)
+ {
+ standardName = string.Empty;
+ isLinear = false;
+
+ if (name.Length < 2 || name[0] != '-') return false;
+
+ foreach (var prefix in Prefixes)
+ {
+ if (!name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) continue;
+
+ var rest = name.Substring(prefix.Length);
+ foreach (var candidate in StandardNames)
+ {
+ if (!rest.Equals(candidate, StringComparison.OrdinalIgnoreCase)) continue;
+
+ standardName = candidate;
+ isLinear = candidate.IndexOf("linear", StringComparison.Ordinal) >= 0;
+ return true;
+ }
+
+ return false;
+ }
+
+ return false;
+ }
+
+ // Splits on top-level commas (nested functions are self-contained tokens, so a flat split is top-level),
+ // trimming surrounding whitespace from each group.
+ private static List> SplitGroups(IEnumerable tokens)
+ {
+ var groups = new List> {new List()};
+
+ foreach (var token in tokens)
+ {
+ if (token.Type == TokenType.Comma) groups.Add(new List