XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFESystem

【Rust】XFE系统,基于Rust和汇编

公开
关注 0 Fork 0 Star 0
UTF-8
using AngleSharp.Dom;
using AngleSharp.Html.Dom;
using AngleSharp.Css.Dom;
using AngleSharp.Css.Parser;
using AngleSharp.Css;
using AngleSharp;

namespace XFESystem.Browser;

/// <summary>Bounded author cascade. Original implementation of the supported CSS subset; no dynamic compiler.</summary>
internal sealed class BrowserCssCascade
{
    private readonly BrowserCssLimits limits;
    private BrowserViewport viewport, parsedViewport;
    private List<string> diagnostics;
    private bool blockAuthorStyles, validSources;
    private readonly InteractionHolder interaction = new();
    private ICssSelectorParser selectorParser;
    public BrowserCssCascade(BrowserCssLimits limits, BrowserViewport viewport, List<string> diagnostics, bool blockAuthorStyles = false,
        BrowserInteractionState? interaction = null)
    {
        this.limits = limits; this.viewport = viewport; this.diagnostics = diagnostics; this.blockAuthorStyles = blockAuthorStyles;
        this.interaction.Value = interaction ?? new(); selectorParser = CreateParser(this.interaction);
    }
    private sealed record Rule(string Selector, ISelector Matcher, int Specificity, List<Declaration> Declarations, int Order, string Pseudo = "");
    private readonly record struct Declaration(string Name, string Value, bool Important);
    private readonly record struct Winner(bool Important, bool Inline, int Specificity, long Order, string Value);
    private readonly List<Rule> rules = [];
    private sealed class RuleBucket
    {
        internal readonly List<Rule> General = [];
        internal readonly Dictionary<string, List<Rule>> Ids = new(StringComparer.OrdinalIgnoreCase),
            Classes = new(StringComparer.OrdinalIgnoreCase), Tags = new(StringComparer.OrdinalIgnoreCase);
    }
    private readonly Dictionary<string, RuleBucket> selectorBuckets = new(StringComparer.Ordinal);
    private IEnumerable<Rule> Candidates(IElement element, string pseudo = "")
    {
        if (!selectorBuckets.TryGetValue(pseudo, out var bucket)) yield break;
        foreach (var rule in bucket.General) yield return rule;
        if (bucket.Tags.TryGetValue(element.LocalName, out var tags)) foreach (var rule in tags) yield return rule;
        if (element.Id is { } id && bucket.Ids.TryGetValue(id, out var ids)) foreach (var rule in ids) yield return rule;
        foreach (string name in element.ClassList.Distinct(StringComparer.OrdinalIgnoreCase))
            if (bucket.Classes.TryGetValue(name, out var matching)) foreach (var rule in matching) yield return rule;
    }
    private void AddRule(Rule rule)
    {
        rules.Add(rule);
        if (!selectorBuckets.TryGetValue(rule.Pseudo, out var bucket)) selectorBuckets[rule.Pseudo] = bucket = new();
        var key = BrowserSelectorKey.Find(rule.Selector);
        if (key is not { } found) { bucket.General.Add(rule); return; }
        var index = found.Kind == '#' ? bucket.Ids : found.Kind == '.' ? bucket.Classes : bucket.Tags;
        if (!index.TryGetValue(found.Value, out var matching)) index[found.Value] = matching = [];
        matching.Add(rule);
    }
    private int characters, declarations, comparisons, order;
    private int parsedDeclarations, parsedOrder;
    private readonly List<string> sources = [], currentSources = [], parseMessages = [], textSegments = [];
    private readonly HashSet<IElement> styleOwners = new(ReferenceEqualityComparer.Instance);
    private readonly Dictionary<IElement, ComplexTextSnapshot> complexTexts = new(ReferenceEqualityComparer.Instance);
    private readonly List<IElement> removedOwners = [];
    private sealed record ComplexTextSnapshot(string[] Segments, string Text);
    private sealed class InteractionHolder { public BrowserInteractionState Value = new(); }
    public long ParseGeneration { get; private set; }
    public long ReuseCount { get; private set; }
    public int SelectorEvaluations => comparisons;
    internal bool InteractionChangesStyles(BrowserInteractionState before, BrowserInteractionState after, IEnumerable<IElement> elements)
    {
        var relevant = rules.Where(rule => rule.Selector.Contains(":hover", StringComparison.OrdinalIgnoreCase) ||
            rule.Selector.Contains(":active", StringComparison.OrdinalIgnoreCase) || rule.Selector.Contains(":focus", StringComparison.OrdinalIgnoreCase)).ToArray();
        if (relevant.Length == 0) return false;
        var saved = interaction.Value;
        bool changed = false; int work = 0;
        try
        {
            foreach (var element in elements)
            {
                foreach (var rule in relevant)
                {
                    if (++work > limits.MaximumSelectorEvaluations) { changed = true; break; }
                    interaction.Value = before; bool first = rule.Matcher.Match(element);
                    interaction.Value = after; bool second = rule.Matcher.Match(element);
                    if (first != second) { changed = true; break; }
                }
                if (changed) break;
            }
        }
        catch { interaction.Value = saved; return true; }
        interaction.Value = saved;
        return changed;
    }
    private readonly HashSet<string> messages = new(StringComparer.Ordinal);
    private static ICssSelectorParser CreateParser(InteractionHolder state)
    {
        var factory = new DefaultPseudoClassSelectorFactory();
        foreach (string name in new[] { "hover", "active", "focus", "focus-within", "focus-visible" })
        { factory.Unregister(name); factory.Register(name, new StateSelector(name, state)); }
        var services = Configuration.Default.Services.Where(service => service is not IPseudoClassSelectorFactory).Append(factory);
        return BrowsingContext.New(new Configuration(services)).GetService<ICssSelectorParser>()!;
    }
    private sealed class StateSelector(string name, InteractionHolder state) : ISelector
    {
        public string Text => ":" + name;
        public Priority Specificity => Priority.OneClass;
        public bool Match(IElement element, IElement? scope)
        {
            var current = state.Value;
            INode? target = name switch { "hover" => current.Hovered, "active" => current.Active, _ => current.Focus };
            if (name is "focus" or "focus-visible") return ReferenceEquals(element, target);
            while (target is not null) { if (ReferenceEquals(element, target)) return true; target = target.Parent; }
            return false;
        }
        public void Accept(ISelectorVisitor visitor) => visitor.PseudoClass(name);
    }
    private static readonly HashSet<string> Inherited = new(StringComparer.OrdinalIgnoreCase)
        { "color", "font-size", "font-family", "font-weight", "font-style", "line-height", "white-space", "visibility", "text-align", "cursor", "letter-spacing", "pointer-events", "list-style-type", "list-style-position", "border-collapse", "border-spacing", "overflow-wrap", "word-break" };
    private static readonly HashSet<string> Supported = new(StringComparer.OrdinalIgnoreCase)
    {
        "display", "position", "visibility", "overflow", "overflow-x", "overflow-y", "pointer-events", "opacity", "z-index", "transform",
        "width", "height", "min-width", "max-width", "min-height", "max-height", "left", "right", "top", "bottom", "box-sizing",
        "margin", "padding", "border", "border-width", "border-color", "border-style", "background", "background-color", "color",
        "font", "font-family", "font-size", "font-weight", "font-style", "line-height", "white-space", "text-align", "text-decoration", "cursor", "letter-spacing",
        "list-style-type", "list-style-position", "list-style", "border-collapse", "border-spacing", "overflow-wrap", "word-break", "box-shadow", "background-image", "vertical-align", "text-transform", "content",
        "flex", "flex-grow", "flex-shrink", "flex-basis", "flex-direction", "flex-wrap", "justify-content", "align-items", "align-self", "gap", "row-gap", "column-gap", "order"
    };

    public void Load(IDocument document, IReadOnlyList<BrowserStyleSheet>? linked)
    {
        currentSources.Clear(); styleOwners.Clear(); int sourceCharacters = 0;
        void Source(string text)
        {
            if (text.Length > limits.MaximumCssCharacters - sourceCharacters) throw new BrowserLimitException("CSS character limit reached.");
            if (currentSources.Count >= limits.MaximumNodes) throw new BrowserLimitException("CSS source count limit reached.");
            sourceCharacters += text.Length; currentSources.Add(text);
        }
        if (!blockAuthorStyles)
        {
            foreach (var element in document.QuerySelectorAll("style,link"))
            {
                if (element.LocalName == "style")
                { styleOwners.Add(element); Source(StyleText(element, limits.MaximumCssCharacters - sourceCharacters)); }
                else if (linked is not null)
                    foreach (var sheet in linked)
                        if (ReferenceEquals(sheet.Owner, element)) Source(sheet.CssText);
            }
            if (linked is not null) foreach (var sheet in linked) if (sheet.Owner is null) Source(sheet.CssText);
        }
        removedOwners.Clear();
        foreach (var owner in complexTexts.Keys) if (!styleOwners.Contains(owner)) removedOwners.Add(owner);
        foreach (var owner in removedOwners) complexTexts.Remove(owner);
        bool reuse = validSources && parsedViewport.Width == viewport.Width && parsedViewport.Height == viewport.Height &&
            sources.Count == currentSources.Count;
        if (reuse)
            for (int index = 0; index < sources.Count; index++)
                if (!string.Equals(sources[index], currentSources[index], StringComparison.Ordinal)) { reuse = false; break; }
        if (reuse)
        {
            // Counters are per layout, including inline declarations. Reusing
            // compiled rules never avoids any configured source/output budget.
            characters = sourceCharacters; declarations = parsedDeclarations; order = parsedOrder;
            if (characters > limits.MaximumCssCharacters || declarations > limits.MaximumDeclarations || rules.Count > limits.MaximumRules)
                throw new BrowserLimitException("Cached CSS exceeds the configured layout budget.");
            foreach (string message in parseMessages) Note(message);
            ReuseCount++;
        }
        else
        {
            validSources = false; rules.Clear(); selectorBuckets.Clear(); sources.Clear(); parseMessages.Clear(); characters = declarations = order = 0;
            foreach (string source in currentSources) ParseSheet(source);
            parsedDeclarations = declarations; parsedOrder = order; parsedViewport = viewport;
            sources.AddRange(currentSources); parseMessages.AddRange(messages); validSources = true; ParseGeneration++;
        }
        if (blockAuthorStyles) Note("Author styles disabled by the browser's conservative CSP profile");
    }

    public void BeginLayout(BrowserViewport viewport, List<string> diagnostics, bool blockAuthorStyles, BrowserInteractionState state)
    {
        this.viewport = viewport; this.diagnostics = diagnostics; this.blockAuthorStyles = blockAuthorStyles;
        interaction.Value = state; comparisons = 0; messages.Clear();
    }
    public void ResetDocument()
    {
        validSources = false; rules.Clear(); selectorBuckets.Clear(); sources.Clear(); currentSources.Clear(); parseMessages.Clear(); complexTexts.Clear();
        styleOwners.Clear(); removedOwners.Clear(); textSegments.Clear(); messages.Clear(); interaction.Value = new();
        selectorParser = CreateParser(interaction); characters = declarations = comparisons = order = parsedDeclarations = parsedOrder = 0;
    }
    private string StyleText(IElement owner, int maximum)
    {
        var children = owner.ChildNodes;
        if (children.Length == 0) { complexTexts.Remove(owner); return ""; }
        if (children.Length == 1 && children[0] is IText text)
        { complexTexts.Remove(owner); return text.Data; }
        return ComplexStyleText(owner, maximum);
    }
    private string ComplexStyleText(IElement owner, int maximum)
    {
        textSegments.Clear(); int characters = 0, nodes = 0;
        void Read(INode node, int depth)
        {
            if (++nodes > limits.MaximumNodes || depth > limits.MaximumDepth) throw new BrowserLimitException("CSS style text DOM bound reached.");
            if (node is IText text)
            {
                string data = text.Data;
                if (data.Length > maximum - characters) throw new BrowserLimitException("CSS character limit reached.");
                characters += data.Length; textSegments.Add(data);
            }
            else foreach (var child in node.ChildNodes) Read(child, depth + 1);
        }
        foreach (var child in owner.ChildNodes) Read(child, 1);
        if (complexTexts.TryGetValue(owner, out var snapshot) && snapshot.Segments.Length == textSegments.Count)
        {
            bool unchanged = true;
            for (int index = 0; index < textSegments.Count; index++)
                if (!string.Equals(snapshot.Segments[index], textSegments[index], StringComparison.Ordinal)) { unchanged = false; break; }
            if (unchanged) return snapshot.Text;
        }
        string result = string.Concat(textSegments);
        complexTexts[owner] = new(textSegments.ToArray(), result);
        return result;
    }

    public CssComputedStyle Compute(IElement element, CssComputedStyle? parent)
    {
        var values = Defaults(element);
        bool initialColor = values.ContainsKey("color");
        bool initialFontSize = values.ContainsKey("font-size");
        if (parent is not null)
        {
            foreach (var pair in parent.OwnProperties)
                if (Inherited.Contains(pair.Key) && !values.ContainsKey(pair.Key)) values[pair.Key] = pair.Value;
            if (!initialFontSize) values["font-size"] = CssValue.Pixels(parent.Font.Size);
            if (values.TryGetValue("line-height", out string? inheritedLine) && inheritedLine != "normal" &&
                !double.IsFinite(CssValue.Number(inheritedLine, double.NaN))) values["line-height"] = CssValue.Pixels(parent.LineHeight);
            if (!initialColor) values["color"] = CssValue.SerializeColor(parent.Color);
        }
        var winning = new Dictionary<string, Winner>(StringComparer.OrdinalIgnoreCase);
        foreach (var rule in Candidates(element))
        {
            if (rule.Pseudo.Length!=0) continue;
            if (++comparisons > limits.MaximumSelectorEvaluations) throw new BrowserLimitException("CSS selector evaluation limit reached.");
            bool matches;
            try { matches = rule.Matcher.Match(element, element.Owner?.DocumentElement ?? element); }
            catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
            { Note("Unsupported selector: " + rule.Selector); continue; }
            if (matches) Apply(rule.Declarations, false, rule.Specificity, rule.Order, winning);
        }
        string? inline = element.GetAttribute("style");
        if (!blockAuthorStyles && !string.IsNullOrWhiteSpace(inline)) Apply(ParseDeclarations(inline), true, 0, ++order, winning);
        foreach (var pair in winning)
        {
            string value = pair.Value.Value;
            if (value is "inherit" or "unset" && (value == "inherit" || Inherited.Contains(pair.Key)))
                value = parent is null ? "initial" : pair.Key == "font-size" ? CssValue.Pixels(parent.Font.Size) :
                    pair.Key == "line-height" && !double.IsFinite(CssValue.Number(parent.Get("line-height", "normal"), double.NaN)) && parent.Get("line-height", "normal") != "normal" ? CssValue.Pixels(parent.LineHeight) : parent.GetPropertyValue(pair.Key);
            if (value == "initial" || value == "unset")
            {
                if (pair.Key.StartsWith("--", StringComparison.Ordinal)) values[pair.Key] = value;
                else values.Remove(pair.Key);
            }
            else values[pair.Key] = value;
        }
        return CompleteStyle(values, parent, parent?.CustomProperties);
    }

    public CssComputedStyle ComputeMarker(IElement element, CssComputedStyle style)=>ComputePseudo(element,"marker",style)??style;
    public CssComputedStyle? ComputePseudo(IElement element,string pseudo,CssComputedStyle style)
    {
        // Most elements have no generated content. Test the bounded, indexed
        // candidates before copying the inherited custom-property environment;
        // the website's theme can otherwise be copied twice for every element.
        Dictionary<string, Winner>? winning = null;
        foreach (var rule in Candidates(element, pseudo))
        {
            if (++comparisons > limits.MaximumSelectorEvaluations) throw new BrowserLimitException("CSS selector evaluation limit reached.");
            if (rule.Matcher.Match(element, element.Owner?.DocumentElement ?? element))
            { winning ??= new(StringComparer.OrdinalIgnoreCase); Apply(rule.Declarations, false, rule.Specificity, rule.Order, winning); }
        }
        if (winning is null) return pseudo=="marker"?style:null;
        // A universal reset can match ::before/::after without generating one.
        // Content is not inherited into this values map, so without a winning
        // content declaration the result below would always be "normal"/null.
        if (pseudo!="marker" && !winning.ContainsKey("content")) return null;
        var values = pseudo=="marker"?new Dictionary<string, string>(style.OwnProperties, StringComparer.OrdinalIgnoreCase):
            style.OwnProperties.Where(pair=>Inherited.Contains(pair.Key)).ToDictionary(pair=>pair.Key,pair=>pair.Value,StringComparer.OrdinalIgnoreCase);
        if(pseudo!="marker")values["display"]="inline";
        values["font-size"] = CssValue.Pixels(style.Font.Size);
        if(values.TryGetValue("line-height",out string? line)&&line!="normal"&&!double.IsFinite(CssValue.Number(line,double.NaN)))values["line-height"]=CssValue.Pixels(style.LineHeight);
        foreach (var pair in winning)
        {
            string value=pair.Value.Value;
            if(value is "inherit" or "unset" &&(value=="inherit"||Inherited.Contains(pair.Key)))value=pair.Key=="font-size"?CssValue.Pixels(style.Font.Size):style.GetPropertyValue(pair.Key);
            if(value is "initial" or "unset")
            { if(pair.Key.StartsWith("--",StringComparison.Ordinal))values[pair.Key]=value;else values.Remove(pair.Key); }
            else values[pair.Key]=value;
        }
        var computed = CompleteStyle(values, style, style.CustomProperties);
        if(pseudo!="marker"&&computed.Get("content","normal") is "normal" or "none")return null;
        return computed;
    }

    private static readonly IReadOnlyDictionary<string, string> EmptyCustomProperties = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
    private CssComputedStyle CompleteStyle(Dictionary<string, string> values, CssComputedStyle? parent,
        IReadOnlyDictionary<string, string>? inherited)
    {
        inherited ??= EmptyCustomProperties;
        Dictionary<string, string>? custom = null;
        foreach (string key in values.Keys.ToArray())
        {
            if (!key.StartsWith("--", StringComparison.Ordinal)) continue;
            custom ??= new(inherited, StringComparer.OrdinalIgnoreCase);
            string value = values[key];
            if (value is "initial" or "unset") custom.Remove(key); else custom[key] = value;
            values.Remove(key);
        }
        // Only a declaring element owns a mutable copy. Descendants and pseudo
        // elements can safely reuse the resolved environment; no parent map is
        // ever edited, including across subsequent layouts and page sessions.
        var properties = new CssPropertyMap(values, custom ?? inherited);
        if (custom is not null)
            foreach (string key in custom.Keys.ToArray())
            {
                string? resolved = Variables(custom[key], properties, 0);
                if (resolved is null) custom.Remove(key); else custom[key] = resolved;
            }
        foreach (string key in values.Keys.ToArray())
        {
            string? resolved = Variables(values[key], properties, 0);
            if (resolved is null) values.Remove(key); else values[key] = resolved;
        }
        return new(values, parent, viewport, custom ?? inherited);
    }

    private void Apply(List<Declaration> source, bool inline, int specificity, int ruleOrder, Dictionary<string, Winner> winners)
    {
        int index = 0;
        foreach (var item in source)
        {
            var candidate = new Winner(item.Important, inline, specificity, ((long)ruleOrder << 32) + index++, item.Value);
            if (!winners.TryGetValue(item.Name, out var old) || Greater(candidate, old)) winners[item.Name] = candidate;
        }
    }
    private static bool Greater(Winner a, Winner b) => a.Important != b.Important ? a.Important : a.Inline != b.Inline ? a.Inline :
        a.Specificity != b.Specificity ? a.Specificity > b.Specificity : a.Order >= b.Order;

    private void ParseSheet(string css, int depth = 0, bool countCharacters = true)
    {
        if (depth > 32) throw new BrowserLimitException("CSS nested rule depth limit reached.");
        if (countCharacters) characters += css.Length;
        if (characters > limits.MaximumCssCharacters) throw new BrowserLimitException("CSS character limit reached.");
        for (int offset = 0; offset < css.Length;)
        {
            while (offset < css.Length && char.IsWhiteSpace(css[offset])) offset++;
            int open = Find(css, offset, '{');
            if (open < 0) break;
            // Only an at-rule's prelude can end at this semicolon. Looking
            // through the rest of the sheet made minified one-declaration
            // rules (notably the icon font) quadratic when they omit ';'.
            int semicolon = Find(css, offset, ';', open);
            if (semicolon >= 0 && semicolon < open) { Note("Unsupported CSS at-rule: " + WithoutComments(css,offset,semicolon-offset)); offset = semicolon + 1; continue; }
            int close = Matching(css, open, '{', '}');
            if (close < 0) { Note("Malformed CSS block ignored"); break; }
            string selector = WithoutComments(css,offset,open-offset).Trim(); string body = css[(open + 1)..close]; offset = close + 1;
            if (selector.StartsWith('@'))
            {
                if (selector.StartsWith("@media", StringComparison.OrdinalIgnoreCase))
                {
                    if (MediaMatches(selector[6..])) ParseSheet(body, depth + 1, false);
                }
                else if (selector.StartsWith("@supports", StringComparison.OrdinalIgnoreCase) && selector.Contains("display: flex", StringComparison.OrdinalIgnoreCase)) ParseSheet(body, depth + 1, false);
                else Note("Unsupported CSS at-rule: " + selector.Split(' ')[0]);
                continue;
            }
            var parsed = ParseDeclarations(body);
            foreach (string part in Split(selector, ','))
            {
                if (rules.Count >= limits.MaximumRules) throw new BrowserLimitException("CSS rule limit reached.");
                string trimmed = part.Trim();
                if (trimmed.Length is 0 or > 2048) { Note("Invalid CSS selector length"); continue; }
                string pseudo="";
                foreach(string name in new[]{"before","after","marker"})
                {
                    string suffix="::"+name;
                    if(trimmed.EndsWith(suffix,StringComparison.OrdinalIgnoreCase)){pseudo=name;trimmed=trimmed[..^suffix.Length];break;}
                    if(name!="marker"&&trimmed.EndsWith(":"+name,StringComparison.OrdinalIgnoreCase)){pseudo=name;trimmed=trimmed[..^(name.Length+1)];break;}
                }
                if (trimmed.Contains("::", StringComparison.Ordinal) || trimmed.EndsWith(":before", StringComparison.Ordinal) || trimmed.EndsWith(":after", StringComparison.Ordinal))
                { Note("CSS generated pseudo-elements are not laid out"); continue; }
                try
                {
                    if(pseudo.Length>0&&trimmed.Length==0)trimmed="*";
                    var matcher = selectorParser.ParseSelector(trimmed);
                    if (matcher is null) { Note("Unsupported selector: " + trimmed); continue; }
                    var priority = matcher.Specificity;
                    AddRule(new(trimmed, matcher, priority.Ids * 65536 + priority.Classes * 256 + priority.Tags + (pseudo.Length>0 ? 1 : 0), parsed, ++order, pseudo));
                }
                catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException)
                { Note("Unsupported selector: " + trimmed); }
            }
        }
    }

    private List<Declaration> ParseDeclarations(string text)
    {
        var result = new List<Declaration>();
        foreach (string rawDeclaration in Split(text, ';'))
        {
            string declaration=WithoutComments(rawDeclaration);
            int colon = Find(declaration, 0, ':'); if (colon < 1) continue;
            string name = declaration[..colon].Trim().ToLowerInvariant(), value = declaration[(colon + 1)..].Trim();
            bool important = value.EndsWith("!important", StringComparison.OrdinalIgnoreCase);
            if (important) value = value[..^10].TrimEnd();
            if (value.Length == 0) continue;
            if (++declarations > limits.MaximumDeclarations) throw new BrowserLimitException("CSS declaration limit reached.");
            foreach (var expanded in Expand(name, value, important)) result.Add(expanded);
        }
        return result;
    }

    private IEnumerable<Declaration> Expand(string name, string value, bool important)
    {
        if (name == "inset")
        {
            string[] tokens = Words(value); if (tokens.Length is < 1 or > 4) yield break;
            string[] sides = [tokens[0], tokens.Length > 1 ? tokens[1] : tokens[0], tokens.Length > 2 ? tokens[2] : tokens[0], tokens.Length > 3 ? tokens[3] : tokens.Length > 1 ? tokens[1] : tokens[0]];
            string[] directions = ["top", "right", "bottom", "left"];
            for (int i = 0; i < 4; i++) yield return new(directions[i], sides[i], important);
            yield break;
        }
        if (name == "list-style")
        {
            foreach (string token in Words(value))
                if (token is "inside" or "outside") yield return new("list-style-position", token, important);
                else if (token is "none" or "disc" or "circle" or "square" or "decimal" or "lower-alpha" or "upper-alpha") yield return new("list-style-type", token, important);
            yield break;
        }
        if (name is "margin" or "padding" or "border-width" or "border-color" or "border-style")
        {
            string[] tokens = Words(value); if (tokens.Length is < 1 or > 4) yield break;
            string[] sides = [tokens[0], tokens.Length > 1 ? tokens[1] : tokens[0], tokens.Length > 2 ? tokens[2] : tokens[0], tokens.Length > 3 ? tokens[3] : tokens.Length > 1 ? tokens[1] : tokens[0]];
            string[] directions = ["top", "right", "bottom", "left"];
            for (int i = 0; i < 4; i++)
                yield return new(name.StartsWith("border-", StringComparison.Ordinal) ? "border-" + directions[i] + name[6..] : name + "-" + directions[i], sides[i], important);
            yield break;
        }
        if (name == "border" || name is "border-top" or "border-right" or "border-bottom" or "border-left")
        {
            string width = "medium", style = "none", color = "currentcolor";
            foreach (string token in Words(value))
                if (token is "none" or "hidden" or "solid" or "dashed" or "dotted" or "double") style = token;
                else if (char.IsDigit(token[0]) || token is "thin" or "medium" or "thick") width = token;
                else color = token;
            width = width switch { "thin" => "1px", "medium" => "3px", "thick" => "5px", _ => width };
            foreach (string side in name == "border" ? new[] { "top", "right", "bottom", "left" } : [name[7..]])
            {
                yield return new("border-" + side + "-width", width, important); yield return new("border-" + side + "-style", style, important);
                yield return new("border-" + side + "-color", color, important);
            }
            yield break;
        }
        if (name == "background")
        {
            foreach (string token in Words(value))
                if (token.StartsWith('#') || token.StartsWith("rgb") || token.StartsWith("color-mix(") || token.StartsWith("var(") || token is "transparent" or "white" or "black" or "red" or "blue" or "green")
                    yield return new("background-color", token, important);
            if (value.Contains("gradient(")) yield return new("background-image", value, important);
            if (value.Contains("url(")) Note("CSS URL background images are not painted");
            yield break;
        }
        if (name == "flex")
        {
            string[] words = Words(value);
            if (value == "none") words = ["0", "0", "auto"];
            if (value == "auto") words = ["1", "1", "auto"];
            if (words.Length > 0) yield return new("flex-grow", words[0], important);
            yield return new("flex-shrink", words.Length > 1 ? words[1] : "1", important);
            yield return new("flex-basis", words.Length > 2 ? words[2] : "0px", important);
            yield break;
        }
        if (name == "gap")
        {
            string[] tokens = Words(value);
            if (tokens.Length is 1 or 2)
            { yield return new("row-gap", tokens[0], important); yield return new("column-gap", tokens[^1], important); }
            yield break;
        }
        if (name == "font")
        {
            if (value is "inherit" or "initial" or "unset")
            { foreach (string property in new[] { "font-size", "line-height", "font-style", "font-weight", "font-family" }) yield return new(property, value, important); yield break; }
            string[] tokens = Words(value); int sizeIndex = -1;
            for (int index = 0; index < tokens.Length; index++)
                if (CssValue.Length(tokens[index].Split('/')[0], 16, 16, viewport, 16) is not null) { sizeIndex = index; break; }
            if (sizeIndex < 0 || sizeIndex == tokens.Length - 1) { Note("Unsupported CSS system/invalid font shorthand"); yield break; }
            string style = "normal", weight = "normal";
            foreach (string token in tokens.Take(sizeIndex))
            { if (token is "italic" or "oblique") style = token; else if (token is "bold" or "bolder" || CssValue.Number(token, 0) >= 100) weight = token; }
            string[] sizeParts = tokens[sizeIndex].Split('/', 2); string line = sizeParts.Length > 1 ? sizeParts[1] : "normal";
            int familyIndex = sizeIndex + 1;
            if (familyIndex < tokens.Length && tokens[familyIndex] == "/")
            { if (familyIndex + 1 >= tokens.Length) yield break; line = tokens[familyIndex + 1]; familyIndex += 2; }
            if (familyIndex >= tokens.Length) yield break;
            yield return new("font-size", sizeParts[0], important); yield return new("line-height", line, important);
            yield return new("font-style", style, important); yield return new("font-weight", weight, important);
            yield return new("font-family", string.Join(' ', tokens.Skip(familyIndex)), important); yield break;
        }
        if (name is "text-decoration" or "letter-spacing")
        { Note("CSS property is retained but not painted: " + name); }
        if (!Supported.Contains(name) && !name.StartsWith("--", StringComparison.Ordinal) &&
            !name.StartsWith("margin-", StringComparison.Ordinal) && !name.StartsWith("padding-", StringComparison.Ordinal) && !name.StartsWith("border-", StringComparison.Ordinal))
        { Note("Unsupported CSS property: " + name); yield break; }
        yield return new(name, value, important);
    }

    private bool MediaMatches(string condition)
    {
        condition = condition.Trim().ToLowerInvariant();
        if (condition.Contains("print") || condition.Contains("forced-colors") || condition.Contains("prefers-")) return false;
        foreach (string feature in condition.Split('(').Skip(1))
        {
            string[] pair = feature.TrimEnd(')', ' ').Split(':', 2); if (pair.Length != 2) continue;
            string name = pair[0].Trim();
            double? length = CssValue.Length(pair[1].TrimEnd(')', ' '), name.EndsWith("height", StringComparison.Ordinal) ? viewport.Height : viewport.Width, 16, viewport, 16);
            if (length is null) return false;
            if (name == "min-width" && viewport.Width < length || name == "max-width" && viewport.Width > length ||
                name == "min-height" && viewport.Height < length || name == "max-height" && viewport.Height > length) return false;
        }
        return true;
    }
    private void Note(string message) { if (messages.Count < 256 && messages.Add(message)) diagnostics.Add(message); }
    private static Dictionary<string, string> Defaults(IElement element)
    {
        string tag = element.LocalName;
        string display = tag is "html" or "body" or "div" or "p" or "pre" or "section" or "main" or "header" or "footer" or "nav" or "article" or "aside" or "ul" or "ol" or "h1" or "h2" or "h3" or "h4" or "h5" or "h6" or "form" or "blockquote" or "hr" ? "block" : tag == "li" ? "list-item" :
            tag == "table" ? "table" : tag is "thead" or "tbody" or "tfoot" ? "table-row-group" : tag == "tr" ? "table-row" : tag is "th" or "td" ? "table-cell" :
            tag is "head" or "script" or "style" or "link" or "meta" or "title" or "template" ? "none" : tag is "input" or "button" or "textarea" or "select" or "img" or "canvas" ? "inline-block" : "inline";
        var values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase) { ["display"] = display };
        if (tag == "body") foreach (string side in new[] { "top", "right", "bottom", "left" }) values["margin-" + side] = "8px";
        if (tag is "input" or "textarea" or "button" or "select")
        {
            values["background-color"] = "white"; values["color"] = "black";
            foreach (string side in new[] { "top", "right", "bottom", "left" })
            {
                values["padding-" + side] = side is "top" or "bottom" ? "2px" : "4px";
                values["border-" + side + "-width"] = "1px"; values["border-" + side + "-style"] = "solid";
                values["border-" + side + "-color"] = "#808080";
            }
            if (tag == "input") values["width"] = "160px";
            if (tag == "textarea") { values["width"] = "320px"; values["height"] = "60px"; values["white-space"] = "pre-wrap"; }
            values["overflow"] = "hidden";
        }
        if (element is IHtmlInputElement input && input.Type == "hidden") values["display"] = "none";
        if (element is IHtmlInputElement mark && mark.Type is "checkbox" or "radio")
        { values["width"] = "13px"; values["height"] = "13px"; }
        if (element is IHtmlCanvasElement canvas)
        { values["width"] = canvas.Width + "px"; values["height"] = canvas.Height + "px"; }
        if (tag == "pre") values["white-space"] = "pre";
        if (tag is "ul" or "ol") { values["list-style-type"] = tag == "ul" ? "disc" : "decimal"; values["padding-left"] = "40px"; }
        if (tag is "h1" or "h2" or "h3" or "h4" or "h5" or "h6")
            values["font-size"] = tag switch { "h1" => "2em", "h2" => "1.5em", "h3" => "1.17em", "h5" => ".83em", "h6" => ".67em", _ => "1em" };
        if (tag is "em" or "i") values["font-style"] = "italic";
        if (tag == "a") values["color"] = "blue";
        if (tag is "strong" or "b" or "h1" or "h2" or "h3") values["font-weight"] = "700";
        return values;
    }
    private static string? Variables(string value, IReadOnlyDictionary<string, string> values, int depth)
    {
        if (depth > 8) return null;
        int start = value.IndexOf("var(", StringComparison.Ordinal); if (start < 0) return value;
        int end = Matching(value, start + 3, '(', ')'); if (end < 0) return null;
        string inside = value[(start + 4)..end]; int comma = inside.IndexOf(',');
        string name = (comma < 0 ? inside : inside[..comma]).Trim();
        string? replacement = values.GetValueOrDefault(name) ?? (comma < 0 ? null : inside[(comma + 1)..].Trim());
        if (replacement is null) return null;
        return Variables(value[..start] + replacement + value[(end + 1)..], values, depth + 1);
    }

    internal static int Specificity(string selector)
    {
        int ids = 0, classes = 0, tags = 0; bool typeStart = true;
        for (int i = 0; i < selector.Length; i++)
        {
            char c = selector[i];
            if (c == '[') { classes++; int close = Matching(selector, i, '[', ']'); if (close >= 0) i = close; typeStart = false; }
            else if (c == '#') { ids++; typeStart = false; }
            else if (c == '.') { classes++; typeStart = false; }
            else if (c == ':')
            {
                int start = i + 1; while (i + 1 < selector.Length && (char.IsLetterOrDigit(selector[i + 1]) || selector[i + 1] == '-')) i++;
                string name = selector[start..(i + 1)];
                if (i + 1 < selector.Length && selector[i + 1] == '(')
                {
                    int close = Matching(selector, i + 1, '(', ')'); if (close < 0) break;
                    if (name is "not" or "is" or "has")
                    {
                        int nested = Split(selector[(i + 2)..close], ',').Select(Specificity).DefaultIfEmpty().Max();
                        ids += nested / 65536; classes += nested / 256 % 256; tags += nested % 256;
                    }
                    else if (name != "where") classes++;
                    i = close;
                }
                else classes++;
                typeStart = false;
            }
            else if (char.IsWhiteSpace(c) || c is '>' or '+' or '~' or ',') typeStart = true;
            else if (typeStart && char.IsLetter(c)) { tags++; typeStart = false; }
            else if (c == '*') typeStart = false;
        }
        return Math.Min(ids, 255) * 65536 + Math.Min(classes, 255) * 256 + Math.Min(tags, 255);
    }
    private static string WithoutComments(string text,int start=0,int length=-1)
    {
        if(length<0)length=text.Length-start;
        int limit=start+length;
        if(text.IndexOf("/*",start,length,StringComparison.Ordinal)<0)return start==0&&length==text.Length?text:text.Substring(start,length);
        var result = new System.Text.StringBuilder(Math.Min(length,1024)); char quote = '\0';
        for (int i = start; i < limit; i++)
        {
            if (quote != '\0') { result.Append(text[i]); if (text[i] == quote && (i == 0 || text[i - 1] != '\\')) quote = '\0'; }
            else if (text[i] is '\'' or '"') { quote = text[i]; result.Append(text[i]); }
            else if (text[i] == '/' && i + 1 < limit && text[i + 1] == '*') { int end = text.IndexOf("*/", i + 2, StringComparison.Ordinal); i = end < 0||end>=limit ? limit : end + 1; }
            else result.Append(text[i]);
        }
        return result.ToString();
    }
    private static int Find(string text, int offset, char target, int limit = -1)
    {
        if (limit < 0) limit = text.Length;
        char quote = '\0'; int parentheses = 0, brackets = 0;
        for (int i = offset; i < limit; i++)
        {
            char c = text[i]; if (quote != '\0') { if (c == quote && (i == 0 || text[i - 1] != '\\')) quote = '\0'; continue; }
            if (c is '\'' or '"') { quote = c; continue; }
            if(c=='/'&&i+1<text.Length&&text[i+1]=='*'){int end=text.IndexOf("*/",i+2,StringComparison.Ordinal);if(end<0)return -1;i=end+1;continue;}
            if (c == target && parentheses == 0 && brackets == 0) return i;
            if (c == '(') parentheses++; else if (c == ')') parentheses--; else if (c == '[') brackets++; else if (c == ']') brackets--;
        }
        return -1;
    }
    private static int Matching(string text, int open, char left, char right)
    {
        int depth = 0; char quote = '\0';
        for (int i = open; i < text.Length; i++)
        {
            char c = text[i]; if (quote != '\0') { if (c == quote && text[i - 1] != '\\') quote = '\0'; continue; }
            if (c is '\'' or '"') { quote = c; continue; }
            if(c=='/'&&i+1<text.Length&&text[i+1]=='*'){int end=text.IndexOf("*/",i+2,StringComparison.Ordinal);if(end<0)return -1;i=end+1;continue;}
            if (c == left) depth++; else if (c == right && --depth == 0) return i;
        }
        return -1;
    }
    private static IEnumerable<string> Split(string text, char delimiter)
    {
        int start = 0;
        while (start < text.Length)
        {
            int end = Find(text, start, delimiter); if (end < 0) { yield return WithoutComments(text,start,text.Length-start); break; }
            yield return WithoutComments(text,start,end-start); start = end + 1;
        }
    }
    private static string[] Words(string value) => Split(value.Replace('\t', ' ').Replace('\n', ' '), ' ').Where(word => !string.IsNullOrWhiteSpace(word)).Select(word => word.Trim()).ToArray();
}