Add a real, spec-compliant CSS3/4 engine (ported from ExCSS via PeachPDF)#244
Open
jhaygood86 wants to merge 21 commits into
Open
Add a real, spec-compliant CSS3/4 engine (ported from ExCSS via PeachPDF)#244jhaygood86 wants to merge 21 commits into
jhaygood86 wants to merge 21 commits into
Conversation
Port of CSS engine from PeachPDF
The Demo app depending on 2 HTML-Renderer extensions for background gradients and border radius. This ports a minimal version of the CSS spec compliant versions from PeachPDF
Fixes <br>
jhaygood86
marked this pull request as ready for review
July 17, 2026 01:58
Author
|
If this is merged after #243 , there's a small one line fix needed for this to swap out one use of the deprecated SubString class with just string. |
Brings the vendored CSS engine's name tables and leaf value types up to PeachPDF v0.9.6 (669ec1e), from the e5842c3 snapshot the port was taken at. - PropertyNames: grid, logical box-model, aspect-ratio, font-palette and the @property/@font-palette-values descriptors - FunctionNames: basic shapes, grid functions, lab/oklab/lch/oklch/color-mix, image-set/cross-fade/element - Keywords, FeatureNames (the prefers-* set), PseudoClassNames (:where), RuleNames and RuleType (@Property, @layer, @font-palette-values) - Combinators: static readonly -> const so they can be used as constant patterns in switch statements - RangeToken: drop the eagerly-expanded SelectedRange in favour of a ToValue() override that re-emits the "U+" prefix - Resolution: accept `x` as the alias for `dppx` - Length: reject arbitrary non-length input parsing as zero in TryParse Length also gets an HTML-Renderer-specific adaptation rather than the upstream change. Upstream resolves absolute units against points (its layout unit); this renderer's layout unit is the CSS pixel, and CssValueParser.ParseLength already resolves 1in as 96px. The vendored table disagreed - `calc(1in)` gave 72 where `width: 1in` gave 96 - so the absolute-unit arms of ToPixels and the inverse arms of To(Unit) are now 96dpi (CSS Values & Units 5.2), matching ParseLength exactly. The fontAdjust px->pt hook is kept, since it mirrors ParseLength's own font-size output flag. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the color-space math and color-function resolution from PeachPDF v0.9.6 (upstream cc1a521, 796bb8c, 7cf1855). - ColorSpaceMath: forward/reverse transforms between gamma-encoded sRGB and srgb-linear/display-p3/lab/oklab/xyz-d65/xyz-d50/hsl/hwb/lch/oklch, plus polar hue interpolation (shorter/longer/increasing/decreasing) - Color: FromLab/FromOklab/FromLch/FromOklch factories and Mix(), the premultiplied-alpha color-mix() interpolation - ColorFunctionExtensions: ToResolvedColor(), which resolves a tokenized value through every color function - rgb/hsl/hwb/gray plus lab/oklab/lch/oklch and color-mix() - and MixPaletteColors() for palette-mix() - ColorInterpolationMethodGrammar: the shared `in <space> [<dir> hue]` prelude grammar, so the value converter and the render-time parser agree on which gradient interpolation methods are valid Downgraded to C# 7.3 for the netstandard2.0 leg: collection expressions, switch expressions, property patterns, index-from-end and Math.Clamp. Math.Cbrt is netstandard2.1+, so ColorSpaceMath carries a sign-preserving cube root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the @Property at-rule (CSS Properties & Values API Level 1) and the calc node types its syntax validation needs, from PeachPDF v0.9.6 (upstream 94f8d94, 008b387, d86abc7, 020c23b, dd7a5ae). - PropertyRule / IPropertyRule, the composer dispatch for `@property --x { }`, and the syntax/initial-value/inherits descriptor table in PropertyFactory (stored raw via UnknownProperty, since their grammar depends on the syntax descriptor itself) - TimeCalcNode / ResolutionCalcNode leaves, threaded through CalcParser, CalcTypeChecker (new Time and Resolution categories), CalcEvaluator and CalcSerializer, plus the previously missing AngleCalcNode evaluation arm CalcContext keeps its fontAdjust flag and CalcSerializer keeps folding straight to px, rather than taking upstream's points-based conversion: Length.ToPixels resolves to CSS pixels here, so no px<->pt round trip belongs in either. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds CSS Logical Properties and Values Level 1 from PeachPDF v0.9.6 (upstream 2fdde30): margin-block/-inline, padding-block/-inline, inset and its block/ inline forms, and the border-block/-inline width/style/colour family, with all their per-edge longhands. Layout here is always LTR / horizontal-tb, so each logical property is registered against the existing *physical* property class - margin-inline-start produces margin-left, inset-block-start produces top, and so on. StyleDeclaration keys by the produced property's name, so these land in the cascade as the physical longhands that the box model already consumes: no downstream change, and no renderer work needed for them to take effect. The logical shorthands are registered through a new AddLogicalShorthand, which also records them in _logicalShorthands so GetShorthands skips them when reconstructing shorthand text. Without that, the serializer could collapse margin-top + margin-bottom into `margin-block` and change existing output. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three independent value grammars from PeachPDF v0.9.6 (upstream cd6d5d7, ec398a8, 893f7b8, a956a7e, 90890e4). - BasicShapeGrammar + ClipPathValueConverter + ClipPathProperty: polygon(), inset() with optional round radius, circle() and ellipse(), including the fill-rule and `at <position>` forms - AspectRatioGrammar + AspectRatioValueConverter + AspectRatioProperty: `<ratio>` and `auto && <ratio>` - BoxShadowGrammar + BoxShadowValueConverter, replacing the previous box-shadow validation with the full comma-separated layer grammar, including the hex-token normalization needed because the render-time tokenizer is not in value mode Downgraded to C# 7.3: init-only setters become private setters, plus the usual list/property/relational patterns, collection expressions and target-typed new. These are parse-and-cascade only for now; the renderer has no clip region, shadow draw handler or intrinsic-sizing hook to consume them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The converter layer from PeachPDF v0.9.6 (upstream 1068034, 4662771, 7116636, ad3e2c4, 8f84648, 24074e3, f0c862e, 11d13b1, 7cf1855, plus the grid grammar work in 4319393, fb7a769, 12ec795). - OrderIndependentOptionsConverter, and the matching rewrite of UnorderedOptionsConverter: upstream deliberately narrowed order-independent shorthand matching, restoring greedy declaration-order matching for the general `||` shorthands and scoping the permutation search to list-style, columns, transform-origin and perspective-origin - GradientConverter: conic gradients and <color-interpolation-method> validation, plus two-position colour stops preserved in the serialized text - ExtendedImageConverters: image-set(), cross-fade() and element() - Model/Converters.cs: the lab/oklab/lch/oklch/color-mix converter singletons (which is what makes CSS Color 4/5 values actually reach the cascade), the lenient rgb/hsl forms, place-*/justify-* and the grid converters - CssProperty<T> / ITypedPropertyValue<T>: the typed cascade value that carries a parsed model alongside its text, with grid templates as the first adopter - The grid grammars (track list, line, template areas) and their value converters, pulled in here because Model/Converters.cs references them The grid *properties* and their factory registrations land separately, so grid values are not yet accepted - that is the next commit. Downgraded to C# 7.3 throughout: init-only setters, list/relational/property patterns, target-typed new, collection expressions, using declarations and index-from-end. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The grid property set from PeachPDF v0.9.6 (upstream 4319393 and the follow-up grid work in fb7a769, 12ec795, 253de51): grid-template-columns/-rows/-areas, the grid-column/-row/-area placement shorthands, grid-auto-flow/-columns/-rows, the `grid` and `grid-template` mega-shorthands, and justify-items/-self plus the place-items/-content/-self shorthands. grid-template-columns/-rows carry a parsed GridTemplate through the cascade via CssProperty<T>, rather than only their authored text. The `grid` and `grid-template` shorthands are registered through AddLogicalShorthand so the serializer will not try to reconstruct them from their longhands - their multi-slash and areas grammar is not worth rebuilding and doing so could change existing output. Parse-and-cascade only: HTML-Renderer has no grid layout engine, so `display: grid` still lays out as a block and these declarations are ignored at the box boundary. Wiring them up needs a layout engine and is a separate piece of work. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From PeachPDF v0.9.6 (upstream cc1a521, 607bcce). - LayerRule / LayerStatementRule / ILayerRule: both @layer forms - the block form that groups rules and the statement form that only declares an order - Seven new media features: prefers-color-scheme, prefers-reduced-motion, prefers-contrast, prefers-reduced-transparency, update, any-hover, any-pointer - MediaFeature gains a Comparison property (derived from the min-/max- prefix or the range-syntax operator) plus AsLength/AsResolution/AsRatio accessors, which is what a media-query matcher needs to actually evaluate a feature Layer *ordering* is not implemented - see the separate CssData change that indexes a layer's contents as unlayered rules. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From PeachPDF v0.9.6 (upstream 2bd95e9). - FontPaletteValuesRule / IFontPaletteValuesRule and their descriptor table (font-family / base-palette / override-colors, stored raw) - The font-palette property and its value converter - PaletteMixGrammar: the palette-mix() grammar, sharing colour-space and hue-direction classification with ColorInterpolationMethodGrammar so the two functions cannot disagree about which spaces are valid Parse-only: there is no palette resolver, and the font backend has no concept of a CPAL palette, so these declarations cascade and are then ignored. The two records in PaletteMixGrammar become hand-written classes for C# 7.3. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
From PeachPDF v0.9.6 (upstream 6f1cd92, 4f2c757, 7cf1855, 2809199, 2f27b52). - CSS Nesting: StyleRule gains a NestedRules list (deliberately separate from Children, so Selector/Style lookups and ToCss are unaffected) and the composer resolves a nested rule's selector against its parent at parse time, so nested selectors reach the cascade already absolutized - :where(): parsed as a MatchesSelector like :is(), but contributing zero specificity - SelectorConstructor: pseudo-element compound wrapping, and An+B handling that accepts the spaced `3n + 1` form while still rejecting `3n + -6` - Lexer/LexerBase: CSS Syntax 4.3.4 hash-token conformance, U+ wildcard-range fixes, and a RewindTo the nesting look-ahead needs A nested rule is not visible through Stylesheet.Rules, so CssData has to recurse into NestedRules for these to apply - that is the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tail of the PeachPDF v0.9.6 delta (upstream d150993, e1c8d21, 71a93e2, ba7df0c, 4035e66, e0c1fd8, 731490f). - AttributeSelectorFactory: reflection-free dispatch, so the two-argument Attr*Selector constructors are statically reachable and survive trimming - PortableExtensions: the reflection-based GetProperties helper is gone, which also retires the trimming-annotation divergence the port carried - Map, ValueBuilder, and the SVG fill/stroke property updates - transform-origin now accepts a single keyword followed by a length This brings the vendored tree to exactly PeachPDF v0.9.6's 589 files, with no file missing and none extra. Map's lookup tables stay plain Dictionary rather than FrozenDictionary, which is net8-only; the comparer moves into the constructor, as elsewhere in the vendored tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The vendored CSS engine and the Html-layer files derived from the same lineage carried doc comments naming the upstream project and its file paths. Provenance belongs in the commit history and the PR description, not in the source, so the comments now describe what the code does rather than where it came from. No functional change. The one remaining mention is the vendor-prefixed property name `-peachpdf-pdf-tag-type` itself, which is a CSS identifier rather than a comment; it is inert here (nothing consumes tagged-PDF output) and renaming a vendor prefix would diverge from the engine it is synced against. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…engine
Three consumer-side changes the refreshed engine needs to actually take effect.
CssData.IndexRules now recurses into IStyleRule.NestedRules and ILayerRule.Rules.
A CSS-Nesting child rule lives in its parent's NestedRules, deliberately outside
Children, so it is invisible to Stylesheet.Rules - without the recursion every
nested rule would parse and then silently never apply. Likewise, once `@layer
name { ... }` parses into a LayerRule, its contents would fall through the
if/else chain and stop applying; they are now indexed as ordinary unlayered
rules. Layer *precedence* is still not implemented.
CssData.FlattenStyleRules replaces the top-level-only Stylesheet.StyleRules scan
in DomUtils.WriteStylesheet/CollectSimpleSelectorStyles and
DomParser.SetTextSelectionStyle, so a ::selection rule nested inside @layer or
another rule is still found.
CssParser.ParseStyle no longer lets a bare `@layer a, b;` statement close the
@import prologue - that is legal before @import per CSS Cascade 5, and treating
it as a non-import rule silently skipped every @import after it.
CssValueParser.GetColorByName falls back to the engine's ToResolvedColor when
the adapter's palette does not recognize a value, so lab()/oklab()/lch()/
oklch()/color-mix() resolve to real colours everywhere a colour is parsed
instead of coming out black. It also fixes `transparent`, which the palette
reports with a zero alpha and which was previously rejected as invalid.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@media conditions were never really evaluated: DomParser cascaded against a hard-coded media type of "all" and CssData compared only the medium's type, so every `@media screen` / `@media print` block was dropped, a feature-only query like `@media (prefers-color-scheme: dark)` was dropped, and `@media all and (prefers-color-scheme: dark)` matched unconditionally - applying rules guarded by a condition nothing had tested. RAdapter gains two virtuals describing the surface being rendered onto: - DefaultMediaType - "screen" by default, overridden to "print" in PdfSharpAdapter - SystemColorScheme - Light by default; WinForms and WPF read the Windows app theme (HKCU\...\Themes\Personalize\AppsUseLightTheme), cached and invalidated on SystemEvents.UserPreferenceChanged rather than read per query MediaQueryContext captures that device - media type, viewport size in CSS px, resolution and colour scheme - and MediaQueryMatcher evaluates a rule's enclosing @media chain against it: width/height, orientation, aspect-ratio, resolution, colour depth, and the prefers-* set. Its central policy is Media Queries 4's "unknown feature is false": a feature the matcher cannot evaluate drops its whole block rather than letting the rules inside apply untested. A feature it does model but whose context is missing (a width query before the viewport is known) stays permissive - that is an unknown context, not an unsupported feature. This is a deliberate behaviour change: @media screen blocks now apply on WinForms/WPF and @media print blocks on PdfSharp, where previously neither did. Also fixes colour resolution inside a function: the render-time tokenizer ran outside value mode, where a digit-leading hex splits into a "#" delimiter plus a dimension, so color-mix(in srgb, #ffffff 0%, #0000ff) failed to resolve while the same value with named operands worked. TestSamples/23 exercises all of it in the WinForms and WPF demos: a card that follows the OS theme, one card per media type, a card proving an unevaluable feature does not apply, and a viewport-width probe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The media context was built once, while generating the CSS tree, so a resize never re-ran the cascade: a `@media (min-width: …)` rule picked at load time stayed picked no matter how the window was resized afterwards. HtmlContainerInt now keeps the html and base stylesheet it was given, and PerformLayout re-runs the cascade when the current viewport would evaluate a @media condition differently than the one the cascade was computed against. The test is deliberately narrow, since PerformLayout runs on every layout pass. CssData collects the distinct @media chains its rules are nested under while indexing (one entry per @media block), and MediaOutcomeChanged evaluates just those against the old and new device. A document with no viewport-dependent @media - the common case - costs one empty loop, and a resize that stays within the same breakpoint costs a handful of comparisons and rebuilds nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The vendored engine carried a vendor-prefixed property for tagging PDF output with standard structure types. It is inert here - nothing reads it, and the PdfSharp backend has no tagged-PDF support - so it only added a foreign vendor prefix to the accepted property set. Removes the property name and its registration, the PdfTagType enum and property class, the value converter and lookup table, and the 42 PDF structure keywords. `span` is kept, since grid placement uses it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The adapters already invalidated their cached theme on a system preference change, but nothing acted on it: the cascade is only re-evaluated during layout, and an idle window never lays out - so switching Windows between light and dark left prefers-color-scheme rules stuck at whatever was true when the document loaded. RAdapter gains a ColorSchemeChanged event. HtmlContainerInt subscribes and asks the host to lay out and repaint, which runs the existing viewport check and re-cascades if the outcome moved. The event only reports a real change: the WinForms and WPF adapters re-read the theme and compare, since the General preference category covers much more than the theme and would otherwise force a re-cascade on every unrelated setting change. The handler is detached on dispose - the adapter is a process-wide singleton and would otherwise keep every container that ever rendered alive. The re-cascade itself stays in PerformLayout rather than the event handler, because SystemEvents can raise on a thread other than the host's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Author
|
@eXpl0it3r I updated it to sync with PeachPDF 0.9.6, which had a decent CSS upgrade, and added some immediately beneficial features which I wired up into RAdapter |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This replaces HTML-Renderer's ad-hoc CSS parsing/matching with a real CSS3/4 spec-compliant CSS engine — a proper cascade, a real selector engine (combinators, attribute selectors, structural pseudo-classes,
:not()/:is()/:has(),::before/::after),@mediaevaluation, andcalc()— in place of the old simple class-name-keyed block lookup.The engine is derived from ExCSS, sourced from the more up-to-date fork maintained in PeachPDF, and ported into
Source/HtmlRenderer/Core/CssEngine. Two of HTML-Renderer's own non-standard CSS extensions —corner-radiusandbackground-gradient— depended on the old parser and have been replaced with their spec-compliant equivalents (border-radius,linear-gradient()) so the demo app keeps working against the new engine.Details
corner-radiusandbackground-gradientextensions with spec-compliant versions (see below).What now works end-to-end
!importanttier and the CSS-wide keywordsinitial/inherit/unset/revert/revert-layer.>,+,~), comma-separated selector lists, attribute selectors ([attr],[attr=v],[attr~=v],[attr*=v],[attr^=v],[attr$=v],[attr|=v]),:not(),:is()/:matches(),:where()(zero specificity),:has()(descendant form), structural pseudo-classes:nth-child()/:nth-last-child()/:nth-of-type()/:nth-last-of-type()(including the CSS4An+B of <selector>form) and:nth-column()/:nth-last-column(),:first-child/:last-child/:first-of-type/:last-of-type,:only-child/:only-of-type,:root,:link, and::before/::aftergenerated-content pseudo-elements.@mediaevaluation: media type and features are evaluated against the platform, not assumed. Two newRAdapterproperties describe the surface being rendered onto —DefaultMediaType(screen, orprintfor PdfSharp) andSystemColorScheme(light/dark, read from the Windows app theme by the WinForms and WPF adapters). Width/height, orientation, aspect-ratio, resolution, colour depth and theprefers-*features are all evaluated, and re-evaluated dynamically when the window is resized or the system theme changes.@layer: rules inside a layer apply (ordered by source position and specificity).@propertyand@font-palette-valuesparse into the CSS-OM.lab(),oklab(),lch(),oklch()andcolor-mix()resolve to real colours wherever a colour is parsed.margin-block/margin-inline,padding-block/padding-inline,inset*andborder-block/border-inline*fully work — the engine resolves them to their physical equivalents (LTR / horizontal-tb), so the box model consumes them unchanged.calc(): parsed and evaluated for property values, agreeing exactly with non-calc()values for every absolute unit (1in= 96px,1pt= 96/72px,1pc= 16px).content:: supportsattr()and the open/close quote keywords.background-image: linear-gradient(...), replacing the old proprietarybackground-gradientextension.border-radius, with independent per-corner horizontal/vertical elliptical radii, replacing the old proprietarycorner-radiusextension.Behaviour changes worth reviewing
@media screenand@media printblocks now apply. Previously the cascade ran against media typealland compared only the medium's type, so every typed@mediablock was dropped and@media all and (<feature>)matched unconditionally — applying rules guarded by a condition nothing had tested. Screen blocks now apply on WinForms/WPF and print blocks on PdfSharp, and each drops the other's.calc(). Previously the vendored engine resolved them against points whileCssValueParserused 96dpi, socalc(1in)gave 72 wherewidth: 1ingave 96.Parsed but not yet rendered
These cascade correctly and are then ignored at the box boundary — nothing crashes, nothing misrenders:
display: gridstill lays out as a block.@layerordering — rules inside layers apply, but layer precedence,!importantlayer reversal andrevert-layerbanding are not implemented.@property— the at-rule parses;syntax,initial-valueandinheritsare not honoured.@font-palette-values/font-palette/palette-mix()— no palette resolver, and the font backend has no CPAL concept.clip-path,box-shadow,aspect-ratio— no clip region, shadow draw handler or intrinsic-sizing hook.image-set()/cross-fade()/element(),radial-gradient()and conic gradients — parsed; onlyurl()andlinear-gradient()are painted.@importand<link>media conditions —@mediablocks are evaluated, but an@import url(…) print;condition is ignored and<link media="…">is not honoured. Both predate this PR.@supports/@container— conditions are not evaluated, so their contents do not apply.::marker— not ported; list markers are still painted procedurally rather than as a generated box.content:— nocounter()/counters()/string()/content().:has()— only the default descendant relative-selector form (no leading>,+,~).:hover— matched and collected, but application remains commented out (pre-existing).Verification
netstandard2.0andnet8.0); the warning count is unchanged from before the engine refresh, and the packed.nuspecdependency list is byte-identical.TestSamples/23.Media queries and color scheme.htmis a new demo sample covering the OS colour scheme, the media type, an unevaluable media feature, and a viewport-width probe. Toggling the Windows app theme, or resizing the window, updates the render live.One deliberate divergence from the upstream engine is worth noting for future re-syncs: the vendor-prefixed
-peachpdf-pdf-tag-typeproperty has been removed, since nothing here consumes it. The vendored tree is therefore not a byte-for-byte mirror of upstream.Related open issues
Confirmed by reading the relevant code paths (not just title-matching):
corner-radiusvsborder-radius: fixed directly (see above).[class*="language-"]) don't match: fixed — the new engine has real attribute-selector support..token.property) don't match: fixed — the old engine's single-class-key lookup couldn't represent this; the new selector engine requires every class in a compound selector to match.url('data:...;base64,...')mis-parsed because of the semicolon inside the quoted string: fixed — the old ad-hoc parsing method is gone, replaced by a real, spec-correct tokenizer.border: 1px inset;mis-parsed: fixed — the old ad-hoc border-shorthand parser is gone, replaced by a real, order-independentbordershorthand implementation.<a>tags: likely resolved or significantly improved — the issue's own root cause (an O(rules × boxes) matching function with a hand-rolled<a>-tag special case) has been replaced by the new tag/class/id rule index. Not independently benchmarked.background:shorthand set via a descendant selector not applying: likely fixed — both prerequisites (real descendant-combinator matching, realbackgroundshorthand parsing) are now in place, though the exact reported case wasn't directly reproduced.Not addressed by this PR (layout/painting gaps, not parsing/cascade): #203 (
position: absolute), #34 (float), #42 (table border-conflict resolution), #104/#103 (<tr>display/background-color).