Skip to content

Preserve @font-face descriptors outside the hardcoded set - #223

Merged
FlorianRappl merged 1 commit into
AngleSharp:develfrom
jafin:fix/font-face-descriptor-preservation
Aug 13, 2026
Merged

Preserve @font-face descriptors outside the hardcoded set#223
FlorianRappl merged 1 commit into
AngleSharp:develfrom
jafin:fix/font-face-descriptor-preservation

Conversation

@jafin

@jafin jafin commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

Prerequisites

Please make sure you can check the following two boxes:

  • I have read the CONTRIBUTING document
  • My code follows the code style of this project

Contribution Type

What types of changes does your code introduce? Put an x in all the boxes that apply:

  • Bug fix (non-breaking change which fixes an issue, please reference the issue id)
  • New feature (non-breaking change which adds functionality, make sure to open an associated issue first)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • My change requires a change to the documentation
  • I have updated the documentation accordingly
  • I have added tests to cover my changes
  • All new and existing tests passed

Description

Preserve @font-face descriptors outside the hardcoded set of seven

Fixes the silent descriptor loss reported downstream in mganss/HtmlSanitizer#541.

Problem

CssFontFaceRule kept only the seven descriptors named in its private ContainedProperties set. CssDeclarationRule.CreateNewProperty returned null for everything else, and SetValue then skipped the declaration entirely - without ever consulting the parser options:

private ICssProperty CreateNewProperty(String propertyName)
{
    if (_contained.Contains(propertyName))
    {
        return Owner.Context.CreateProperty(propertyName);
    }

    return null;   // <- declaration is silently discarded
}

So CssParserOptions.IsIncludingUnknownDeclarations - the switch that keeps unrecognized declarations alive in ordinary style rules - had no effect inside @font-face. Standard CSS Fonts Level 4 descriptors (font-display, size-adjust, ascent-override, font-feature-settings, …) and vendor descriptors (mso-*, common in email HTML) were both lost. The drop is silent: no exception, no diagnostic, and ToCss() emits a rule that looks well-formed, so a caller round-tripping a stylesheet has no way to notice.

CssViewportRule, CssCounterStyleRule and CssFontFeatureValuesRule derive from the same base class. The latter two pass an empty contained set, so they were dropping every declaration:

@counter-style thumbs { system: cyclic; symbols: "X" }   ->   @counter-style { }

Before

var parser = new CssParser(new CssParserOptions { IsIncludingUnknownDeclarations = true });
var css = """
@font-face {
    font-family: "FontName";
    src: url("https://example.com/font.woff") format("woff");
    font-display: swap;
    size-adjust: 100%;
}
""";
parser.ParseStyleSheet(css).ToCss();
@font-face { font-family: "FontName"; src: url("https://example.com/font.woff") format("woff") }

Output was byte-identical with IsIncludingUnknownDeclarations = false.

After

@font-face { font-family: "FontName"; src: url("https://example.com/font.woff") format("woff"); font-display: swap; size-adjust: 100% }

Changes

1. Honour IsIncludingUnknownDeclarations in CssDeclarationRule

Declarations outside a rule's descriptor set now fall through to the same IsAllowingUnknownDeclarations() gate that style rules already use, instead of being dropped unconditionally. @font-face and style rules now agree in every configuration. This applies to @counter-style, @font-feature-values and @viewport too, since they share the base class.

BrowsingContextExtensions.IsAllowingUnknownDeclarations changed from private to internal to make this reusable. No public API change.

2. Register the missing standard descriptors

Of the dropped descriptors, only font-display was actually a known declaration. size-adjust, ascent-override, descent-override, line-gap-override and font-feature-settings had no declaration registered at all - extending ContainedProperties alone would not have saved them, since they would still be flagged PropertyFlags.Unknown and gated by the option.

They are now registered with real value grammars, so they are kept - typed - by default:

Descriptor Grammar Initial
size-adjust <percentage> 100%
ascent-override normal | <percentage> normal
descent-override normal | <percentage> normal
line-gap-override normal | <percentage> normal
font-feature-settings normal | [ <string> [ <integer> | on | off ]? ]# normal

Supporting additions: OnlyPercentConverter / PercentConverter (there was no percentage-only converter - LengthOrPercentConverter would have accepted 10px for size-adjust), and the on / off keywords.

CssFontFaceRule.ContainedProperties gains all seven Fonts L4 descriptors.

3. Three defects this exposed

  • font-variation-settings accepted only normal. Its converter was Assign(CssKeywords.Normal, …), so "wght" 400 was rejected in style rules and would have been rejected in the newly preserved @font-face. Now implements normal | [<string> <number>]#.
  • Invalid values produced malformed output. CssDeclarationRule.SetValue added properties without checking they parsed, so @font-face { size-adjust: 10px } serialized as @font-face { size-adjust: ; }. Invalid values are now ignored and leave an existing valid declaration standing (font-weight: 400; font-weight: bogus keeps 400), matching CssStyleDeclaration's behaviour.
  • ICssFontFaceRule.Features was a stub - get => String.Empty; set { } - despite its featureSettings DOM name. It now maps to font-feature-settings.

Behaviour summary

Declaration in @font-face Before After (default) After (IsIncludingUnknownDeclarations)
font-family, src, font-style, font-weight, font-stretch, font-variant, unicode-range kept kept kept
font-display dropped kept, typed kept, typed
size-adjust, ascent-override, descent-override, line-gap-override dropped kept, typed kept, typed
font-feature-settings, font-variation-settings dropped kept, typed kept, typed
mso-generic-font-family, --custom-thing, color dropped dropped kept

Scope

  1. @counter-style and @font-feature-values are only fixed under the opt-in. Their descriptors (system, symbols, suffix, …) are still unregistered, so by default they remain dropped. Registering the ~10 counter-style descriptors is a separate piece of work.
  2. Their prelude is still lost on serialization. CssDeclarationRule.ToCss passes null as the prelude, so @counter-style thumbs { … } serializes as @counter-style { … } even when the declarations survive. Separate defect, worth its own issue.

One pre-existing quirk worth noting, unchanged here: IsAllowingUnknownDeclarations resolves via GetProvider<CssParser>(), which misses a factory-registered parser that has not been resolved yet and then defaults to permissive. That is why the original report saw identical output for true and false. This PR reuses that same gate rather than working around it, so the @font-face and style-rule paths stay consistent whatever it resolves to.

Testing

  • src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs - descriptor preservation, vendor/custom descriptors under both option values, invalid-value handling, overwrite semantics, Features mapping, ToCss round-trip, and sibling-rule coverage for @counter-style / @viewport.
  • src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs - legal and illegal values for each new grammar.

CssFontFaceRule kept only the seven descriptors named in its private
ContainedProperties set; CssDeclarationRule discarded everything else
silently and without consulting CssParserOptions, so
IsIncludingUnknownDeclarations had no effect inside @font-face even
though it is what keeps unrecognized declarations alive in style rules.
Standard CSS Fonts Level 4 descriptors (font-display, size-adjust,
ascent-override, font-feature-settings, ...) and vendor descriptors
(mso-*) were both lost, and ToCss emitted a well-formed looking rule so
a caller round-tripping a stylesheet had no way to notice.

Non-descriptor declarations now fall through to the same
IsAllowingUnknownDeclarations gate that style rules use, which also
covers @counter-style, @font-feature-values and @Viewport - the sibling
rules on the same base class.

Register the standard descriptors that had no declaration at all
(size-adjust, ascent-override, descent-override, line-gap-override,
font-feature-settings) with real value grammars, add a percentage
converter, and extend ContainedProperties so they are kept, typed, by
default rather than only under the opt-in.

Along the way:

- font-variation-settings accepted only `normal`, so `"wght" 400` was
  rejected in style rules and would have been rejected in the newly
  preserved @font-face. It now implements normal | [<string> <number>]#.
- SetValue added properties without checking they parsed, so an invalid
  descriptor serialized as a malformed `size-adjust: ;`. Invalid values
  are now ignored and leave an existing valid declaration standing,
  matching CssStyleDeclaration.
- ICssFontFaceRule.Features was a String.Empty/no-op stub despite its
  featureSettings DOM name; it now maps to font-feature-settings.

Fixes the silent loss reported downstream in mganss/HtmlSanitizer#541.
Copilot AI lite review requested due to automatic review settings August 13, 2026 00:11

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes loss of @font-face (and other declaration-rule) descriptors by honoring CssParserOptions.IsIncludingUnknownDeclarations in CssDeclarationRule, and registers several CSS Fonts Level 4 descriptors so they are preserved/typed by default. It also improves declaration assignment behavior by ignoring invalid values (preventing malformed serialization) and wires ICssFontFaceRule.Features to font-feature-settings.

Changes:

  • Update CssDeclarationRule to preserve non-contained descriptors only when unknown declarations are enabled, and to ignore invalid values instead of storing empty declarations.
  • Add converters, keywords, property names, initial values, and factory registrations for missing font descriptors (e.g. size-adjust, *-override, font-feature-settings) and expand font-variation-settings parsing.
  • Add targeted NUnit tests covering preservation, round-tripping, overwrite semantics, and value validation for these descriptors.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/AngleSharp.Css/ValueConverters.cs Adds percent-only parsing and new converters for font descriptors / variation settings.
src/AngleSharp.Css/Factories/DefaultDeclarationFactory.cs Registers new descriptor declarations so they’re recognized/typed.
src/AngleSharp.Css/Dom/Internal/Rules/CssFontFaceRule.cs Expands the contained descriptor set and implements Features mapping.
src/AngleSharp.Css/Dom/Internal/Rules/CssDeclarationRule.cs Preserves unknown declarations per parser option and ignores invalid values.
src/AngleSharp.Css/Declarations/SizeAdjustDeclaration.cs Adds declaration metadata for size-adjust.
src/AngleSharp.Css/Declarations/LineGapOverrideDeclaration.cs Adds declaration metadata for line-gap-override.
src/AngleSharp.Css/Declarations/FontFeatureSettingsDeclaration.cs Adds declaration metadata for font-feature-settings.
src/AngleSharp.Css/Declarations/DescentOverrideDeclaration.cs Adds declaration metadata for descent-override.
src/AngleSharp.Css/Declarations/AscentOverrideDeclaration.cs Adds declaration metadata for ascent-override.
src/AngleSharp.Css/Constants/PropertyNames.cs Adds constants for new @font-face descriptor names.
src/AngleSharp.Css/Constants/InitialValues.cs Adds initial values for the new descriptors.
src/AngleSharp.Css/Constants/CssKeywords.cs Adds on / off keywords used by font-feature-settings.
src/AngleSharp.Css/BrowsingContextExtensions.cs Makes unknown-declaration option check reusable (internal).
src/AngleSharp.Css.Tests/Rules/FontFaceDescriptors.cs Adds rule-level tests for preservation, serialization, and overwrite semantics.
src/AngleSharp.Css.Tests/Declarations/CssFontDescriptorProperty.cs Adds declaration parsing tests for legal/illegal descriptor values.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/AngleSharp.Css/ValueConverters.cs

@FlorianRappl FlorianRappl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@FlorianRappl FlorianRappl added this to the v1.1.0 milestone Aug 13, 2026
@FlorianRappl
FlorianRappl merged commit e13ccc5 into AngleSharp:devel Aug 13, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants