Recursive AI

Validate CSS declarations with your own grammar

All onehtml recipes

Give the cascade a declaration validator so values your layout engine can’t use are dropped, and the fallback declared before them wins, as it would in a browser that doesn’t support them.

Pages often write a fallback first and a newer value second: width: 100%; width: -webkit-fill-available. A browser that can’t parse the second value drops it at parse time, so the first one wins. onehtml checks values only as far as it knows each property’s grammar, so for a property it doesn’t type, a second value it can’t rule out is kept and wins. If your code consumes the computed styles, for example a renderer or a layout engine, you can supply the grammar you support as StyleSourceOptions.DeclarationValidator. It needs 1.1.0 or later.

C#
using RecursiveAI.OneHtml;
using RecursiveAI.OneHtml.Css;
 
// Fallbacks a browser resolves for itself: each rule's second declaration is one our layout engine can't use.
const string html = """
    <style>
      .panel { width: 100%; width: -webkit-fill-available }
      .card  { width: 20rem; width: stretch; max-width: calc(100% - 2rem) }
      .grid  { grid-template-columns: repeat(3, 1fr); grid-template-columns: subgrid }
    </style>
    <div class="panel"><div class="card">One</div><div class="grid">Two</div></div>
    """;
 
Document doc = Document.Parse(html);
 
// The grammar our engine supports for sizes: auto, a length, a percentage, 0 or a calc().
static bool IsSize(TokenRange value)
{
    if (value.Next(value.From) != value.To) return false; // exactly one component value
    int i = value.From;
    return value.Kind(i) switch
    {
        CssTokenKind.Ident => value.IsIdent(i, "auto") || value.IsIdent(i, "none"),
        CssTokenKind.Percentage => true,
        CssTokenKind.Number => value.Number(i) == 0,
        CssTokenKind.Dimension => value.TryUnit(i, out CssUnit unit) && CssUnits.GetDimension(unit) == CssDimension.Length,
        CssTokenKind.Function => value.IsFunction(i, "calc"),
        _ => false,
    };
}
 
DeclarationValidator validator = (property, value, origin) =>
{
    bool ok = property switch
    {
        "width" or "min-width" or "max-width" or "height" or "min-height" or "max-height" => IsSize(value),
        "grid-template-columns" => !value.IsIdent(value.From, "subgrid"),
        _ => true, // properties we have no grammar for: keep the library's own check
    };
    Console.WriteLine($"  {(ok ? "keep" : "drop")} {property}: {value.RawText()}");
    return ok;
};
 
Console.WriteLine("Validator calls:");
DocumentStyles strict = DocumentStyles.Create(doc, StyleContext.Default,
    new StyleSourceOptions { DeclarationValidator = validator });
DocumentStyles plain = DocumentStyles.Create(doc, StyleContext.Default);
 
foreach (var (selector, property) in new[] { (".panel", "width"), (".card", "width"), (".card", "max-width"), (".grid", "grid-template-columns") })
{
    Node node = doc.Root.QuerySelector(selector);
    Console.WriteLine($"{selector} {property}: {plain.GetStyle(node).GetPropertyValue(property)} -> {strict.GetStyle(node).GetPropertyValue(property)}");
}
 
// TokenRange works on any value text, not only inside the cascade.
TokenRange parsed = TokenRange.Parse("min(50%, 30rem)");
Console.WriteLine($"min(50%, 30rem) is a size: {IsSize(parsed.Trim())}; first token: {parsed.Kind(parsed.From)} {parsed.IdentString(parsed.From)}");
Text
Validator calls:
  keep width: 100%
  drop width: -webkit-fill-available
.panel width: -webkit-fill-available -> 100%
  keep width: 20rem
  drop width: stretch
  keep max-width: calc(100% - 2rem)
.card width: stretch -> 20rem
.card max-width: calc(100% - 2rem) -> calc(100% - 2rem)
  keep grid-template-columns: repeat(3, 1fr)
  drop grid-template-columns: subgrid
.grid grid-template-columns: subgrid -> repeat(3, 1fr)
min(50%, 30rem) is a size: False; first token: Function min

How to read it:

  • The validator returns true to keep a declaration and false to drop it. A dropped declaration behaves as if it never parsed: it doesn’t win and doesn’t count as !important, so the earlier declaration, or the inherited or initial value, applies.
  • TokenRange is the token cursor onehtml’s own value parsers use. Kind, Next, IsIdent, IsFunction, Number and TryUnit read the value without allocating. TokenRange.Parse makes one from any text, which is how the last line tests a grammar on its own. Note that min() fails here, because this engine accepts only calc().
  • Declarations are validated once per rule, on first use. That’s why the calls appear as each element’s style is first read. Keep the callback deterministic, quick and thread-safe.
  • The validator is asked about author and user declarations only, and only for properties onehtml doesn’t type itself. It isn’t asked about custom properties, CSS-wide keywords such as inherit, or values containing var(), which aren’t known until computed-value time. For a shorthand onehtml expands, such as margin, it sees each longhand with its expanded value, and one rejected longhand drops the whole declaration.
  • Without a validator, nothing changes: the plain cascade keeps the last declaration, as the left side of each line shows.

The full rules are in Declaration validators. Compute CSS styles covers the rest of the cascade API.