using AngleSharp.Css.Dom; using AngleSharp.Css.Parser; using Jint; using Jint.Native; using Jint.Native.Object; namespace XFESystem.Browser; public sealed partial class DomBridge { private readonly Dictionary constructedSheets = []; private readonly CssParser constructedCssParser = new(new CssParserOptions { IsIncludingUnknownDeclarations = true, IsIncludingUnknownRules = true }); private int nextSheetId, constructedCssCharacters; private void InstallCssomCallbacks() { var callbacks = new BrowserHostObject(engine); Function(callbacks, "create", _ => { if (constructedSheets.Count >= limits.MaximumStyleSheets) throw new BrowserLimitException("The constructed stylesheet count limit was reached."); var id = checked(++nextSheetId); constructedSheets.Add(id, (constructedCssParser.ParseStyleSheet(string.Empty), 0)); return id; }); Function(callbacks, "rules", args => { var sheet = Sheet(Value(args, 0)); return ListValues(sheet.Sheet.Rules.Select(rule => (JsValue)rule.CssText)); }); Function(callbacks, "replace", args => { var id = SheetId(Value(args, 0)); var previous = constructedSheets[id]; var text = LimitedText(Value(args, 1)); CheckCssCharacters(text.Length - previous.Characters); var replacement = constructedCssParser.ParseStyleSheet(text); CheckCssRules(replacement); constructedSheets[id] = (replacement, text.Length); constructedCssCharacters += text.Length - previous.Characters; return JsValue.Undefined; }); Function(callbacks, "insert", args => { var id = SheetId(Value(args, 0)); var previous = constructedSheets[id]; var text = LimitedText(Value(args, 1)); var index = CssIndex(Value(args, 2), previous.Sheet.Rules.Length, allowEnd: true); var parsed = constructedCssParser.ParseStyleSheet(text); if (parsed.Rules.Length != 1) throw AdjacentError("SyntaxError", "insertRule requires exactly one CSS rule.", 12); CheckCssCharacters(text.Length); if (previous.Sheet.Rules.Length >= 4096) throw new BrowserLimitException("The CSSOM rule limit was reached."); try { previous.Sheet.Insert(text, index); } catch (Exception error) when (error is not OutOfMemoryException and not StackOverflowException) { throw AdjacentError("SyntaxError", error.Message, 12); } constructedSheets[id] = (previous.Sheet, previous.Characters + text.Length); constructedCssCharacters += text.Length; return index; }); Function(callbacks, "delete", args => { var id = SheetId(Value(args, 0)); var previous = constructedSheets[id]; var index = CssIndex(Value(args, 1), previous.Sheet.Rules.Length, allowEnd: false); previous.Sheet.RemoveAt(index); // Serialize after deletion so the retained budget reflects actual // remaining content rather than a potentially different input form. var characters = previous.Sheet.Rules.Sum(rule => rule.CssText.Length); constructedSheets[id] = (previous.Sheet, characters); constructedCssCharacters += characters - previous.Characters; return JsValue.Undefined; }); engine.SetValue("__xfeCssom", callbacks); } private int SheetId(JsValue value) { if (!value.IsNumber() || value.AsNumber() != Math.Truncate(value.AsNumber()) || value.AsNumber() > int.MaxValue || value.AsNumber() <= 0 || !constructedSheets.ContainsKey((int)value.AsNumber())) throw new ArgumentException("Expected this document's constructed stylesheet."); return (int)value.AsNumber(); } private (ICssStyleSheet Sheet, int Characters) Sheet(JsValue value) => constructedSheets[SheetId(value)]; private void CheckCssCharacters(int difference) { if (difference > limits.MaximumStyleCharacters - constructedCssCharacters) throw new BrowserLimitException("The constructed CSS character limit was reached."); } private static void CheckCssRules(ICssStyleSheet sheet) { if (sheet.Rules.Length > 4096) throw new BrowserLimitException("The CSSOM rule limit was reached."); } private Jint.Runtime.JavaScriptException CssIndexError() => AdjacentError("IndexSizeError", "The CSS rule index is outside the stylesheet.", 1); private int CssIndex(JsValue value, int count, bool allowEnd) { var number = value.IsUndefined() ? 0 : Number(value); if (!double.IsFinite(number) || number < 0 || number > uint.MaxValue) throw CssIndexError(); var index = (long)Math.Truncate(number); if (index > count || !allowEnd && index == count) throw CssIndexError(); return (int)index; } }