Skip to content

Commit 0186430

Browse files
jhaygood86eXpl0it3r
authored andcommitted
CSS engine: sync @property and the calc() time/resolution leaves
Adds the @Property at-rule (CSS Properties & Values API Level 1) and the calc node types its syntax validation needs, from PeachPDF v0.9.6 (upstream 94f8d94, 008b387, d86abc7, 020c23b, dd7a5ae). - PropertyRule / IPropertyRule, the composer dispatch for `@property --x { }`, and the syntax/initial-value/inherits descriptor table in PropertyFactory (stored raw via UnknownProperty, since their grammar depends on the syntax descriptor itself) - TimeCalcNode / ResolutionCalcNode leaves, threaded through CalcParser, CalcTypeChecker (new Time and Resolution categories), CalcEvaluator and CalcSerializer, plus the previously missing AngleCalcNode evaluation arm CalcContext keeps its fontAdjust flag and CalcSerializer keeps folding straight to px, rather than taking upstream's points-based conversion: Length.ToPixels resolves to CSS pixels here, so no px<->pt round trip belongs in either.
1 parent 2771555 commit 0186430

10 files changed

Lines changed: 205 additions & 1 deletion

File tree

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,18 @@ internal enum CalcCategory
2020
/// convert via fixed constants), so a valid Angle-category expression always folds fully at
2121
/// Layer A parse time - see CalcSerializer.
2222
/// </summary>
23-
Angle = 8
23+
Angle = 8,
24+
25+
/// <summary>
26+
/// &lt;time&gt; (s/ms). Like Angle, it resolves without layout context. Currently produced only by
27+
/// <c>@property</c> <c>syntax: "&lt;time&gt;"</c> validation - no layout property consumes a time
28+
/// calc(), so it never reaches CalcEvaluator during a render.
29+
/// </summary>
30+
Time = 16,
31+
32+
/// <summary>
33+
/// &lt;resolution&gt; (dpi/dpcm/dppx). Context-free like Angle/Time; same @property-only provenance.
34+
/// </summary>
35+
Resolution = 32
2436
}
2537
}

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,23 @@ internal static class CalcEvaluator
6161
return new Length((float)percentage.Value, Length.Unit.Percent)
6262
.ToPixels(context.EmFactor, context.RemFactor, context.HundredPercent, context.FontAdjust);
6363

64+
case AngleCalcNode angle:
65+
// An angle leaf evaluates to radians (the canonical angle unit), so an angle-typed
66+
// calc such as `calc(1turn * 0.35)` yields 0.35 turn in radians (a Number leaf stays a
67+
// unitless scalar). Length calc()s never contain an angle leaf, so this never perturbs
68+
// length evaluation; the only current caller is conic-gradient stop-position parsing.
69+
return new Angle((float)angle.Value, angle.Unit).ToRadian();
70+
71+
case TimeCalcNode time:
72+
// Canonical time unit is milliseconds. No layout property evaluates a time calc() today
73+
// (these nodes only arise from @property `syntax: "<time>"` validation, which type-checks
74+
// rather than evaluates), so this case is defensive completeness paralleling the angle leaf.
75+
return new Time((float)time.Value, time.Unit).ToMilliseconds();
76+
77+
case ResolutionCalcNode resolution:
78+
// Canonical resolution unit is dots-per-pixel; same @property-only provenance as TimeCalcNode.
79+
return new Resolution((float)resolution.Value, resolution.Unit).ToDotsPerPixel();
80+
6481
case UnaryCalcNode unary:
6582
{
6683
var operand = Evaluate(unary.Operand, context);

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcNode.cs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,30 @@ public AngleCalcNode(double value, Angle.Unit unit)
5555
public Angle.Unit Unit { get; }
5656
}
5757

58+
internal sealed class TimeCalcNode : CalcNode
59+
{
60+
public TimeCalcNode(double value, Time.Unit unit)
61+
{
62+
Value = value;
63+
Unit = unit;
64+
}
65+
66+
public double Value { get; }
67+
public Time.Unit Unit { get; }
68+
}
69+
70+
internal sealed class ResolutionCalcNode : CalcNode
71+
{
72+
public ResolutionCalcNode(double value, Resolution.Unit unit)
73+
{
74+
Value = value;
75+
Unit = unit;
76+
}
77+
78+
public double Value { get; }
79+
public Resolution.Unit Unit { get; }
80+
}
81+
5882
/// <summary>A leading unary sign directly before a parenthesized group or a nested function call.</summary>
5983
internal sealed class UnaryCalcNode : CalcNode
6084
{

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcParser.cs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,20 @@ private static CalcNode ParseValue(List<Token> tokens, ref int pos)
172172
return new AngleCalcNode(unitToken.Value, angleUnit);
173173
}
174174

175+
var timeUnit = Time.GetUnit(unitToken.Unit);
176+
if (timeUnit != Time.Unit.None)
177+
{
178+
pos++;
179+
return new TimeCalcNode(unitToken.Value, timeUnit);
180+
}
181+
182+
var resolutionUnit = Resolution.GetUnit(unitToken.Unit);
183+
if (resolutionUnit != Resolution.Unit.None)
184+
{
185+
pos++;
186+
return new ResolutionCalcNode(unitToken.Value, resolutionUnit);
187+
}
188+
175189
return null;
176190
}
177191

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcSerializer.cs‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,17 @@ private static string SerializeOperand(CalcNode node, bool isRightOperand, int p
141141
case PercentageCalcNode percentage:
142142
return new Length((float)percentage.Value, Length.Unit.Percent).ToString(null, CultureInfo.InvariantCulture);
143143

144+
// Time/Resolution leaves are only produced by @property `<time>`/`<resolution>` validation,
145+
// which type-checks rather than serializes, so these arms are defensive completeness. A
146+
// Time/Resolution-category Serialize() (were one ever created) folds through neither the Number
147+
// nor Angle branch nor TryFoldPixels, so it reaches SerializeExpression and re-serializes to
148+
// normalized calc() text using these leaf renderings.
149+
case TimeCalcNode time:
150+
return new Time((float)time.Value, time.Unit).ToString(null, CultureInfo.InvariantCulture);
151+
152+
case ResolutionCalcNode resolution:
153+
return new Resolution((float)resolution.Value, resolution.Unit).ToString(null, CultureInfo.InvariantCulture);
154+
144155
case UnaryCalcNode unary:
145156
{
146157
var operand = SerializeOperand(unary.Operand, false, 3);

‎Source/HtmlRenderer/Core/CssEngine/Calc/CalcTypeChecker.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ internal static class CalcTypeChecker
2828
case AngleCalcNode _:
2929
return CalcCategory.Angle;
3030

31+
case TimeCalcNode _:
32+
return CalcCategory.Time;
33+
34+
case ResolutionCalcNode _:
35+
return CalcCategory.Resolution;
36+
3137
case UnaryCalcNode unary:
3238
return Check(unary.Operand);
3339

‎Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ internal sealed class PropertyFactory
2020

2121
private readonly Dictionary<string, LonghandCreator> _fonts;
2222

23+
// @property descriptors (syntax / initial-value / inherits). Their values have no fixed CSS grammar
24+
// (initial-value depends on the syntax; syntax is an arbitrary string), so each is stored raw via an
25+
// UnknownProperty (Converters.Any) and validated later against the syntax string.
26+
private readonly Dictionary<string, LonghandCreator> _propertyDescriptors;
27+
2328
private readonly Dictionary<string, LonghandCreator> _longhands;
2429

2530
private readonly Dictionary<string, string[]> _mappings;
@@ -353,6 +358,14 @@ private PropertyFactory()
353358
_fontsBuilder.Add(PropertyNames.UnicodeRange, () => new UnicodeRangeProperty());
354359

355360
_fonts = _fontsBuilder;
361+
362+
_propertyDescriptors = new Dictionary<string, LonghandCreator>(StringComparer.OrdinalIgnoreCase)
363+
{
364+
{ PropertyNames.Syntax, () => new UnknownProperty(PropertyNames.Syntax) },
365+
{ PropertyNames.InitialValue, () => new UnknownProperty(PropertyNames.InitialValue) },
366+
{ PropertyNames.Inherits, () => new UnknownProperty(PropertyNames.Inherits) },
367+
};
368+
356369
_longhands = _longhandsBuilder;
357370
_mappings = _mappingsBuilder;
358371
_shorthands = _shorthandsBuilder;
@@ -395,6 +408,11 @@ public Property CreateFont(string name)
395408
return _fonts.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null;
396409
}
397410

411+
public Property CreatePropertyDescriptor(string name)
412+
{
413+
return _propertyDescriptors.TryGetValue(name, out var propertyCreator) ? propertyCreator() : null;
414+
}
415+
398416
public Property CreateViewport(string name)
399417
{
400418
var feature = MediaFeatureFactory.Instance.Create(name);

‎Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ public Rule CreateAtRule(Token token)
3939

4040
if (token.Data.Is(RuleNames.Container)) return CreateContainer(token);
4141

42+
if (token.Data.Is(RuleNames.Property)) return CreateProperty(token);
43+
4244
return token.Data.Is(RuleNames.Document) ? CreateDocument(token) : CreateUnknown(token);
4345
}
4446

@@ -147,6 +149,28 @@ public Rule CreateFontFace(Token current)
147149
return SkipDeclarations(token);
148150
}
149151

152+
public Rule CreateProperty(Token current)
153+
{
154+
var rule = new PropertyRule(_parser);
155+
var start = current.Position;
156+
var token = NextToken();
157+
_nodes.Push(rule);
158+
ParseComments(ref token);
159+
rule.Name = GetRuleName(ref token);
160+
ParseComments(ref token);
161+
162+
if (token.Type == TokenType.CurlyBracketOpen)
163+
{
164+
var end = FillDeclarations(rule, PropertyFactory.Instance.CreatePropertyDescriptor);
165+
rule.StylesheetText = CreateView(start, end);
166+
_nodes.Pop();
167+
return rule;
168+
}
169+
170+
_nodes.Pop();
171+
return SkipDeclarations(token);
172+
}
173+
150174
public Rule CreateImport(Token current)
151175
{
152176
var rule = new ImportRule(_parser);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
2+
{
3+
/// <summary>
4+
/// A registered custom property declared with an <c>@property</c> at-rule
5+
/// (CSS Properties &amp; Values API Level 1 §3). Exposes the three descriptors.
6+
/// </summary>
7+
internal interface IPropertyRule : IRule, IProperties
8+
{
9+
/// <summary>The registered custom property name, e.g. <c>--my-color</c>.</summary>
10+
string Name { get; set; }
11+
12+
/// <summary>The raw <c>syntax</c> descriptor value (a string, e.g. <c>&lt;color&gt;</c> or <c>*</c>).</summary>
13+
string Syntax { get; set; }
14+
15+
/// <summary>The raw <c>initial-value</c> descriptor value.</summary>
16+
string InitialValue { get; set; }
17+
18+
/// <summary>The raw <c>inherits</c> descriptor value (<c>true</c> or <c>false</c>).</summary>
19+
string Inherits { get; set; }
20+
}
21+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
using System.IO;
2+
using System.Linq;
3+
4+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
5+
{
6+
/// <summary>
7+
/// An <c>@property</c> at-rule (CSS Properties &amp; Values API Level 1 §3): registers a typed custom
8+
/// property with a <c>syntax</c>, <c>initial-value</c>, and <c>inherits</c> descriptor. The descriptor
9+
/// block is stored like <see cref="FontFaceRule"/>; the registered name (a dashed-ident such as
10+
/// <c>--my-color</c>) is captured from the prelude like <see cref="KeyframesRule.Name"/>.
11+
/// </summary>
12+
internal sealed class PropertyRule : DeclarationRule, IPropertyRule
13+
{
14+
internal PropertyRule(StylesheetParser parser)
15+
: base(RuleType.Property, RuleNames.Property, parser)
16+
{
17+
}
18+
19+
protected override Property CreateNewProperty(string name)
20+
{
21+
return PropertyFactory.Instance.CreatePropertyDescriptor(name);
22+
}
23+
24+
protected override void ReplaceWith(IRule rule)
25+
{
26+
var other = rule as PropertyRule;
27+
if (other != null) Name = other.Name;
28+
base.ReplaceWith(rule);
29+
}
30+
31+
public string Name { get; set; }
32+
33+
public string Syntax
34+
{
35+
get { return GetValue(PropertyNames.Syntax); }
36+
set { SetValue(PropertyNames.Syntax, value); }
37+
}
38+
39+
public string InitialValue
40+
{
41+
get { return GetValue(PropertyNames.InitialValue); }
42+
set { SetValue(PropertyNames.InitialValue, value); }
43+
}
44+
45+
public string Inherits
46+
{
47+
get { return GetValue(PropertyNames.Inherits); }
48+
set { SetValue(PropertyNames.Inherits, value); }
49+
}
50+
51+
public override void ToCss(TextWriter writer, IStyleFormatter formatter)
52+
{
53+
var declarations = formatter.Declarations(Declarations.Where(d => d.HasValue).Select(d => d.ToCss(formatter)));
54+
writer.Write(string.Concat("@property ", Name, " { ", declarations, " }"));
55+
}
56+
}
57+
}

0 commit comments

Comments
 (0)