Skip to content

Commit 072886e

Browse files
jhaygood86eXpl0it3r
authored andcommitted
CSS engine: sync value converter infrastructure and grid grammars
The converter layer from PeachPDF v0.9.6 (upstream 1068034, 4662771, 7116636, ad3e2c4, 8f84648, 24074e3, f0c862e, 11d13b1, 7cf1855, plus the grid grammar work in 4319393, fb7a769, 12ec795). - OrderIndependentOptionsConverter, and the matching rewrite of UnorderedOptionsConverter: upstream deliberately narrowed order-independent shorthand matching, restoring greedy declaration-order matching for the general `||` shorthands and scoping the permutation search to list-style, columns, transform-origin and perspective-origin - GradientConverter: conic gradients and <color-interpolation-method> validation, plus two-position colour stops preserved in the serialized text - ExtendedImageConverters: image-set(), cross-fade() and element() - Model/Converters.cs: the lab/oklab/lch/oklch/color-mix converter singletons (which is what makes CSS Color 4/5 values actually reach the cascade), the lenient rgb/hsl forms, place-*/justify-* and the grid converters - CssProperty<T> / ITypedPropertyValue<T>: the typed cascade value that carries a parsed model alongside its text, with grid templates as the first adopter - The grid grammars (track list, line, template areas) and their value converters, pulled in here because Model/Converters.cs references them The grid *properties* and their factory registrations land separately, so grid values are not yet accepted - that is the next commit. Downgraded to C# 7.3 throughout: init-only setters, list/relational/property patterns, target-typed new, collection expressions, using declarations and index-from-end.
1 parent 430a4ed commit 072886e

29 files changed

Lines changed: 2232 additions & 164 deletions
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
2+
{
3+
/// <summary>The five CSS-wide keywords (CSS Cascade &amp; Inheritance §7.3) a property value can be.</summary>
4+
internal enum CssGlobalKeyword
5+
{
6+
Inherit,
7+
Initial,
8+
Unset,
9+
Revert,
10+
RevertLayer
11+
}
12+
13+
/// <summary>
14+
/// A typed box-side property value that carries the <b>parsed</b> value through the cascade to the box,
15+
/// so a layout consumer reads the object rather than re-parsing the authored string. A value is in exactly
16+
/// one of three states:
17+
/// <list type="bullet">
18+
/// <item>a CSS-wide keyword (<see cref="GlobalValue"/> non-null),</item>
19+
/// <item><i>unresolved</i> — the authored text still contains <c>var()</c> and has not been resolved yet
20+
/// (<see cref="IsUnresolved"/>; <see cref="Value"/> is default), or</item>
21+
/// <item>resolved — <see cref="Value"/> holds the parsed <typeparamref name="T"/>.</item>
22+
/// </list>
23+
/// The authored/keyword text is retained so the string-based getter/snapshot/revert cascade path keeps
24+
/// working (<see cref="ToString"/>). Grid templates (<c>T = GridTemplate</c>) are the first adopter; the
25+
/// mechanism is intended to eventually back every box property.
26+
/// </summary>
27+
internal sealed class CssProperty<T>
28+
{
29+
private readonly string _cssText;
30+
31+
private CssProperty(CssGlobalKeyword? global, bool unresolved, T value, string cssText)
32+
{
33+
GlobalValue = global;
34+
IsUnresolved = unresolved;
35+
Value = value;
36+
_cssText = cssText;
37+
}
38+
39+
/// <summary>The CSS-wide keyword, when this value is one; otherwise null.</summary>
40+
public CssGlobalKeyword? GlobalValue { get; }
41+
42+
public bool IsGlobalValue => GlobalValue.HasValue;
43+
44+
/// <summary>The authored text still contains an unresolved <c>var()</c> — <see cref="Value"/> is not
45+
/// meaningful until resolution replaces this with a resolved value.</summary>
46+
public bool IsUnresolved { get; }
47+
48+
/// <summary>The parsed value — meaningful only when the value is neither global nor unresolved.</summary>
49+
public T Value { get; }
50+
51+
public static CssProperty<T> Global(CssGlobalKeyword keyword) =>
52+
new CssProperty<T>(keyword, unresolved: false, value: default(T), CssGlobalKeywords.ToText(keyword));
53+
54+
public static CssProperty<T> Unresolved(string rawText) =>
55+
new CssProperty<T>(global: null, unresolved: true, value: default(T), rawText);
56+
57+
public static CssProperty<T> FromValue(string cssText, T value) =>
58+
new CssProperty<T>(global: null, unresolved: false, value, cssText);
59+
60+
public override string ToString() => _cssText;
61+
}
62+
63+
/// <summary>Maps between the CSS-wide keyword string literals and <see cref="CssGlobalKeyword"/>.</summary>
64+
internal static class CssGlobalKeywords
65+
{
66+
public static bool TryParse(string value, out CssGlobalKeyword keyword)
67+
{
68+
if (value.Isi(Keywords.Inherit)) { keyword = CssGlobalKeyword.Inherit; return true; }
69+
if (value.Isi(Keywords.Initial)) { keyword = CssGlobalKeyword.Initial; return true; }
70+
if (value.Isi(Keywords.Unset)) { keyword = CssGlobalKeyword.Unset; return true; }
71+
if (value.Isi(Keywords.Revert)) { keyword = CssGlobalKeyword.Revert; return true; }
72+
if (value.Isi(Keywords.RevertLayer)) { keyword = CssGlobalKeyword.RevertLayer; return true; }
73+
keyword = default;
74+
return false;
75+
}
76+
77+
public static string ToText(CssGlobalKeyword keyword)
78+
{
79+
switch (keyword)
80+
{
81+
case CssGlobalKeyword.Inherit: return Keywords.Inherit;
82+
case CssGlobalKeyword.Initial: return Keywords.Initial;
83+
case CssGlobalKeyword.Unset: return Keywords.Unset;
84+
case CssGlobalKeyword.Revert: return Keywords.Revert;
85+
case CssGlobalKeyword.RevertLayer: return Keywords.RevertLayer;
86+
default: return Keywords.Initial;
87+
}
88+
}
89+
}
90+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
5+
{
6+
/// <summary>
7+
/// A parsed grid <c>&lt;grid-line&gt;</c> (one edge of <c>grid-column</c>/<c>grid-row</c>/<c>grid-area</c>):
8+
/// <c>auto</c>, an integer line number (may be negative — counting from the end edge), <c>span N</c>, or
9+
/// a named line reference (a bare <c>&lt;custom-ident&gt;</c>, or <c>&lt;custom-ident&gt; &lt;integer&gt;</c>
10+
/// for the Nth line with that name). <c>span &lt;custom-ident&gt;</c> is out of v1 scope.
11+
/// </summary>
12+
internal sealed class GridLine
13+
{
14+
public bool IsAuto { get; private set; }
15+
public bool IsSpan { get; private set; }
16+
17+
/// <summary>The line number (for a line reference), the span count (when <see cref="IsSpan"/>), or
18+
/// the 1-based Nth index for a named line (defaults to 1).</summary>
19+
public int Value { get; private set; } = 1;
20+
21+
/// <summary>The custom-ident of a named line reference, or null when this is not a named line.</summary>
22+
public string Name { get; private set; }
23+
24+
public static readonly GridLine Auto = new GridLine { IsAuto = true };
25+
public static GridLine Span(int n) => new GridLine { IsSpan = true, Value = n };
26+
public static GridLine Line(int n) => new GridLine { Value = n };
27+
public static GridLine Named(string name) => new GridLine { Name = name, Value = 1 };
28+
public static GridLine NamedNth(string name, int n) => new GridLine { Name = name, Value = n };
29+
}
30+
31+
/// <summary>
32+
/// Shared grammar for a single grid <c>&lt;grid-line&gt;</c> value. Used by Layer A (to validate the
33+
/// <c>grid-column-start</c>/<c>-end</c>/<c>grid-row-start</c>/<c>-end</c> longhands) and by Layer B (the
34+
/// grid placement engine).
35+
/// </summary>
36+
internal static class GridLineGrammar
37+
{
38+
internal static GridLine TryParse(IReadOnlyList<Token> tokens)
39+
{
40+
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
41+
if (toks.Length == 0) return null;
42+
43+
// auto
44+
if (toks.Length == 1 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Auto))
45+
return GridLine.Auto;
46+
47+
// <integer>
48+
if (toks.Length == 1 && toks[0] is NumberToken number && number.IsInteger && number.IntegerValue != 0)
49+
return GridLine.Line(number.IntegerValue);
50+
51+
// span <integer [1,∞]>
52+
if (toks.Length == 2 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Span)
53+
&& toks[1] is NumberToken spanCount && spanCount.IsInteger && spanCount.IntegerValue >= 1)
54+
return GridLine.Span(spanCount.IntegerValue);
55+
56+
// <custom-ident> — a named line reference.
57+
if (toks.Length == 1 && IsCustomIdent(toks[0]))
58+
return GridLine.Named(toks[0].Data);
59+
60+
// <custom-ident> <integer> / <integer> <custom-ident> — the Nth line with that name (order-independent).
61+
if (toks.Length == 2)
62+
{
63+
if (IsCustomIdent(toks[0]) && toks[1] is NumberToken n1 && n1.IsInteger && n1.IntegerValue != 0)
64+
return GridLine.NamedNth(toks[0].Data, n1.IntegerValue);
65+
if (IsCustomIdent(toks[1]) && toks[0] is NumberToken n2 && n2.IsInteger && n2.IntegerValue != 0)
66+
return GridLine.NamedNth(toks[1].Data, n2.IntegerValue);
67+
}
68+
69+
return null;
70+
}
71+
72+
/// <summary>Whether the token is a <c>&lt;custom-ident&gt;</c> usable as a grid line name — an ident
73+
/// that is not one of the reserved keywords a grid-line value or the CSS-wide keywords may take.</summary>
74+
private static bool IsCustomIdent(Token token)
75+
{
76+
if (token.Type != TokenType.Ident) return false;
77+
var d = token.Data;
78+
return !d.Isi(Keywords.Auto) && !d.Isi(Keywords.Span) && !d.Isi(Keywords.None)
79+
&& !d.Isi(Keywords.Inherit) && !d.Isi(Keywords.Initial) && !d.Isi(Keywords.Unset)
80+
&& !d.Isi(Keywords.Revert) && !d.Isi(Keywords.RevertLayer) && !d.Isi("default");
81+
}
82+
}
83+
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
4+
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
5+
{
6+
/// <summary>
7+
/// A parsed <c>grid-template-areas</c> value (CSS Grid §7.3): a rectangular grid of named cells. Each
8+
/// named area's 0-based inclusive bounding rectangle is recorded; a <c>.</c> (or a run of only dots) is
9+
/// an unnamed empty cell.
10+
/// </summary>
11+
internal sealed class GridAreas
12+
{
13+
public int RowCount { get; set; }
14+
public int ColCount { get; set; }
15+
16+
/// <summary>The area name at each cell (<c>[row, col]</c>), or null for an empty <c>.</c> cell.</summary>
17+
public string[,] Cells { get; set; }
18+
19+
/// <summary>Each named area's 0-based inclusive rectangle (rowStart, colStart, rowEnd, colEnd).</summary>
20+
public IReadOnlyDictionary<string, (int R1, int C1, int R2, int C2)> Areas { get; set; }
21+
}
22+
23+
/// <summary>
24+
/// Shared grammar for a <c>grid-template-areas</c> value (a series of quoted strings). Used by Layer A
25+
/// (accept/reject) and Layer B (the engine derives implicit <c>name-start</c>/<c>name-end</c> lines).
26+
/// Returns null for anything invalid — including the <c>none</c> keyword, which the property accepts
27+
/// separately.
28+
/// </summary>
29+
internal static class GridTemplateAreasGrammar
30+
{
31+
internal static GridAreas TryParse(IReadOnlyList<Token> tokens)
32+
{
33+
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
34+
if (toks.Length == 0) return null;
35+
36+
// Every token must be a string; each string is one row.
37+
if (toks.Any(t => t.Type != TokenType.String)) return null;
38+
39+
var rows = new List<string[]>(toks.Length);
40+
int colCount = -1;
41+
foreach (var t in toks)
42+
{
43+
var cells = ((StringToken)t).Data
44+
.Split((char[])null, System.StringSplitOptions.RemoveEmptyEntries);
45+
if (cells.Length == 0) return null; // an empty string is invalid
46+
if (colCount < 0) colCount = cells.Length;
47+
else if (cells.Length != colCount) return null; // ragged rows are invalid
48+
49+
var normalized = new string[colCount];
50+
for (var c = 0; c < colCount; c++)
51+
{
52+
if (IsEmptyCell(cells[c])) { normalized[c] = null; continue; }
53+
// A named cell is a <custom-ident> — a null token (a mixed "a.b" is invalid, not a name).
54+
if (cells[c].Contains('.')) return null;
55+
normalized[c] = cells[c];
56+
}
57+
rows.Add(normalized);
58+
}
59+
60+
var rowCount = rows.Count;
61+
var grid = new string[rowCount, colCount];
62+
var bounds = new Dictionary<string, (int R1, int C1, int R2, int C2)>();
63+
64+
for (var r = 0; r < rowCount; r++)
65+
for (var c = 0; c < colCount; c++)
66+
{
67+
var name = rows[r][c];
68+
grid[r, c] = name;
69+
if (name is null) continue;
70+
if (bounds.TryGetValue(name, out var b))
71+
bounds[name] = (System.Math.Min(b.R1, r), System.Math.Min(b.C1, c),
72+
System.Math.Max(b.R2, r), System.Math.Max(b.C2, c));
73+
else
74+
bounds[name] = (r, c, r, c);
75+
}
76+
77+
// Each named area must be a single filled rectangle: every cell in its bounding box carries the
78+
// name (which also guarantees no cell with the name lies outside the box).
79+
foreach (var entry in bounds)
80+
{
81+
var name = entry.Key;
82+
var b = entry.Value;
83+
for (var r = b.R1; r <= b.R2; r++)
84+
for (var c = b.C1; c <= b.C2; c++)
85+
if (grid[r, c] != name) return null;
86+
}
87+
88+
return new GridAreas { RowCount = rowCount, ColCount = colCount, Cells = grid, Areas = bounds };
89+
}
90+
91+
/// <summary>A cell of only U+002E FULL STOP characters (<c>.</c>, <c>...</c>) is an empty cell.</summary>
92+
private static bool IsEmptyCell(string cell) => cell.All(ch => ch == '.');
93+
}
94+
}

0 commit comments

Comments
 (0)