Skip to content

Commit 8b7a4c8

Browse files
jhaygood86eXpl0it3r
authored andcommitted
Port of CSS engine from PeachPDF (ExCSS-based)
1 parent e9e3d39 commit 8b7a4c8

526 files changed

Lines changed: 27395 additions & 1652 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Source/HtmlRenderer/Adapters/RAdapter.cs‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,16 @@ protected RAdapter()
8585
/// </summary>
8686
public CssData DefaultCssData
8787
{
88-
get { return _defaultCssData ?? (_defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false)); }
88+
get
89+
{
90+
if (_defaultCssData == null)
91+
{
92+
_defaultCssData = CssData.Parse(this, CssDefaults.DefaultStyleSheet, false);
93+
foreach (var stylesheet in _defaultCssData.Stylesheets)
94+
stylesheet.IsUserAgent = true;
95+
}
96+
return _defaultCssData;
97+
}
8998
}
9099

91100
/// <summary>

‎Source/HtmlRenderer/Core/CssData.cs‎

Lines changed: 826 additions & 109 deletions
Large diffs are not rendered by default.

‎Source/HtmlRenderer/Core/CssDefaults.cs‎

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,5 +124,116 @@ @media print {
124124
padding: 8px;
125125
Font: 9pt Tahoma;
126126
}";
127+
128+
/// <summary>
129+
/// The initial value of every property this engine recognizes (<see cref="Utils.CssUtils"/>'s exact
130+
/// whitelist), used to resolve the <c>initial</c>/<c>unset</c>/<c>revert</c>/<c>revert-layer</c>
131+
/// CSS-wide keywords in the six-phase cascade (<see cref="Parse.DomParser"/>). Deliberately matches
132+
/// this engine's own existing field defaults (<see cref="Dom.CssBoxProperties"/>'s private field
133+
/// initializers) rather than the CSS spec's initial values where the two differ (e.g. this engine's
134+
/// border-color default is "black", not the spec's "currentcolor"; text-align/text-decoration/
135+
/// list-style-image default to an empty string here, not "left"/"none"/"none") - so resolving
136+
/// "initial" produces exactly what an unstyled box already looks like today, not a new value.
137+
/// </summary>
138+
internal static readonly System.Collections.Generic.Dictionary<string, string> InitialValues =
139+
new System.Collections.Generic.Dictionary<string, string>(System.StringComparer.OrdinalIgnoreCase)
140+
{
141+
{ "background-color", "transparent" },
142+
{ "background-gradient", "none" },
143+
{ "background-gradient-angle", "90" },
144+
{ "background-image", "none" },
145+
{ "background-position", "0% 0%" },
146+
{ "background-repeat", "repeat" },
147+
{ "border-top-width", "medium" },
148+
{ "border-right-width", "medium" },
149+
{ "border-bottom-width", "medium" },
150+
{ "border-left-width", "medium" },
151+
{ "border-top-color", "black" },
152+
{ "border-right-color", "black" },
153+
{ "border-bottom-color", "black" },
154+
{ "border-left-color", "black" },
155+
{ "border-top-style", "none" },
156+
{ "border-right-style", "none" },
157+
{ "border-bottom-style", "none" },
158+
{ "border-left-style", "none" },
159+
{ "border-spacing", "0" },
160+
{ "border-collapse", "separate" },
161+
{ "color", "black" },
162+
{ "content", "normal" },
163+
{ "corner-nw-radius", "0" },
164+
{ "corner-ne-radius", "0" },
165+
{ "corner-se-radius", "0" },
166+
{ "corner-sw-radius", "0" },
167+
{ "corner-radius", "0" },
168+
{ "empty-cells", "show" },
169+
{ "direction", "ltr" },
170+
{ "display", "inline" },
171+
{ "font-size", "medium" },
172+
{ "font-style", "normal" },
173+
{ "font-variant", "normal" },
174+
{ "font-weight", "normal" },
175+
{ "float", "none" },
176+
{ "height", "auto" },
177+
{ "margin-bottom", "0" },
178+
{ "margin-left", "0" },
179+
{ "margin-right", "0" },
180+
{ "margin-top", "0" },
181+
{ "left", "auto" },
182+
{ "line-height", "normal" },
183+
{ "list-style-type", "disc" },
184+
{ "list-style-image", "" },
185+
{ "list-style-position", "outside" },
186+
{ "list-style", "" },
187+
{ "overflow", "visible" },
188+
{ "padding-left", "0" },
189+
{ "padding-bottom", "0" },
190+
{ "padding-right", "0" },
191+
{ "padding-top", "0" },
192+
{ "page-break-inside", "auto" },
193+
{ "text-align", "" },
194+
{ "text-decoration", "" },
195+
{ "text-indent", "0" },
196+
{ "top", "auto" },
197+
{ "position", "static" },
198+
{ "vertical-align", "baseline" },
199+
{ "width", "auto" },
200+
{ "max-width", "none" },
201+
{ "word-spacing", "normal" },
202+
{ "word-break", "normal" },
203+
{ "visibility", "visible" },
204+
{ "white-space", "normal" },
205+
};
206+
207+
/// <summary>
208+
/// Properties inherited from parent to child during cascade - kept exactly in sync with the set
209+
/// <see cref="Dom.CssBoxProperties.InheritStyle"/> already (independently) copies, since this is
210+
/// used only to decide whether the <c>unset</c> keyword should behave like <c>inherit</c> or
211+
/// <c>initial</c> for a given property, and the two must agree for "unset" to make sense.
212+
/// </summary>
213+
internal static readonly System.Collections.Generic.HashSet<string> InheritedProperties =
214+
new System.Collections.Generic.HashSet<string>(System.StringComparer.OrdinalIgnoreCase)
215+
{
216+
"border-spacing", "border-collapse",
217+
"color",
218+
"empty-cells",
219+
"white-space",
220+
"visibility",
221+
"text-indent", "text-align",
222+
"font-family", "font-size", "font-style", "font-variant", "font-weight",
223+
"list-style-image", "list-style-position", "list-style-type", "list-style",
224+
"line-height",
225+
"word-break",
226+
"direction",
227+
};
228+
229+
/// <summary>
230+
/// Returns the initial value for the given property name, or null if this engine doesn't
231+
/// recognize the property at all.
232+
/// </summary>
233+
internal static string GetInitialValue(string propertyName)
234+
{
235+
string value;
236+
return InitialValues.TryGetValue(propertyName, out value) ? value : null;
237+
}
127238
}
128239
}
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
using System.Collections.Generic;
2+
3+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
4+
{
5+
/// <summary>
6+
/// Shared grammar for the CSS <c>background-position</c> value (the 1/2/3/4-token forms,
7+
/// including the edge-relative offset syntax, e.g. "right 20px bottom 10px"). Used by both
8+
/// <see cref="Converters.PointConverter"/> (CSS-OM grammar validation/serialization) and
9+
/// PeachPDF.Html.Core.Utils.BackgroundLayerResolver (render-time pixel-offset arithmetic) so
10+
/// there is one parser for this grammar, not two independent re-implementations - mirroring how
11+
/// CalcParser is shared between CalcValueConverter and CssValueParser rather than re-parsed twice.
12+
/// </summary>
13+
internal static class BackgroundPositionGrammar
14+
{
15+
internal enum AxisKeyword { None, Left, Right, Top, Bottom, Center }
16+
17+
internal readonly struct Component
18+
{
19+
public AxisKeyword Keyword { get; }
20+
21+
/// <summary>The length/percentage/calc() token for this component, or null when there is
22+
/// none (a keyword-only component, or the implicit "center" filled in for a 1-token value).</summary>
23+
public Token Offset { get; }
24+
25+
public Component(AxisKeyword keyword, Token offset)
26+
{
27+
Keyword = keyword;
28+
Offset = offset;
29+
}
30+
}
31+
32+
internal sealed class ParsedPosition
33+
{
34+
public Component X { get; }
35+
public Component Y { get; }
36+
37+
/// <summary>The 1-2 authored components in original left-to-right order, needed to
38+
/// re-serialize in the exact order/form the author wrote (CSS round-trips verbatim).</summary>
39+
public IReadOnlyList<Component> AuthoredOrder { get; }
40+
41+
public ParsedPosition(Component x, Component y, IReadOnlyList<Component> authoredOrder)
42+
{
43+
X = x;
44+
Y = y;
45+
AuthoredOrder = authoredOrder;
46+
}
47+
}
48+
49+
internal static ParsedPosition TryParse(IReadOnlyList<Token> tokens)
50+
{
51+
switch (tokens.Count)
52+
{
53+
case 1:
54+
return ParseSingle(tokens[0]);
55+
case 2:
56+
return ParseDouble(tokens[0], tokens[1]);
57+
case 3:
58+
return ParseTriple(tokens[0], tokens[1], tokens[2]);
59+
case 4:
60+
return ParseQuadruple(tokens[0], tokens[1], tokens[2], tokens[3]);
61+
default:
62+
return null;
63+
}
64+
}
65+
66+
private static ParsedPosition ParseSingle(Token token)
67+
{
68+
var keyword = KeywordOf(token);
69+
if (keyword == AxisKeyword.None)
70+
{
71+
if (!IsLengthOrPercent(token)) return null;
72+
73+
var bare = new Component(AxisKeyword.None, token);
74+
var center = new Component(AxisKeyword.Center, null);
75+
return new ParsedPosition(bare, center, new[] { bare });
76+
}
77+
78+
if (IsVerticalOnly(keyword))
79+
{
80+
var y = new Component(keyword, null);
81+
var x = new Component(AxisKeyword.Center, null);
82+
return new ParsedPosition(x, y, new[] { y });
83+
}
84+
85+
var xComp = new Component(keyword, null);
86+
var yComp = new Component(AxisKeyword.Center, null);
87+
return new ParsedPosition(xComp, yComp, new[] { xComp });
88+
}
89+
90+
private static ParsedPosition ParseDouble(Token t0, Token t1)
91+
{
92+
// "h then v" (horizontal-ish first): left/right/center/bare-length, then top/bottom/center/bare-length.
93+
if (IsHSlotValid(t0) && IsVSlotValid(t1))
94+
{
95+
var x = ToComponent(t0);
96+
var y = ToComponent(t1);
97+
return new ParsedPosition(x, y, new[] { x, y });
98+
}
99+
100+
// "v then h" (vertical-ish first, e.g. "bottom center", "top left").
101+
if (IsVSlotValid(t0) && IsHSlotValid(t1))
102+
{
103+
var y = ToComponent(t0);
104+
var x = ToComponent(t1);
105+
return new ParsedPosition(x, y, new[] { y, x });
106+
}
107+
108+
return null;
109+
}
110+
111+
private static ParsedPosition ParseTriple(Token t0, Token t1, Token t2)
112+
{
113+
// hi, vi, offset (offset applies to the vertical group): e.g. "right bottom 20px".
114+
if (IsEdgeKeywordSlot(t0, horizontal: true) && IsEdgeKeywordSlot(t1, horizontal: false) && IsLengthOrPercent(t2))
115+
{
116+
var x = new Component(KeywordOf(t0), null);
117+
var y = new Component(KeywordOf(t1), t2);
118+
return new ParsedPosition(x, y, new[] { x, y });
119+
}
120+
121+
// hi, offset, vi (offset applies to the horizontal group): e.g. "right 20px bottom".
122+
if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) && IsEdgeKeywordSlot(t2, horizontal: false))
123+
{
124+
var x = new Component(KeywordOf(t0), t1);
125+
var y = new Component(KeywordOf(t2), null);
126+
return new ParsedPosition(x, y, new[] { x, y });
127+
}
128+
129+
return null;
130+
}
131+
132+
private static ParsedPosition ParseQuadruple(Token t0, Token t1, Token t2, Token t3)
133+
{
134+
// hi, offset, vi, offset: e.g. "right 20px bottom 10px".
135+
if (IsEdgeKeywordSlot(t0, horizontal: true) && IsLengthOrPercent(t1) &&
136+
IsEdgeKeywordSlot(t2, horizontal: false) && IsLengthOrPercent(t3))
137+
{
138+
var x = new Component(KeywordOf(t0), t1);
139+
var y = new Component(KeywordOf(t2), t3);
140+
return new ParsedPosition(x, y, new[] { x, y });
141+
}
142+
143+
return null;
144+
}
145+
146+
private static Component ToComponent(Token token) =>
147+
new Component(KeywordOf(token), KeywordOf(token) == AxisKeyword.None ? token : null);
148+
149+
private static AxisKeyword KeywordOf(Token token)
150+
{
151+
if (token.Type != TokenType.Ident) return AxisKeyword.None;
152+
if (token.Data.Isi(Keywords.Left)) return AxisKeyword.Left;
153+
if (token.Data.Isi(Keywords.Right)) return AxisKeyword.Right;
154+
if (token.Data.Isi(Keywords.Top)) return AxisKeyword.Top;
155+
if (token.Data.Isi(Keywords.Bottom)) return AxisKeyword.Bottom;
156+
if (token.Data.Isi(Keywords.Center)) return AxisKeyword.Center;
157+
return AxisKeyword.None;
158+
}
159+
160+
private static bool IsHorizontalOnly(AxisKeyword k) => k == AxisKeyword.Left || k == AxisKeyword.Right;
161+
private static bool IsVerticalOnly(AxisKeyword k) => k == AxisKeyword.Top || k == AxisKeyword.Bottom;
162+
163+
private static bool IsLengthOrPercent(Token token) =>
164+
Converters.LengthOrPercentConverter.Convert(new[] { token }) != null;
165+
166+
/// <summary>Valid for the 2-value "h" slot: left/right/center keywords, or a bare length/percent - not top/bottom.</summary>
167+
private static bool IsHSlotValid(Token token)
168+
{
169+
var k = KeywordOf(token);
170+
return k != AxisKeyword.None ? !IsVerticalOnly(k) : IsLengthOrPercent(token);
171+
}
172+
173+
/// <summary>Valid for the 2-value "v" slot: top/bottom/center keywords, or a bare length/percent - not left/right.</summary>
174+
private static bool IsVSlotValid(Token token)
175+
{
176+
var k = KeywordOf(token);
177+
return k != AxisKeyword.None ? !IsHorizontalOnly(k) : IsLengthOrPercent(token);
178+
}
179+
180+
/// <summary>Valid for the 3/4-value "hi"/"vi" edge-keyword slots: keyword only, no bare-length fallback.</summary>
181+
private static bool IsEdgeKeywordSlot(Token token, bool horizontal)
182+
{
183+
var k = KeywordOf(token);
184+
if (k == AxisKeyword.Center) return true;
185+
if (k == AxisKeyword.None) return false;
186+
return horizontal ? IsHorizontalOnly(k) : IsVerticalOnly(k);
187+
}
188+
}
189+
}

0 commit comments

Comments
 (0)