diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 199c766..f180e0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ env: DOCS_BRANCH: ${{ secrets.DOCS_BRANCH }} # Snapshot baselines are per-platform and committed; a missing one is a gap, never a pass. ANGLESHARP_SNAPSHOT_STRICT: 1 - ANGLESHARP_VERSION: 1.8.0 - ANGLESHARP_CSS_VERSION: 1.1.0 + ANGLESHARP_VERSION: 1.8.1 + ANGLESHARP_CSS_VERSION: 1.1.1 jobs: can_document: diff --git a/.github/workflows/update-snapshots.yml b/.github/workflows/update-snapshots.yml index 5bce9e1..985c05d 100644 --- a/.github/workflows/update-snapshots.yml +++ b/.github/workflows/update-snapshots.yml @@ -15,8 +15,8 @@ permissions: contents: write env: - ANGLESHARP_VERSION: 1.8.0 - ANGLESHARP_CSS_VERSION: 1.1.0 + ANGLESHARP_VERSION: 1.8.1 + ANGLESHARP_CSS_VERSION: 1.1.1 jobs: # Keep this matrix identical to the `test` matrix in ci.yml, images included - a baseline diff --git a/AGENTS.md b/AGENTS.md index 94cae8d..eee098a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -52,7 +52,99 @@ Font handling resolves each entry of a `font-family` list in order, and the firs Table spans: a cell covers the columns and rows it spans, and a spanning cell's height is shared across the rows it covers rather than imposed on each of them. With `border-collapse: collapse` each cell paints only its top and left edge and the table adds the frame, so shared edges are drawn once and no rule is painted across a spanning cell. Cell content honours `vertical-align` (`top`, `middle`, `bottom`), defaulting to the `middle` that AngleSharp.Css resolves for cells; `baseline` is treated as `top`, since baselines are not aligned across a row. Note that on a cell `vertical-align` positions the content box, which is a different meaning from the inline shift `ParseVerticalAlign` applies to `super`, `sub` and friends. -Current behavior includes block layout, margins, padding, borders, floats, inline-block, relative/fixed/absolute positioning, z-index ordering, outlines, text styling, text alignment, line-height, letter-spacing, text-indent, vertical-align, and generic font-family handling. +`border-radius` (rounded corners): the shorthand and all four longhands (`border-top-left-radius` etc.) are supported, each independently elliptical (`border-top-left-radius: 20px 10px`, or the whole-box `border-radius: 40px / 20px` slash shorthand) and represented backend-agnostically as `RenderCornerRadii` (`Rendering/RenderCornerRadii.cs`) - eight floats, X/Y per corner. `RenderCornerRadii.ClampToBox` implements the CSS corner-overlap-prevention algorithm (https://www.w3.org/TR/css-backgrounds-3/#corner-overlap): a single global scale factor, not a per-axis one, is applied to every radius so adjacent corners along the same edge never overlap - this is what turns a `border-radius` larger than half a box's height into a pill/stadium shape rather than a self-intersecting curve. A background fills with `SKRoundRect`/`DrawRoundRect` (`SkiaRenderBackend.DrawFillRect`) instead of `DrawRect` whenever `Radii` is non-zero. A border with rounded corners is painted differently depending on whether every edge shares the same width: a uniform-width border becomes a single `StrokeRoundedRectCommand` (one stroked ring, inset by half the border width, with the radius reduced by that same half-width since the stroke sits on the border's centerline); a mixed-width border (e.g. `border-right-width` wider than the others) has no single stroke width to give that ring, so it falls back to the existing four-straight-rectangle border path and the corners render square - a deliberate, documented limitation rather than an attempt at four independently-rounded quads. `outline` never rounds regardless of `border-radius`, matching the CSS spec, because `PaintOutline`'s internal `PaintBorder` call never passes the element's radii. Percentage radii resolve to pixels through AngleSharp.Css's computed-style engine before this renderer ever sees them. A confirmed AngleSharp.Css 1.1.0 bug - both the horizontal and vertical component of a percentage radius resolving against the *containing block's width* instead of per-axis against the element's own width/height, confirmed by rendering the same `%` radius against different viewport widths and observing the resulting pixel value track the viewport, not the element's own box - was reported with a reproducing test in AngleSharp.Css's own suite (`AngleSharp.Css.Tests/Styling/BorderRadiusPercentageResolutionTests.cs`) and is now **fixed upstream**, confirmed by `BuildDisplayList_ResolvesPercentageBorderRadiusAgainstBox` flipping from the old (wrong) 30px/30px to the correct per-axis 20px/8px with no renderer-side code change of its own - this renderer never carried a workaround for it, since there was nothing to intercept short of re-deriving the radius itself. + +`box-shadow` and `text-shadow`: both are parsed from AngleSharp.Css's `GetBoxShadow()`/`GetTextShadow()` computed-style accessors, which return every comma-separated layer already normalized with the color as `rgba(r, g, b, a)` regardless of how it was authored (`red`, `#f00`, ...) - `ExtractColorToken` (`HtmlRenderer.cs`) relies on that normalization to split a layer's color from its lengths by locating the `rgba(...)` call rather than naively splitting on whitespace, which the spaces after that function's commas would otherwise break. `text-shadow` is an inherited property and AngleSharp.Css already resolves that inheritance before this renderer sees it (a child with no `text-shadow` of its own reports its ancestor's value verbatim, and an explicit `text-shadow: none` reports the literal string `"none"`, not an empty one) - `ParseTextShadows` still takes an `inherited` fallback for defense in depth, mirroring how `color` and the other inherited text properties are already handled in `ResolveTextStyle`. `box-shadow` is not inherited, matching spec, so `ResolveBoxStyle` parses it directly with no inherited fallback. Multiple shadows paint back-to-front in reverse authoring order (`PaintBoxShadows`/`PaintTextShadows`), because CSS defines the first-listed shadow as topmost. A `box-shadow` layer is `RenderBoxShadow` (`Rendering/RenderShadow.cs`) - offset, blur, spread, color, and `inset` - painted via `DrawBoxShadowCommand`/`SkiaRenderBackend.DrawBoxShadow`, which reuses the same `RenderCornerRadii`/`SKRoundRect` machinery `border-radius` already established so a shadow follows the box's own corner rounding. An outset shadow is the border box's `SKRoundRect` grown by `SpreadRadius` (via `SKRoundRect.Inflate`/`Deflate` - verified functionally to adjust the rect *and* clamp each radius down to zero together, exactly matching the CSS spread semantics for free) and offset, blurred with `SKMaskFilter.CreateBlur`, then clipped with `SKClipOperation.Difference` against the *un-inflated* border box so the shadow never bleeds into the box's own interior even when its background is transparent - this clip-out is required by spec, not an approximation. An inset shadow is the mirror image: the border box shrunk by spread and offset, painted as the even-odd "ring" between that shrunk shape and a rect well outside the box, clipped with `SKClipOperation.Intersect` to the border box so it only shows inside. Both cases derive the Gaussian blur sigma as half the CSS blur radius (`shadow.BlurRadius / 2f`), the same approximation browsers commonly use; there is no dedicated conformance test pinning that constant, so treat it as adjustable if a real blurred-shadow comparison ever disagrees with it. `text-shadow` has no spread or inset, and paints as a second, blurred copy of the same text run (`DrawTextShadowCommand`/`SkiaRenderBackend.DrawTextShadow`) offset behind the real glyphs, shaped through the same `SkiaTextShaping` path as ordinary text so a shadow never drifts out of alignment with what it is shadowing. Painting order for a box is background, then `box-shadow` (all layers), then border, then outline - box-shadow must sit on top of the background (not behind it) even though it is visually "behind" the box's own content, because an inset shadow paints inside the box and would otherwise be hidden by an opaque background; this was a real bug caught by rendering the inset-shadow visual test and seeing no shadow at all, fixed by reordering `PaintBoxShadows` after `PaintBackground` at all three box-painting call sites. + +A box's own background/border/`box-shadow`/outline must paint *behind* its children, but an auto-sized box's height is only known once its children have been laid out (and their commands already appended to the display list) - so `LayoutElement`, `LayoutFlexContainer`, and `LayoutGridContainer` each capture `displayList.Commands.Count` as `boxPaintInsertIndex` before laying out their children/items, build the box's own paint commands into a scratch `DisplayList` buffer, and splice that buffer in at the captured index via `DisplayList.InsertRange` once the box's final size is known - rather than simply appending, which used to paint the box's own background *after*, and therefore on top of, its children. This was a real, confirmed bug (not hypothetical): a `
` with both a `background-color` and direct text content rendered the text completely invisible, and an absolutely-positioned or negative-`z-index` child was fully hidden behind its own containing block's background. Fixed by the splice approach and locked in by `BuildDisplayList_PaintsOwnBackgroundBehindOwnDirectTextContent`/`RenderToPng_PaintsOwnBackgroundBehindOwnDirectTextContent`; two pre-existing tests (`negative-z-index-behind-in-flow`/`absolute-positioned-out-of-flow` and their structural counterpart) had encoded the old, incorrect order as "expected" and were corrected to match CSS 2.1 Appendix E's actual stacking order (a stacking context's own background paints first, *then* its negative-z-index descendants, on top of it). + +`display: list-item` (`
    `/`