diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 9335d30d9..b3d67b4b7 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -82,6 +82,18 @@ jobs:
env:
PDF_OUTPUT_DIRECTORY: ${{ github.workspace }}/pdf-artifacts/${{ matrix.platform.name }}-${{ matrix.dotnet.name }}
+ - name: Spec compliance report
+ if: ${{ !cancelled() && runner.os == 'Windows' && matrix.dotnet.name == '.NET 8' }}
+ run: dotnet run --project Source/Tools/HtmlRenderer.SpecCompliance --configuration Release --no-build -- report --output spec-compliance.md --summary $env:GITHUB_STEP_SUMMARY
+
+ - name: Upload spec compliance report
+ if: ${{ !cancelled() && runner.os == 'Windows' && matrix.dotnet.name == '.NET 8' }}
+ uses: actions/upload-artifact@v7
+ with:
+ name: Spec compliance report
+ path: spec-compliance.md
+ if-no-files-found: ignore
+
- name: Create HtmlRenderer.Core NuGet package
run: dotnet pack Source/HtmlRenderer/HtmlRenderer.csproj --configuration Release --include-symbols -p:SymbolPackageFormat=snupkg --no-build --verbosity normal --output ${{ env.NuGetDirectory }}
diff --git a/Source/HtmlRenderer.CrossPlatform.slnf b/Source/HtmlRenderer.CrossPlatform.slnf
index 7b78f8ba8..f756150bb 100644
--- a/Source/HtmlRenderer.CrossPlatform.slnf
+++ b/Source/HtmlRenderer.CrossPlatform.slnf
@@ -11,7 +11,8 @@
"HtmlRenderer.WPF\\HtmlRenderer.WPF.csproj",
"Test\\HtmlRenderer.IntegrationTest\\HtmlRenderer.IntegrationTest.csproj",
"Test\\HtmlRenderer.PdfSharp.Test\\HtmlRenderer.PdfSharp.Test.csproj",
- "Test\\HtmlRenderer.Test\\HtmlRenderer.Test.csproj"
+ "Test\\HtmlRenderer.Test\\HtmlRenderer.Test.csproj",
+ "Tools\\HtmlRenderer.SpecCompliance\\HtmlRenderer.SpecCompliance.csproj"
]
}
}
diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs
index f8af02288..f01110b3d 100644
--- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs
+++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontFamilyModel.cs
@@ -5,18 +5,11 @@
namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution
{
///
- /// One registered face of a : its CSS Fonts Level 4 matching axes
- /// (numeric weight, italic, stretch), the codepoint s it is restricted to
- /// (from an @font-faceunicode-range descriptor) or null when it has none - a face with
- /// no explicit ranges is treated as covering everything (matching the same, deliberately simpler,
- /// scope as the shared Core.Handlers.FontsHandler face registry - see its own doc comments for
- /// why cmap-based coverage extraction isn't ported here: this codebase resolves one font per box, not
- /// per glyph, so there is no per-glyph precision to exploit) - and the sniffed description
- /// hands back once this face is
- /// chosen; doubles as this face's storage
- /// key into .
+ /// One registered face of a : its CSS Fonts 4 matching axes, its
+ /// unicode-range or null when it has none (then it covers everything), and its description.
+ /// is the face's key into
+ /// .
///
- /// Ported from PeachPDF's Fonts\FontFamilyModel.cs.
internal sealed class FontFaceEntry
{
public FontFaceEntry(int weight, bool italic, int stretch, IReadOnlyList explicitRanges, TtfFontDescription description)
@@ -35,7 +28,7 @@ public FontFaceEntry(int weight, bool italic, int stretch, IReadOnlyListOne CSS font-family: its registered faces. Ported from PeachPDF's Fonts\FontFamilyModel.cs.
+ /// One CSS font-family: its registered faces.
internal sealed class FontFamilyModel
{
public string Name { get; set; }
diff --git a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs
index 45187f3a5..5460c1a7d 100644
--- a/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs
+++ b/Source/HtmlRenderer.PdfSharp/FontResolution/FontResolver.cs
@@ -14,32 +14,14 @@
namespace TheArtOfDev.HtmlRenderer.PdfSharp.FontResolution
{
///
- /// PDFsharp implementation backing the PdfSharp backend's font handling -
- /// OS font discovery plus CSS Fonts Level 4 §5 nearest-match (slant→stretch→weight) face selection,
- /// ported from PeachPDF's Fonts\FontResolver.cs. Unlike WinForms/WPF (which register an
- /// @font-face face as an opaque platform font-family handle and let the OS/UI framework do
- /// matching), PDFsharp's contract needs raw font bytes at PDF-generation
- /// time for embedding, so this type does its own matching entirely and is called directly by
- /// Adapters.PdfSharpAdapter's AddFontFace/GetFont overrides, bypassing the shared
- /// Core.Handlers.FontsHandler registry other backends use.
+ /// PDFsharp of the PdfSharp backend: system font discovery and
+ /// CSS Fonts 4 §5 face matching (slant, stretch, weight). PDFsharp needs the font bytes for embedding,
+ /// so this does its own matching instead of going through Core.Handlers.FontsHandler.
///
///
- /// Two deliberate scope departures from PeachPDF's own resolver, both already established for this
- /// port's shared Core.Handlers.FontsHandler (see its own doc comments):
- ///
- /// No cmap-coverage extraction fallback for unicode-range-less faces - a face with no
- /// explicit unicode-range is simply treated as covering everything. This codebase resolves one
- /// font per box (not per glyph/run), so there's no per-glyph precision for cmap coverage to buy.
- /// No per-FontResolver-instance glyph-typeface/descriptor caching. PeachPDF supports many
- /// concurrent PdfGenerators each with their own FontResolver, so it isolates their custom
- /// font caches from each other. This backend's PdfSharpAdapter (and this resolver with it) is a
- /// single process-wide singleton via , matching this project's pre-existing
- /// architecture - there's only ever one instance, so nothing to isolate.
- ///
- /// System font discovery itself is also adapted, not verbatim: no Android/iOS branches (this project's
- /// netstandard2.0;net8.0 target framework list has no mobile leg to run them on), and Linux
- /// discovery shells out to the fc-list CLI rather than P/Invoking libfontconfig.so.1
- /// directly (see 's own doc comment for why).
+ /// A face without unicode-range is assumed to cover everything, its glyphs aren't checked,
+ /// there's one font per box anyway. The resolver is a process-wide singleton ().
+ /// On Linux, fonts are found with the fc-list CLI, see .
///
public sealed class FontResolver : IFontResolver
{
diff --git a/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs b/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs
index b47ff9184..8042e993e 100644
--- a/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs
+++ b/Source/HtmlRenderer.PdfSharp/PdfGenerator.cs
@@ -197,12 +197,7 @@ public static async Task AddPdfPages(PdfDocument document, string html, PdfGener
container.PerformLayout(measure);
}
- // One PDF page per fragmentainer the fragment tree actually materialized - a
- // content-empty page slot (CSS Paged Media 3 3.2, e.g. a huge margin that would
- // otherwise paginate through blank vertical space - see the margin-truncation
- // correction in BlockFragmentation) is simply never in this list, which is what
- // gives blank-page skipping for free here instead of the old ceil(height/pageHeight)
- // loop's naive page count.
+ // One PDF page per fragmentainer, pages without content have none.
var tree = container.FragmentTree;
foreach (var fragmentainer in tree?.Fragmentainers ?? (IReadOnlyList)Array.Empty())
{
@@ -237,10 +232,7 @@ private static void HandleLinks(PdfDocument document, HtmlContainer container, X
if (tree == null || tree.Fragmentainers.Count == 0)
return;
- // Pagination slot -> PDF page index. Not a bare multiply/divide by page height any more:
- // a content-empty slot is never materialized as a fragmentainer at all (blank-page
- // skipping), so slot indices are not contiguous across tree.Fragmentainers the way a
- // fixed-size page grid's would be.
+ // Pagination slot -> PDF page index, a content-empty slot gets no page (css-page-3 §3.2).
var slotToPage = new Dictionary();
for (var pageIndex = 0; pageIndex < tree.Fragmentainers.Count; pageIndex++)
{
diff --git a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
index 384f93983..a38363338 100644
--- a/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
+++ b/Source/HtmlRenderer.WinForms/Adapters/WinFormsAdapter.cs
@@ -50,13 +50,10 @@ internal sealed class WinFormsAdapter : RAdapter
///
private static readonly WinFormsAdapter _instance = new WinFormsAdapter();
- // Adapter-level PrivateFontCollection for @font-face-loaded faces - one collection shared for the
- // adapter's (process) lifetime, growing by one family per LoadFontFaceFontInt call.
+ // The @font-face faces, kept for the lifetime of the process.
private readonly PrivateFontCollection _fontFaceCollection = new PrivateFontCollection();
- // Backs LoadFontFaceFontInt's temp-file registration (see its own doc comment for why a temp file
- // is used instead of PrivateFontCollection.AddMemoryFont) - one directory per process, cleaned up
- // by the OS's normal temp-file housekeeping, not by this process.
+ // The font files of the @font-face faces, see LoadFontFaceFontInt. Left to the OS to clean up.
private static readonly string _fontFaceTempDirectory = CreateFontFaceTempDirectory();
private static string CreateFontFaceTempDirectory()
@@ -211,37 +208,13 @@ protected override RFont CreateFontInt(RFontFamily family, double size, RFontSty
}
///
- /// Loads one @font-face face's bytes into the adapter's
- /// via - through a temp file, matching this
- /// adapter's own pre-existing DemoForm.LoadCustomFonts pattern (that path is untouched,
- /// this is a new, separate mechanism specific to @font-face).
+ /// Adds the font to the and returns its family, found by the
+ /// family name in the font file. groups the faces of
+ /// a family and isn't in insertion order.
///
///
- /// Deliberately NOT , despite it needing no temp
- /// file: empirically (a throwaway repro project registering a dozen distinct families into one
- /// ), AddMemoryFont is unreliable on this target
- /// framework - permanently fails to reflect several of
- /// the added families (not a timing race: polling for up to 300ms after the call never finds them
- /// either), while the identical sequence of files through AddFontFile succeeds 100% of the
- /// time across repeated runs. This is a known-flaky area of GDI+'s AddMemoryFont P/Invoke
- /// path, not a bug in this port. The temp file is deliberately never deleted: GDI+ keeps it
- /// memory-mapped for as long as this process-lifetime singleton's
- /// references it, and the OS's own temp-directory housekeeping reclaims it afterward - the same
- /// "small, bounded, process-lifetime" rationale the removed AddMemoryFont/AllocHGlobal
- /// approach relied on.
- ///
- /// The returned is found by sniffing the font's own internal
- /// family name via and matching it against
- /// - not by comparing Families.Length before
- /// and after, nor by taking the array's last entry. Two bugs made that approach unreliable: (1)
- /// Families groups every face by family name (the whole point of
- /// - it lets pick the right face via
- /// automatically), so registering a second face of an *already-registered*
- /// family (e.g. this face set's own Bold after its Regular) never changes the count at all; and
- /// (2) even when the count does change, Families is returned in a GDI-defined (effectively
- /// alphabetical) order, not insertion order, so "the last entry" is often a completely unrelated,
- /// alphabetically-later family, not the one just added.
- ///
+ /// Goes through a temp file, doesn't reliably
+ /// list the added families. The file can't be deleted while the collection uses it.
///
protected override RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath)
{
diff --git a/Source/HtmlRenderer.sln b/Source/HtmlRenderer.sln
index 521afb251..0d7e1e03b 100644
--- a/Source/HtmlRenderer.sln
+++ b/Source/HtmlRenderer.sln
@@ -31,6 +31,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.IntegrationTes
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.Test", "Test\HtmlRenderer.Test\HtmlRenderer.Test.csproj", "{7427E12F-1524-4405-AAD1-5AF8ACB3BBBC}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{07C2787E-EAC7-C090-1BA3-A61EC2A24D84}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HtmlRenderer.SpecCompliance", "Tools\HtmlRenderer.SpecCompliance\HtmlRenderer.SpecCompliance.csproj", "{8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -167,6 +171,18 @@ Global
{7427E12F-1524-4405-AAD1-5AF8ACB3BBBC}.Release|Mixed Platforms.Build.0 = Release|Any CPU
{7427E12F-1524-4405-AAD1-5AF8ACB3BBBC}.Release|x86.ActiveCfg = Release|Any CPU
{7427E12F-1524-4405-AAD1-5AF8ACB3BBBC}.Release|x86.Build.0 = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Debug|x86.Build.0 = Debug|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|Mixed Platforms.Build.0 = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|x86.ActiveCfg = Release|Any CPU
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -179,6 +195,7 @@ Global
{AF39851E-37B9-409E-A34D-CE091A098C86} = {E508FA78-D57E-492F-9BF7-23640001848A}
{A12F4432-337B-4030-B834-5D312A9FB16C} = {E508FA78-D57E-492F-9BF7-23640001848A}
{7427E12F-1524-4405-AAD1-5AF8ACB3BBBC} = {E508FA78-D57E-492F-9BF7-23640001848A}
+ {8E0BFCAC-F676-4EE1-B639-7D8F2F24656D} = {07C2787E-EAC7-C090-1BA3-A61EC2A24D84}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {902788D6-3165-491D-B860-DF2F74E66C1D}
diff --git a/Source/HtmlRenderer/Adapters/RAdapter.cs b/Source/HtmlRenderer/Adapters/RAdapter.cs
index 6632f6942..dad5b9cf1 100644
--- a/Source/HtmlRenderer/Adapters/RAdapter.cs
+++ b/Source/HtmlRenderer/Adapters/RAdapter.cs
@@ -94,7 +94,7 @@ protected RAdapter()
}
///
- /// The CSS media type this adapter renders for, used to evaluate @media queries -
+ /// The CSS media type this adapter renders for (CSS 2.1 §7.3), used to evaluate @media rules.
/// "screen" for on-screen adapters (the default), "print" for paged output.
///
public virtual string DefaultMediaType
@@ -192,11 +192,8 @@ protected void OnColorSchemeChanged()
/// the resolved resource, or null if it could not be resolved
public Task GetResourceStream(RUri uri)
{
- // BaseUri is normally never null, so every reference resolves to an absolute URI and loaders
- // only ever see those - RUri.Scheme throws on a relative URI, and neither DataUriNetworkLoader
- // nor HttpClientNetworkLoader checks. Denying local file access is what makes BaseUri nullable,
- // so a relative reference can now survive resolution; answer "unresolved" for it here rather
- // than handing a loader a URI it cannot inspect.
+ // Without local file access BaseUri can be null and a relative reference stays relative.
+ // RUri.Scheme throws on it and the loaders don't check, so it's answered as unresolved here.
if (!uri.IsAbsoluteUri)
{
return Task.FromResult(null);
@@ -322,18 +319,14 @@ public RImage ImageFromStream(Stream memoryStream)
}
///
- /// Check if the given font exists in the system by font family name. Consulted by
- /// to decide whether a font-family
- /// candidate should be kept as-is or the next fallback (ultimately )
- /// tried instead - so this must recognize a family the moment it's usable, including one
- /// registered via , not just /system fonts.
+ /// Check if the given font exists in the system by font family name, including the families
+ /// registered by . uses
+ /// it to pick the first available family of a font-family list.
///
/// the font name to check
/// true - font exists by given family name, false - otherwise
///
- /// Virtual so the PdfSharp backend can override it - its @font-face registrations live
- /// entirely in its own FontResolver (see 's doc comment for why),
- /// invisible to the shared FontsHandler this base implementation checks.
+ /// Virtual for the PdfSharp backend, which keeps its @font-face faces in its own font resolver.
///
public virtual bool IsFontExists(string font)
{
@@ -362,26 +355,19 @@ public void AddFontFamilyMapping(string fromFamily, string toFamily)
}
///
- /// Loads one @font-facesrc: url(...) candidate and registers it as a face of
- /// for CSS Fonts Level 4 matching. Resolves the resource through
- /// - the same funnel used for images/stylesheets, so this supports
- /// file:/data:/http(s): uniformly - loads the platform-specific face via
- /// , and registers it with .
+ /// Loads one url() item of an @font-facesrc (CSS Fonts 4 §4.3.2) through
+ /// and registers it as a face of .
///
- /// the CSS family name declared by the @font-face rule
- /// the resolved, absolute src URI to fetch (already resolved against the document base/a stylesheet's own location by the caller)
- /// CSS Fonts Level 4 numeric weight (1-1000) this face matches for
- /// whether this face matches an italic/oblique request
- /// CSS Fonts Level 3 stretch (1-9) this face matches for
- /// the face's unicode-range restriction, or null for "covers whatever is asked of it"
- /// true if the face was loaded and registered, false if the resource could not be resolved/loaded (the caller tries the next src candidate)
+ /// the family name of the @font-face rule
+ /// the absolute URI of the item
+ /// numeric weight (1-1000)
+ /// whether the face is italic or oblique
+ /// width on the 1-9 scale of
+ /// the unicode-range, or null for any character
+ /// false if the resource couldn't be loaded, the caller then tries the next item
///
- /// Virtual so the PdfSharp backend can override it to bypass 's shared
- /// registry entirely and register directly with its own FontResolver instead - PDFsharp
- /// needs raw font bytes at PDF-generation time for embedding (via its own richer
- /// IFontResolver-based CSS-Fonts-L4 matching), unlike WinForms/WPF, which just need an
- /// opaque platform font-family handle. See the base implementation's own doc comment for the
- /// shared-registry path this overrides.
+ /// Virtual for the PdfSharp backend, which needs the font bytes for embedding and keeps the faces
+ /// in its own font resolver.
///
public virtual async Task AddFontFace(string familyName, RUri uri, int weight, bool isItalic, int stretch, IReadOnlyList ranges)
{
@@ -426,14 +412,12 @@ public virtual async Task AddFontFace(string familyName, RUri uri, int wei
}
///
- /// Satisfies an @font-facesrc: local(...) candidate: rather than fetching a resource
- /// at all, this looks for a family already registered under (a
- /// system font, or an earlier / registration)
- /// and, if found, registers *that same* as a face of
- /// too.
+ /// Registers the family named by a local() item of an @font-facesrc
+ /// (CSS Fonts 4 §4.3.3.1) as a face of . The name is matched against
+ /// the registered families, not against full font names.
///
- /// true if a local family by that name was found and registered, false otherwise (the caller tries the next src candidate)
- /// Virtual for the same reason as - see its doc comment.
+ /// false if there's no such family, the caller then tries the next item
+ /// Virtual for the PdfSharp backend, see .
public virtual bool AddFontFaceFromLocalFamily(string familyName, string localFamilyName, int weight, bool isItalic, int stretch, IReadOnlyList ranges)
{
var localFamily = _fontsHandler.TryGetExistingFamily(localFamilyName);
@@ -667,16 +651,12 @@ internal RFont CreateFont(RFontFamily family, double size, RFontStyle style)
protected abstract RFont CreateFontInt(RFontFamily family, double size, RFontStyle style);
///
- /// Registers (a loaded @font-facesrc candidate's raw
- /// font file bytes) with the platform text engine and returns an opaque
- /// handle for it - one file's bytes in, one face's family handle out (a WinForms/WPF
- /// implementation registers with the OS/GDI text engine and returns a handle to that one face;
- /// this is not called for backends - like PdfSharp - that override to
- /// bypass this entirely).
+ /// Registers the bytes of an @font-face font file with the platform text engine and returns
+ /// the family of the loaded face. Not called when is overridden.
///
- /// the font file's raw bytes (TTF/OTF)
- /// the resolved source this was loaded from, for diagnostics/error messages only
- /// the registered face's family handle, or null if the bytes could not be loaded as a font (the caller tries the next src candidate)
+ /// the font file (TTF/OTF)
+ /// where the bytes came from, for diagnostics
+ /// the family of the face, or null if the bytes aren't a font
protected abstract RFontFamily LoadFontFaceFontInt(byte[] fontBytes, string filePath);
///
diff --git a/Source/HtmlRenderer/Core/CssData.cs b/Source/HtmlRenderer/Core/CssData.cs
index 5957e3d9f..7996cb334 100644
--- a/Source/HtmlRenderer/Core/CssData.cs
+++ b/Source/HtmlRenderer/Core/CssData.cs
@@ -22,16 +22,9 @@
namespace TheArtOfDev.HtmlRenderer.Core
{
///
- /// Holds parsed stylesheets (UA default + author + inline), and provides origin-aware,
- /// specificity-ordered selector matching against the box tree.
+ /// Holds the parsed style sheets (UA default and author) and matches their rules against the
+ /// box tree, ordered for the cascade.
///
- ///
- /// Derived from the same ExCSS lineage as the vendored CSS-OM (originally Tyler Brinks' ExCSS),
- /// adapted to HTML-Renderer's CssBox/HtmlTag shape. One deliberate omission from the source: the
- /// ::marker pseudo-element box-synthesis path is removed - ::before/::after
- /// synthesis is kept, but this codebase has no CssBox.IsMarkerPseudoElement concept and no
- /// consumer for it (list markers are painted procedurally, not via a synthesized box).
- ///
public sealed class CssData
{
///
@@ -51,7 +44,7 @@ internal CssData()
///
/// Parse the given stylesheet to object.
/// If is true the parsed css blocks are added to the
- /// default css data (as defined by W3), merged if class name already exists. If false only the data in the given stylesheet is returned.
+ /// default (user agent) css data. If false only the data in the given stylesheet is returned.
///
///
/// Platform adapter
@@ -181,15 +174,10 @@ internal bool MediaOutcomeChanged(MediaQueryContext before, MediaQueryContext af
}
///
- /// Every rule in a rule list, in document order, descending into @layer, @media,
- /// @container, and (when its condition evaluates true) @supports blocks - matching
- /// PeachPDF's own CssData.FlattenRules exactly (full descent, not the narrower
- /// style-rule-only/no-@supports-or-@container predecessor this replaces). Callers that only want
- /// style rules (the common case - e.g. scanning for a rule by selector rather than matching
- /// against a box, since only sees the top level) filter with
- /// .OfType<IStyleRule>(), matching PeachPDF's own call-site pattern of filtering the
- /// general enumerator per use rather than a dedicated pre-filtered helper. CSS-Nesting child rules
- /// are reached because is itself yielded before recursing.
+ /// Every rule in a rule list, in document order, descending into nested style rules,
+ /// @layer, @media and @container without evaluating their conditions, and
+ /// into @supports when its condition is true. Used for @font-face, ::selection
+ /// and the HTML export, the cascade walks the rules itself in .
///
internal static IEnumerable FlattenRules(IEnumerable rules)
{
@@ -301,11 +289,8 @@ private static void IndexRules(
}
}
- // CSS Nesting: a nested rule lives in its parent's NestedRules, deliberately not in
- // Children, so it is invisible to Stylesheet.Rules and would otherwise never be
- // indexed. Its selector was already resolved against the parent at parse time, so it
- // indexes like any other rule - under the same media context, and with a document
- // order that continues from the parent so it tie-breaks after it.
+ // CSS Nesting 1 §3.4: nested rules aren't in Stylesheet.Rules. Indexed right after the
+ // parent in the same media context, so they come after it in order of appearance.
if (styleRule.NestedRules.Count > 0)
{
IndexRules(styleRule.NestedRules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order);
@@ -320,11 +305,7 @@ private static void IndexRules(
}
else if (rule is ILayerRule layerRule)
{
- // @layer's contents are indexed as ordinary, unlayered rules: they apply, ordered by
- // source position and specificity. Layer precedence, !important layer reversal and
- // revert-layer are deliberately not implemented - that needs a layer-banded cascade,
- // which is separate work. Without this branch the block would fall through and every
- // rule inside a layer would silently stop applying.
+ // CSS Cascade 5 §6.4: layer order isn't tracked, the rules cascade as if unlayered
IndexRules(layerRule.Rules, isUserAgent, enclosingMedia, tagIndex, classIndex, idIndex, universal, keys, ref order);
}
else if (rule is ISupportsRule supportsRule)
@@ -491,9 +472,7 @@ private IEnumerable GetStyleRulesByOrigin(MediaQueryContext media, C
}
}
- // Stable sort by specificity, tie-broken by DocumentOrder (true source order, including
- // across the plain-rule/@media boundary, assigned once up front by IndexRules) - equal-
- // specificity rules keep true document order for correct source-order tiebreaking.
+ // CSS 2.1 §6.4.1 steps 3 and 4
return matched
.OrderBy(indexed => GetMatchedSpecificity(indexed.Rule.Selector, box))
.ThenBy(indexed => indexed.DocumentOrder)
@@ -501,14 +480,9 @@ private IEnumerable GetStyleRulesByOrigin(MediaQueryContext media, C
}
///
- /// The effective specificity of a matched rule's selector for cascade-ordering purposes,
- /// relative to a specific matched box. This deliberately differs from calling
- /// selector.Specificity directly for a top-level (comma-
- /// separated rule selector, e.g. "h1, h2 {}"): per spec a selector list used as a whole
- /// rule's selector is cascade-equivalent to separate rules, so the specificity that governs
- /// THIS box's cascade is whichever one alternative actually matched it - not every
- /// alternative's static max (which is what reports,
- /// correctly, for its OTHER use as an argument to :is()/:not()/:has()).
+ /// The specificity of a matched rule for , CSS 2.1 §6.4.3 and Selectors 4
+ /// §15. A rule's selector list counts with the highest matching selector, unlike
+ /// , which is the static maximum used inside :is() and :not().
///
private static Priority GetMatchedSpecificity(ISelector selector, CssBox box)
{
@@ -566,10 +540,6 @@ private static bool DoesSelectorMatch(CompoundSelector compoundSelector, CssBox
var lastSelector = compoundSelector.Last();
- // Structural pseudo-classes (ChildSelector subtypes, OnlyChildSelector, OnlyOfTypeSelector)
- // are matched against `box` itself here, same as every other compound member - their own
- // DoesSelectorMatch overload re-derives sibling scope from `box` as needed, so no special
- // handling is required for them beyond the plain path below.
var pseudoElementSelector = lastSelector as PseudoElementSelector;
if (pseudoElementSelector == null)
return compoundSelector.All(selector => DoesSelectorMatch(selector, box));
@@ -587,9 +557,10 @@ private static bool DoesSelectorMatch(CompoundSelector compoundSelector, CssBox
return DoesSelectorMatch(pseudoElementSelector, box);
}
- // Only ::before/::after synthesize a box here - ::marker is intentionally not ported
- // (no CssBox.IsMarkerPseudoElement concept in this codebase; list markers are painted
- // procedurally, not via a synthesized box - see PaintListStyleShapeMarker and friends).
+ // Matching the element creates the ::before/::after box, which then matches on its own
+ // cascade pass. Other pseudo-elements get no box here. This runs before a ComplexSelector
+ // checks the rest of the chain, so the box can be created on an element the rule doesn't
+ // match, and stays empty.
if (pseudoElementSelector.Name == CssConstants.Before && !box.Boxes.Any(b => b.IsBeforePseudoElement))
{
var beforePseudoBox = new CssBox(box, null);
@@ -746,17 +717,14 @@ private static bool DoesSelectorMatch(PseudoClassSelector pseudoClassSelector, C
{
if (box == null) return false;
- // ":hover" is matched structurally (ignoring live mouse-hover state, which this engine
- // doesn't track at cascade time) so DomParser.AssignCssBlock can intercept the match and
- // divert it to HtmlContainerInt.AddHoverBox instead of applying it - preserving the
- // "matched but inert" behavior this engine has always had for :hover (see DomParser).
+ // CSS 2.1 §5.11.3. Matches whatever the mouse does, DomParser.AssignCssBlocks then hands
+ // rules with :hover in the subject to HtmlContainerInt.AddHoverBox instead of applying them.
if (pseudoClassSelector.Class == "hover") return true;
- // ":root" always matches the document's element - this engine has no separate
- // CssBox.IsRoot flag, so match on tag name directly (correct for any HTML document).
if (pseudoClassSelector.Class == "root")
return box.HtmlTag != null && box.HtmlTag.Name.Equals("html", StringComparison.InvariantCultureIgnoreCase);
+ // CSS 2.1 §5.11.2. There's no history, every link is unvisited and :visited never matches.
return pseudoClassSelector.Class == "link" && box.IsClickable;
}
@@ -773,12 +741,8 @@ private static bool DoesSelectorMatch(PseudoElementSelector pseudoElementSelecto
}
///
- /// Matches the six structural "An+B" pseudo-classes (:nth-child, :nth-last-child,
- /// :nth-of-type, :nth-last-of-type, :nth-column, :nth-last-column - including their bare-ident
- /// forms like :first-child, wired to Step=0/Offset=1 by PseudoClassSelectorFactory).
- /// Each variant differs only in (a) which sibling scope to count within and (b) whether to
- /// count from the start or the end; the actual "does this position satisfy An+B" test is
- /// shared via .
+ /// 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 DoesSelectorMatch(ChildSelector childSelector, CssBox box)
{
@@ -807,11 +771,7 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
b.HtmlTag.Name.Equals(box.HtmlTag.Name, StringComparison.InvariantCultureIgnoreCase));
}
- // CSS4 "of " extension (:nth-child(An+B of S)/:nth-last-child(An+B of S)).
- // Kind defaults to AllSelector (matches unconditionally) whenever no "of" clause was
- // written, and the parser only ever allows a non-default Kind on FirstChildSelector/
- // LastChildSelector - so this filter is always a no-op for the four other subtypes.
- // Applying it unconditionally is simpler than special-casing which subtypes can have it.
+ // The "of S" filter of :nth-child()/:nth-last-child(). Without it Kind is AllSelector.
siblingBoxes = siblingBoxes.Where(b => DoesSelectorMatch(childSelector.Kind, b));
var siblings = siblingBoxes.ToList();
@@ -828,10 +788,9 @@ private static bool DoesSelectorMatch(ChildSelector childSelector, CssBox box)
}
///
- /// Matches :nth-column()/:nth-last-column() against the cell's occupied column position(s)
- /// within its own row. Only accounts for colspan of preceding cells in the SAME row.
- /// A colspan-N cell occupies N columns; per spec this matches if ANY occupied column position
- /// satisfies the An+B formula, not just the cell's starting column.
+ /// :nth-column()/:nth-last-column() (Selectors 5 :nth-col()). A cell matches if any column it
+ /// spans does. Only the colspans of the cells in the same row count, rowspans from rows above
+ /// are ignored.
///
private static bool DoesColumnSelectorMatch(ChildSelector childSelector, CssBox box, bool fromEnd)
{
@@ -876,10 +835,7 @@ private static bool DoesColumnSelectorMatch(ChildSelector childSelector, CssBox
}
///
- /// Reads a box's "colspan" HTML attribute (default 1 if absent/invalid). Deliberately
- /// duplicated from 's private equivalent rather than
- /// shared - this runs at cascade time, before the layout-phase state that method's column
- /// bookkeeping otherwise depends on exists.
+ /// Reads a cell's colspan, 1 when it's missing or invalid (HTML §4.9.12.1).
///
private static int GetColSpan(CssBox box)
{
@@ -889,9 +845,8 @@ private static int GetColSpan(CssBox box)
}
///
- /// The standard CSS "An+B" existence test: does there exist a non-negative integer n such that
- /// position == step*n + offset? position is always the 1-based index within whatever sibling
- /// scope the caller has already resolved.
+ /// Whether the 1-based is step*n + offset for some n >= 0,
+ /// Selectors 4 §13.3.1.
///
private static bool MatchesAnPlusB(int position, int step, int offset)
{
@@ -949,9 +904,8 @@ private static bool DoesSelectorMatch(HasSelector hasSelector, CssBox box)
}
///
- /// Recursively searches box's descendants (at any depth) for one matching inner, short-
- /// circuiting on the first hit. Backs :has()'s default (descendant) relative-selector form;
- /// leading-combinator forms (":has(> x)"/"+"/"~") aren't supported.
+ /// The descendant form of :has(), Selectors 4 §4.5. Walks all boxes, including anonymous and
+ /// text boxes, which "*" matches.
///
private static bool HasDescendantMatch(CssBox box, ISelector inner)
{
diff --git a/Source/HtmlRenderer/Core/CssDefaults.cs b/Source/HtmlRenderer/Core/CssDefaults.cs
index 00dc80708..109022872 100644
--- a/Source/HtmlRenderer/Core/CssDefaults.cs
+++ b/Source/HtmlRenderer/Core/CssDefaults.cs
@@ -15,11 +15,8 @@ namespace TheArtOfDev.HtmlRenderer.Core
internal static class CssDefaults
{
///
- /// CSS Specification's Default Style Sheet for HTML 4
+ /// The user agent style sheet, based on CSS 2.1 Appendix D rather than the HTML §15 one.
///
- ///
- /// http://www.w3.org/TR/CSS21/sample.html
- ///
public const string DefaultStyleSheet = @"
html, address,
blockquote,
@@ -91,30 +88,25 @@ internal static class CssDefaults
:link, :visited { text-decoration: underline }
:focus { outline: thin dotted invert }
- /* Begin bidirectionality settings (do not change) */
+ /* HTML §15.3.5 */
BDO[DIR=""ltr""] { direction: ltr; unicode-bidi: bidi-override }
BDO[DIR=""rtl""] { direction: rtl; unicode-bidi: bidi-override }
*[DIR=""ltr""] { direction: ltr; unicode-bidi: embed }
*[DIR=""rtl""] { direction: rtl; unicode-bidi: embed }
- /* Ported from PeachPDF's CssDefaults (spelt with css-break-3's break-* properties rather
- than the legacy page-break-* aliases - the two share their storage and initial value,
- see InitialValues below, so this is the same cascade either way). Replaces this engine's
- own older `h1 { page-break-before: always }` default, which forced a leading blank page
- before any document that opened with a heading now that break-before is actually
- consumed by layout - break-after: avoid (keep-with-next) is the behavior real print
- engines give headings by default. */
+ /* Unlike CSS 2.1 Appendix D, h1 doesn't break before itself, a document starting
+ with one would get an empty first page. */
@media print {
h1, h2, h3,
h4, h5, h6 { break-after: avoid }
- /* css-tables-3 6.2 repeats a header or footer group across the pages a table spans only
- where the group carries an avoid break-inside. */
+ /* CSS Tables 3 §6.2 */
thead, tfoot { break-inside: avoid }
}
- /* Not in the specification but necessary */
+ /* Not in CSS 2.1 Appendix D: the hidden elements of HTML §15.3.1 and this renderer's own look
+ for links, tables, hr and pre */
a { color: #0055BB; text-decoration:underline }
table { border-color:#dfdfdf; }
td, th { border-color:#dfdfdf; overflow: hidden; }
@@ -125,7 +117,7 @@ where the group carries an avoid break-inside. */
hr { border-top-color: #9A9A9A; border-left-color: #9A9A9A; border-bottom-color: #EEEEEE; border-right-color: #EEEEEE; }
pre { font-size: 10pt; margin-top: 15px; }
- /*This is the background of the HtmlToolTip*/
+ /* The look of HtmlToolTip */
.htmltooltip {
border:solid 1px #767676;
background-color:#E4E5F0;
@@ -134,14 +126,9 @@ where the group carries an avoid break-inside. */
}";
///
- /// The initial value of every property this engine recognizes ('s exact
- /// whitelist), used to resolve the initial/unset/revert/revert-layer
- /// CSS-wide keywords in the six-phase cascade (). Deliberately matches
- /// this engine's own existing field defaults ('s private field
- /// initializers) rather than the CSS spec's initial values where the two differ (e.g. this engine's
- /// border-color default is "black", not the spec's "currentcolor"; text-align/text-decoration/
- /// list-style-image default to an empty string here, not "left"/"none"/"none") - so resolving
- /// "initial" produces exactly what an unstyled box already looks like today, not a new value.
+ /// The initial value of every property in 's whitelist, for the
+ /// CSS-wide keywords. Must match the field initializers in ,
+ /// which is why some differ from the spec (e.g. border colors are "black", text-align is "").
///
internal static readonly System.Collections.Generic.Dictionary InitialValues =
new System.Collections.Generic.Dictionary(System.StringComparer.OrdinalIgnoreCase)
@@ -223,10 +210,8 @@ where the group carries an avoid break-inside. */
};
///
- /// Properties inherited from parent to child during cascade - kept exactly in sync with the set
- /// already (independently) copies, since this is
- /// used only to decide whether the unset keyword should behave like inherit or
- /// initial for a given property, and the two must agree for "unset" to make sense.
+ /// The inherited properties, used to resolve unset. Must match what
+ /// copies.
///
internal static readonly System.Collections.Generic.HashSet InheritedProperties =
new System.Collections.Generic.HashSet(System.StringComparer.OrdinalIgnoreCase)
diff --git a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs
index 2570eafff..54e2e7907 100644
--- a/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/AspectRatioGrammar.cs
@@ -4,29 +4,19 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The shared grammar for the aspect-ratio value ([ auto || <ratio> ], where
- /// <ratio> = <number [0,∞]> [ / <number [0,∞]> ]?). Used by both Layer A (to
- /// validate/accept-or-reject at parse time) and Layer B (to compute the used ratio during layout), so the
- /// grammar is defined once — the precedent.
+ /// The aspect-ratio grammar (CSS Sizing 4 §4.1) and the <ratio> type (CSS Values 4 §5.7).
///
internal static class AspectRatioGrammar
{
///
- /// Validates the token stream as an aspect-ratio value. Returns false for anything that is not a
- /// valid value. On success, is the used width/height ratio, or null when there
- /// is no usable ratio (a bare auto, or a ratio with a zero term — both of which mean "no
- /// preferred aspect ratio" for a non-replaced box).
+ /// 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 _);
///
- /// Validates the token stream as an aspect-ratio value, additionally reporting whether the
- /// auto keyword was present ([ auto || <ratio> ]). lets a
- /// replaced element prefer its natural aspect ratio and fall back to the specified <ratio>
- /// (CSS Box Sizing 4 §5): auto <ratio> ⇒ hasAuto=true with a usable
- /// ; a bare <ratio> ⇒ hasAuto=false (the ratio always
- /// overrides); a bare auto ⇒ hasAuto=true with a null .
+ /// 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)
{
@@ -54,11 +44,7 @@ internal static bool TryParse(IReadOnlyList tokens, out double? ratio, ou
}
///
- /// Validates a bare <ratio> value (<number [0,∞]> [ / <number [0,∞]> ]?),
- /// with no auto — the CSS Values 4 <ratio> data type, as used by the @property
- /// syntax: "<ratio>" matcher. Distinct from , whose aspect-ratio
- /// grammar additionally permits auto. On success is the used width/height
- /// ratio, or null when a term is zero.
+ /// A bare <ratio> without auto, for the @property syntax matcher.
///
internal static bool TryParseRatio(IReadOnlyList tokens, out double? ratio)
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
index b9b05f2ac..b3e52ec8f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/BackgroundPositionGrammar.cs
@@ -3,12 +3,9 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Shared grammar for the CSS background-position value (the 1/2/3/4-token forms,
- /// including the edge-relative offset syntax, e.g. "right 20px bottom 10px"). Used by both
- /// (CSS-OM grammar validation/serialization) and
- /// the render-time background-layer resolution (pixel-offset arithmetic) so
- /// there is one parser for this grammar, not two independent re-implementations - mirroring how
- /// CalcParser is shared between CalcValueConverter and CssValueParser rather than re-parsed twice.
+ /// 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
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
index 99bf73e5d..9d56175fd 100644
--- a/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/BackgroundSizeGrammar.cs
@@ -3,11 +3,9 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Shared grammar for the CSS background-size value (auto, cover,
- /// contain, or a 1-2 token length/percentage/auto form). Used by both
- /// (CSS-OM grammar validation/serialization)
- /// and the render-time background-layer resolution (size resolution), so there
- /// is one parser for this grammar, not two.
+ /// 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
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs
index b234ccd70..451ee9959 100644
--- a/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/BasicShapeGrammar.cs
@@ -4,14 +4,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Shared, layer-agnostic grammar for the CSS <basic-shape> function values used by
- /// clip-path (CSS Masking Level 1 / CSS Shapes Level 1): polygon(), inset(),
- /// circle() and ellipse(). Like /
- /// , it validates the grammar and captures the value's structure
- /// as raw component strings / enums (never resolved numbers), so both Layer A (the CSS-OM
- /// converter, which only needs to accept/reject and preserve the authored text) and Layer B (the
- /// render-time resolver in TheArtOfDev.HtmlRenderer.Core, which resolves each component against the
- /// element's reference box) share a single parser rather than re-implementing the grammar twice.
+ /// The <basic-shape> functions polygon(), inset(), circle() and ellipse()
+ /// (CSS Shapes 1 §3), parsed into unresolved component strings and enums.
///
internal static class BasicShapeGrammar
{
@@ -69,9 +63,7 @@ internal sealed class ParsedBasicShape
/// each an authored length-percentage component string.
public IReadOnlyList InsetEdges { get; private set; }
- /// Whether an inset(... round <border-radius>) corner radius was present.
- /// The radius itself is captured in but not rendered - inset is
- /// drawn as a rectangle (see the resolver / docs).
+ /// Whether an inset(... round <border-radius>) corner radius was present.
public bool InsetHasRound { get; private set; }
public IReadOnlyList InsetRoundRadius { get; private set; }
@@ -120,10 +112,7 @@ internal sealed class ParsedBasicShape
}
///
- /// Parses a clip-path value's tokens into a , or returns
- /// null when the value is not a valid basic shape - including the literal none,
- /// which callers treat as "no clip". (Layer A therefore accepts none separately, since a
- /// null result here can't distinguish none from an invalid value.)
+ /// Null for anything that isn't a basic shape, none included.
///
internal static ParsedBasicShape TryParse(IReadOnlyList tokens)
{
@@ -366,8 +355,7 @@ private static bool TryShapeRadius(Token token, out ShapeRadius radius)
if (IsLengthPercentage(token))
{
- // A is a non-negative (CSS Shapes 1 §3.2); a negative
- // radius makes the whole clip-path value invalid.
+ // , 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))
@@ -387,10 +375,7 @@ private static bool TryShapeRadius(Token token, out ShapeRadius radius)
private static bool IsLengthPercentage(Token token)
{
if (token.Type == TokenType.Dimension || token.Type == TokenType.Percentage) return true;
- // A calc()-family function computes to a at used-value time. The render-time
- // resolver (CssClipPathResolver → CssValueParser.ParseLength) evaluates it, and the token's
- // ToValue() reconstructs the full "calc(…)" text, so accept it here rather than invalidating the
- // whole shape (this is what lets a Charts.css area/line polygon vertex be a calc() expression).
+ // 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.
diff --git a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs
index fcee98a0e..02661e877 100644
--- a/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/BoxShadowGrammar.cs
@@ -5,23 +5,14 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Shared, layer-agnostic grammar for the CSS box-shadow value (CSS Backgrounds & Borders
- /// Level 3 §5): a comma-separated list of shadow layers, each
- /// [ inset? && <length>{2,4} && <color>? ]. Like
- /// / , it validates the grammar and
- /// captures each layer's structure as raw component strings (never resolved numbers), so both
- /// Layer A (the CSS-OM converter, which only needs to accept/reject and preserve the authored text)
- /// and Layer B (the paint-time resolver in TheArtOfDev.HtmlRenderer.Core, which resolves each length
- /// against the box's own font/size) share a single parser rather than re-implementing the grammar.
+ /// 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
{
///
- /// A single parsed shadow layer. Lengths and the color are kept as the authored component strings -
- /// box-shadow lengths may be font-relative (em/rem), so only Layer B can resolve them.
- /// defaults to "0" and to "0" when the author
- /// omitted them; is null when omitted, meaning "use the element's own color"
- /// (currentColor) at paint time.
+ /// One shadow layer. and default to "0", a null
+ /// means currentcolor.
///
internal sealed class ShadowLayer
{
@@ -33,15 +24,10 @@ internal sealed class ShadowLayer
public string Color { get; set; }
}
- // The one color grammar (named/hex/rgb()/rgba()/hsl()/currentColor/transparent), reused so box-shadow
- // never re-implements color parsing - matches the CLAUDE.md "one grammar per value" rule.
private static readonly IValueConverter ColorValidator = Converters.ColorConverter.WithCurrentColor();
///
- /// Parses a box-shadow value's tokens into an ordered list of s
- /// (first-declared first), or returns null when the value is not a valid box-shadow. The literal
- /// keyword none returns an empty list (no shadows), distinct from a null (invalid)
- /// result.
+ /// The layers in declaration order, an empty list for none, or null if the value is invalid.
///
internal static List TryParse(IReadOnlyList tokens)
{
@@ -120,12 +106,8 @@ private static ShadowLayer ParseLayer(IReadOnlyList group)
}
///
- /// Validates the color component. The full color grammar (named/rgb()/rgba()/hsl()/currentColor)
- /// is checked via the shared , but a hex color is special-cased: the
- /// raw CssValueParser.GetCssTokens tokenizer Layer B feeds this grammar produces a bare
- /// token for #fff (only the in-declaration-value tokenizer path
- /// yields a the converter understands), so accept a well-formed hex token
- /// directly - otherwise a hex shadow color accepted by Layer A would be dropped at paint time.
+ /// 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)
{
@@ -142,14 +124,8 @@ private static bool IsHexColor(string data)
}
///
- /// Merges a # delimiter followed by a number/dimension back into one hex-color token. The
- /// CssValueParser.GetCssTokens tokenizer Layer B feeds this grammar is not in "value" mode,
- /// so a digit-leading hex color (#08f, #000) is split into a bare
- /// # plus a /
- /// (e.g. 08f becomes a dimension "08" + unit "f"), whereas a letter-leading hex (#c00)
- /// becomes a single token. Without this, a digit-leading hex shadow color
- /// accepted by Layer A (whose value-mode tokenizer yields a proper color token) would fail to re-parse
- /// at paint time and the shadow would silently vanish.
+ /// 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)
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
index ddf28cfd8..7c327ee19 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcCategory.cs
@@ -3,9 +3,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The CSS calc() type-checking category a (sub-)expression resolves to. Flags so that
- /// "Length | Percentage" can represent the combined <length-percentage> type once a
- /// length and a percentage have been added/subtracted together.
+ /// 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.
///
[Flags]
internal enum CalcCategory
@@ -14,24 +13,8 @@ internal enum CalcCategory
Length = 2,
Percentage = 4,
LengthPercentage = Length | Percentage,
-
- ///
- /// Unlike Length/Percentage, Angle never needs layout context to resolve (deg/grad/rad/turn all
- /// convert via fixed constants), so a valid Angle-category expression always folds fully at
- /// Layer A parse time - see CalcSerializer.
- ///
Angle = 8,
-
- ///
- /// <time> (s/ms). Like Angle, it resolves without layout context. Currently produced only by
- /// @propertysyntax: "<time>" validation - no layout property consumes a time
- /// calc(), so it never reaches CalcEvaluator during a render.
- ///
Time = 16,
-
- ///
- /// <resolution> (dpi/dpcm/dppx). Context-free like Angle/Time; same @property-only provenance.
- ///
Resolution = 32
}
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
index 2ec7b8d08..ec72254a2 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Calc/CalcEvaluator.cs
@@ -4,9 +4,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The layout-time context needed to resolve relative units within a calc-family expression: how
- /// many pixels 1em/1rem are, and what 100% means for this particular call site (e.g. containing-block
- /// width for width, parent font-size for font-size's em-relative resolution).
+ /// What em, rem and 100% are in pixels where a calc() is resolved.
///
internal readonly struct CalcContext
{
@@ -25,21 +23,16 @@ public CalcContext(double hundredPercent, double emFactor, double remFactor, boo
public bool FontAdjust { get; }
///
- /// Mirrors ParseLength's returnPoints parameter: when the caller's em/rem/percent factors are
- /// already expressed in points (as CssBoxProperties.FontSize's caller does), a bare pt leaf
- /// must bypass the normal pt->px factor rather than being converted into this already-points space.
+ /// The factors are in points instead of pixels, so a pt value is taken as is, like
+ /// .
///
public bool ReturnPoints { get; }
}
///
- /// Evaluates a validated calc-family AST to a pixel-space number. This is the one place calc()
- /// numbers actually get computed — called only from Layer B (CssValueParser.ParseLength),
- /// since only layout has the a percentage/em/rem leaf needs to resolve.
- /// Reuses for every leaf, so no unit-conversion arithmetic is duplicated
- /// here. A null result signals a divide-by-zero; per the type-checker's rules every legal divisor is
- /// a constant that Layer A already folds and rejects at validation time, so in practice this is a
- /// defensive fallback rather than a load-bearing check.
+ /// Evaluates a type-checked calc() tree at layout time, lengths to pixels, angles to radians,
+ /// times to milliseconds and resolutions to dppx. Null on a division by zero, which the type
+ /// checker already rejects.
///
internal static class CalcEvaluator
{
@@ -62,20 +55,12 @@ internal static class CalcEvaluator
.ToPixels(context.EmFactor, context.RemFactor, context.HundredPercent, context.FontAdjust);
case AngleCalcNode angle:
- // An angle leaf evaluates to radians (the canonical angle unit), so an angle-typed
- // calc such as `calc(1turn * 0.35)` yields 0.35 turn in radians (a Number leaf stays a
- // unitless scalar). Length calc()s never contain an angle leaf, so this never perturbs
- // length evaluation; the only current caller is conic-gradient stop-position parsing.
return new Angle((float)angle.Value, angle.Unit).ToRadian();
case TimeCalcNode time:
- // Canonical time unit is milliseconds. No layout property evaluates a time calc() today
- // (these nodes only arise from @property `syntax: "
internal static Color MixPaletteColors(Color c1, Color c2, string spaceName, string hueName, double? p1, double? p2)
{
- // CSS keywords are case-insensitive; MapSpace/MapHueName match lowercase literals, so fold first
- // (a mixed-case space would otherwise silently fall through to the sRGB/Shorter default).
var space = MapSpace(spaceName.ToLowerInvariant());
var hue = MapHueName(hueName.ToLowerInvariant());
diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs
index f767eeda4..fd5b48a17 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PropertyFactory.cs
@@ -18,18 +18,13 @@ internal sealed class PropertyFactory
private readonly Dictionary _shorthandsBuilder = new Dictionary(StringComparer.OrdinalIgnoreCase);
- // Shorthands that parse and expand normally but are excluded from serialization reconstruction:
- // the logical shorthands, whose longhands are the physical ones (margin-top/-bottom/…), so letting
- // the serializer collapse e.g. margin-top+margin-bottom into `margin-block` would change existing
- // output. Excluded from GetShorthands (the serialization query) only; CreateShorthand,
- // GetLonghands and IsShorthand still work.
+ // Shorthands that parse and expand but are never rebuilt from their longhands when serializing:
+ // the flow-relative ones, whose longhands are the physical ones, and grid/grid-template.
private readonly HashSet _logicalShorthands = new HashSet(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _fonts;
- // @property descriptors (syntax / initial-value / inherits). Their values have no fixed CSS grammar
- // (initial-value depends on the syntax; syntax is an arbitrary string), so each is stored raw via an
- // UnknownProperty (Converters.Any) and validated later against the syntax string.
+ // @property descriptors, stored raw, nothing reads them yet
private readonly Dictionary _propertyDescriptors;
// @font-palette-values descriptors (font-family / base-palette / override-colors). Stored raw via
@@ -370,9 +365,7 @@ private PropertyFactory()
_fontsBuilder.Add(PropertyNames.Src, () => new SrcProperty());
_fontsBuilder.Add(PropertyNames.UnicodeRange, () => new UnicodeRangeProperty());
- // CSS Grid Layout Level 1/2. The engine parses and validates the full grid property set;
- // the renderer has no grid layout engine yet, so these cascade and are then ignored at the
- // box boundary.
+ // CSS Grid 2, parsed and cascaded, there's no grid layout
AddLonghand(PropertyNames.GridTemplateColumns, () => new GridTemplateColumnsProperty());
AddLonghand(PropertyNames.GridTemplateRows, () => new GridTemplateRowsProperty());
AddLonghand(PropertyNames.GridTemplateAreas, () => new GridTemplateAreasProperty());
@@ -393,9 +386,7 @@ private PropertyFactory()
AddLonghand(PropertyNames.GridAutoColumns, () => new GridAutoColumnsProperty());
AddLonghand(PropertyNames.GridAutoRows, () => new GridAutoRowsProperty());
- // The grid mega-shorthands parse/expand like any other, but are excluded from serialization
- // reconstruction (via _logicalShorthands) - reconstructing a `grid`/`grid-template` from its
- // longhands is not worth the complexity and could change existing output.
+ // Never reconstructed from their longhands when serializing.
AddLogicalShorthand(PropertyNames.GridTemplate, () => new GridTemplateProperty(),
PropertyNames.GridTemplateRows, PropertyNames.GridTemplateColumns, PropertyNames.GridTemplateAreas);
AddLogicalShorthand(PropertyNames.Grid, () => new GridProperty(),
@@ -411,18 +402,12 @@ private PropertyFactory()
AddShorthand(PropertyNames.PlaceSelf, () => new PlaceSelfProperty(),
PropertyNames.AlignSelf, PropertyNames.JustifySelf);
- // CSS Logical Properties and Values Level 1. Layout here is always LTR / horizontal-tb,
- // so each logical property is registered to produce/export the existing physical
- // longhands (block-start = top, block-end = bottom, inline-start = left,
- // inline-end = right). Nothing downstream needs to change, since it already handles
- // every physical longhand.
-
- // Logical margin longhands (aliases to the physical longhand classes).
+ // CSS Logical 1 §4. The flow-relative properties are aliases of the physical ones, mapped as
+ // horizontal-tb ltr whatever the element's direction.
AddLonghand(PropertyNames.MarginBlockStart, () => new MarginTopProperty(), true);
AddLonghand(PropertyNames.MarginBlockEnd, () => new MarginBottomProperty(), true);
AddLonghand(PropertyNames.MarginInlineStart, () => new MarginLeftProperty(), true);
AddLonghand(PropertyNames.MarginInlineEnd, () => new MarginRightProperty(), true);
- // Logical margin shorthands.
AddLogicalShorthand(PropertyNames.MarginBlock, () => new MarginBlockProperty(),
PropertyNames.MarginTop,
PropertyNames.MarginBottom);
@@ -430,12 +415,10 @@ private PropertyFactory()
PropertyNames.MarginLeft,
PropertyNames.MarginRight);
- // Logical padding longhands (aliases to the physical longhand classes).
AddLonghand(PropertyNames.PaddingBlockStart, () => new PaddingTopProperty(), true);
AddLonghand(PropertyNames.PaddingBlockEnd, () => new PaddingBottomProperty(), true);
AddLonghand(PropertyNames.PaddingInlineStart, () => new PaddingLeftProperty(), true);
AddLonghand(PropertyNames.PaddingInlineEnd, () => new PaddingRightProperty(), true);
- // Logical padding shorthands.
AddLogicalShorthand(PropertyNames.PaddingBlock, () => new PaddingBlockProperty(),
PropertyNames.PaddingTop,
PropertyNames.PaddingBottom);
@@ -443,12 +426,10 @@ private PropertyFactory()
PropertyNames.PaddingLeft,
PropertyNames.PaddingRight);
- // Logical inset longhands (aliases to the physical coordinate longhand classes).
AddLonghand(PropertyNames.InsetBlockStart, () => new TopProperty(), true);
AddLonghand(PropertyNames.InsetBlockEnd, () => new BottomProperty(), true);
AddLonghand(PropertyNames.InsetInlineStart, () => new LeftProperty(), true);
AddLonghand(PropertyNames.InsetInlineEnd, () => new RightProperty(), true);
- // Inset shorthands.
AddLogicalShorthand(PropertyNames.Inset, () => new InsetProperty(),
PropertyNames.Top,
PropertyNames.Right,
@@ -461,7 +442,6 @@ private PropertyFactory()
PropertyNames.Left,
PropertyNames.Right);
- // Logical per-edge border longhands (aliases to the physical longhand classes).
AddLonghand(PropertyNames.BorderBlockStartWidth, () => new BorderTopWidthProperty(), true);
AddLonghand(PropertyNames.BorderBlockStartStyle, () => new BorderTopStyleProperty());
AddLonghand(PropertyNames.BorderBlockStartColor, () => new BorderTopColorProperty(), true);
@@ -475,7 +455,6 @@ private PropertyFactory()
AddLonghand(PropertyNames.BorderInlineEndStyle, () => new BorderRightStyleProperty());
AddLonghand(PropertyNames.BorderInlineEndColor, () => new BorderRightColorProperty(), true);
- // Logical per-edge border shorthands (aliases to the physical edge shorthand classes).
AddLogicalShorthand(PropertyNames.BorderBlockStart, () => new BorderTopProperty(),
PropertyNames.BorderTopWidth,
PropertyNames.BorderTopStyle,
@@ -493,7 +472,6 @@ private PropertyFactory()
PropertyNames.BorderRightStyle,
PropertyNames.BorderRightColor);
- // Logical two-edge border shorthands (both block/inline edges the same value).
AddLogicalShorthand(PropertyNames.BorderBlock, () => new BorderBlockProperty(),
PropertyNames.BorderTopWidth,
PropertyNames.BorderTopStyle,
@@ -509,7 +487,6 @@ private PropertyFactory()
PropertyNames.BorderRightStyle,
PropertyNames.BorderRightColor);
- // Logical two-edge border width/style/color shorthands.
AddLogicalShorthand(PropertyNames.BorderBlockWidth, () => new BorderBlockWidthProperty(),
PropertyNames.BorderTopWidth,
PropertyNames.BorderBottomWidth);
@@ -558,8 +535,6 @@ private void AddShorthand(string name, ShorthandCreator creator, params string[]
_mappingsBuilder.Add(name, longhands);
}
- // A logical shorthand: registered for parsing/expansion, but excluded from serialization
- // reconstruction (see the _logicalShorthands field comment).
private void AddLogicalShorthand(string name, ShorthandCreator creator, params string[] longhands)
{
AddShorthand(name, creator, longhands);
diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs
index 30a454772..19a77715f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoClassSelectorFactory.cs
@@ -55,11 +55,8 @@ private static Dictionary BuildSelectors()
selectors.Add(PseudoElementNames.FirstLetter,
PseudoElementSelectorFactory.Instance.Create(PseudoElementNames.FirstLetter));
- // Structural pseudo-classes: bare idents are equivalent to their nth-*(1) function form
- // (see CSS Selectors spec), so wire them to the same ChildSelector subtypes that
- // "nth-child(...)" etc. produce, rather than a generic PseudoClassSelector with no
- // positional semantics. Kind uses AllSelector.Create() (never null) to match the
- // invariant ChildFunctionState.Produce() upholds for the function-form parse path.
+ // :first-child (CSS 2.1 §5.11.1) and friends are their nth-*() function with An+B = 1.
+ // Kind is never null, same as what ChildFunctionState.Produce() creates.
selectors.Add(PseudoClassNames.FirstChild, new FirstChildSelector().With(0, 1, AllSelector.Create()));
selectors.Add(PseudoClassNames.LastChild, new LastChildSelector().With(0, 1, AllSelector.Create()));
selectors.Add(PseudoClassNames.FirstOfType, new FirstTypeSelector().With(0, 1, AllSelector.Create()));
diff --git a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs
index 46dc7947e..4d581eccb 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Factories/PseudoElementSelectorFactory.cs
@@ -16,8 +16,6 @@ internal sealed class PseudoElementSelectorFactory
private readonly Dictionary _selectors =
new HashSet(StringComparer.OrdinalIgnoreCase)
{
- //TODO some lack implementation (selection, content, ...)
- // some implementations are dubious (first-line, first-letter, ...)
PseudoElementNames.Before,
PseudoElementNames.After,
PseudoElementNames.Marker,
diff --git a/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs
index 70682250f..aed72c3e4 100644
--- a/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/GridLineGrammar.cs
@@ -3,22 +3,15 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// A parsed grid <grid-line> (one edge of grid-column/grid-row/grid-area):
- /// auto, an integer line number (may be negative — counting from the end edge), span N, or
- /// a named line reference (a bare <custom-ident>, or <custom-ident> <integer>
- /// for the Nth line with that name). span <custom-ident> is out of v1 scope.
- ///
+ /// A <grid-line>, CSS Grid 2 §8.3.
internal sealed class GridLine
{
public bool IsAuto { get; private set; }
public bool IsSpan { get; private set; }
- /// The line number (for a line reference), the span count (when ), or
- /// the 1-based Nth index for a named line (defaults to 1).
+ /// The line number, the span count, or which line of the name it is.
public int Value { get; private set; } = 1;
- /// The custom-ident of a named line reference, or null when this is not a named line.
public string Name { get; private set; }
public static readonly GridLine Auto = new GridLine { IsAuto = true };
@@ -28,11 +21,7 @@ internal sealed class GridLine
public static GridLine NamedNth(string name, int n) => new GridLine { Name = name, Value = n };
}
- ///
- /// Shared grammar for a single grid <grid-line> value. Used by Layer A (to validate the
- /// grid-column-start/-end/grid-row-start/-end longhands) and by Layer B (the
- /// grid placement engine).
- ///
+ /// The <grid-line> of the grid-row-*/grid-column-* longhands, CSS Grid 2 §8.3.
internal static class GridLineGrammar
{
internal static GridLine TryParse(IReadOnlyList tokens)
@@ -40,24 +29,19 @@ internal static GridLine TryParse(IReadOnlyList tokens)
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
if (toks.Length == 0) return null;
- // auto
if (toks.Length == 1 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Auto))
return GridLine.Auto;
- //
if (toks.Length == 1 && toks[0] is NumberToken number && number.IsInteger && number.IntegerValue != 0)
return GridLine.Line(number.IntegerValue);
- // span
if (toks.Length == 2 && toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Span)
&& toks[1] is NumberToken spanCount && spanCount.IsInteger && spanCount.IntegerValue >= 1)
return GridLine.Span(spanCount.IntegerValue);
- // — a named line reference.
if (toks.Length == 1 && IsCustomIdent(toks[0]))
return GridLine.Named(toks[0].Data);
- // / — the Nth line with that name (order-independent).
if (toks.Length == 2)
{
if (IsCustomIdent(toks[0]) && toks[1] is NumberToken n1 && n1.IsInteger && n1.IntegerValue != 0)
@@ -69,8 +53,6 @@ internal static GridLine TryParse(IReadOnlyList tokens)
return null;
}
- /// Whether the token is a <custom-ident> usable as a grid line name — an ident
- /// that is not one of the reserved keywords a grid-line value or the CSS-wide keywords may take.
private static bool IsCustomIdent(Token token)
{
if (token.Type != TokenType.Ident) return false;
diff --git a/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs
index d03e444c7..55bdf6033 100644
--- a/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/GridTemplateAreasGrammar.cs
@@ -3,28 +3,22 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// A parsed grid-template-areas value (CSS Grid §7.3): a rectangular grid of named cells. Each
- /// named area's 0-based inclusive bounding rectangle is recorded; a . (or a run of only dots) is
- /// an unnamed empty cell.
- ///
+ /// A grid-template-areas value, CSS Grid 2 §7.3.
internal sealed class GridAreas
{
public int RowCount { get; set; }
public int ColCount { get; set; }
- /// The area name at each cell ([row, col]), or null for an empty . cell.
+ /// The area name per [row, col], null for a null cell token.
public string[,] Cells { get; set; }
- /// Each named area's 0-based inclusive rectangle (rowStart, colStart, rowEnd, colEnd).
+ /// 0-based inclusive cell rectangle per area name.
public IReadOnlyDictionary Areas { get; set; }
}
///
- /// Shared grammar for a grid-template-areas value (a series of quoted strings). Used by Layer A
- /// (accept/reject) and Layer B (the engine derives implicit name-start/name-end lines).
- /// Returns null for anything invalid — including the none keyword, which the property accepts
- /// separately.
+ /// The <string>+ of grid-template-areas, CSS Grid 2 §7.3. Returns null for none
+ /// too, the property handles that keyword.
///
internal static class GridTemplateAreasGrammar
{
@@ -33,7 +27,6 @@ internal static GridAreas TryParse(IReadOnlyList tokens)
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
if (toks.Length == 0) return null;
- // Every token must be a string; each string is one row.
if (toks.Any(t => t.Type != TokenType.String)) return null;
var rows = new List(toks.Length);
@@ -42,15 +35,14 @@ internal static GridAreas TryParse(IReadOnlyList tokens)
{
var cells = ((StringToken)t).Data
.Split((char[])null, System.StringSplitOptions.RemoveEmptyEntries);
- if (cells.Length == 0) return null; // an empty string is invalid
+ if (cells.Length == 0) return null;
if (colCount < 0) colCount = cells.Length;
- else if (cells.Length != colCount) return null; // ragged rows are invalid
+ else if (cells.Length != colCount) return null;
var normalized = new string[colCount];
for (var c = 0; c < colCount; c++)
{
if (IsEmptyCell(cells[c])) { normalized[c] = null; continue; }
- // A named cell is a — a null token (a mixed "a.b" is invalid, not a name).
if (cells[c].Contains('.')) return null;
normalized[c] = cells[c];
}
@@ -74,8 +66,7 @@ internal static GridAreas TryParse(IReadOnlyList tokens)
bounds[name] = (r, c, r, c);
}
- // Each named area must be a single filled rectangle: every cell in its bounding box carries the
- // name (which also guarantees no cell with the name lies outside the box).
+ // Every cell in an area's bounding box has to carry its name.
foreach (var entry in bounds)
{
var name = entry.Key;
@@ -88,7 +79,6 @@ internal static GridAreas TryParse(IReadOnlyList tokens)
return new GridAreas { RowCount = rowCount, ColCount = colCount, Cells = grid, Areas = bounds };
}
- /// A cell of only U+002E FULL STOP characters (., ...) is an empty cell.
private static bool IsEmptyCell(string cell) => cell.All(ch => ch == '.');
}
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs b/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs
index c344d1a6c..f60120aca 100644
--- a/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/GridTrackListGrammar.cs
@@ -3,28 +3,23 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The kind of a single grid <track-size>/<track-breadth>
- /// (CSS Grid Layout Module Level 1/2 §7.2).
+ /// CSS Grid 2 §7.2.1.
internal enum GridTrackKind
{
- Length, // a (raw component string in Value, resolved by Layer B ParseLength)
- Percent, // a (raw component string in Value)
- Flex, // a / fr value (Flex holds the fr number)
+ Length, // authored text in Value, a length or a length-percentage math function
+ Percent, // authored text in Value
+ Flex, // factor in Flex
Auto,
MinContent,
MaxContent,
- FitContent, // fit-content() — the argument is in Value
- Minmax // minmax(Min, Max)
+ FitContent, // argument in Value
+ Minmax // Min, Max
}
- /// Which flavour of layout-time repeat() a template carries (CSS Grid §7.2.3.2).
+ /// CSS Grid 2 §7.2.3.2.
internal enum GridAutoRepeatKind { None, AutoFill, AutoFit }
- ///
- /// A single grid <track-size>. Fixed/percentage/fit-content values keep their authored
- /// component string () for Layer B to resolve against the container; fr keeps
- /// its numeric factor (); minmax() keeps its two sub-breadths.
- ///
+ /// A <track-size>, lengths and percentages keep their authored text.
internal sealed class GridTrackSize
{
public GridTrackKind Kind { get; private set; }
@@ -44,62 +39,42 @@ internal sealed class GridTrackSize
}
///
- /// A parsed grid-template-columns/grid-template-rows value: the fixed (fully
- /// repeat(N,…)-expanded) tracks, plus at most one layout-time repeat(auto-fill|auto-fit,…)
- /// section recorded as an insertion point since its count is resolved during layout.
+ /// A grid-template-columns/-rows value. repeat(<integer>, …) is expanded into
+ /// , an auto-fill/auto-fit repeat only depends on the container size and is kept apart.
///
internal sealed class GridTemplate
{
- /// The fixed tracks (with the auto-repeat section, if any, NOT included here — it is
- /// spliced in at at layout time).
+ /// The tracks without the auto-repeat ones, those go in at .
public IReadOnlyList Tracks { get; set; } = new List();
public GridAutoRepeatKind AutoRepeat { get; set; } = GridAutoRepeatKind.None;
- /// The repeated track template for an auto-fill/auto-fit section.
public IReadOnlyList AutoRepeatTracks { get; set; }
- /// The index into at which the auto-repeat section is inserted.
public int AutoRepeatInsertIndex { get; set; }
- /// Named lines declared with [name] in the top-level track list: name → the sorted,
- /// deduped 1-based line numbers it labels (line 1 is before the first track). Empty when none.
+ /// Line name → sorted 1-based line numbers, line 1 is before the first track.
public IReadOnlyDictionary> LineNames { get; set; }
= new Dictionary>();
- /// The subgrid keyword (CSS Grid Layout Module Level 2 §9): this axis adopts the parent
- /// grid's tracks instead of defining its own. A subgrid template carries no — only
- /// any explicit [name] line names declared after the keyword (in ).
+ /// CSS Grid 2 §9, no , only the line names after the keyword.
public bool IsSubgrid { get; set; }
public bool IsNone => !IsSubgrid && Tracks.Count == 0 && AutoRepeat == GridAutoRepeatKind.None;
}
///
- /// Shared grammar for the grid <track-list> (grid-template-columns/
- /// grid-template-rows) and <track-size>+ (grid-auto-columns/
- /// grid-auto-rows). Used by both Layer A (accept/reject at parse) and Layer B (compute used
- /// tracks during layout), so the grammar is defined once — the /
- /// precedent. Top-level named lines ([name]) are collected into
- /// ; named lines inside repeat() are out of scope and
- /// rejected.
+ /// CSS Grid 2 §7.2 track lists of grid-template-columns/-rows and the
+ /// <track-size>+ of grid-auto-columns/-rows.
///
internal static class GridTrackListGrammar
{
- ///
- /// Parses a <track-list> (the grid-template-columns/-rows value). Returns
- /// null when the value is not a valid track list — including the literal none, which
- /// the property accepts separately.
- ///
+ /// Returns null for none too, the property handles that keyword.
internal static GridTemplate TryParse(IReadOnlyList tokens)
{
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
if (toks.Length == 0) return null;
- // subgrid (CSS Grid Level 2 §9): the axis adopts the parent grid's tracks. The keyword may be
- // followed by an optional ([name] groups only; repeat() of line names is a v1
- // deferral, mirroring the "named lines inside repeat()" restriction below). No is
- // allowed after subgrid.
if (toks[0].Type == TokenType.Ident && toks[0].Data.Isi(Keywords.Subgrid))
return TryParseSubgrid(toks);
@@ -112,8 +87,6 @@ internal static GridTemplate TryParse(IReadOnlyList tokens)
var i = 0;
while (i < toks.Length)
{
- // [name …] — one or more named lines at the current line position (1-based; line 1 is before
- // the first track). Whitespace is already stripped, so the bracket group is contiguous.
if (toks[i].Type == TokenType.SquareBracketOpen)
{
var lineIndex = fixedTracks.Count + 1;
@@ -138,12 +111,10 @@ internal static GridTemplate TryParse(IReadOnlyList tokens)
if (repeatKind == GridAutoRepeatKind.None)
{
- // repeat(N, …) — expand inline.
fixedTracks.AddRange(repeatTracks);
}
else
{
- // repeat(auto-fill|auto-fit, …) — at most one per track list.
if (autoRepeat != GridAutoRepeatKind.None) return null;
autoRepeat = repeatKind;
autoRepeatTracks = repeatTracks;
@@ -173,13 +144,7 @@ internal static GridTemplate TryParse(IReadOnlyList tokens)
};
}
- ///
- /// Parses a subgrid [ <line-name-list> ]? value (the leading subgrid ident already
- /// matched by the caller). The optional line-name list is a run of [name …] bracket groups, one
- /// group per grid line (line 1 is before the first adopted track); each group's names are recorded at
- /// that 1-based line index. Anything else after subgrid (a track size, an unclosed/ill-formed
- /// bracket, a non-ident inside [ ]) is invalid.
- ///
+ /// subgrid <line-name-list>?, the caller matched the keyword.
private static GridTemplate TryParseSubgrid(Token[] toks)
{
var lineNames = new Dictionary>();
@@ -188,7 +153,7 @@ private static GridTemplate TryParseSubgrid(Token[] toks)
while (i < toks.Length)
{
- if (toks[i].Type != TokenType.SquareBracketOpen) return null; // only [name] groups may follow
+ if (toks[i].Type != TokenType.SquareBracketOpen) return null;
i++;
while (i < toks.Length && toks[i].Type != TokenType.SquareBracketClose)
{
@@ -213,10 +178,7 @@ private static GridTemplate TryParseSubgrid(Token[] toks)
};
}
- ///
- /// Parses a <track-size>+ list (grid-auto-columns/grid-auto-rows) — one or
- /// more track sizes, no repeat(). Returns null on any invalid token.
- ///
+ /// <track-size>+, no repeat().
internal static IReadOnlyList TryParseTrackSizeList(IReadOnlyList tokens)
{
var toks = tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
@@ -241,7 +203,6 @@ private static bool TryParseRepeat(FunctionToken fn, out GridAutoRepeatKind kind
var groups = SplitByComma(args);
if (groups.Count < 2) return false;
- // First group is the repetition count: an integer, or auto-fill / auto-fit.
var first = groups[0];
if (first.Count != 1) return false;
@@ -251,8 +212,7 @@ private static bool TryParseRepeat(FunctionToken fn, out GridAutoRepeatKind kind
else if (first[0] is NumberToken n && n.IsInteger && n.IntegerValue >= 1) countTokens = n.IntegerValue;
else return false;
- // Remaining groups are the repeated s. (A repeat body may not itself contain a
- // repeat(), which SplitByComma naturally enforces since a nested function is one token.)
+ // A nested repeat() is a single function token, which TryParseTrackSize rejects.
var body = new List();
for (var gi = 1; gi < groups.Count; gi++)
{
@@ -271,7 +231,6 @@ private static bool TryParseRepeat(FunctionToken fn, out GridAutoRepeatKind kind
return true;
}
- // repeat(N, body) — expand N copies now.
tracks = new List(body.Count * countTokens);
for (var r = 0; r < countTokens; r++)
tracks.AddRange(body);
@@ -282,8 +241,6 @@ private static bool TryParseTrackSize(Token token, out GridTrackSize track)
{
track = null;
- // minmax(, ), fit-content(), and a
- // math function (calc()/min()/max()/clamp()) that resolves to a .
if (token is FunctionToken fn)
{
if (fn.Data.Isi(FunctionNames.Minmax)) return TryParseMinmax(fn, out track);
@@ -300,9 +257,6 @@ private static bool TryParseBreadth(Token token, out GridTrackSize track)
{
track = null;
- // A math function (calc()/min()/max()/clamp()) computes to a at used-value
- // time; keep its reconstructed text (Layer B ParseLength evaluates it). This lets a calc() be a
- // track breadth anywhere a length/percentage is allowed — bare, and inside minmax()/repeat().
if (token is FunctionToken calc && IsLengthPercentageCalc(calc))
{
track = GridTrackSize.Length(calc.ToValue());
@@ -328,12 +282,10 @@ private static bool TryParseBreadth(Token token, out GridTrackSize track)
return true;
}
- // Any other dimension is a — the raw string is resolved by Layer B ParseLength.
track = GridTrackSize.Length(token.ToValue());
return true;
}
- // Unitless zero is a valid length.
var zero = token as NumberToken;
if (zero != null && zero.Value == 0f) { track = GridTrackSize.Length("0"); return true; }
@@ -349,7 +301,6 @@ private static bool TryParseMinmax(FunctionToken fn, out GridTrackSize track)
if (!TryParseBreadth(groups[0][0], out var min)) return false;
if (!TryParseBreadth(groups[1][0], out var max)) return false;
- // The min of a minmax() is an — a flex value is invalid there.
if (min.Kind == GridTrackKind.Flex) return false;
track = GridTrackSize.Minmax(min, max);
@@ -362,7 +313,6 @@ private static bool TryParseFitContent(FunctionToken fn, out GridTrackSize track
var args = fn.ArgumentTokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
if (args.Length != 1) return false;
- // fit-content().
var arg = args[0];
if (arg.Type == TokenType.Percentage) { track = GridTrackSize.FitContentTo(arg.ToValue()); return true; }
if (arg is UnitToken && arg.Type == TokenType.Dimension && !((UnitToken)arg).Unit.Isi("fr"))
@@ -382,13 +332,7 @@ private static bool TryParseFitContent(FunctionToken fn, out GridTrackSize track
return false;
}
- ///
- /// Whether is a math function (calc()/min()/max()/
- /// clamp()) that resolves to a <length-percentage>. Validated at parse time
- /// (mirroring 's calc handling) so a wrong-category math function
- /// (e.g. an angle) drops the whole track list rather than silently resolving to 0 during layout;
- /// its reconstructed text is resolved by Layer B ParseLength.
- ///
+ /// Whether is a math function of type <length-percentage>.
private static bool IsLengthPercentageCalc(FunctionToken fn)
{
if (!CalcParser.IsCalcFamily(fn.Data)) return false;
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs
index a58ee27a0..4aa0f727a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyHoverMediaFeature.cs
@@ -1,8 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The any-hover media feature (Media Queries 4). Matched against the renderer's fixed
- /// none stance (a static PDF has no hover-capable input) in MediaQueryMatcher.
+ /// Media Queries 5 §7.3, matched in MediaQueryMatcher.
///
internal sealed class AnyHoverMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs
index 576e2c3c5..252ed534b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/AnyPointerMediaFeature.cs
@@ -1,8 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The any-pointer media feature (Media Queries 4). Matched against the renderer's fixed
- /// none stance (a static PDF has no pointing device) in MediaQueryMatcher.
+ /// Media Queries 5 §7.3, matched in MediaQueryMatcher.
///
internal sealed class AnyPointerMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs
index 1f4bbb601..64197a593 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersColorSchemeMediaFeature.cs
@@ -1,9 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The prefers-color-scheme media feature (Media Queries 5). The value is validated as any
- /// identifier here; the actual light/dark match against the configured scheme happens
- /// in MediaQueryMatcher.
+ /// Media Queries 5 §12.5, matched against the adapter's color scheme in MediaQueryMatcher.
///
internal sealed class PrefersColorSchemeMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs
index 40f3301d2..52e2faff1 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersContrastMediaFeature.cs
@@ -1,8 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The prefers-contrast media feature (Media Queries 5). Matched against the renderer's fixed
- /// no-preference stance in MediaQueryMatcher.
+ /// Media Queries 5 §12.3, matched in MediaQueryMatcher.
///
internal sealed class PrefersContrastMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs
index 717844821..a7e69c3fb 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedMotionMediaFeature.cs
@@ -1,8 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The prefers-reduced-motion media feature (Media Queries 5). Matched against the renderer's
- /// fixed reduce stance (a static PDF has no motion) in MediaQueryMatcher.
+ /// Media Queries 5 §12.1, matched in MediaQueryMatcher.
///
internal sealed class PrefersReducedMotionMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs
index ef84cf8ce..124a93849 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/PrefersReducedTransparencyMediaFeature.cs
@@ -1,8 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The prefers-reduced-transparency media feature (Media Queries 5). Matched against the
- /// renderer's fixed no-preference stance in MediaQueryMatcher.
+ /// Media Queries 5 §12.2, matched in MediaQueryMatcher.
///
internal sealed class PrefersReducedTransparencyMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs
index 59eab7e2b..7e8f564b5 100644
--- a/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/MediaFeatures/UpdateMediaFeature.cs
@@ -1,9 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The update media feature (Media Queries 4): how frequently the output device can modify the
- /// appearance of content. A printed/exported PDF cannot update once produced, so it is matched against
- /// none in MediaQueryMatcher.
+ /// Media Queries 5 §5.4, matched in MediaQueryMatcher.
///
internal sealed class UpdateMediaFeature : MediaFeature
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs
index cb018d112..893bec5ca 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Model/Converters.cs
@@ -89,10 +89,6 @@ public static readonly IValueConverter
Assign(Keywords.Top, 1.0).Or(Keywords.Bottom, -1.0).Option(0.0)
);
- // The 1/2/3/4-token background-position grammar (including the edge-relative offset syntax,
- // e.g. "right 20px bottom 10px") is implemented once in BackgroundPositionGrammar and shared
- // with the render layer's BackgroundLayerResolver, rather than re-implemented as a second,
- // independent parser of the same value string - see that class's doc comment.
public static readonly IValueConverter PointConverter = new BackgroundPositionValueConverter();
public static readonly IValueConverter AttrConverter = new FunctionValueConverter(
@@ -299,15 +295,9 @@ public static readonly IValueConverter
public static readonly IValueConverter ContainerTypeConverter = Map.ContainerTypes.ToConverter();
public static readonly IValueConverter ClearModeConverter = Map.ClearModes.ToConverter();
public static readonly IValueConverter FontStretchConverter = Map.FontStretches.ToConverter();
- // "oblique" alone matches via the plain keyword map (tried first); "oblique " (CSS Fonts
- // Level 4 - e.g. "oblique 10deg") only reaches the StartsWithValueConverter branch once the
- // plain single-identifier match has already failed, i.e. there are more tokens to account for.
- // AngleConverter is intentionally NOT Option()-wrapped here (unlike e.g. LineHeightConverter's
- // own StartsWithDelimiter().Option() composition elsewhere) - StartsWithValueConverter.Construct
- // trusts "the wrapped converter returned non-null" as its own "matched" signal, and an Option()
- // converter's Construct never returns null, which would make every OTHER possible font-style
- // value (plain keywords, initial, absent) falsely reconstruct as "oblique" whenever the "font"
- // shorthand needs to be re-serialized from its decomposed longhand properties.
+ // CSS Fonts 4 §2.4 "oblique ". The angle isn't Option() wrapped: StartsWithValueConverter
+ // takes a non-null Construct as a match, and an Option() never returns null, so every font-style
+ // would serialize as "oblique" when the font shorthand is rebuilt from its longhands.
public static readonly IValueConverter FontStyleConverter = Map.FontStyles.ToConverter()
.Or(new StartsWithValueConverter(TokenType.Ident, Keywords.Oblique, AngleConverter));
public static readonly IValueConverter FontWeightConverter = Map.FontWeights.ToConverter();
@@ -349,12 +339,9 @@ public static readonly IValueConverter
public static readonly IValueConverter AlignSelfConverter = AlignItemsConverter.OrAuto();
- // justify-items / justify-self share align-items/align-self's value grammar for the keywords the
- // grid engine honors (start/end/center/stretch/normal; baseline falls back to start at layout).
public static readonly IValueConverter JustifyItemsConverter = AlignItemsConverter;
public static readonly IValueConverter JustifySelfConverter = AlignSelfConverter;
- // place-items / place-content / place-self: ? — one value applies to both axes.
public static readonly IValueConverter PlaceItemsConverter =
AlignItemsConverter.Periodic(PropertyNames.AlignItems, PropertyNames.JustifyItems);
public static readonly IValueConverter PlaceContentConverter =
@@ -406,8 +393,7 @@ public static readonly IValueConverter
var directionConverter = FlexDirectionConverter.For(PropertyNames.FlexDirection);
var wrapConverter = FlexWrapConverter.For(PropertyNames.FlexWrap);
- // flex-flow is "<'flex-direction'> || <'flex-wrap'>" (CSS Flexbox 1 §5.1): the double bar means
- // the two values may appear in either order, so the pair has to be WithAny, not WithOrder.
+ // CSS Flexbox 1 §5.3: "||", the two values can come in either order
return directionConverter
.Or(wrapConverter)
.Or(WithAny(directionConverter, wrapConverter));
@@ -474,12 +460,8 @@ public static readonly IValueConverter
IntegerConverter.Required(),
IntegerConverter.StartsWithDelimiter().Required());
- // A single grid (auto | | span ), validated by GridLineGrammar.
public static readonly IValueConverter GridLineConverter = new GridLineValueConverter();
- // grid-column / grid-row / grid-area: slash-separated components with the CSS Grid
- // §8.3.1 omitted-value copy rule (a bare propagates to the paired/all edges) — the
- // generic WithOrder(...).Option() DSL resets omitted slots to auto, which is wrong for named areas.
public static readonly IValueConverter GridColumnConverter =
new GridColumnRowShorthandValueConverter(PropertyNames.GridColumnStart, PropertyNames.GridColumnEnd);
@@ -499,11 +481,7 @@ public static readonly IValueConverter
public static readonly IValueConverter MultipleShadowConverter = ShadowConverter.FromList().OrNone();
- // The image functions the engine validates but the renderer does not paint (CSS Images 4). Composed into
- // ImageSourceConverter so every property (background-image, list-style-image, cursor,
- // content, and @property syntax:"") accepts them syntactically; the render path
- // (CssValueParser.ParseImage → CssImagePainter) still handles only url()/gradients, so they paint
- // nothing (an unchanged engine-wide gap). See ExtendedImageConverters.cs.
+ // Accepted wherever an is, but never painted
public static readonly IValueConverter ImageSetImageConverter =
Construct(() => new FunctionValueConverter(FunctionNames.ImageSet, new ImageSetConverter()));
public static readonly IValueConverter CrossFadeImageConverter =
@@ -523,8 +501,6 @@ public static readonly IValueConverter
public static readonly IValueConverter FontFamiliesConverter =
DefaultFontFamiliesConverter.Or(StringConverter).Or(LiteralsConverter).FromList();
- // Shared with the render layer's BackgroundLayerResolver via BackgroundSizeGrammar - see
- // BackgroundPositionGrammar's doc comment for why.
public static readonly IValueConverter BackgroundSizeConverter = new BackgroundSizeValueConverter();
public static readonly IValueConverter BackgroundRepeatsConverter = BackgroundRepeatConverter.Or(
@@ -611,4 +587,4 @@ private static IValueConverter WithArgs(params IValueConverter[] converters)
#endregion
}
-}
\ No newline at end of file
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Parser/Lexer.cs b/Source/HtmlRenderer/Core/CssEngine/Parser/Lexer.cs
index d920be4fd..b3f3db7e0 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Parser/Lexer.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Parser/Lexer.cs
@@ -321,7 +321,7 @@ private Token ColorLiteral()
{
var current = GetNext();
- // '#' not followed by a name code point or a valid escape is a plain delimiter (CSS Syntax §4.3.4).
+ // CSS Syntax 3 §4.3.1
if (!current.IsName() && !IsValidEscape(current))
{
Back();
@@ -330,10 +330,7 @@ private Token ColorLiteral()
Back();
- // A '#' always begins a , consuming a whole (CSS Syntax §4.3.4). Classify it as
- // a color literal only when the name is entirely hex digits (e.g. "#f00"); otherwise keep it as an
- // id hash-token (e.g. "#hero" — an id-selector inside element()), instead of truncating at the
- // first non-hex character (which turned "#hero" into an empty color plus a stray "hero" ident).
+ // In a value an all-hex name ("#f00") becomes a color token, anything else ("#hero") stays a hash.
var allHex = true;
while (true)
@@ -550,23 +547,6 @@ private Token IdentRest(char current)
current = GetNext();
}
}
- //private Token TransformFunctionWhitespace(char current)
- //{
- // while (true)
- // {
- // current = GetNext();
- // if (current == Symbols.RoundBracketOpen)
- // {
- // Back();
- // return NewFunction(FlushBuffer());
- // }
- // if (!current.IsSpaceCharacter())
- // {
- // Back(2);
- // return NewIdent(FlushBuffer());
- // }
- // }
- //}
private Token NumberStart(char current)
{
@@ -941,15 +921,10 @@ private Token UnicodeRange(char current)
current = GetNext();
}
- // Fewer than 6 hex digits may be followed by '?' wildcards (which pad the value and are
- // mutually exclusive with the start-end range form) OR - like a full 6-digit start - by a
- // '-' range end (e.g. U+41-5A). Only the wildcard form is handled here; a '-' falls
- // through to the shared range handling below.
+ // CSS Syntax 3 §4.3.14: wildcards ("U+4??") and a range end ("U+41-5A") exclude each other.
if (StringBuffer.Length != 6 && current == Symbols.QuestionMark)
{
- // The wildcard budget must be captured up front: appending to StringBuffer inside the loop
- // would otherwise shrink a "6 - StringBuffer.Length" bound on every iteration, so only half
- // the available wildcards were ever consumed (U+?????? stopped after three).
+ // Captured up front, the loop appends to StringBuffer.
var wildcards = 6 - StringBuffer.Length;
for (var i = 0; i < wildcards; i++)
@@ -1093,12 +1068,8 @@ private Token NewFunction(string value)
{
var function = new FunctionToken(value, _position);
- // Tracks paren depth so a bare (non-function) parenthesized group nested inside the
- // function's arguments - e.g. calc((1px + 2px) * 3) - doesn't get mistaken for the end of
- // the function: only the closing paren that matches this function's own opening paren (depth
- // returning to 0) should terminate it. A nested function call's own parens are already fully
- // consumed by its own (recursive) call to this method before it's added as a single token
- // here, so they never surface as bare RoundBracketOpen/Close tokens at this level.
+ // Depth of bare parentheses, e.g. calc((1px + 2px) * 3). Nested functions consume their own
+ // parentheses recursively.
var depth = 1;
var token = Get();
while (token.Type != TokenType.EndOfFile)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Parser/LexerBase.cs b/Source/HtmlRenderer/Core/CssEngine/Parser/LexerBase.cs
index c326435f4..8bcd72b0b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Parser/LexerBase.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Parser/LexerBase.cs
@@ -43,18 +43,14 @@ public TextPosition GetCurrentPosition()
}
///
- /// Repositions the lexer so the next token is re-lexed from character index
- /// (a raw index, as returned by
- /// ). Unlike setting (whose
- /// BackNative loop is not a faithful inverse across \r\n normalization), this
- /// restores the character stream exactly — line/column tracking is not rewound (it only affects
- /// reported positions, never token content). Used by to rewind a
- /// CSS-Nesting classification look-ahead so a declaration re-lexes cleanly in value mode.
+ /// Lexes the next token from the raw index , e.g. one
+ /// returned by . Unlike setting this is exact
+ /// across \r\n normalization. Line and column aren't rewound.
///
public void RewindTo(int sourceIndex)
{
Source.Index = sourceIndex;
- // Non-EOF so the next Advance() actually reads Source[sourceIndex] rather than short-circuiting.
+ // Anything but EOF, Advance() doesn't read past EOF.
Current = Symbols.Null;
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Parser/SelectorConstructor.cs b/Source/HtmlRenderer/Core/CssEngine/Parser/SelectorConstructor.cs
index 12abeb4b9..75bc1959b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Parser/SelectorConstructor.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Parser/SelectorConstructor.cs
@@ -386,24 +386,9 @@ private void Insert(ISelector selector)
}
}
- // CssData.DoesSelectorMatch(CompoundSelector, box) is the only place that synthesizes
- // ::before/::after (and ::marker) child boxes as a match side effect, and it only runs for a
- // pseudo-element that's the last member of a CompoundSelector. A pseudo-element preceded by a
- // combinator (e.g. "E :after", with whitespace) would otherwise become a bare, unwrapped _temp
- // here - which CssData's ComplexSelector subject-match dispatches straight to the plain
- // DoesSelectorMatch(PseudoElementSelector, box) overload (which only checks whether the flag is
- // already set, never sets it), so the pseudo-element box would never actually get created.
- // Wrapping it in a single-member CompoundSelector as soon as it becomes a standalone selector
- // routes it through the one, already-correct synthesis path instead of needing a second
- // implementation of the same logic in CssData.
- //
- // Deliberately NOT applied in the other "_temp == null" branch above (a pseudo-element with no
- // combinator at all, i.e. it's the entire selector - "*:before" written as bare ":before"): that
- // shape has no ancestor restriction, so synthesizing on every match would mean synthesizing on
- // literally every element in the document. The UA stylesheet's own
- // ":before, :after { white-space: pre-line }" rule (CssDefaults.cs) is exactly this shape and
- // relies on the existing non-synthesizing behavior - it exists to style already-synthesized
- // pseudo-element boxes, not to create one on every element.
+ // CssData creates the ::before/::after box only when matching a CompoundSelector, so a
+ // pseudo-element after a combinator ("div ::before") is wrapped in one. A whole selector of
+ // just "::before" isn't, it creates no box, and the UA rule ":before, :after" relies on that.
private static ISelector WrapPseudoElement(ISelector selector) =>
selector is PseudoElementSelector ? new CompoundSelector { selector } : selector;
@@ -884,12 +869,7 @@ private bool OnOffset(Token token)
case TokenType.Whitespace:
return false;
case TokenType.Delim when token.Data.IsOneOf("+", "-"):
- // The An+B offset's sign is a standalone delim token when whitespace separates it
- // from B - the " ['+' | '-'] " production of
- // (CSS Syntax 3 §6.2); §6.1 gives `3n + 1` as a valid example. The
- // compact form `10n+1` instead arrives as a single signed , handled by the
- // Number case below. Without this branch a spaced sign fell through to OnBeforeOf
- // and invalidated the selector.
+ // CSS Syntax 3 §6.2: "3n + 1", the compact "3n+1" is a signed number below.
_sign = token.Data == "-" ? -1 : 1;
_state = ParseState.AfterOffsetSign;
return false;
@@ -910,9 +890,7 @@ private bool OnAfterOffsetSign(Token token)
case TokenType.Whitespace:
return false;
case TokenType.Number when !token.Data.StartsWith("+", StringComparison.Ordinal) && !token.Data.StartsWith("-", StringComparison.Ordinal):
- // The production requires a here, defined as "a
- // with its type flag set to integer, and no sign character" (CSS Syntax 3 §6.2), so
- // `10n + -1` and `10n + +1` are invalid - §6.1 lists `3n + -6` as an invalid example.
+ // CSS Syntax 3 §6.2: a , "3n + -6" is invalid.
_valid = _valid && ((NumberToken)token).IsInteger && int.TryParse(token.Data, out _offset);
_offset *= _sign;
_state = ParseState.BeforeOf;
@@ -963,4 +941,4 @@ private enum ParseState : byte
}
}
}
-}
\ No newline at end of file
+}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs b/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs
index d28f5fff8..714923d6b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetComposer.cs
@@ -10,10 +10,8 @@ internal sealed class StylesheetComposer
private readonly StylesheetParser _parser;
private readonly Stack _nodes;
- // The source index (raw, into _lexer.Source) immediately before the most recently read token.
- // Captured by NextToken so a CSS-Nesting classification look-ahead can rewind to a construct's
- // exact start without any position arithmetic (which is unreliable across \r\n normalization and
- // unicode escapes) and can slice the nested prelude's source text.
+ // Raw source index before the token NextToken read last, lets the nested rule look-ahead rewind to
+ // the start of a construct and slice a nested prelude. TextPosition is off across \r\n and escapes.
private int _markBeforeLastToken;
public StylesheetComposer(Lexer lexer, StylesheetParser parser)
@@ -654,13 +652,8 @@ public KeyframeSelector CreateKeyframeSelector(ref Token token)
private PageSelector CreatePageSelector(ref Token token)
{
- // Parses the CSS Paged Media selector grammar per comma-separated entry: an optional
- // page name followed by an optional : pseudo-class, e.g.
- // "@page chapter1:left, chapter2:left { ... }" (name+pseudo), "@page chapter { ... }"
- // (name only), "@page :first { ... }" (pseudo only). Page names are case-sensitive
- // custom-idents; pseudo-class keywords (first/left/right) are matched case-insensitively
- // by the caller. Chaining multiple pseudo-classes on one entry (e.g. ":first:left") isn't
- // supported — out of scope, not used by any known real-world stylesheet.
+ // CSS Paged Media 3 §4.3. One pseudo-class per selector, a second one or a functional one (GCPM's
+ // :nth()) leaves the selector unfinished and the caller drops the rule.
var entries = new List();
while (true)
@@ -697,30 +690,6 @@ private PageSelector CreatePageSelector(ref Token token)
var selector = new PageSelector(entries);
- //var start = token.Position;
-
- //while (token.IsNot(TokenType.EndOfFile, TokenType.CurlyBracketOpen, TokenType.CurlyBracketClose))
- //{
- // var a = 1;
- // token = NextToken();
- //}
-
- //var result = selector.ToPool();
-
- //if (result is StylesheetNode node)
- //{
- // var end = token.Position.Shift(-1);
- //node.StylesheetText = CreateView(start, end);
- //}
-
- //if (!selectorIsValid && !_parser.Options.AllowInvalidValues)
- //{
- // RaiseErrorOccurred(ParseError.InvalidSelector, start);
- // result = null;
- //}
-
- //return result;
-
return selector;
}
@@ -772,7 +741,6 @@ public TextPosition FillDeclarations(StyleDeclaration style)
}
else if (TryCreateNestedRule(ref token))
{
- // CSS Nesting: a nested style rule was parsed and attached to the enclosing rule.
}
else
{
@@ -839,16 +807,11 @@ public TextPosition FillDeclarations(StyleDeclaration style)
}
///
- /// CSS Nesting ([CSS Nesting 1](https://www.w3.org/TR/css-nesting-1/)): if the current construct
- /// in a declaration block is a nested style rule (a selector prelude followed by a { }
- /// block) rather than a declaration, parses it, resolves its selector against the enclosing rule,
- /// attaches it, and returns true (with advanced past the block). Returns
- /// false for an ordinary declaration, having rewound the lexer so the caller re-reads it.
+ /// CSS Nesting 1 §3.1: parses a nested style rule at and attaches it to the
+ /// enclosing rule. Returns false for a declaration, with the lexer rewound so the caller re-reads it.
///
private bool TryCreateNestedRule(ref Token token)
{
- // Raw source index just before the current (first) token — the construct start, captured by
- // NextToken. Faithful across \r\n normalization, unlike deriving it from token.Position.
var preludeStart = _markBeforeLastToken;
if (!IsNestedRuleAhead(ref token, out var braceStart))
@@ -860,15 +823,10 @@ private bool TryCreateNestedRule(ref Token token)
}
///
- /// Looks ahead (bracket-aware) to classify the construct starting at as a
- /// nested style rule vs a declaration: the first top-level { means a nested rule (the stream
- /// is left just after it and its source index returned via ); a
- /// top-level ;/}/EOF means a declaration, in which case the lexer is rewound to the
- /// construct start and re-read so the unchanged declaration path re-lexes
- /// it in value mode (avoiding the # value-mode tokenization hazard a token buffer would hit).
- /// Custom properties (--x) are always declarations, even with a { in their value.
- /// Function tokens (e.g. url(…)) are opaque — the lexer already consumed their parentheses —
- /// so only bare round/square brackets contribute to depth.
+ /// A top-level { before a ; or } makes the construct a nested rule, the stream is
+ /// left after the brace and is its source index. For a declaration the
+ /// lexer is rewound and re-read, so the value is lexed in value mode. Custom
+ /// properties are always declarations. A function token already holds its parentheses.
///
private bool IsNestedRuleAhead(ref Token token, out int braceStart)
{
@@ -916,11 +874,8 @@ private bool IsNestedRuleAhead(ref Token token, out int braceStart)
}
///
- /// Builds a from a nested prelude (source span
- /// [preludeStart, bracePosition)) resolved against the enclosing rule's selector, consumes
- /// its declaration block from the live stream, and attaches it to the parent rule via
- /// . The resolved selector is absolute (& →
- /// :is(parent)) so the cascade/matcher need no nesting awareness.
+ /// Parses the block of a nested rule and attaches the rule to its parent with the selector resolved
+ /// against the parent's, so the cascade needs no nesting support.
///
private void CreateNestedStyleRule(int preludeStart, int braceStart)
{
@@ -931,8 +886,7 @@ private void CreateNestedStyleRule(int preludeStart, int braceStart)
? null
: _parser.ParseSelector(ResolveNestedSelector(preludeText, parent.SelectorText));
- // Always consume the block (via a rule pushed on _nodes so nested-within-nested resolves) to
- // keep the parser in sync; only attach it when the selector actually resolved.
+ // CSS Nesting 1 §3.1: an invalid nested rule is dropped, its block is still consumed.
var rule = new StyleRule(_parser);
if (selector != null)
@@ -947,21 +901,15 @@ private void CreateNestedStyleRule(int preludeStart, int braceStart)
}
///
- /// Resolves a nested selector's prelude text against its parent's selector text per CSS Nesting:
- /// each & becomes :is(parent) (giving & the parent's specificity); a
- /// prelude with no & is made relative to the parent (:is(parent) <prelude>),
- /// which correctly yields both the implicit-descendant (.b → :is(parent) .b) and the
- /// leading-combinator (> .b → :is(parent) > .b) forms.
+ /// CSS Nesting 1 §3.1 and §4: & becomes :is(parent), a prelude without one is
+ /// prefixed with :is(parent) and a space, which covers both .b and > .b. An
+ /// & in a string is kept.
///
private static string ResolveNestedSelector(string prelude, string parentText)
{
var trimmed = prelude.Trim();
var parentIs = ":is(" + (string.IsNullOrEmpty(parentText) ? "*" : parentText) + ")";
- // Substitute `&` token-aware: only a `&` that is a real nesting selector is replaced, never a
- // `&` inside a string/attribute value (e.g. `[data-x="a&b"]`), which must be preserved. This
- // also decides the "has &" branch correctly, so a prelude whose only `&` is inside a string is
- // still scoped under the parent (:is(parent) ) rather than mis-taking the replace path.
var sb = Pool.NewStringBuilder();
var hasNestingSelector = false;
var quote = '\0';
@@ -1403,19 +1351,8 @@ private TextPosition FillRules(GroupingRule group)
while (token.IsNot(TokenType.EndOfFile, TokenType.CurlyBracketClose))
{
- // "Consume a block's contents" discards a outright, exactly as it does a
- // (CSS Syntax 3 §5.5.5). Passing it on to CreateStyle instead feeds it to
- // SelectorConstructor, which has no recovery for an unexpected semicolon: it folds the token
- // into the next rule's selector and invalidates it, and because the run-on rule keeps
- // consuming, the block's own closing brace is swallowed and the following sibling rule is
- // lost with it.
- //
- // Deliberately NOT done in CreateRules: "consume a stylesheet's contents" (§5.5.1) has no
- // case, so a stray semicolon at the top level falls to "anything else" and
- // is consumed into the next qualified rule's prelude, invalidating it. Only a block passes
- // as the stop token to "consume a qualified rule" (§5.5.3). The Acid2
- // parser-torture block relies on that: its ".parser { m\argin: 2em; };" is followed by
- // ".parser { height: 3em; }", which must NOT apply.
+ // CSS Syntax 3 §5.5.5. Not in CreateRules, at the top level (§5.5.1) a semicolon starts a
+ // qualified rule and invalidates it, Acid2 relies on that.
if (token.Type == TokenType.Semicolon)
{
token = NextToken();
diff --git a/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetParser.cs b/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetParser.cs
index fa60a6f57..7977e8b95 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetParser.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Parser/StylesheetParser.cs
@@ -120,7 +120,6 @@ internal async Task ParseAsync(Stylesheet sheet, TextSource source)
var tokenizer = new Lexer(source);
var start = tokenizer.GetCurrentPosition();
var builder = new StylesheetComposer(tokenizer, this);
- //var tasks = new List();
var end = builder.CreateRules(sheet);
var range = new TextRange(start, end);
sheet.StylesheetText = new StylesheetText(range, source);
@@ -131,7 +130,6 @@ internal async Task ParseAsync(Stylesheet sheet, TextSource source)
if (rule.Type != RuleType.Import) break;
}
- //await TaskEx.WhenAll(tasks).ConfigureAwait(false);
return sheet;
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs
index 7957e4d24..0ec9e01bb 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerRule.cs
@@ -3,9 +3,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The block form of @layer (@layer name { rules } or anonymous @layer { rules }).
- /// A grouping rule holding the style rules assigned to a cascade layer; cascade precedence between
- /// layers is resolved when the rules are indexed (see CssData), not here.
+ /// The block form of @layer, CSS Cascade 5 §6.4.4.1. CssData indexes its rules as
+ /// if they were unlayered.
///
internal sealed class LayerRule : GroupingRule, ILayerRule
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs
index e0bc077f9..d065656c2 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Rules/LayerStatementRule.cs
@@ -4,10 +4,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The statement form of @layer (@layer a, b, c;). It declares the relative order of
- /// one or more named cascade layers without providing any rules; a later block-form
- /// attaches rules to a layer already ordered by this statement, per
- /// CSS Cascade 5.
+ /// The statement form of @layer, CSS Cascade 5 §6.4.4.2. Parsed, but the layer order it
+ /// declares isn't used.
///
internal sealed class LayerStatementRule : Rule
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs
index a7675f8ae..893497a79 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Rules/PropertyRule.cs
@@ -4,10 +4,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// An @property at-rule (CSS Properties & Values API Level 1 §3): registers a typed custom
- /// property with a syntax, initial-value, and inherits descriptor. The descriptor
- /// block is stored like ; the registered name (a dashed-ident such as
- /// --my-color) is captured from the prelude like .
+ /// An @property rule (CSS Properties and Values API 1 §3). Parsed only, the registration has no
+ /// effect on the cascade.
///
internal sealed class PropertyRule : DeclarationRule, IPropertyRule
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs b/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs
index 7e38cbda2..9bfaadd37 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Rules/StyleRule.cs
@@ -6,9 +6,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal sealed class StyleRule : Rule, IStyleRule
{
- // CSS Nesting: rules written inside this rule's block, resolved to absolute selectors against
- // this parent. Kept in a dedicated list (not Children) so Selector/Style lookups and ToCss are
- // unaffected. Populated by StylesheetComposer.FillDeclarations.
+ // CSS Nesting 1 §3, with selectors already resolved against this rule. Not in Children, so Selector,
+ // Style and ToCss don't see them.
private readonly List _nestedRules = new List();
public StyleRule(StylesheetParser parser) : base(RuleType.Style, parser)
diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs
index 19be53893..43cf86b82 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/HasSelector.cs
@@ -3,11 +3,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The relational pseudo-class ":has(S)" - matches an element that has a descendant matching S.
- /// v1 only supports the default (descendant) relative-selector form; leading-combinator forms
- /// like ":has(> S)"/":has(+ S)"/":has(~ S)" are not supported (the parser silently discards a
- /// leading combinator today - see SelectorConstructor.Insert(ISelector) - and fixing that touches
- /// shared parsing code well beyond :has() itself, so it's an explicit, documented follow-up).
+ /// :has(), Selectors 4 §4.5. Only the descendant form, SelectorConstructor drops a leading
+ /// combinator.
///
internal sealed class HasSelector : StylesheetNode, ISelector
{
@@ -18,7 +15,7 @@ public HasSelector(ISelector inner)
public ISelector Inner { get; }
- // Per spec, :has() takes the specificity of its most specific argument, same as :is()/:not().
+ // Selectors 4 §15
public Priority Specificity => Inner.Specificity;
public string Text => this.ToCss();
diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs
index d93549ac9..a0c4581b7 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/ListSelector.cs
@@ -7,15 +7,8 @@ internal sealed class ListSelector : Selectors, ISelector
{
public bool IsInvalid { get; internal set; }
- // A comma-separated selector list's specificity is NOT the sum of its alternatives (that's
- // only correct for a CompoundSelector, whose members all apply to the SAME element at once).
- // Per spec, when used as the "forgiving selector list" argument to :is()/:not()/:has(), it's
- // the static max across all alternatives - regardless of which one (if any) actually matches
- // a given element. When a ListSelector is a style rule's own top-level selector instead
- // (".a, .b { }"), the spec treats it as shorthand for separate rules, so the specificity that
- // matters there is whichever ONE alternative matched a given box - that per-match resolution
- // is handled separately by CssData.GetMatchedSpecificity, which deliberately does not use this
- // property for that case.
+ // Selectors 4 §15, the most specific selector of an :is()/:not()/:has() argument. A rule's
+ // own selector list counts with the selector that matched, see CssData.GetMatchedSpecificity.
public override Priority Specificity =>
_selectors.Count == 0 ? Priority.Zero : _selectors.Max(s => s.Specificity);
diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs
index fe5403c58..f3edb4a17 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/MatchesSelector.cs
@@ -3,10 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The ":matches(S)"/":is(S)"/":where(S)" pseudo-class - matches an element that matches ANY
- /// selector in S. All three keywords share this same class (":is()" is the modern name,
- /// ":matches()" the legacy alias, ":where()" the zero-specificity variant);
- /// controls how it round-trips back to CSS text and, for ":where()", drops its specificity to zero.
+ /// :is(), its legacy alias :matches() and :where(), Selectors 4 §4.2 and §4.4.
///
internal sealed class MatchesSelector : StylesheetNode, ISelector
{
@@ -20,10 +17,7 @@ public MatchesSelector(ISelector inner, string keyword)
public string Keyword { get; }
- // Per CSS Selectors 4 §16: :is()/:matches() take the specificity of their most specific
- // argument (the static max, via ListSelector's own Specificity, when Inner is a
- // comma-separated list), while :where() always contributes zero specificity - this is what
- // lets a utility-framework reset authored in :where(...) be overridden by any real selector.
+ // Selectors 4 §15
public Priority Specificity =>
string.Equals(Keyword, PseudoClassNames.Where, System.StringComparison.OrdinalIgnoreCase)
? Priority.Zero
diff --git a/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs b/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs
index 551558773..48986d77f 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Selectors/NotSelector.cs
@@ -3,7 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The negation pseudo-class ":not(S)" - matches an element that does NOT match S.
+ /// :not(), Selectors 4 §4.3.
///
internal sealed class NotSelector : StylesheetNode, ISelector
{
@@ -14,8 +14,7 @@ public NotSelector(ISelector inner)
public ISelector Inner { get; }
- // Per spec, :not() takes the specificity of its argument (the most specific selector in a
- // comma-separated argument, via ListSelector's own static-max Specificity).
+ // Selectors 4 §15
public Priority Specificity => Inner.Specificity;
public string Text => this.ToCss();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs
index 7e0cb5e87..2917e270e 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/AspectRatioProperty.cs
@@ -1,9 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The aspect-ratio property (CSS Box Sizing Level 4). The value is validated by the shared
- /// and the authored text preserved for the layout engine, which computes
- /// an auto dimension from the definite one.
+ /// aspect-ratio, CSS Sizing 4 §4.1.
///
internal sealed class AspectRatioProperty : Property
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs
index 6e3c2d7e9..2798696c0 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Box/BoxShadowProperty.cs
@@ -2,9 +2,6 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
internal sealed class BoxShadowProperty : Property
{
- // The box-shadow grammar (none | [ inset? && {2,4} && ? ]#) is validated once by
- // the shared BoxShadowGrammar via BoxShadowValueConverter; the authored text is preserved so the
- // paint-time resolver (CssBox.PaintBoxShadows) sees exactly what was written.
private static readonly IValueConverter StyleConverter = new BoxShadowValueConverter().OrDefault();
internal BoxShadowProperty()
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs
index cf90c3d2a..0e71c5b2b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Columns/ColumnsProperty.cs
@@ -4,9 +4,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal sealed class ColumnsProperty : ShorthandProperty
{
- // columns is order-independent (CSS Multi-column 1 - "<'column-width'> || <'column-count'>"), and
- // both longhands accept "auto", so a positional match lets column-width claim "auto" and strand the
- // length in "columns: auto 12em". Match in any order, like list-style.
+ // CSS Multi-column 1 §3.3. Both longhands take "auto", a positional match would let column-width
+ // claim the "auto" of "columns: auto 12em" and strand the length.
private static readonly IValueConverter StyleConverter = WithAnyOrderIndependent(
AutoLengthConverter.Option().For(PropertyNames.ColumnWidth),
OptionalIntegerConverter.Option().For(PropertyNames.ColumnCount)).OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs
index b77b527d5..2d3368061 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAreaProperty.cs
@@ -1,10 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-area shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]{0,3},
- /// expanding to grid-row-start / grid-column-start / grid-row-end /
- /// grid-column-end. The single named-area form is out of v1 scope.
- ///
+ /// grid-area, CSS Grid 2 §8.4.
internal sealed class GridAreaProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.GridAreaConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs
index 9a6f6105a..a87f81d62 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoColumnsProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The grid-auto-columns property (CSS Grid §7.6): the size of implicitly-created columns.
+ /// grid-auto-columns, CSS Grid 2 §7.6.
internal sealed class GridAutoColumnsProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridAutoTracksValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs
index da7d1f48f..23b2ee980 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoFlowProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The grid-auto-flow property (CSS Grid §7.7): the auto-placement direction and packing.
+ /// grid-auto-flow, CSS Grid 2 §7.7.
internal sealed class GridAutoFlowProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridAutoFlowValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs
index d558f3a59..3824e2080 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridAutoRowsProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The grid-auto-rows property (CSS Grid §7.6): the size of implicitly-created rows.
+ /// grid-auto-rows, CSS Grid 2 §7.6.
internal sealed class GridAutoRowsProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridAutoTracksValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs
index 9552efa71..baff211cd 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnEndProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-column-end longhand (CSS Grid §8.3), one edge of a
- /// grid item's placement. Validated by the shared .
- ///
+ /// grid-column-end, CSS Grid 2 §8.3.
internal sealed class GridColumnEndProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs
index 9a0672ff0..97b789e54 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-column shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]?,
- /// expanding to grid-column-start / grid-column-end.
- ///
+ /// grid-column, CSS Grid 2 §8.4.
internal sealed class GridColumnProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.GridColumnConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs
index fee4a1a02..72bf6b915 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridColumnStartProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-column-start longhand (CSS Grid §8.3), one edge of a
- /// grid item's placement. Validated by the shared .
- ///
+ /// grid-column-start, CSS Grid 2 §8.3.
internal sealed class GridColumnStartProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs
index c8b932657..9d124d866 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid shorthand (CSS Grid §7.8): expands to the three grid-template-* longhands plus
- /// grid-auto-flow / grid-auto-rows / grid-auto-columns.
- ///
+ /// grid, CSS Grid 2 §7.8.
internal sealed class GridProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.GridConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs
index 6f3e1986a..e771a471d 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowEndProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-row-end longhand (CSS Grid §8.3), one edge of a
- /// grid item's placement. Validated by the shared .
- ///
+ /// grid-row-end, CSS Grid 2 §8.3.
internal sealed class GridRowEndProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs
index 55839a80f..4d92a4734 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-row shorthand (CSS Grid §8.3.1): <grid-line> [ / <grid-line> ]?,
- /// expanding to grid-row-start / grid-row-end.
- ///
+ /// grid-row, CSS Grid 2 §8.4.
internal sealed class GridRowProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.GridRowConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs
index 6ebbd6256..a4a7d2506 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridRowStartProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-row-start longhand (CSS Grid §8.3), one edge of a
- /// grid item's placement. Validated by the shared .
- ///
+ /// grid-row-start, CSS Grid 2 §8.3.
internal sealed class GridRowStartProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.GridLineConverter.OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs
index 33f1fcf86..bdf390075 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateAreasProperty.cs
@@ -1,10 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-template-areas property (CSS Grid §7.3). Validated by the shared
- /// ; the authored text is preserved for the grid layout engine,
- /// which derives implicit name-start/name-end lines from each named area.
- ///
+ /// grid-template-areas, CSS Grid 2 §7.3.
internal sealed class GridTemplateAreasProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridTemplateAreasValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs
index d7e110c79..3b08f0021 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateColumnsProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-template-columns property (CSS Grid Layout Module Level 1/2 §7.2). Validated by the
- /// shared ; the authored text is preserved for the grid layout engine.
- ///
+ /// grid-template-columns, CSS Grid 2 §7.2.
internal sealed class GridTemplateColumnsProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridTemplateValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs
index cea068bdf..b920ada57 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-template shorthand (CSS Grid §7.4): expands to grid-template-rows /
- /// grid-template-columns / grid-template-areas.
- ///
+ /// grid-template, CSS Grid 2 §7.4.
internal sealed class GridTemplateProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.GridTemplateConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs
index 754dde2b7..bc6958572 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/GridTemplateRowsProperty.cs
@@ -1,9 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-template-rows property (CSS Grid Layout Module Level 1/2 §7.2). Validated by the
- /// shared ; the authored text is preserved for the grid layout engine.
- ///
+ /// grid-template-rows, CSS Grid 2 §7.2.
internal sealed class GridTemplateRowsProperty : Property
{
private static readonly IValueConverter StyleConverter = new GridTemplateValueConverter().OrDefault();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs
index 9014a682d..5f0f1411a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifyItemsProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The justify-items property (CSS Box Alignment): default inline-axis alignment of grid items.
+ /// CSS Box Alignment 3 §7.1
internal sealed class JustifyItemsProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.JustifyItemsConverter;
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs
index 281659998..e22f19827 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/JustifySelfProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The justify-self property (CSS Box Alignment): a grid item's own inline-axis alignment.
+ /// CSS Box Alignment 3 §6.1
internal sealed class JustifySelfProperty : Property
{
private static readonly IValueConverter StyleConverter = Converters.JustifySelfConverter;
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs
index 954b116fe..50559aa1a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceContentProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The place-content shorthand (CSS Box Alignment): <align-content> <justify-content>?.
+ /// CSS Box Alignment 3 §5.2
internal sealed class PlaceContentProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.PlaceContentConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs
index 5ec9dc74c..0675eadbe 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceItemsProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The place-items shorthand (CSS Box Alignment): <align-items> <justify-items>?.
+ /// CSS Box Alignment 3 §7.3
internal sealed class PlaceItemsProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.PlaceItemsConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs
index 149ac0c7c..2be5023ef 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Grid/PlaceSelfProperty.cs
@@ -1,6 +1,6 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// The place-self shorthand (CSS Box Alignment): <align-self> <justify-self>?.
+ /// CSS Box Alignment 3 §6.3
internal sealed class PlaceSelfProperty : ShorthandProperty
{
private static readonly IValueConverter StyleConverter = Converters.PlaceSelfConverter.OrGlobalValue();
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs
index a2039dbf3..23478381e 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/List/ListStyleProperty.cs
@@ -4,9 +4,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal sealed class ListStyleProperty : ShorthandProperty
{
- // list-style's operands are order-independent (CSS Lists 3 - "in any order"), so a token such as
- // "none" that both list-style-type and list-style-image accept must not be claimed positionally
- // ("list-style: none square" must parse like "list-style: square none").
+ // CSS Lists 3 §3.6, a "none" goes to whichever of type and image isn't set otherwise, so it can't
+ // be claimed by position ("none square" parses like "square none").
private static readonly IValueConverter StyleConverter = WithAnyOrderIndependent(
ListStyleConverter.Option().For(PropertyNames.ListStyleType),
ListPositionConverter.Option().For(PropertyNames.ListStylePosition),
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs
index 121804ee7..03b1cfe4a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/StringSetProperty.cs
@@ -1,9 +1,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Represents the CSS string-set property from CSS Generated Content for Paged Media Module.
- /// Syntax: string-set: [ <custom-ident> <content-list> ]# | none
- /// where <content-list> = [ <string> | <counter()> | <counters()> | <content()> | <attr()> ]+
+ /// string-set (CSS Generated Content 3 §2.7.1).
///
internal sealed class StringSetProperty : Property
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveOriginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveOriginProperty.cs
index 1b2d37dec..b9ffb2aa7 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveOriginProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/PerspectiveOriginProperty.cs
@@ -4,10 +4,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal sealed class PerspectiveOriginProperty : Property
{
- // perspective-origin is a (CSS Transforms 1). As with transform-origin, the two-value
- // form is ordered (horizontal then vertical) and only the keyword-only form may
- // be reordered - so "2px left" / "top 2px" are invalid (a length forces the ordered form and the
- // other token is a keyword for the wrong axis).
+ // CSS Transforms 2 §9, only the one- and two-value forms. A length forces
+ // horizontal-then-vertical order, only keyword pairs may be swapped.
private static readonly IValueConverter Horizontal =
LengthOrPercentConverter
.Or(Keywords.Left, Length.Zero)
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs
index 244186094..3c36ceacf 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Transform/TransformOriginProperty.cs
@@ -4,14 +4,7 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
internal sealed class TransformOriginProperty : Property
{
- // CSS Transforms 1 (https://www.w3.org/TR/css-transforms-1/#transform-origin-property):
- // [ left | center | right | top | bottom | ]
- // | [ left | center | right | ] [ top | center | bottom | ] ?
- // | [ [ center | left | right ] && [ center | top | bottom ] ] ?
- //
- // The two-value form is ORDERED (horizontal then vertical). Only the keyword-only
- // form may be reordered ("center left" == "left center"). So "2px left" and "top 2px" are invalid: a
- // length in one axis forces the ordered form, and the remaining token is a keyword for the wrong axis.
+ // CSS Transforms 1 §5. A length forces horizontal-then-vertical order, only keyword pairs may be swapped.
private static readonly IValueConverter Horizontal =
LengthOrPercentConverter
.Or(Keywords.Left, Length.Zero)
@@ -34,28 +27,19 @@ internal sealed class TransformOriginProperty : Property
.Or(Keywords.Bottom, Length.Full)
.Or(Keywords.Center, Length.Half);
- // A one- or two-value with no z-offset (this is exactly the perspective-origin grammar).
private static readonly IValueConverter Position =
LengthOrPercentConverter.Or(Keywords.Center, Point.Center)
- // Ordered horizontal-then-vertical (rejects a keyword in the wrong axis position).
.Or(WithOrder(Horizontal.Option(Length.Half), Vertical.Option(Length.Half)))
- // Keyword-only form, any order ("center left"): a token accepted by both axes must not be
- // claimed positionally, so this needs order-independent matching.
+ // "center" fits both axes, so it must not be claimed positionally
.Or(WithAnyOrderIndependent(HorizontalKeyword.Option(Length.Half), VerticalKeyword.Option(Length.Half)));
- // A genuine two-value position: BOTH a horizontal and a vertical component must be present. The axis
- // converters are bare (not .Option), so each must actually consume a token. The z-offset only
- // exists in this two-value production, so gating the z-offset behind this (rather than the shared outer
- // WithOrder) is what makes a single-value position + length such as "top 2px" invalid.
+ // Both axes required, the z-offset only follows a two-value position ("top 2px" is invalid)
private static readonly IValueConverter TwoValuePosition =
WithOrder(Horizontal, Vertical)
.Or(WithAnyOrderIndependent(HorizontalKeyword, VerticalKeyword));
private static readonly IValueConverter StyleConverter =
- // Two-value position followed by a (required) z-offset - this branch only matches when a
- // third length token is actually present...
WithOrder(TwoValuePosition, LengthConverter)
- // ...otherwise a plain one- or two-value with no z-offset.
.Or(Position)
.OrDefault(Point.Center);
diff --git a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs
index 77125131d..ea3021c45 100644
--- a/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/StyleProperties/Visibility/ClipPathProperty.cs
@@ -1,15 +1,10 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The clip-path property (CSS Masking Module Level 1). The basic-shape grammar
- /// (polygon()/inset()/circle()/ellipse()) is resolved against the box's
- /// reference box at paint time, so Layer A only needs to accept and preserve the raw value text.
+ /// clip-path, CSS Masking 1 §5.1. Only none and a <basic-shape> without a <geometry-box>.
///
internal sealed class ClipPathProperty : Property
{
- // The basic-shape grammar (polygon()/inset()/circle()/ellipse()) plus the "none" keyword is
- // validated once by the shared BasicShapeGrammar via ClipPathValueConverter; the authored text
- // is preserved so the paint-time resolver (CssClipPathResolver) sees exactly what was written.
private static readonly IValueConverter StyleConverter = new ClipPathValueConverter().OrDefault();
internal ClipPathProperty()
diff --git a/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs b/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs
index bbb3f2af5..17ebb909a 100644
--- a/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/TextEncoding.cs
@@ -65,8 +65,6 @@ private static Encoding GetEncoding(string name)
{
}
-
- // Catch all since WP8 does throw a different exception than W*.
return Utf8;
}
diff --git a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
index fee373664..66f0cd0fa 100644
--- a/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/Tokens/RangeToken.cs
@@ -16,8 +16,7 @@ public RangeToken(string start, string end, TextPosition position)
End = end;
}
- // Data holds the range without its "U+" prefix, so serializing it bare would emit "41-5A" - not a
- // valid and not what was authored.
+ // Data is the range without "U+".
public override string ToValue()
{
return string.Concat("U+", Data);
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs
index 0abcced98..0de3402d5 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/AspectRatioValueConverter.cs
@@ -4,10 +4,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Validates an aspect-ratio value through the shared — accepting
- /// auto, a <ratio>, or auto && <ratio>, and rejecting everything
- /// else so an invalid value is dropped at parse time. The authored text is preserved so Layer B
- /// () re-parses the same value during layout.
+ /// Validates an aspect-ratio value (CSS Sizing 4 §4.1) and keeps the authored text.
///
internal sealed class AspectRatioValueConverter : IValueConverter
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs
index f254c0901..5e2e9ef85 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BackgroundPositionValueConverter.cs
@@ -4,9 +4,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Validates and serializes a single-layer background-position value via the shared
- /// (see that class for why this isn't a hand-written
- /// combinator tree like most other converters in this file).
+ /// Validates and serializes a single-layer background-position value through
+ /// .
///
internal sealed class BackgroundPositionValueConverter : IValueConverter
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs
index d67adef47..049acc252 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/BoxShadowValueConverter.cs
@@ -4,12 +4,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Validates a box-shadow value through the shared - accepting
- /// none or a comma-separated list of [ inset? && <length>{2,4} &&
- /// <color>? ] shadow layers, and rejecting everything else so an invalid value (e.g.
- /// box-shadow: banana) is dropped at parse time. The authored value text is preserved verbatim
- /// so the paint-time resolver (Layer B, CssBox.PaintBoxShadows) re-tokenizes and resolves the
- /// same value against the box - a single grammar shared across both layers.
+ /// Validates a box-shadow value with and keeps the authored text.
///
internal sealed class BoxShadowValueConverter : IValueConverter
{
@@ -31,7 +26,6 @@ public BoxShadowValue(IEnumerable tokens)
Original = new TokenValue(tokens);
}
- // Preserve the authored text verbatim - Layer B re-parses and resolves this string.
public string CssText => Original.Text;
public TokenValue Original { get; }
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs
index 1e60ffd15..b4b509c94 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ClipPathValueConverter.cs
@@ -4,11 +4,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Validates a clip-path value: accepts the keyword none or any <basic-shape>
- /// the shared recognizes (polygon()/inset()/circle()/
- /// ellipse()), and rejects everything else so an unknown value (e.g. clip-path: banana)
- /// is dropped at parse time. The authored value text is preserved verbatim (
- /// returns the original tokens) so the render-time resolver (Layer B) sees exactly what the author wrote.
+ /// Accepts none or a shape recognizes and keeps the authored tokens as the value.
///
internal sealed class ClipPathValueConverter : IValueConverter
{
@@ -36,7 +32,6 @@ public ClipPathValue(IEnumerable tokens)
Original = new TokenValue(tokens);
}
- // Preserve the authored text verbatim - Layer B re-tokenizes and resolves this string.
public string CssText => Original.Text;
public TokenValue Original { get; }
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs
index b48dbacd9..6c576fbbb 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ContentFunctionConverter.cs
@@ -4,8 +4,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// Converter for the content() function used in string-set property.
- /// Supports: content(text), content(before), content(after), content(first-letter)
+ /// content() (CSS Generated Content 3 §2.7.3), without the marker argument.
///
internal sealed class ContentFunctionConverter : IValueConverter
{
@@ -18,7 +17,6 @@ public IPropertyValue Convert(IEnumerable value)
if (!(first is FunctionToken funcToken) || !funcToken.Data.Equals(FunctionNames.Content, System.StringComparison.OrdinalIgnoreCase))
return null;
- // Default mode is "text" if no argument provided
var mode = "text";
if (funcToken.ArgumentTokens.Any())
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs
index 908e59aea..990f69815 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/ExtendedImageConverters.cs
@@ -6,11 +6,8 @@ namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
using static Converters;
///
- /// A validated-but-not-rendered image function (image-set()/cross-fade()/element(),
- /// CSS Images 4 §2/§3). The engine accepts these as syntactically-valid <image> values (so a
- /// property or @property registration using one is valid, per spec) but paints nothing for them —
- /// the render path (CssValueParser.ParseImage → CssImagePainter) only handles url()/gradients.
- /// This value carries the original tokens for serialization only.
+ /// An image-set(), cross-fade() or element() value, CSS Images 4 §2.4, §2.6 and §2.7.
+ /// Only validated and serialized, the painter doesn't handle them.
///
internal sealed class ImageFunctionValue : IPropertyValue
{
@@ -26,12 +23,13 @@ public ImageFunctionValue(IEnumerable tokens)
public TokenValue ExtractFor(string name) => Original;
}
- /// The inner <image> of an image-set()/cross-fade() option — a url() or a
- /// gradient. Nested image-set() isn't modeled (not a real authoring pattern); referencing the two
- /// base converters directly also avoids a construction cycle with ImageSourceConverter.
+ ///
+ /// The <image> inside image-set() and cross-fade(), only a url() or a gradient.
+ /// Using ImageSourceConverter here would be a construction cycle.
+ ///
internal static class ExtendedImage
{
- // Qualify GradientConverter — the bare name also resolves to the abstract GradientConverter *type*.
+ // Qualified, the bare name is the GradientConverter type
public static readonly IValueConverter InnerImage = UrlConverter.Or(Converters.GradientConverter);
}
@@ -40,11 +38,10 @@ internal sealed class ElementImageConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
{
- // ToArray (LINQ) — a bare ToList() on IEnumerable resolves to the CSS comma-group splitter.
+ // ToList() on tokens is the comma group splitter
var tokens = value.Where(t => t.Type != TokenType.Whitespace).ToArray();
- // An '#id' selector is a single (CSS Syntax §4). A '#f00'-shaped id lexes as a Color
- // token, any other id as a Hash token; both are the single '#'-prefixed form.
+ // An id that looks like a hex color, like #f00, is tokenized as a color
return tokens.Length == 1 && (tokens[0].Type == TokenType.Hash || tokens[0].Type == TokenType.Color)
? new ImageFunctionValue(value)
: null;
@@ -72,8 +69,6 @@ private static bool IsOption(List option)
if (StringConverter.Convert(items[0]) == null && ExtendedImage.InnerImage.Convert(items[0]) == null)
return false;
- // Any remaining items are a (incl. the `x` dppx alias, modeled by Resolution.GetUnit)
- // and/or a type(), in either order.
for (var i = 1; i < items.Count; i++)
if (ResolutionConverter.Convert(items[i]) == null && !IsTypeFunction(items[i]))
return false;
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs
index 4090331f1..6475ccdaa 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GradientConverter.cs
@@ -20,11 +20,7 @@ public IPropertyValue Construct(Property[] properties)
return properties.Guard();
}
- ///
- /// The converter used to validate a colour stop's position. Length/percentage for linear and
- /// radial gradients (the default); overridden by , whose
- /// angular stops are positioned by an <angle> or <percentage> instead.
- ///
+ /// Validates a color stop position, a length or percentage unless overridden.
protected virtual IValueConverter StopPositionConverter => LengthOrPercentConverter;
private IPropertyValue[] ToGradientStops(List> values, int offset)
@@ -55,13 +51,8 @@ private IPropertyValue ToGradientStop(List value)
if (firstPosition != null) items.RemoveAt(items.Count - 1);
}
- // = {1,2} (CSS Images 4 §3.5.1): a stop may carry two
- // positions, equivalent to two same-colour stops, one at each position. Parsing right-to-left,
- // the position taken above is the second (rightmost); a position immediately before it is the
- // first. Both are kept, in source order, so the value round-trips as authored - the render
- // layer (CssValueParser.ParseLinearGradient) reads the property's serialized value and already
- // expands two positions into two stops, so dropping one here collapsed the solid band it draws.
- // (For conic gradients the same two-position shorthand applies, positioned by .)
+ // CSS Images 4 §3.5.1: a stop can have two positions. Both are kept, the painter reads the
+ // serialized value.
if (firstPosition != null && items.Count != 0)
{
var earlier = StopPositionConverter.Convert(items[items.Count - 1]);
@@ -81,8 +72,7 @@ private IPropertyValue ToGradientStop(List value)
if (color != null) items.RemoveAt(items.Count - 1);
}
- // The two-position form is only defined for a ; a bare with no
- // colour is a , which takes exactly one position.
+ // A position without a color is a color hint, which only takes one position
if (secondPosition != null && color == null) return null;
return items.Count == 0 ? new StopValue(color, firstPosition, secondPosition, value) : null;
@@ -127,9 +117,7 @@ public TokenValue ExtractFor(string name)
}
}
- // Factory used by derived classes to return an opaque placeholder IPropertyValue that echoes
- // its tokens back as CssText - for a group whose grammar is accepted but not further modelled
- // here (e.g. an "in " modifier, or a conic bare-0 / calc() angular stop position).
+ // A validated value that isn't modeled further, serialized as written
protected static IPropertyValue CreatePlaceholder(IEnumerable tokens)
=> new PlaceholderValue(tokens);
@@ -193,8 +181,7 @@ public LinearGradientConverter()
protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
{
- // "[ | to ] || " (CSS Images 4). Validate
- // any "in []" prelude, then validate what remains as the direction.
+ // CSS Images 4 §3.1
if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn))
return null;
@@ -205,32 +192,24 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
}
}
- // Validates conic-gradient() at parse time (mirroring RadialGradientConverter), so a malformed
- // value is dropped per CSS Cascade §4.1 instead of silently painting nothing at render. The
- // grammar matches CssValueParser.ParseConicGradient exactly.
+ // CSS Images 4 §3.3.1
internal sealed class ConicGradientConverter : GradientConverter
{
private readonly IValueConverter _prelude;
public ConicGradientConverter()
{
- // [ from ]? [ at ]? — same combinator shape RadialGradientConverter
- // uses for its own prelude, just with conic's from/at keywords.
_prelude = WithAny(
AngleConverter.StartsWithKeyword(Keywords.From).Option(),
PointConverter.StartsWithKeyword(Keywords.At).Option(Point.Center));
}
- // A conic is positioned by | , with the same bare-0
- // and calc()-family acceptance the renderer's TryParseConicAngle allows (AngleConverter already
- // covers calc()-typed angles; a length such as 5px is correctly rejected).
+ // |
protected override IValueConverter StopPositionConverter { get; } =
AngleConverter.Or(PercentConverter).Or(new ZeroOrCalcPositionConverter());
protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
{
- // "[ from ]? [ at ]? || " — validate any
- // "in []" prelude, then validate what remains as from/at.
if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn))
return null;
@@ -242,8 +221,7 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
private IPropertyValue ConvertPrelude(IEnumerable value)
{
- // If the first non-whitespace token is an ident that is not a prelude keyword, this comma
- // group is a color stop, not a prelude — return null so it flows to stop validation.
+ // A group starting with any other ident is the first color stop
Token first = null;
foreach (var t in value)
{
@@ -260,9 +238,7 @@ private IPropertyValue ConvertPrelude(IEnumerable value)
return _prelude.Convert(value);
}
- // Accepts a bare `0` (the universal zero, which otherwise requires a unit for) or a
- // single calc()-family function used as an angular stop position — matching the corresponding
- // branches of CssValueParser.TryParseConicAngle, using the same primitives.
+ // A unitless 0, or any calc()-family function
private sealed class ZeroOrCalcPositionConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
@@ -306,8 +282,7 @@ public RadialGradientConverter()
protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
{
- // "[ || ]? [ at ]? || " — validate
- // any "in []" prelude, then validate what remains as shape/size/position.
+ // CSS Images 4 §3.2.1
if (!ColorInterpolationMethodGrammar.TryExtractInterpolationMethod(value, out var remainder, out var hasIn))
return null;
@@ -319,9 +294,7 @@ protected override IPropertyValue ConvertFirstArgument(IEnumerable value)
private IPropertyValue ConvertShape(IEnumerable value)
{
- // If the first non-whitespace token is an ident that is not a known gradient
- // shape/size keyword, it must be a CSS color name (e.g. "red" in "red 0 8px").
- // In that case, this comma group is a color stop, not a shape/size modifier.
+ // A group starting with any other ident is the first color stop
Token first = null;
foreach (var t in value)
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs
index e989e5811..f07193eac 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAreaShorthandValueConverter.cs
@@ -2,12 +2,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid-area shorthand: <grid-line> [ / <grid-line> ]{0,3} in the order
- /// row-start / column-start / row-end / column-end. Implements the CSS Grid §8.3.1 omitted-value copy
- /// rule — a bare <custom-ident> propagates to the paired/all edges, so grid-area: main
- /// fills the whole main area, while grid-area: 2 leaves the other edges auto.
- ///
+ /// grid-area, CSS Grid 2 §8.4.
internal sealed class GridAreaShorthandValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
@@ -16,13 +11,10 @@ public IPropertyValue Convert(IEnumerable value)
if (c is null) return null;
var rowStart = c[0];
- // column-start omitted → if row-start is a bare custom-ident, all four take it; else auto.
var colStart = c.Count > 1 ? c[1]
: rowStart.IsBareCustomIdent ? rowStart : (GridPlacementShorthand.Component?)null;
- // row-end omitted → copy a bare custom-ident row-start, else auto.
var rowEnd = c.Count > 2 ? c[2]
: rowStart.IsBareCustomIdent ? rowStart : (GridPlacementShorthand.Component?)null;
- // column-end omitted → copy a bare custom-ident column-start, else auto.
var colEnd = c.Count > 3 ? c[3]
: colStart.HasValue && colStart.Value.IsBareCustomIdent ? colStart : (GridPlacementShorthand.Component?)null;
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs
index 9211cc287..baeece372 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoFlowValueConverter.cs
@@ -3,11 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// Validates a grid-auto-flow value (CSS Grid §7.7): [ row | column ] || dense — one of
- /// row/column optionally combined with dense, in either order. The authored text is
- /// preserved for the grid layout engine.
- ///
+ /// grid-auto-flow, CSS Grid 2 §7.7.
internal sealed class GridAutoFlowValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
@@ -23,7 +19,7 @@ public IPropertyValue Convert(IEnumerable value)
{
if (t.Data.Isi(Keywords.Row) || t.Data.Isi(Keywords.Column))
{
- if (hasAxis) return null; // row and column are mutually exclusive
+ if (hasAxis) return null;
hasAxis = true;
}
else if (t.Data.Isi(Keywords.Dense))
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs
index 4003b1d7a..6a35f47ba 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridAutoTracksValueConverter.cs
@@ -3,11 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// Validates a grid-auto-columns/grid-auto-rows value: a <track-size>+ list
- /// (no repeat()) via the shared . The authored text is preserved
- /// for the grid layout engine.
- ///
+ /// grid-auto-columns/-rows, CSS Grid 2 §7.6.
internal sealed class GridAutoTracksValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs
index 8287de6c7..c50a47e04 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridColumnRowShorthandValueConverter.cs
@@ -3,11 +3,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The grid-column / grid-row shorthand: <grid-line> [ / <grid-line> ]?
- /// expanding to the axis's -start/-end longhands. Implements the CSS Grid §8.3.1
- /// omitted-value copy rule that the generic WithOrder(...).Option() DSL cannot: when the end is
- /// omitted, it copies a bare <custom-ident> start (so grid-column: main spans the
- /// whole main area) but leaves a line number / span as auto.
+ /// grid-row/grid-column, CSS Grid 2 §8.4. Not built from WithOrder(...).Option(),
+ /// that resets an omitted end to auto instead of copying a bare <custom-ident>.
///
internal sealed class GridColumnRowShorthandValueConverter : IValueConverter
{
@@ -26,7 +23,6 @@ public IPropertyValue Convert(IEnumerable value)
if (components is null) return null;
var start = components[0];
- // end omitted → copy a bare custom-ident start, else auto.
var end = components.Count > 1 ? components[1]
: start.IsBareCustomIdent ? start : (GridPlacementShorthand.Component?)null;
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs
index 91a845e90..d37e42c28 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridLineValueConverter.cs
@@ -3,11 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// Validates a single grid <grid-line> (grid-column-start/-end,
- /// grid-row-start/-end) through the shared — auto, an
- /// integer line, or span N. The authored text is preserved for the grid placement engine.
- ///
+ /// grid-row-start/-end, grid-column-start/-end, CSS Grid 2 §8.3.
internal sealed class GridLineValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs
index a50980dd4..9a6626592 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridPlacementShorthand.cs
@@ -3,13 +3,10 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- /// Shared helpers for the grid placement shorthands (grid-column/grid-row/
- /// grid-area): splitting a value on / into <grid-line> components, validating
- /// each, and building the omitted-value copy result (CSS Grid §8.3.1).
+ /// Helpers for grid-row, grid-column and grid-area, CSS Grid 2 §8.4.
internal static class GridPlacementShorthand
{
- /// A parsed <grid-line> component: its authored tokens and whether it is a bare
- /// <custom-ident> (the only form the omitted-value copy rule propagates).
+ /// A <grid-line>, only a bare <custom-ident> is copied to omitted lines.
internal struct Component
{
public IReadOnlyList Tokens { get; set; }
@@ -18,9 +15,7 @@ internal struct Component
private static readonly Token AutoToken = new Token(TokenType.Ident, Keywords.Auto, TextPosition.Empty);
- /// Splits the value on top-level / delimiters into <grid-line> components,
- /// validating each via . Returns null on any invalid component or an
- /// out-of-range component count.
+ /// Splits on /, null if a component is invalid or there are too many.
internal static List Split(IEnumerable value, int maxComponents)
{
var groups = new List> { new List() };
@@ -37,9 +32,7 @@ internal static List Split(IEnumerable value, int maxComponent
var significant = group.Where(t => t.Type != TokenType.Whitespace).ToArray();
var parsed = GridLineGrammar.TryParse(significant);
if (parsed is null) return null;
- // Preserve internal whitespace in the stored tokens (only trim the ends) so a two-token
- // component like "span 2" round-trips as "span 2", not "span2" (which would re-lex as one
- // ident). A bare is a single ident token that parsed to a named (non-Nth) line.
+ // Keep the inner whitespace, "span2" would lex as one ident.
components.Add(new Component
{
Tokens = Trim(group),
@@ -50,13 +43,11 @@ internal static List Split(IEnumerable value, int maxComponent
return components.Count >= 1 && components.Count <= maxComponents ? components : null;
}
- /// The tokens for a longhand: the specified component's tokens, or auto when omitted.
internal static IReadOnlyList Or(Component? component)
{
return component.HasValue ? component.Value.Tokens : new[] { AutoToken };
}
- /// Drops leading and trailing whitespace tokens, keeping internal whitespace.
private static IReadOnlyList Trim(List group)
{
var start = 0;
@@ -67,8 +58,7 @@ private static IReadOnlyList Trim(List group)
}
}
- /// A shorthand that emits a precomputed token stream for each of
- /// its longhands (via ), used by the grid placement shorthands.
+ /// A shorthand value with precomputed tokens per longhand.
internal sealed class MappedShorthandValue : IPropertyValue
{
private readonly IReadOnlyDictionary> _map;
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs
index 34969d11b..a4be38913 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridShorthandValueConverter.cs
@@ -3,24 +3,14 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// The grid shorthand (CSS Grid §7.8): either a <grid-template>, or one of the two
- /// auto-flow forms
- /// <grid-template-rows> / [ auto-flow && dense? ] <grid-auto-columns>? and
- /// [ auto-flow && dense? ] <grid-auto-rows>? / <grid-template-columns>. Expands to
- /// the three grid-template-* longhands plus grid-auto-flow/-rows/-columns;
- /// every longhand it doesn't set is reset to its initial value (an absent slice — §7.8).
- ///
+ /// grid, CSS Grid 2 §7.8. A longhand without a slice is reset to its initial value.
internal sealed class GridShorthandValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
{
- // NB: the domain-specific IEnumerable.ToList() extension splits on commas; materialize the
- // flat token list explicitly instead.
+ // Not ToList(), the IEnumerable extension of that name splits on commas.
var tokens = new List(value);
- // Form 1: . The three template longhands are set; grid-auto-flow/-rows/-columns
- // are omitted here so they reset to their initial values (row / auto / auto).
if (GridTemplateShorthand.TryParseTemplate(tokens, out var rows, out var columns, out var areas))
{
var templateMap = new Dictionary>();
@@ -30,7 +20,6 @@ public IPropertyValue Convert(IEnumerable value)
return new MappedShorthandValue(templateMap, tokens);
}
- // Forms 2/3: an auto-flow form — requires exactly one top-level '/'.
var groups = GridTemplateShorthand.SplitTopLevelSlash(tokens);
if (groups.Count != 2) return null;
@@ -41,7 +30,7 @@ public IPropertyValue Convert(IEnumerable value)
var leftHasFlow = left.Any(t => t.Type == TokenType.Ident && t.Data.Isi(Keywords.AutoFlow));
var rightHasFlow = right.Any(t => t.Type == TokenType.Ident && t.Data.Isi(Keywords.AutoFlow));
- if (leftHasFlow == rightHasFlow) return null; // exactly one side carries auto-flow
+ if (leftHasFlow == rightHasFlow) return null;
var map = new Dictionary>();
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs
index 491ba7ab8..dc646aac5 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateAreasValueConverter.cs
@@ -3,12 +3,7 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// Validates a grid-template-areas value: the keyword none or a rectangular grid of quoted
- /// strings accepted by the shared (equal columns per row, each
- /// named area a single filled rectangle). The authored text is preserved so the grid layout engine
- /// re-parses the same value.
- ///
+ /// grid-template-areas, CSS Grid 2 §7.3.
internal sealed class GridTemplateAreasValueConverter : IValueConverter
{
public IPropertyValue Convert(IEnumerable value)
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs
index 045d99fe6..85333c25b 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthand.cs
@@ -3,25 +3,15 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
- ///
- /// Shared parsing for the CSS Grid mega-shorthands grid-template (§7.4) and grid (§7.8): splitting the value on the
- /// top-level /, running the areas-form row scanner, and validating each partitioned slice
- /// against the existing longhand grammars (/). The two converters stay thin over this helper.
- ///
+ /// Parsing shared by grid-template (CSS Grid 2 §7.4) and grid (§7.8).
internal static class GridTemplateShorthand
{
internal static readonly Token AutoToken = new Token(TokenType.Ident, Keywords.Auto, TextPosition.Empty);
private static readonly Token NoneToken = new Token(TokenType.Ident, Keywords.None, TextPosition.Empty);
///
- /// Splits the flat token stream on top-level / delimiters, preserving each group's
- /// whitespace. No bracket/paren depth counter is needed: functions are single s (their args nested) and [name] groups hold only idents, so a
- /// / never appears nested at the flat level (same reasoning as
- /// ).
+ /// No nesting depth needed, a function is a single and [name]
+ /// groups only hold idents.
///
internal static List> SplitTopLevelSlash(IEnumerable value)
{
@@ -39,11 +29,7 @@ internal static List> SplitTopLevelSlash(IEnumerable value)
internal static Token[] Significant(IEnumerable tokens) =>
tokens.Where(t => t.Type != TokenType.Whitespace).ToArray();
- ///
- /// Validates one axis of the <rows> / <columns> form — a <track-list> or
- /// the keyword none (both accepted by the grid-template-rows/-columns longhands) —
- /// returning the edge-trimmed slice, or if invalid.
- ///
+ /// One side of <rows> / <columns>, trimmed, or null if invalid.
internal static IReadOnlyList TryTemplateAxis(IReadOnlyList raw)
{
var significant = Significant(raw);
@@ -55,7 +41,6 @@ internal static IReadOnlyList TryTemplateAxis(IReadOnlyList raw)
private static bool ContainsRepeat(IEnumerable significant) =>
significant.Any(t => t is FunctionToken fn && fn.Data.Isi(FunctionNames.Repeat));
- /// Drops leading/trailing whitespace tokens, keeping internal whitespace.
internal static IReadOnlyList Trim(IReadOnlyList tokens)
{
var start = 0;
@@ -68,7 +53,6 @@ internal static IReadOnlyList Trim(IReadOnlyList tokens)
return result;
}
- /// The grid-auto-flow token slice for the grid auto-flow forms.
internal static IReadOnlyList BuildAutoFlow(bool column, bool dense)
{
var flow = new List { new Token(TokenType.Ident, column ? Keywords.Column : Keywords.Row, TextPosition.Empty) };
@@ -80,25 +64,20 @@ internal static IReadOnlyList BuildAutoFlow(bool column, bool dense)
return flow;
}
- ///
- /// Parses a grid-template value (§7.4) into the three longhand slices. A
- /// out-slice means "omit it" (the longhand resets to its initial value). Returns
- /// for an invalid value (the whole declaration is then dropped).
- ///
+ /// A null slice means the longhand is reset to its initial value.
internal static bool TryParseTemplate(IEnumerable value,
out IReadOnlyList rows, out IReadOnlyList columns, out IReadOnlyList areas)
{
rows = columns = areas = null;
var groups = SplitTopLevelSlash(value);
- if (groups.Count > 2) return false; // at most one top-level '/'
+ if (groups.Count > 2) return false;
var leftRaw = groups[0];
var rightRaw = groups.Count == 2 ? groups[1] : null;
var left = Significant(leftRaw);
var right = rightRaw is null ? null : Significant(rightRaw);
- // grid-template: none → all three longhands none.
if (right == null && left.Length == 1 && left[0].Type == TokenType.Ident && left[0].Data.Isi(Keywords.None))
{
rows = columns = areas = new[] { NoneToken };
@@ -109,7 +88,6 @@ internal static bool TryParseTemplate(IEnumerable value,
if (left.Any(t => t.Type == TokenType.String))
{
- // Areas form: [ ? ? ? ]+ [ / ]?
if (!TryScanAreasForm(left, out var rowsTokens, out var areaTokens)) return false;
if (GridTemplateAreasGrammar.TryParse(areaTokens) is null) return false;
if (GridTrackListGrammar.TryParse(rowsTokens) is null) return false;
@@ -119,7 +97,7 @@ internal static bool TryParseTemplate(IEnumerable value,
if (rightRaw != null)
{
- // The trailing columns are an — a with no repeat().
+ // , no repeat()
if (right.Length == 0 || ContainsRepeat(right) || GridTrackListGrammar.TryParse(rightRaw) is null)
return false;
columns = Trim(rightRaw);
@@ -128,7 +106,6 @@ internal static bool TryParseTemplate(IEnumerable value,
return true;
}
- // rows / columns form: requires the slash; each side is a or `none`.
if (rightRaw is null) return false;
var rowsSlice = TryTemplateAxis(leftRaw);
var columnsSlice = TryTemplateAxis(rightRaw);
@@ -140,10 +117,8 @@ internal static bool TryParseTemplate(IEnumerable value,
}
///
- /// Scans the areas-form left side, synthesizing the grid-template-rows track list
- /// () and collecting the ordered <string> area tokens
- /// (). Every emitted token is whitespace-separated so both round-trip
- /// serialization (via ToText) and re-parsing are valid.
+ /// Splits the areas form into the grid-template-rows track list and the area strings, a row
+ /// without a track size gets auto.
///
private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out List areaStrings)
{
@@ -158,8 +133,6 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out
if (t.Type == TokenType.SquareBracketOpen)
{
- // Copy the [ name … ] line-name group (idents only) at the current line position, kept
- // compact as `[a b]` (edge spaces omitted, single space between names).
if (rowsOut.Count > 0) rowsOut.Add(Token.Whitespace);
rowsOut.Add(t);
i++;
@@ -172,8 +145,8 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out
firstName = false;
i++;
}
- if (i >= toks.Length) return false; // unclosed '['
- rowsOut.Add(toks[i]); // ']'
+ if (i >= toks.Length) return false;
+ rowsOut.Add(toks[i]);
i++;
continue;
}
@@ -184,7 +157,6 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out
sawString = true;
i++;
- // Optional single-token for this row (else the row track is `auto`).
if (i < toks.Length && toks[i].Type != TokenType.SquareBracketOpen && toks[i].Type != TokenType.String)
{
if (GridTrackListGrammar.TryParseTrackSizeList(new[] { toks[i] }) is null) return false;
@@ -199,7 +171,6 @@ private static bool TryScanAreasForm(Token[] toks, out List rowsOut, out
continue;
}
- // A track-size or stray token with no preceding string at this position is invalid.
return false;
}
@@ -213,9 +184,8 @@ private static void Emit(List tokens, Token token)
}
///
- /// Parses the [ auto-flow && dense? ] <track-size>* side of a grid auto-flow
- /// form. Returns when auto-flow is absent or the value is malformed;
- /// is null when the optional track-size list is omitted (reset to initial).
+ /// The [ auto-flow && dense? ] <track-size>* side of grid, a null
+ /// means it's reset to its initial value.
///
internal static bool TryParseAutoFlowSide(IReadOnlyList raw, out bool dense, out IReadOnlyList autoTracks)
{
diff --git a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs
index 82537bac0..c2b99c572 100644
--- a/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs
+++ b/Source/HtmlRenderer/Core/CssEngine/ValueConverters/GridTemplateShorthandValueConverter.cs
@@ -3,9 +3,8 @@
namespace TheArtOfDev.HtmlRenderer.Core.CssEngine
{
///
- /// The