Skip to content

Commit 430a4ed

Browse files
jhaygood86eXpl0it3r
authored andcommitted
CSS engine: sync clip-path, aspect-ratio and box-shadow
Three independent value grammars from PeachPDF v0.9.6 (upstream cd6d5d7, ec398a8, 893f7b8, a956a7e, 90890e4). - BasicShapeGrammar + ClipPathValueConverter + ClipPathProperty: polygon(), inset() with optional round radius, circle() and ellipse(), including the fill-rule and `at <position>` forms - AspectRatioGrammar + AspectRatioValueConverter + AspectRatioProperty: `<ratio>` and `auto && <ratio>` - BoxShadowGrammar + BoxShadowValueConverter, replacing the previous box-shadow validation with the full comma-separated layer grammar, including the hex-token normalization needed because the render-time tokenizer is not in value mode Downgraded to C# 7.3: init-only setters become private setters, plus the usual list/property/relational patterns, collection expressions and target-typed new. These are parse-and-cascade only for now; the renderer has no clip region, shadow draw handler or intrinsic-sizing hook to consume them.
1 parent 580bab1 commit 430a4ed

10 files changed

Lines changed: 910 additions & 2 deletions

File tree

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
5+
{
6+
/// <summary>
7+
/// The shared grammar for the <c>aspect-ratio</c> value (<c>[ auto || &lt;ratio&gt; ]</c>, where
8+
/// <c>&lt;ratio&gt; = &lt;number [0,∞]&gt; [ / &lt;number [0,∞]&gt; ]?</c>). Used by both Layer A (to
9+
/// validate/accept-or-reject at parse time) and Layer B (to compute the used ratio during layout), so the
10+
/// grammar is defined once — the <see cref="BasicShapeGrammar"/> precedent.
11+
/// </summary>
12+
internal static class AspectRatioGrammar
13+
{
14+
/// <summary>
15+
/// Validates the token stream as an <c>aspect-ratio</c> value. Returns false for anything that is not a
16+
/// valid value. On success, <paramref name="ratio"/> is the used width/height ratio, or null when there
17+
/// is no usable ratio (a bare <c>auto</c>, or a ratio with a zero term — both of which mean "no
18+
/// preferred aspect ratio" for a non-replaced box).
19+
/// </summary>
20+
internal static bool TryParse(IReadOnlyList<Token> tokens, out double? ratio) =>
21+
TryParse(tokens, out ratio, out _);
22+
23+
/// <summary>
24+
/// Validates the token stream as an <c>aspect-ratio</c> value, additionally reporting whether the
25+
/// <c>auto</c> keyword was present (<c>[ auto || &lt;ratio&gt; ]</c>). <paramref name="hasAuto"/> lets a
26+
/// replaced element prefer its natural aspect ratio and fall back to the specified <c>&lt;ratio&gt;</c>
27+
/// (CSS Box Sizing 4 §5): <c>auto &lt;ratio&gt;</c> ⇒ <c>hasAuto=true</c> with a usable
28+
/// <paramref name="ratio"/>; a bare <c>&lt;ratio&gt;</c> ⇒ <c>hasAuto=false</c> (the ratio always
29+
/// overrides); a bare <c>auto</c> ⇒ <c>hasAuto=true</c> with a null <paramref name="ratio"/>.
30+
/// </summary>
31+
internal static bool TryParse(IReadOnlyList<Token> tokens, out double? ratio, out bool hasAuto)
32+
{
33+
ratio = null;
34+
hasAuto = false;
35+
36+
Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
37+
if (toks.Length == 0) return false;
38+
39+
var i = 0;
40+
41+
if (IsAuto(toks[i])) { hasAuto = true; i++; }
42+
43+
if (i == toks.Length) return hasAuto; // bare `auto`
44+
45+
// <ratio> = <number> [ / <number> ]?
46+
if (!TryRatioCore(toks, ref i, out ratio)) return false;
47+
48+
// An optional trailing `auto` (the `||` allows either order: `<ratio> auto`).
49+
if (i < toks.Length && !hasAuto && IsAuto(toks[i])) { hasAuto = true; i++; }
50+
51+
if (i != toks.Length) return false; // trailing junk
52+
53+
return true;
54+
}
55+
56+
/// <summary>
57+
/// Validates a bare <c>&lt;ratio&gt;</c> value (<c>&lt;number [0,∞]&gt; [ / &lt;number [0,∞]&gt; ]?</c>),
58+
/// with no <c>auto</c> — the CSS Values 4 <c>&lt;ratio&gt;</c> data type, as used by the <c>@property</c>
59+
/// <c>syntax: "&lt;ratio&gt;"</c> matcher. Distinct from <see cref="TryParse(IReadOnlyList{Token}, out double?)"/>, whose <c>aspect-ratio</c>
60+
/// grammar additionally permits <c>auto</c>. On success <paramref name="ratio"/> is the used width/height
61+
/// ratio, or null when a term is zero.
62+
/// </summary>
63+
internal static bool TryParseRatio(IReadOnlyList<Token> tokens, out double? ratio)
64+
{
65+
ratio = null;
66+
67+
Token[] toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
68+
69+
var i = 0;
70+
if (!TryRatioCore(toks, ref i, out ratio)) return false;
71+
return i == toks.Length; // no trailing junk (and, since we never consume `auto`, no `auto`)
72+
}
73+
74+
private static bool TryRatioCore(Token[] toks, ref int i, out double? ratio)
75+
{
76+
ratio = null;
77+
78+
// <number> [ / <number> ]?
79+
if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out var width)) return false;
80+
double height = 1;
81+
i++;
82+
83+
if (i < toks.Length && IsSlash(toks[i]))
84+
{
85+
i++;
86+
if (i >= toks.Length || !TryNonNegativeNumber(toks[i], out height)) return false;
87+
i++;
88+
}
89+
90+
// A zero term degenerates to "no preferred aspect ratio".
91+
ratio = width <= 0 || height <= 0 ? (double?)null : width / height;
92+
return true;
93+
}
94+
95+
private static bool IsAuto(Token token) => token.Type == TokenType.Ident && token.Data.Isi(Keywords.Auto);
96+
97+
private static bool IsSlash(Token token) => token.Type == TokenType.Delim && token.Data == "/";
98+
99+
private static bool TryNonNegativeNumber(Token token, out double value)
100+
{
101+
if (token is NumberToken number && number.Value >= 0f)
102+
{
103+
value = number.Value;
104+
return true;
105+
}
106+
107+
value = 0;
108+
return false;
109+
}
110+
}
111+
}

0 commit comments

Comments
 (0)