diff --git a/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Query.cs b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Query.cs
new file mode 100644
index 0000000..af466ea
--- /dev/null
+++ b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Query.cs
@@ -0,0 +1,129 @@
+using System.Runtime.InteropServices;
+
+namespace AngleSharp.ReadOnlyDom.Compact.Document;
+
+public sealed partial class CompactDocument
+{
+ ///
+ /// Returns the next node at or after with the given name ID, or -1.
+ /// Frozen columns use a vectorized scan; packed documents use a scalar scan.
+ ///
+ internal int IndexOfName(string name, int start = 0, int endExclusive = int.MaxValue)
+ {
+ return IndexOfName(name.AsSpan(), start, endExclusive);
+ }
+
+ ///
+ /// Resolves once, then returns the next matching node at or after
+ /// , or -1.
+ ///
+ internal int IndexOfName(ReadOnlySpan name, int start = 0, int endExclusive = int.MaxValue)
+ {
+ return IndexOfName(ResolveNameId(name), start, endExclusive);
+ }
+
+ ///
+ /// Returns the next node at or after with the given pre-resolved name ID, or -1.
+ ///
+ internal int IndexOfName(ushort nameId, int start = 0, int endExclusive = int.MaxValue)
+ {
+ if (start < 0)
+ start = 0;
+ endExclusive = Math.Min(endExclusive, _nodeCount);
+ if (start >= endExclusive)
+ return -1;
+ if (_arena is not null)
+ {
+ var column = _arena.NameIdColumn;
+ var relative = MemoryMarshal
+ .Cast(column.Slice(start, endExclusive - start))
+ .IndexOf((char)nameId);
+ return relative < 0 ? -1 : start + relative;
+ }
+
+ for (var handle = start; handle < endExclusive; handle++)
+ if (_nodes![handle].NameId == nameId)
+ return handle;
+ return -1;
+ }
+
+ internal int CountElements(string name)
+ {
+ return CountElements(name.AsSpan());
+ }
+
+ /// Resolves once, then counts matching elements.
+ internal int CountElements(ReadOnlySpan name)
+ {
+ return CountElements(ResolveNameId(name));
+ }
+
+ /// Counts elements using a previously resolved name ID.
+ internal int CountElements(ushort nameId)
+ {
+ var count = 0;
+ for (var handle = 0; handle < _nodeCount; handle++)
+ {
+ if (TryGetContainingTemplateContentEnd(handle, out var contentEnd))
+ {
+ handle = contentEnd - 1;
+ continue;
+ }
+
+ if (KindAt(handle) == CompactNodeKind.Element && NameIdAt(handle) == nameId)
+ count++;
+ }
+
+ return count;
+ }
+
+ internal string GetName(ushort id)
+ {
+ return id < GeneratedTagMetadata.KnownNameCount
+ ? GeneratedTagMetadata.GetKnownName(id)
+ : _names[id - GeneratedTagMetadata.KnownNameCount];
+ }
+
+ ///
+ /// Resolves a name only when it occurs in this document. This scans nodes and attributes.
+ ///
+ internal ushort FindNameId(string name)
+ {
+ return FindNameId(name.AsSpan());
+ }
+
+ internal ushort FindNameId(ReadOnlySpan name)
+ {
+ var id = ResolveNameId(name);
+ return id != ushort.MaxValue && ContainsNameId(id) ? id : ushort.MaxValue;
+ }
+
+ ///
+ /// Resolves a stable name ID without checking whether it occurs in this document.
+ ///
+ internal ushort ResolveNameId(string name)
+ {
+ return ResolveNameId(name.AsSpan());
+ }
+
+ internal ushort ResolveNameId(ReadOnlySpan name)
+ {
+ if (GeneratedTagMetadata.TryGetKnownNameId(name, out var knownId))
+ return knownId;
+ for (ushort i = 0; i < _nameCount; i++)
+ if (name.SequenceEqual(_names[i]))
+ return checked((ushort)(GeneratedTagMetadata.KnownNameCount + i));
+ return ushort.MaxValue;
+ }
+
+ private bool ContainsNameId(ushort id)
+ {
+ for (var handle = 0; handle < _nodeCount; handle++)
+ if (NameIdAt(handle) == id)
+ return true;
+ for (var attribute = 0; attribute < _attributeCount; attribute++)
+ if (AttributeNameIdAt(attribute) == id)
+ return true;
+ return false;
+ }
+}
diff --git a/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Storage.cs b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Storage.cs
new file mode 100644
index 0000000..5b0dca8
--- /dev/null
+++ b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Storage.cs
@@ -0,0 +1,196 @@
+namespace AngleSharp.ReadOnlyDom.Compact.Document;
+
+public sealed partial class CompactDocument
+{
+ internal CompactNode GetNode(int handle)
+ {
+ if (_arena is null)
+ return _nodes![handle];
+ return new CompactNode(
+ _arena.FrozenFirstChild(handle),
+ _arena.FrozenSubtreeEnd(handle),
+ _arena.FrozenPayloadIndex(handle),
+ _arena.FrozenNameId(handle),
+ _arena.FrozenKind(handle),
+ _arena.FrozenFlags(handle)
+ );
+ }
+
+ internal CompactNodeKind KindAt(int handle)
+ {
+ return _arena is null ? _nodes![handle].Kind : _arena.FrozenKind(handle);
+ }
+
+ internal ushort NameIdAt(int handle)
+ {
+ return _arena is null ? _nodes![handle].NameId : _arena.FrozenNameId(handle);
+ }
+
+ internal int PayloadIndexAt(int handle)
+ {
+ return _arena is null ? _nodes![handle].PayloadIndex : _arena.FrozenPayloadIndex(handle);
+ }
+
+ internal int SubtreeEndAt(int handle)
+ {
+ return _arena is null ? _nodes![handle].SubtreeEndExclusive : _arena.FrozenSubtreeEnd(handle);
+ }
+
+ internal CompactNodePayload GetPayload(int index)
+ {
+ if (_arena is null)
+ return _payloads![index];
+ var value = _arena.FrozenPayloadValue(index);
+ return new CompactNodePayload(
+ _arena.FrozenFirstAttribute(index),
+ value.IsEmpty ? -1 : EncodePayloadValue(index),
+ value.Length,
+ _arena.FrozenAttributeCount(index)
+ );
+ }
+
+ internal CompactAttribute GetAttribute(int index)
+ {
+ if (_arena is null)
+ return _attributes![index];
+ var value = _arena.FrozenAttributeValue(index);
+ return new CompactAttribute(
+ _arena.FrozenAttributeNameId(index),
+ value.IsEmpty ? -1 : EncodeAttributeValue(index),
+ value.Length
+ );
+ }
+
+ // Lightweight column accessors used by the hot attribute-lookup loops. They avoid materializing a
+ // CompactNode/CompactNodePayload/CompactAttribute struct (and, on the frozen path, the encode/decode
+ // round-trip) when the caller only needs the name ID or the value span.
+ internal int PayloadFirstAttributeAt(int payloadIndex)
+ {
+ return _arena is null ? _payloads![payloadIndex].FirstAttribute : _arena.FrozenFirstAttribute(payloadIndex);
+ }
+
+ internal int PayloadAttributeCountAt(int payloadIndex)
+ {
+ return _arena is null ? _payloads![payloadIndex].AttributeCount : _arena.FrozenAttributeCount(payloadIndex);
+ }
+
+ internal ushort AttributeNameIdAt(int index)
+ {
+ return _arena is null ? _attributes![index].NameId : _arena.FrozenAttributeNameId(index);
+ }
+
+ internal ReadOnlySpan AttributeValueSpanAt(int index)
+ {
+ if (_arena is null)
+ {
+ ref readonly var attribute = ref _attributes![index];
+ return attribute.ValueLength == 0 ? [] : _text!.AsSpan(attribute.ValueStart, attribute.ValueLength);
+ }
+
+ return _arena.FrozenAttributeValue(index).Span;
+ }
+
+ /// The value span for a payload index, without materializing a .
+ internal ReadOnlySpan PayloadValueSpanAt(int payloadIndex)
+ {
+ if (_arena is null)
+ {
+ ref readonly var payload = ref _payloads![payloadIndex];
+ return payload.ValueLength == 0 ? [] : _text!.AsSpan(payload.ValueStart, payload.ValueLength);
+ }
+
+ return _arena.FrozenPayloadValue(payloadIndex).Span;
+ }
+
+ /// Returns the first-attribute index and count for a node handle, or false when it has no payload.
+ internal bool TryGetAttributeRange(int handle, out int firstAttribute, out int count)
+ {
+ var payloadIndex = PayloadIndexAt(handle);
+ if (payloadIndex < 0)
+ {
+ firstAttribute = 0;
+ count = 0;
+ return false;
+ }
+
+ firstAttribute = PayloadFirstAttributeAt(payloadIndex);
+ count = PayloadAttributeCountAt(payloadIndex);
+ return true;
+ }
+
+ internal bool TryGetAttribute(int handle, ushort nameId, out CompactAttribute attribute, ref int inspected)
+ {
+ if (nameId != ushort.MaxValue && TryGetAttributeRange(handle, out var first, out var count))
+ for (var index = first; index < first + count; index++)
+ {
+ inspected++;
+ if (AttributeNameIdAt(index) == nameId)
+ {
+ attribute = GetAttribute(index);
+ return true;
+ }
+ }
+
+ attribute = default;
+ return false;
+ }
+
+ internal ReadOnlySpan GetValue(int start, int length)
+ {
+ if (length == 0)
+ return [];
+ if (_arena is null)
+ return _text!.AsSpan(start, length);
+ var memory = IsAttributeValue(start)
+ ? _arena.FrozenAttributeValue(DecodeValueIndex(start))
+ : _arena.FrozenPayloadValue(DecodeValueIndex(start));
+ return memory.Span[..length];
+ }
+
+ internal int GetParent(int handle)
+ {
+ if (!RetainsParentLinks)
+ throw new InvalidOperationException("Parent links were not retained.");
+ return _arena is null ? _parents![handle] : _arena.FrozenParent(handle);
+ }
+
+ internal bool TryGetSourceLocation(int handle, out CompactSourceLocation source)
+ {
+ if (_arena is not null)
+ {
+ if (RetainsSourceLocations)
+ return _arena.TryGetFrozenSourceLocation(handle, out source);
+ source = default;
+ return false;
+ }
+
+ if (_sources is not null && _sources[handle].Index >= 0)
+ {
+ source = _sources[handle];
+ return true;
+ }
+
+ source = default;
+ return false;
+ }
+
+ private static int EncodePayloadValue(int index)
+ {
+ return checked(index << 1);
+ }
+
+ private static int EncodeAttributeValue(int index)
+ {
+ return checked((index << 1) | 1);
+ }
+
+ private static bool IsAttributeValue(int value)
+ {
+ return (value & 1) != 0;
+ }
+
+ private static int DecodeValueIndex(int value)
+ {
+ return value >> 1;
+ }
+}
diff --git a/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Templates.cs b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Templates.cs
new file mode 100644
index 0000000..29017c6
--- /dev/null
+++ b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.Templates.cs
@@ -0,0 +1,60 @@
+namespace AngleSharp.ReadOnlyDom.Compact.Document;
+
+public sealed partial class CompactDocument
+{
+ internal bool IsTemplate(int handle)
+ {
+ if (!_hasTemplates)
+ return false;
+ foreach (var boundary in _templateBoundaries)
+ if (boundary.Handle == handle)
+ return true;
+ return false;
+ }
+
+ internal bool TryGetTemplateContent(int handle, out int contentStart)
+ {
+ if (!_hasTemplates)
+ {
+ contentStart = -1;
+ return false;
+ }
+
+ foreach (var boundary in _templateBoundaries)
+ {
+ if (boundary.Handle != handle)
+ continue;
+ contentStart = boundary.ContentStart;
+ return contentStart >= 0;
+ }
+
+ contentStart = -1;
+ return false;
+ }
+
+ internal bool TryGetContainingTemplateContentEnd(int handle, out int contentEnd)
+ {
+ contentEnd = -1;
+ if (!_hasTemplates)
+ return false;
+ foreach (var boundary in _templateBoundaries)
+ if (handle >= boundary.ContentStart && handle < boundary.ContentEnd)
+ contentEnd = Math.Max(contentEnd, boundary.ContentEnd);
+ return contentEnd >= 0;
+ }
+
+ internal bool IsInSameTreeScope(int first, int second)
+ {
+ if (!_hasTemplates)
+ return true;
+ foreach (var boundary in _templateBoundaries)
+ {
+ var firstInContent = first >= boundary.ContentStart && first < boundary.ContentEnd;
+ var secondInContent = second >= boundary.ContentStart && second < boundary.ContentEnd;
+ if (firstInContent != secondInContent)
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.cs b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.cs
index a243711..759d37a 100644
--- a/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.cs
+++ b/src/AngleSharp.ReadOnlyDom.Compact/Document/CompactDocument.cs
@@ -1,12 +1,11 @@
using System.Buffers;
using System.Runtime.CompilerServices;
-using System.Runtime.InteropServices;
using AngleSharp.Text;
using ArenaStorage = AngleSharp.ReadOnlyDom.Compact.Arena.Arena;
namespace AngleSharp.ReadOnlyDom.Compact.Document;
-public sealed class CompactDocument : IDisposable
+public sealed partial class CompactDocument : IDisposable
{
private readonly ArenaStorage? _arena;
private readonly CompactAttribute[]? _attributes;
@@ -181,375 +180,4 @@ public void Dispose()
if (_sources is not null)
ArrayPool.Shared.Return(_sources);
}
-
- internal CompactNode GetNode(int handle)
- {
- if (_arena is null)
- return _nodes![handle];
- return new CompactNode(
- _arena.FrozenFirstChild(handle),
- _arena.FrozenSubtreeEnd(handle),
- _arena.FrozenPayloadIndex(handle),
- _arena.FrozenNameId(handle),
- _arena.FrozenKind(handle),
- _arena.FrozenFlags(handle)
- );
- }
-
- internal CompactNodeKind KindAt(int handle)
- {
- return _arena is null ? _nodes![handle].Kind : _arena.FrozenKind(handle);
- }
-
- internal ushort NameIdAt(int handle)
- {
- return _arena is null ? _nodes![handle].NameId : _arena.FrozenNameId(handle);
- }
-
- internal int PayloadIndexAt(int handle)
- {
- return _arena is null ? _nodes![handle].PayloadIndex : _arena.FrozenPayloadIndex(handle);
- }
-
- internal int SubtreeEndAt(int handle)
- {
- return _arena is null ? _nodes![handle].SubtreeEndExclusive : _arena.FrozenSubtreeEnd(handle);
- }
-
- internal CompactNodePayload GetPayload(int index)
- {
- if (_arena is null)
- return _payloads![index];
- var value = _arena.FrozenPayloadValue(index);
- return new CompactNodePayload(
- _arena.FrozenFirstAttribute(index),
- value.IsEmpty ? -1 : EncodePayloadValue(index),
- value.Length,
- _arena.FrozenAttributeCount(index)
- );
- }
-
- internal CompactAttribute GetAttribute(int index)
- {
- if (_arena is null)
- return _attributes![index];
- var value = _arena.FrozenAttributeValue(index);
- return new CompactAttribute(
- _arena.FrozenAttributeNameId(index),
- value.IsEmpty ? -1 : EncodeAttributeValue(index),
- value.Length
- );
- }
-
- // Lightweight column accessors used by the hot attribute-lookup loops. They avoid materializing a
- // CompactNode/CompactNodePayload/CompactAttribute struct (and, on the frozen path, the encode/decode
- // round-trip) when the caller only needs the name ID or the value span.
- internal int PayloadFirstAttributeAt(int payloadIndex)
- {
- return _arena is null ? _payloads![payloadIndex].FirstAttribute : _arena.FrozenFirstAttribute(payloadIndex);
- }
-
- internal int PayloadAttributeCountAt(int payloadIndex)
- {
- return _arena is null ? _payloads![payloadIndex].AttributeCount : _arena.FrozenAttributeCount(payloadIndex);
- }
-
- internal ushort AttributeNameIdAt(int index)
- {
- return _arena is null ? _attributes![index].NameId : _arena.FrozenAttributeNameId(index);
- }
-
- internal ReadOnlySpan AttributeValueSpanAt(int index)
- {
- if (_arena is null)
- {
- ref readonly var attribute = ref _attributes![index];
- return attribute.ValueLength == 0 ? [] : _text!.AsSpan(attribute.ValueStart, attribute.ValueLength);
- }
-
- return _arena.FrozenAttributeValue(index).Span;
- }
-
- /// The value span for a payload index, without materializing a .
- internal ReadOnlySpan PayloadValueSpanAt(int payloadIndex)
- {
- if (_arena is null)
- {
- ref readonly var payload = ref _payloads![payloadIndex];
- return payload.ValueLength == 0 ? [] : _text!.AsSpan(payload.ValueStart, payload.ValueLength);
- }
-
- return _arena.FrozenPayloadValue(payloadIndex).Span;
- }
-
- /// Returns the first-attribute index and count for a node handle, or false when it has no payload.
- internal bool TryGetAttributeRange(int handle, out int firstAttribute, out int count)
- {
- var payloadIndex = PayloadIndexAt(handle);
- if (payloadIndex < 0)
- {
- firstAttribute = 0;
- count = 0;
- return false;
- }
-
- firstAttribute = PayloadFirstAttributeAt(payloadIndex);
- count = PayloadAttributeCountAt(payloadIndex);
- return true;
- }
-
- internal bool TryGetAttribute(int handle, ushort nameId, out CompactAttribute attribute, ref int inspected)
- {
- if (nameId != ushort.MaxValue && TryGetAttributeRange(handle, out var first, out var count))
- for (var index = first; index < first + count; index++)
- {
- inspected++;
- if (AttributeNameIdAt(index) == nameId)
- {
- attribute = GetAttribute(index);
- return true;
- }
- }
-
- attribute = default;
- return false;
- }
-
- ///
- /// Returns the next node at or after with the given name ID, or -1.
- /// Frozen columns use a vectorized scan; packed documents use a scalar scan.
- ///
- internal int IndexOfName(string name, int start = 0, int endExclusive = int.MaxValue)
- {
- return IndexOfName(name.AsSpan(), start, endExclusive);
- }
-
- ///
- /// Resolves once, then returns the next matching node at or after
- /// , or -1.
- ///
- internal int IndexOfName(ReadOnlySpan name, int start = 0, int endExclusive = int.MaxValue)
- {
- return IndexOfName(ResolveNameId(name), start, endExclusive);
- }
-
- ///
- /// Returns the next node at or after with the given pre-resolved name ID, or -1.
- ///
- internal int IndexOfName(ushort nameId, int start = 0, int endExclusive = int.MaxValue)
- {
- if (start < 0)
- start = 0;
- endExclusive = Math.Min(endExclusive, _nodeCount);
- if (start >= endExclusive)
- return -1;
- if (_arena is not null)
- {
- var column = _arena.NameIdColumn;
- var relative = MemoryMarshal
- .Cast(column.Slice(start, endExclusive - start))
- .IndexOf((char)nameId);
- return relative < 0 ? -1 : start + relative;
- }
-
- for (var handle = start; handle < endExclusive; handle++)
- if (_nodes![handle].NameId == nameId)
- return handle;
- return -1;
- }
-
- internal string GetName(ushort id)
- {
- return id < GeneratedTagMetadata.KnownNameCount
- ? GeneratedTagMetadata.GetKnownName(id)
- : _names[id - GeneratedTagMetadata.KnownNameCount];
- }
-
- internal ReadOnlySpan GetValue(int start, int length)
- {
- if (length == 0)
- return [];
- if (_arena is null)
- return _text!.AsSpan(start, length);
- var memory = IsAttributeValue(start)
- ? _arena.FrozenAttributeValue(DecodeValueIndex(start))
- : _arena.FrozenPayloadValue(DecodeValueIndex(start));
- return memory.Span[..length];
- }
-
- internal int GetParent(int handle)
- {
- if (!RetainsParentLinks)
- throw new InvalidOperationException("Parent links were not retained.");
- return _arena is null ? _parents![handle] : _arena.FrozenParent(handle);
- }
-
- internal bool TryGetSourceLocation(int handle, out CompactSourceLocation source)
- {
- if (_arena is not null)
- {
- if (RetainsSourceLocations)
- return _arena.TryGetFrozenSourceLocation(handle, out source);
- source = default;
- return false;
- }
-
- if (_sources is not null && _sources[handle].Index >= 0)
- {
- source = _sources[handle];
- return true;
- }
-
- source = default;
- return false;
- }
-
- internal int CountElements(string name)
- {
- return CountElements(name.AsSpan());
- }
-
- /// Resolves once, then counts matching elements.
- internal int CountElements(ReadOnlySpan name)
- {
- return CountElements(ResolveNameId(name));
- }
-
- /// Counts elements using a previously resolved name ID.
- internal int CountElements(ushort nameId)
- {
- var count = 0;
- for (var handle = 0; handle < _nodeCount; handle++)
- {
- if (TryGetContainingTemplateContentEnd(handle, out var contentEnd))
- {
- handle = contentEnd - 1;
- continue;
- }
-
- if (KindAt(handle) == CompactNodeKind.Element && NameIdAt(handle) == nameId)
- count++;
- }
-
- return count;
- }
-
- internal bool IsTemplate(int handle)
- {
- if (!_hasTemplates)
- return false;
- foreach (var boundary in _templateBoundaries)
- if (boundary.Handle == handle)
- return true;
- return false;
- }
-
- internal bool TryGetTemplateContent(int handle, out int contentStart)
- {
- if (!_hasTemplates)
- {
- contentStart = -1;
- return false;
- }
-
- foreach (var boundary in _templateBoundaries)
- {
- if (boundary.Handle != handle)
- continue;
- contentStart = boundary.ContentStart;
- return contentStart >= 0;
- }
-
- contentStart = -1;
- return false;
- }
-
- internal bool TryGetContainingTemplateContentEnd(int handle, out int contentEnd)
- {
- contentEnd = -1;
- if (!_hasTemplates)
- return false;
- foreach (var boundary in _templateBoundaries)
- if (handle >= boundary.ContentStart && handle < boundary.ContentEnd)
- contentEnd = Math.Max(contentEnd, boundary.ContentEnd);
- return contentEnd >= 0;
- }
-
- internal bool IsInSameTreeScope(int first, int second)
- {
- if (!_hasTemplates)
- return true;
- foreach (var boundary in _templateBoundaries)
- {
- var firstInContent = first >= boundary.ContentStart && first < boundary.ContentEnd;
- var secondInContent = second >= boundary.ContentStart && second < boundary.ContentEnd;
- if (firstInContent != secondInContent)
- return false;
- }
-
- return true;
- }
-
- ///
- /// Resolves a name only when it occurs in this document. This scans nodes and attributes.
- ///
- internal ushort FindNameId(string name)
- {
- return FindNameId(name.AsSpan());
- }
-
- internal ushort FindNameId(ReadOnlySpan name)
- {
- var id = ResolveNameId(name);
- return id != ushort.MaxValue && ContainsNameId(id) ? id : ushort.MaxValue;
- }
-
- ///
- /// Resolves a stable name ID without checking whether it occurs in this document.
- ///
- internal ushort ResolveNameId(string name)
- {
- return ResolveNameId(name.AsSpan());
- }
-
- internal ushort ResolveNameId(ReadOnlySpan name)
- {
- if (GeneratedTagMetadata.TryGetKnownNameId(name, out var knownId))
- return knownId;
- for (ushort i = 0; i < _nameCount; i++)
- if (name.SequenceEqual(_names[i]))
- return checked((ushort)(GeneratedTagMetadata.KnownNameCount + i));
- return ushort.MaxValue;
- }
-
- private static int EncodePayloadValue(int index)
- {
- return checked(index << 1);
- }
-
- private static int EncodeAttributeValue(int index)
- {
- return checked((index << 1) | 1);
- }
-
- private static bool IsAttributeValue(int value)
- {
- return (value & 1) != 0;
- }
-
- private static int DecodeValueIndex(int value)
- {
- return value >> 1;
- }
-
- private bool ContainsNameId(ushort id)
- {
- for (var handle = 0; handle < _nodeCount; handle++)
- if (NameIdAt(handle) == id)
- return true;
- for (var attribute = 0; attribute < _attributeCount; attribute++)
- if (AttributeNameIdAt(attribute) == id)
- return true;
- return false;
- }
}
diff --git a/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.Captures.cs b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.Captures.cs
new file mode 100644
index 0000000..e204905
--- /dev/null
+++ b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.Captures.cs
@@ -0,0 +1,197 @@
+using System.Numerics;
+using System.Runtime.CompilerServices;
+
+namespace AngleSharp.ReadOnlyDom.Streaming.Query.Execution;
+
+internal partial class QueryExecution
+ where TResourceLimits : struct, IResourceLimitPolicy
+{
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void DisposeCompletedCaptures()
+ {
+ foreach (var captures in _completedCaptures)
+ {
+ if (captures is null)
+ continue;
+ foreach (var capture in captures)
+ capture.Dispose();
+ }
+ if (_reusableCaptures is not null)
+ {
+ foreach (var capture in _reusableCaptures)
+ capture.Dispose();
+ _reusableCaptures.Clear();
+ }
+ Array.Clear(_completedCaptures);
+ }
+
+ private void StartCompletedCaptures(ulong matches)
+ {
+ var completed = matches & _plan.CompletedHandlerMask;
+ while (completed != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(completed);
+ completed &= completed - 1;
+ var node = _plan.Nodes[index];
+ var capture = _reusableCaptures!.Count == 0 ? new CapturedElementBuffer() : _reusableCaptures.Pop();
+ capture.Reset(node.CompletedTextMode, node.CapturedAttributeIndexes.Length);
+ for (var attribute = 0; attribute < node.CapturedAttributeIndexes.Length; attribute++)
+ {
+ var attributeIndex = node.CapturedAttributeIndexes[attribute];
+ if (_attributeLengths[attributeIndex] >= 0)
+ {
+ var value = GetAttributeValue(attributeIndex);
+ capture.SetAttribute(attribute, value);
+ if (TResourceLimits.Enabled)
+ {
+ _queryCaptureBytes += value.Length;
+ }
+ }
+ }
+ capture.BeginText();
+ var captures = _completedCaptures[index] ??= [];
+ captures.Add(capture);
+ if (node.CompletedTextMode != CompletedTextMode.None)
+ _activeCompletedTextCaptures++;
+ if (node.CompletedTextMode == CompletedTextMode.Normalized)
+ _activeNormalizedTextCaptures++;
+ }
+ }
+
+ ///
+ /// Separates words in every open normalized capture. Callers have already established that this
+ /// tag is a boundary and that at least one normalized capture is open, so this walks only the
+ /// normalized nodes and never the raw ones.
+ ///
+ [MethodImpl(MethodImplOptions.NoInlining)]
+ private void MarkTextBoundary()
+ {
+ var completed = _normalizedTextMask;
+ while (completed != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(completed);
+ completed &= completed - 1;
+ var captures = _completedCaptures[index];
+ if (captures is null)
+ continue;
+ foreach (var capture in captures)
+ capture.MarkBoundary();
+ }
+ }
+
+ private void AppendCompletedText(ReadOnlySpan utf8)
+ {
+ var completed = _plan.CompletedHandlerMask;
+ while (completed != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(completed);
+ completed &= completed - 1;
+ var captures = _completedCaptures[index];
+ if (captures is null)
+ continue;
+ foreach (var capture in captures)
+ {
+ var previousLength = capture.BufferedByteCount;
+ capture.Append(utf8);
+ if (TResourceLimits.Enabled)
+ {
+ _queryCaptureBytes += capture.BufferedByteCount - previousLength;
+ }
+ }
+ }
+ }
+
+ private void CompleteCapture(int index)
+ {
+ var node = _plan.Nodes[index];
+ if (node.Completed is null)
+ return;
+ var captures = _completedCaptures[index];
+ if (captures is null || captures.Count == 0)
+ throw new InvalidOperationException("The completed-element capture stack is unbalanced.");
+ var captureIndex = captures.Count - 1;
+ var capture = captures[captureIndex];
+ captures.RemoveAt(captureIndex);
+ if (node.CompletedTextMode != CompletedTextMode.None)
+ _activeCompletedTextCaptures--;
+ if (node.CompletedTextMode == CompletedTextMode.Normalized)
+ _activeNormalizedTextCaptures--;
+ if (TResourceLimits.Enabled)
+ {
+ _queryCaptureBytes -= capture.BufferedByteCount;
+ }
+ try
+ {
+ var element = new CompletedElement(
+ capture,
+ _plan.AttributeNames,
+ _plan.AttributeNamesUtf8,
+ node.CapturedAttributeIndexes
+ );
+ node.Completed.Invoke(ref _state, in element);
+ }
+ finally
+ {
+ _reusableCaptures!.Push(capture);
+ }
+ }
+
+ private long GetCompletedAttributeBytes(ulong matches)
+ {
+ var total = 0L;
+ var completed = matches & _plan.CompletedHandlerMask;
+ while (completed != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(completed);
+ completed &= completed - 1;
+ foreach (var attributeIndex in _plan.Nodes[index].CapturedAttributeIndexes)
+ {
+ var length = _attributeLengths[attributeIndex];
+ if (length > 0)
+ total = SaturatingAdd(total, length);
+ }
+ }
+ return total;
+ }
+
+ private long GetCompletedTextUpperBound(int textLength)
+ {
+ if (textLength == 0)
+ return 0;
+
+ var total = 0L;
+ var completed = _plan.CompletedHandlerMask;
+ while (completed != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(completed);
+ completed &= completed - 1;
+ if (_plan.Nodes[index].CompletedTextMode == CompletedTextMode.None)
+ continue;
+ var captures = _completedCaptures[index];
+ if (captures is null)
+ continue;
+ foreach (var capture in captures)
+ {
+ total = SaturatingAdd(total, textLength);
+ if (capture.HasPendingNormalizedSpace)
+ total = SaturatingAdd(total, 1);
+ }
+ }
+ return total;
+ }
+
+ private void EnsureQueryCaptureCapacity(long additional)
+ {
+ var observed =
+ _queryCaptureBytes > long.MaxValue - additional ? long.MaxValue : _queryCaptureBytes + additional;
+ if (observed > _maximumQueryCaptureBytes)
+ throw new HtmlStreamingLimitExceededException(
+ HtmlStreamingLimit.QueryCaptureBytes,
+ _maximumQueryCaptureBytes,
+ observed
+ );
+ }
+
+ private static long SaturatingAdd(long value, long additional) =>
+ value > long.MaxValue - additional ? long.MaxValue : value + additional;
+}
diff --git a/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.TokenSink.cs b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.TokenSink.cs
new file mode 100644
index 0000000..f5ecdc8
--- /dev/null
+++ b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.TokenSink.cs
@@ -0,0 +1,364 @@
+using System.Buffers;
+using System.Numerics;
+using AngleSharp.ReadOnlyDom.Streaming.Query.Rewriting;
+using AngleSharp.ReadOnlyDom.Streaming.Tokenization;
+
+namespace AngleSharp.ReadOnlyDom.Streaming.Query.Execution;
+
+internal partial class QueryExecution
+ where TResourceLimits : struct, IResourceLimitPolicy
+{
+ public void ObserveNormalizedUtf8End(long sourceStart, ReadOnlySpan utf8, long publishableOffset)
+ {
+ _observedUtf8End = sourceStart + utf8.Length;
+ _streamingRewriteCollector?.PublishWindow(sourceStart, utf8, publishableOffset);
+ }
+
+ public void RawText(long sourceStart, ReadOnlySpan utf8, Utf8HtmlTextType textType, bool isLastInTextNode)
+ {
+ if (!WantsRawText)
+ return;
+ var chunk = new TextChunk(utf8, (HtmlTextType)textType, isLastInTextNode);
+ var rewriter = new TextChunkRewriter(_rewriteCollector!, sourceStart, sourceStart + utf8.Length);
+ TextRewriteHandler.Invoke(ref _state, in chunk, ref rewriter);
+ rewriter.Commit();
+ }
+
+ public Utf8HtmlStartTagCapture StartTag(Utf8HtmlName name)
+ {
+ ReleasePendingFallbackTagName();
+ var identityLength = 0;
+ if (!name.TryGetCompactKey(out var identity))
+ {
+ identity = name.SemanticHash;
+ identityLength = name.Verbatim.Length;
+ _pendingFallbackTagNameUtf8 = ArrayPool.Shared.Rent(identityLength);
+ name.Verbatim.CopyTo(_pendingFallbackTagNameUtf8);
+ }
+ _pendingTagIdentity = identity;
+ _pendingTagIdentityLength = identityLength;
+ _pendingTagNameLength = name.Verbatim.Length;
+ _pendingCandidateBits = 0;
+ _pendingAttributeBits = 0;
+ _pendingAttributeFilter = 0;
+ _pendingAttributeNameLengths = 0;
+ _pendingAttributeIndex = -1;
+ var candidates = FindTagCandidates(identity, identityLength);
+ while (candidates != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(candidates);
+ candidates &= candidates - 1;
+ var node = _plan.Nodes[index];
+ if ((identityLength != 0 && !name.SemanticEquals(node.TagNameUtf8)) || !ParentMatches(node))
+ continue;
+ _pendingCandidateBits |= 1UL << node.Index;
+ _pendingAttributeBits |= node.RequestedAttributeMask;
+ _pendingAttributeFilter |= node.RequestedAttributeFilter;
+ _pendingAttributeNameLengths |= node.RequestedAttributeNameLengths;
+ }
+ ResetAttributes();
+ return _pendingAttributeBits == 0 ? Utf8HtmlStartTagCapture.None : Utf8HtmlStartTagCapture.Attributes;
+ }
+
+ ///
+ /// Bloom of the semantic hashes of every attribute name any candidate node on the current
+ /// tag requests. WantsAttribute is a pure function of the semantic name for the duration of
+ /// the tag (it only consults _pendingAttributeBits, fixed at StartTag), so the tokenizer may
+ /// reject filter-missed names without calling back.
+ ///
+ public ulong StartTagAttributeFilter => _pendingAttributeFilter;
+
+ ///
+ /// Byte lengths of the attribute names any candidate node on the current tag requests, as bits.
+ /// Same purity contract as , and cheaper for the tokenizer
+ /// to consult: a length is known before the name has been hashed.
+ ///
+ public ulong StartTagAttributeNameLengths => _pendingAttributeNameLengths;
+
+ public bool WantsAttribute(Utf8HtmlName name)
+ {
+ _pendingAttributeIndex = -1;
+ var identity = 0UL;
+ var hasCompactIdentity =
+ (_pendingAttributeBits & _plan.CompactAttributeMask) != 0 && name.TryGetCompactKey(out identity);
+
+ var attributes = _pendingAttributeBits;
+ while (attributes != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(attributes);
+ attributes &= attributes - 1;
+ var expected = _plan.AttributeIdentities[index];
+ if (hasCompactIdentity)
+ {
+ if (expected.Length != 0 || expected.Value != identity)
+ continue;
+ }
+ else if (
+ expected.Length == 0
+ || expected.Length != name.Verbatim.Length
+ || !name.SemanticEquals(_plan.AttributeNamesUtf8[index])
+ )
+ {
+ continue;
+ }
+ _pendingAttributeIndex = index;
+ return true;
+ }
+ return false;
+ }
+
+ public void Attribute(Utf8HtmlName name, ReadOnlySpan value, bool valueMayContainReferences)
+ {
+ var index = _pendingAttributeIndex;
+ _pendingAttributeIndex = -1;
+ if (index < 0 || _attributeLengths[index] >= 0)
+ return;
+ if (TResourceLimits.Enabled)
+ {
+ EnsureQueryCaptureCapacity(value.Length);
+ }
+ EnsureAttributeCapacity(value.Length);
+ _attributeStarts[index] = _attributeValueLength;
+ _attributeLengths[index] = value.Length;
+ _seenAttributeBits |= 1UL << index;
+ if (valueMayContainReferences)
+ _rawAttributeBits |= 1UL << index;
+ value.CopyTo(_attributeValues.AsSpan(_attributeValueLength));
+ _attributeValueLength += value.Length;
+ if (TResourceLimits.Enabled)
+ {
+ _queryCaptureBytes += value.Length;
+ }
+ }
+
+ public void StartTagSourceRange(long sourceStart, long sourceEnd)
+ {
+ _startTagSourceStart = sourceStart;
+ _startTagSourceEnd = sourceEnd;
+ }
+
+ public void StartTagEnd(bool selfClosing)
+ {
+ StartTagEndCore(selfClosing, _startTagSourceStart, _startTagSourceEnd);
+ _startTagSourceStart = -1;
+ _startTagSourceEnd = -1;
+ }
+
+ private void StartTagEndCore(bool selfClosing, long sourceStart, long sourceEnd)
+ {
+ // Classify only inside an open normalized capture, then carry the result to the close in
+ // the frame's sign bit. A frame opened before the outermost capture cannot close while that
+ // capture is active: lexical recovery closes inner frames first.
+ var isTextBoundary =
+ _activeNormalizedTextCaptures != 0
+ && HtmlTextBoundaryElements.IsBoundary(_pendingTagIdentity, _pendingTagIdentityLength);
+ if (isTextBoundary)
+ MarkTextBoundary();
+ var matches = 0UL;
+ var candidates = _pendingCandidateBits;
+ while (candidates != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(candidates);
+ candidates &= candidates - 1;
+ var node = _plan.Nodes[index];
+ if (!PredicatesMatch(node.Predicates))
+ continue;
+ matches |= 1UL << node.Index;
+ }
+
+ var closesImmediately = IsVoidTag(_pendingTagIdentity, _pendingTagIdentityLength, _pendingTagNameLength);
+ if (TResourceLimits.Enabled && !closesImmediately && _frameCount >= _maximumNestingDepth)
+ throw new HtmlStreamingLimitExceededException(
+ HtmlStreamingLimit.NestingDepth,
+ _maximumNestingDepth,
+ (long)_frameCount + 1
+ );
+ if (TResourceLimits.Enabled)
+ {
+ EnsureQueryCaptureCapacity(GetCompletedAttributeBytes(matches));
+ }
+
+ var starts = matches;
+ while (starts != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(starts);
+ starts &= starts - 1;
+ var node = _plan.Nodes[index];
+ if (node.Start is null)
+ continue;
+ var element = CreateElement(node.RequestedAttributeMask);
+ node.Start.Invoke(ref _state, in element);
+ }
+ var rewriteScopeId = -1;
+ var rewriteHandler = ElementRewriteHandler;
+ if (rewriteHandler is not null && (matches & _plan.TerminalNodeMask) != 0)
+ {
+ if (sourceStart < 0 || sourceEnd <= sourceStart)
+ throw new InvalidOperationException("The tokenizer did not provide a valid start-tag source range.");
+ var element = CreateElement(GetRequestedAttributeMask(matches & _plan.TerminalNodeMask));
+ var editor = new ElementRewriter(
+ _rewriteCollector!,
+ sourceStart,
+ sourceEnd,
+ !closesImmediately,
+ selfClosing
+ );
+ rewriteHandler.Invoke(ref _state, in element, ref editor);
+ editor.Commit();
+ rewriteScopeId = editor.ScopeId;
+ }
+ StartCompletedCaptures(matches);
+
+ if (closesImmediately)
+ {
+ try
+ {
+ _rewriteCollector?.EndElement(rewriteScopeId, sourceEnd, sourceEnd, hasExplicitEndTag: false);
+ CloseMatches(matches);
+ }
+ finally
+ {
+ ReleasePendingFallbackTagName();
+ }
+ return;
+ }
+
+ EnsureFrameCapacity();
+ _frames[_frameCount++] = new QueryFrame(
+ _pendingTagIdentity,
+ isTextBoundary ? _pendingTagIdentityLength | TextBoundaryFrameFlag : _pendingTagIdentityLength,
+ _pendingFallbackTagNameUtf8,
+ matches,
+ rewriteScopeId
+ );
+ _pendingFallbackTagNameUtf8 = null;
+ IncrementActive(matches);
+ }
+
+ private Element CreateElement(ulong allowedAttributeMask) =>
+ new(_plan.AttributeNames, _plan.AttributeNamesUtf8, this, allowedAttributeMask);
+
+ bool IElementAttributeSource.TryGetAttributeValue(int index, out ReadOnlySpan value)
+ {
+ if (_attributeLengths[index] < 0)
+ {
+ value = default;
+ return false;
+ }
+ value = GetAttributeValue(index);
+ return true;
+ }
+
+ private ulong GetRequestedAttributeMask(ulong nodes)
+ {
+ var attributes = 0UL;
+ while (nodes != 0)
+ {
+ var index = BitOperations.TrailingZeroCount(nodes);
+ nodes &= nodes - 1;
+ attributes |= _plan.Nodes[index].RequestedAttributeMask;
+ }
+ return attributes;
+ }
+
+ public void Text(ReadOnlySpan utf8)
+ {
+ if (_plan.TextHandlerMask == 0 && _plan.CompletedHandlerMask == 0)
+ return;
+ if (TResourceLimits.Enabled)
+ {
+ EnsureQueryCaptureCapacity(GetCompletedTextUpperBound(utf8.Length));
+ }
+ var handlers = _plan.TextHandlerMask;
+ while (handlers != 0)
+ {
+ var nodeIndex = BitOperations.TrailingZeroCount(handlers);
+ handlers &= handlers - 1;
+ if (_activeCounts[nodeIndex] == 0)
+ continue;
+ _plan.Nodes[nodeIndex].Text!.Invoke(ref _state, utf8);
+ }
+ AppendCompletedText(utf8);
+ }
+
+ bool IUtf8HtmlStreamingCommentSink.BeginComment() => false;
+
+ void IUtf8HtmlStreamingCommentSink.CommentChunk(ReadOnlySpan utf8) { }
+
+ void IUtf8HtmlStreamingCommentSink.EndComment() { }
+
+ public void EndTagSourceRange(long sourceStart, long sourceEnd)
+ {
+ _endTagSourceStart = sourceStart;
+ _endTagSourceEnd = sourceEnd;
+ }
+
+ public void EndTag(Utf8HtmlName name)
+ {
+ var identityLength = 0;
+ if (!name.TryGetCompactKey(out var identity))
+ {
+ identity = name.SemanticHash;
+ identityLength = name.Verbatim.Length;
+ }
+ for (var index = _frameCount - 1; index >= 0; index--)
+ {
+ if (
+ _frames[index].TagIdentity != identity
+ || (_frames[index].TagIdentityLength & TagIdentityLengthMask) != identityLength
+ )
+ continue;
+ if (
+ identityLength != 0
+ && !name.SemanticEquals(_frames[index].FallbackTagNameUtf8.AsSpan(0, identityLength))
+ )
+ continue;
+ for (var popped = _frameCount - 1; popped >= index; popped--)
+ {
+ var frame = _frames[popped];
+ _frames[popped] = default;
+ _frameCount = popped;
+ var explicitEnd = popped == index;
+ CloseFrame(frame, _endTagSourceStart, explicitEnd ? _endTagSourceEnd : _endTagSourceStart, explicitEnd);
+ }
+ _endTagSourceStart = -1;
+ _endTagSourceEnd = -1;
+ return;
+ }
+ _endTagSourceStart = -1;
+ _endTagSourceEnd = -1;
+ }
+
+ private ulong FindTagCandidates(ulong identity, int identityLength)
+ {
+ var entries = _plan.TagDispatch;
+ var low = 0;
+ var high = entries.Length - 1;
+ while (low <= high)
+ {
+ var middle = (low + high) >>> 1;
+ var entry = entries[middle];
+ var comparison = entry.Identity.CompareTo(identity);
+ if (comparison == 0)
+ comparison = entry.IdentityLength.CompareTo(identityLength);
+ if (comparison < 0)
+ low = middle + 1;
+ else if (comparison > 0)
+ high = middle - 1;
+ else
+ return entry.CandidateBits;
+ }
+ return 0;
+ }
+
+ public void EndOfFile()
+ {
+ for (var index = _frameCount - 1; index >= 0; index--)
+ {
+ var frame = _frames[index];
+ _frames[index] = default;
+ _frameCount = index;
+ CloseFrame(frame, _observedUtf8End, _observedUtf8End, hasExplicitEndTag: false);
+ }
+ }
+}
diff --git a/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.cs b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.cs
index cd006e0..eea467a 100644
--- a/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.cs
+++ b/src/AngleSharp.ReadOnlyDom.Streaming/Query/Execution/QueryExecution.cs
@@ -11,7 +11,7 @@ internal interface IQueryExecution : IUtf8HtmlTokenSink, IDisposable
TState State { get; }
}
-internal class QueryExecution
+internal partial class QueryExecution
: IUtf8HtmlStartTagSourceRangeSink,
IUtf8HtmlRawTextSink,
IUtf8HtmlStreamingCommentSink,
@@ -45,7 +45,8 @@ internal class QueryExecution
private bool _disposed;
private readonly int _maximumNestingDepth;
private readonly long _maximumQueryCaptureBytes;
- private readonly object? _rewriteHandlers;
+ private readonly RewriteHandler? _elementRewriteHandler;
+ private readonly TextRewriteHandler? _textRewriteHandler;
private readonly IHtmlRewriteCollector? _rewriteCollector;
private readonly Utf8StreamingRewriteCollector? _streamingRewriteCollector;
private long _startTagSourceStart = -1;
@@ -83,18 +84,8 @@ internal QueryExecution(
_state = state;
_maximumNestingDepth = limits.MaximumNestingDepth;
_maximumQueryCaptureBytes = limits.MaximumQueryCaptureBytes;
- if (rewriteHandler is not null && textRewriteHandler is not null)
- {
- _rewriteHandlers = new RewriteHandlerPair(rewriteHandler, textRewriteHandler);
- }
- else if (rewriteHandler is not null)
- {
- _rewriteHandlers = rewriteHandler;
- }
- else if (textRewriteHandler is not null)
- {
- _rewriteHandlers = textRewriteHandler;
- }
+ _elementRewriteHandler = rewriteHandler;
+ _textRewriteHandler = textRewriteHandler;
_rewriteCollector = rewriteCollector;
_normalizedTextMask = plan.NormalizedTextHandlerMask;
_streamingRewriteCollector = rewriteCollector as Utf8StreamingRewriteCollector;
@@ -116,7 +107,7 @@ internal QueryExecution(
? Utf8HtmlTokenCapture.Text
: Utf8HtmlTokenCapture.None;
- public bool WantsStartTagSourceRanges => _rewriteHandlers is not null;
+ public bool WantsStartTagSourceRanges => _elementRewriteHandler is not null || _textRewriteHandler is not null;
public bool IsRawTextEnabled => HasTextRewriteHandler;
@@ -125,360 +116,6 @@ internal QueryExecution(
public bool WantsEndTagSourceRanges => HasTextRewriteHandler || _rewriteCollector?.NeedsEndTagSourceRanges == true;
- public void ObserveNormalizedUtf8End(long sourceStart, ReadOnlySpan utf8, long publishableOffset)
- {
- _observedUtf8End = sourceStart + utf8.Length;
- _streamingRewriteCollector?.PublishWindow(sourceStart, utf8, publishableOffset);
- }
-
- public void RawText(long sourceStart, ReadOnlySpan utf8, Utf8HtmlTextType textType, bool isLastInTextNode)
- {
- if (!WantsRawText)
- return;
- var chunk = new TextChunk(utf8, (HtmlTextType)textType, isLastInTextNode);
- var rewriter = new TextChunkRewriter(_rewriteCollector!, sourceStart, sourceStart + utf8.Length);
- TextRewriteHandler.Invoke(ref _state, in chunk, ref rewriter);
- rewriter.Commit();
- }
-
- public Utf8HtmlStartTagCapture StartTag(Utf8HtmlName name)
- {
- ReleasePendingFallbackTagName();
- var identityLength = 0;
- if (!name.TryGetCompactKey(out var identity))
- {
- identity = name.SemanticHash;
- identityLength = name.Verbatim.Length;
- _pendingFallbackTagNameUtf8 = ArrayPool.Shared.Rent(identityLength);
- name.Verbatim.CopyTo(_pendingFallbackTagNameUtf8);
- }
- _pendingTagIdentity = identity;
- _pendingTagIdentityLength = identityLength;
- _pendingTagNameLength = name.Verbatim.Length;
- _pendingCandidateBits = 0;
- _pendingAttributeBits = 0;
- _pendingAttributeFilter = 0;
- _pendingAttributeNameLengths = 0;
- _pendingAttributeIndex = -1;
- var candidates = FindTagCandidates(identity, identityLength);
- while (candidates != 0)
- {
- var index = BitOperations.TrailingZeroCount(candidates);
- candidates &= candidates - 1;
- var node = _plan.Nodes[index];
- if ((identityLength != 0 && !name.SemanticEquals(node.TagNameUtf8)) || !ParentMatches(node))
- continue;
- _pendingCandidateBits |= 1UL << node.Index;
- _pendingAttributeBits |= node.RequestedAttributeMask;
- _pendingAttributeFilter |= node.RequestedAttributeFilter;
- _pendingAttributeNameLengths |= node.RequestedAttributeNameLengths;
- }
- ResetAttributes();
- return _pendingAttributeBits == 0 ? Utf8HtmlStartTagCapture.None : Utf8HtmlStartTagCapture.Attributes;
- }
-
- ///
- /// Bloom of the semantic hashes of every attribute name any candidate node on the current
- /// tag requests. WantsAttribute is a pure function of the semantic name for the duration of
- /// the tag (it only consults _pendingAttributeBits, fixed at StartTag), so the tokenizer may
- /// reject filter-missed names without calling back.
- ///
- public ulong StartTagAttributeFilter => _pendingAttributeFilter;
-
- ///
- /// Byte lengths of the attribute names any candidate node on the current tag requests, as bits.
- /// Same purity contract as , and cheaper for the tokenizer
- /// to consult: a length is known before the name has been hashed.
- ///
- public ulong StartTagAttributeNameLengths => _pendingAttributeNameLengths;
-
- public bool WantsAttribute(Utf8HtmlName name)
- {
- _pendingAttributeIndex = -1;
- var identity = 0UL;
- var hasCompactIdentity =
- (_pendingAttributeBits & _plan.CompactAttributeMask) != 0 && name.TryGetCompactKey(out identity);
-
- var attributes = _pendingAttributeBits;
- while (attributes != 0)
- {
- var index = BitOperations.TrailingZeroCount(attributes);
- attributes &= attributes - 1;
- var expected = _plan.AttributeIdentities[index];
- if (hasCompactIdentity)
- {
- if (expected.Length != 0 || expected.Value != identity)
- continue;
- }
- else if (
- expected.Length == 0
- || expected.Length != name.Verbatim.Length
- || !name.SemanticEquals(_plan.AttributeNamesUtf8[index])
- )
- {
- continue;
- }
- _pendingAttributeIndex = index;
- return true;
- }
- return false;
- }
-
- public void Attribute(Utf8HtmlName name, ReadOnlySpan value, bool valueMayContainReferences)
- {
- var index = _pendingAttributeIndex;
- _pendingAttributeIndex = -1;
- if (index < 0 || _attributeLengths[index] >= 0)
- return;
- if (TResourceLimits.Enabled)
- {
- EnsureQueryCaptureCapacity(value.Length);
- }
- EnsureAttributeCapacity(value.Length);
- _attributeStarts[index] = _attributeValueLength;
- _attributeLengths[index] = value.Length;
- _seenAttributeBits |= 1UL << index;
- if (valueMayContainReferences)
- _rawAttributeBits |= 1UL << index;
- value.CopyTo(_attributeValues.AsSpan(_attributeValueLength));
- _attributeValueLength += value.Length;
- if (TResourceLimits.Enabled)
- {
- _queryCaptureBytes += value.Length;
- }
- }
-
- public void StartTagSourceRange(long sourceStart, long sourceEnd)
- {
- _startTagSourceStart = sourceStart;
- _startTagSourceEnd = sourceEnd;
- }
-
- public void StartTagEnd(bool selfClosing)
- {
- StartTagEndCore(selfClosing, _startTagSourceStart, _startTagSourceEnd);
- _startTagSourceStart = -1;
- _startTagSourceEnd = -1;
- }
-
- private void StartTagEndCore(bool selfClosing, long sourceStart, long sourceEnd)
- {
- // Classify only inside an open normalized capture, then carry the result to the close in
- // the frame's sign bit. A frame opened before the outermost capture cannot close while that
- // capture is active: lexical recovery closes inner frames first.
- var isTextBoundary =
- _activeNormalizedTextCaptures != 0
- && HtmlTextBoundaryElements.IsBoundary(_pendingTagIdentity, _pendingTagIdentityLength);
- if (isTextBoundary)
- MarkTextBoundary();
- var matches = 0UL;
- var candidates = _pendingCandidateBits;
- while (candidates != 0)
- {
- var index = BitOperations.TrailingZeroCount(candidates);
- candidates &= candidates - 1;
- var node = _plan.Nodes[index];
- if (!PredicatesMatch(node.Predicates))
- continue;
- matches |= 1UL << node.Index;
- }
-
- var closesImmediately = IsVoidTag(_pendingTagIdentity, _pendingTagIdentityLength, _pendingTagNameLength);
- if (TResourceLimits.Enabled && !closesImmediately && _frameCount >= _maximumNestingDepth)
- throw new HtmlStreamingLimitExceededException(
- HtmlStreamingLimit.NestingDepth,
- _maximumNestingDepth,
- (long)_frameCount + 1
- );
- if (TResourceLimits.Enabled)
- {
- EnsureQueryCaptureCapacity(GetCompletedAttributeBytes(matches));
- }
-
- var starts = matches;
- while (starts != 0)
- {
- var index = BitOperations.TrailingZeroCount(starts);
- starts &= starts - 1;
- var node = _plan.Nodes[index];
- if (node.Start is null)
- continue;
- var element = CreateElement(node.RequestedAttributeMask);
- node.Start.Invoke(ref _state, in element);
- }
- var rewriteScopeId = -1;
- var rewriteHandler = ElementRewriteHandler;
- if (rewriteHandler is not null && (matches & _plan.TerminalNodeMask) != 0)
- {
- if (sourceStart < 0 || sourceEnd <= sourceStart)
- throw new InvalidOperationException("The tokenizer did not provide a valid start-tag source range.");
- var element = CreateElement(GetRequestedAttributeMask(matches & _plan.TerminalNodeMask));
- var editor = new ElementRewriter(
- _rewriteCollector!,
- sourceStart,
- sourceEnd,
- !closesImmediately,
- selfClosing
- );
- rewriteHandler.Invoke(ref _state, in element, ref editor);
- editor.Commit();
- rewriteScopeId = editor.ScopeId;
- }
- StartCompletedCaptures(matches);
-
- if (closesImmediately)
- {
- try
- {
- _rewriteCollector?.EndElement(rewriteScopeId, sourceEnd, sourceEnd, hasExplicitEndTag: false);
- CloseMatches(matches);
- }
- finally
- {
- ReleasePendingFallbackTagName();
- }
- return;
- }
-
- EnsureFrameCapacity();
- _frames[_frameCount++] = new QueryFrame(
- _pendingTagIdentity,
- isTextBoundary ? _pendingTagIdentityLength | TextBoundaryFrameFlag : _pendingTagIdentityLength,
- _pendingFallbackTagNameUtf8,
- matches,
- rewriteScopeId
- );
- _pendingFallbackTagNameUtf8 = null;
- IncrementActive(matches);
- }
-
- private Element CreateElement(ulong allowedAttributeMask) =>
- new(_plan.AttributeNames, _plan.AttributeNamesUtf8, this, allowedAttributeMask);
-
- bool IElementAttributeSource.TryGetAttributeValue(int index, out ReadOnlySpan value)
- {
- if (_attributeLengths[index] < 0)
- {
- value = default;
- return false;
- }
- value = GetAttributeValue(index);
- return true;
- }
-
- private ulong GetRequestedAttributeMask(ulong nodes)
- {
- var attributes = 0UL;
- while (nodes != 0)
- {
- var index = BitOperations.TrailingZeroCount(nodes);
- nodes &= nodes - 1;
- attributes |= _plan.Nodes[index].RequestedAttributeMask;
- }
- return attributes;
- }
-
- public void Text(ReadOnlySpan utf8)
- {
- if (_plan.TextHandlerMask == 0 && _plan.CompletedHandlerMask == 0)
- return;
- if (TResourceLimits.Enabled)
- {
- EnsureQueryCaptureCapacity(GetCompletedTextUpperBound(utf8.Length));
- }
- var handlers = _plan.TextHandlerMask;
- while (handlers != 0)
- {
- var nodeIndex = BitOperations.TrailingZeroCount(handlers);
- handlers &= handlers - 1;
- if (_activeCounts[nodeIndex] == 0)
- continue;
- _plan.Nodes[nodeIndex].Text!.Invoke(ref _state, utf8);
- }
- AppendCompletedText(utf8);
- }
-
- bool IUtf8HtmlStreamingCommentSink.BeginComment() => false;
-
- void IUtf8HtmlStreamingCommentSink.CommentChunk(ReadOnlySpan utf8) { }
-
- void IUtf8HtmlStreamingCommentSink.EndComment() { }
-
- public void EndTagSourceRange(long sourceStart, long sourceEnd)
- {
- _endTagSourceStart = sourceStart;
- _endTagSourceEnd = sourceEnd;
- }
-
- public void EndTag(Utf8HtmlName name)
- {
- var identityLength = 0;
- if (!name.TryGetCompactKey(out var identity))
- {
- identity = name.SemanticHash;
- identityLength = name.Verbatim.Length;
- }
- for (var index = _frameCount - 1; index >= 0; index--)
- {
- if (
- _frames[index].TagIdentity != identity
- || (_frames[index].TagIdentityLength & TagIdentityLengthMask) != identityLength
- )
- continue;
- if (
- identityLength != 0
- && !name.SemanticEquals(_frames[index].FallbackTagNameUtf8.AsSpan(0, identityLength))
- )
- continue;
- for (var popped = _frameCount - 1; popped >= index; popped--)
- {
- var frame = _frames[popped];
- _frames[popped] = default;
- _frameCount = popped;
- var explicitEnd = popped == index;
- CloseFrame(frame, _endTagSourceStart, explicitEnd ? _endTagSourceEnd : _endTagSourceStart, explicitEnd);
- }
- _endTagSourceStart = -1;
- _endTagSourceEnd = -1;
- return;
- }
- _endTagSourceStart = -1;
- _endTagSourceEnd = -1;
- }
-
- private ulong FindTagCandidates(ulong identity, int identityLength)
- {
- var entries = _plan.TagDispatch;
- var low = 0;
- var high = entries.Length - 1;
- while (low <= high)
- {
- var middle = (low + high) >>> 1;
- var entry = entries[middle];
- var comparison = entry.Identity.CompareTo(identity);
- if (comparison == 0)
- comparison = entry.IdentityLength.CompareTo(identityLength);
- if (comparison < 0)
- low = middle + 1;
- else if (comparison > 0)
- high = middle - 1;
- else
- return entry.CandidateBits;
- }
- return 0;
- }
-
- public void EndOfFile()
- {
- for (var index = _frameCount - 1; index >= 0; index--)
- {
- var frame = _frames[index];
- _frames[index] = default;
- _frameCount = index;
- CloseFrame(frame, _observedUtf8End, _observedUtf8End, hasExplicitEndTag: false);
- }
- }
-
public void Dispose()
{
if (_disposed)
@@ -516,25 +153,6 @@ private void ReleaseLiveFrames()
_frameCount = 0;
}
- [MethodImpl(MethodImplOptions.NoInlining)]
- private void DisposeCompletedCaptures()
- {
- foreach (var captures in _completedCaptures)
- {
- if (captures is null)
- continue;
- foreach (var capture in captures)
- capture.Dispose();
- }
- if (_reusableCaptures is not null)
- {
- foreach (var capture in _reusableCaptures)
- capture.Dispose();
- _reusableCaptures.Clear();
- }
- Array.Clear(_completedCaptures);
- }
-
private bool ParentMatches(QueryPlanNode node)
{
if (node.ParentIndex < 0)
@@ -641,117 +259,6 @@ private void CloseMatches(ulong matches)
}
}
- private void StartCompletedCaptures(ulong matches)
- {
- var completed = matches & _plan.CompletedHandlerMask;
- while (completed != 0)
- {
- var index = BitOperations.TrailingZeroCount(completed);
- completed &= completed - 1;
- var node = _plan.Nodes[index];
- var capture = _reusableCaptures!.Count == 0 ? new CapturedElementBuffer() : _reusableCaptures.Pop();
- capture.Reset(node.CompletedTextMode, node.CapturedAttributeIndexes.Length);
- for (var attribute = 0; attribute < node.CapturedAttributeIndexes.Length; attribute++)
- {
- var attributeIndex = node.CapturedAttributeIndexes[attribute];
- if (_attributeLengths[attributeIndex] >= 0)
- {
- var value = GetAttributeValue(attributeIndex);
- capture.SetAttribute(attribute, value);
- if (TResourceLimits.Enabled)
- {
- _queryCaptureBytes += value.Length;
- }
- }
- }
- capture.BeginText();
- var captures = _completedCaptures[index] ??= [];
- captures.Add(capture);
- if (node.CompletedTextMode != CompletedTextMode.None)
- _activeCompletedTextCaptures++;
- if (node.CompletedTextMode == CompletedTextMode.Normalized)
- _activeNormalizedTextCaptures++;
- }
- }
-
- ///
- /// Separates words in every open normalized capture. Callers have already established that this
- /// tag is a boundary and that at least one normalized capture is open, so this walks only the
- /// normalized nodes and never the raw ones.
- ///
- [MethodImpl(MethodImplOptions.NoInlining)]
- private void MarkTextBoundary()
- {
- var completed = _normalizedTextMask;
- while (completed != 0)
- {
- var index = BitOperations.TrailingZeroCount(completed);
- completed &= completed - 1;
- var captures = _completedCaptures[index];
- if (captures is null)
- continue;
- foreach (var capture in captures)
- capture.MarkBoundary();
- }
- }
-
- private void AppendCompletedText(ReadOnlySpan utf8)
- {
- var completed = _plan.CompletedHandlerMask;
- while (completed != 0)
- {
- var index = BitOperations.TrailingZeroCount(completed);
- completed &= completed - 1;
- var captures = _completedCaptures[index];
- if (captures is null)
- continue;
- foreach (var capture in captures)
- {
- var previousLength = capture.BufferedByteCount;
- capture.Append(utf8);
- if (TResourceLimits.Enabled)
- {
- _queryCaptureBytes += capture.BufferedByteCount - previousLength;
- }
- }
- }
- }
-
- private void CompleteCapture(int index)
- {
- var node = _plan.Nodes[index];
- if (node.Completed is null)
- return;
- var captures = _completedCaptures[index];
- if (captures is null || captures.Count == 0)
- throw new InvalidOperationException("The completed-element capture stack is unbalanced.");
- var captureIndex = captures.Count - 1;
- var capture = captures[captureIndex];
- captures.RemoveAt(captureIndex);
- if (node.CompletedTextMode != CompletedTextMode.None)
- _activeCompletedTextCaptures--;
- if (node.CompletedTextMode == CompletedTextMode.Normalized)
- _activeNormalizedTextCaptures--;
- if (TResourceLimits.Enabled)
- {
- _queryCaptureBytes -= capture.BufferedByteCount;
- }
- try
- {
- var element = new CompletedElement(
- capture,
- _plan.AttributeNames,
- _plan.AttributeNamesUtf8,
- node.CapturedAttributeIndexes
- );
- node.Completed.Invoke(ref _state, in element);
- }
- finally
- {
- _reusableCaptures!.Push(capture);
- }
- }
-
private void IncrementActive(ulong matches)
{
while (matches != 0)
@@ -820,65 +327,6 @@ private void EnsureFrameCapacity()
_frames = replacement;
}
- private long GetCompletedAttributeBytes(ulong matches)
- {
- var total = 0L;
- var completed = matches & _plan.CompletedHandlerMask;
- while (completed != 0)
- {
- var index = BitOperations.TrailingZeroCount(completed);
- completed &= completed - 1;
- foreach (var attributeIndex in _plan.Nodes[index].CapturedAttributeIndexes)
- {
- var length = _attributeLengths[attributeIndex];
- if (length > 0)
- total = SaturatingAdd(total, length);
- }
- }
- return total;
- }
-
- private long GetCompletedTextUpperBound(int textLength)
- {
- if (textLength == 0)
- return 0;
-
- var total = 0L;
- var completed = _plan.CompletedHandlerMask;
- while (completed != 0)
- {
- var index = BitOperations.TrailingZeroCount(completed);
- completed &= completed - 1;
- if (_plan.Nodes[index].CompletedTextMode == CompletedTextMode.None)
- continue;
- var captures = _completedCaptures[index];
- if (captures is null)
- continue;
- foreach (var capture in captures)
- {
- total = SaturatingAdd(total, textLength);
- if (capture.HasPendingNormalizedSpace)
- total = SaturatingAdd(total, 1);
- }
- }
- return total;
- }
-
- private void EnsureQueryCaptureCapacity(long additional)
- {
- var observed =
- _queryCaptureBytes > long.MaxValue - additional ? long.MaxValue : _queryCaptureBytes + additional;
- if (observed > _maximumQueryCaptureBytes)
- throw new HtmlStreamingLimitExceededException(
- HtmlStreamingLimit.QueryCaptureBytes,
- _maximumQueryCaptureBytes,
- observed
- );
- }
-
- private static long SaturatingAdd(long value, long additional) =>
- value > long.MaxValue - additional ? long.MaxValue : value + additional;
-
private static bool ContainsToken(ReadOnlySpan tokens, ReadOnlySpan wanted)
{
var index = 0;
@@ -930,20 +378,11 @@ private static bool IsVoidTag(ulong identity, int identityLength, int nameLength
|| (nameLength == 6 && identity == HtmlVoidElements.Source)
);
- private bool HasTextRewriteHandler => _rewriteHandlers is TextRewriteHandler or RewriteHandlerPair;
-
- private RewriteHandler? ElementRewriteHandler =>
- _rewriteHandlers switch
- {
- RewriteHandler handler => handler,
- RewriteHandlerPair pair => pair.Element,
- _ => null,
- };
+ private bool HasTextRewriteHandler => _textRewriteHandler is not null;
- private TextRewriteHandler TextRewriteHandler =>
- _rewriteHandlers is TextRewriteHandler handler ? handler : ((RewriteHandlerPair)_rewriteHandlers!).Text;
+ private RewriteHandler? ElementRewriteHandler => _elementRewriteHandler;
- private sealed record RewriteHandlerPair(RewriteHandler Element, TextRewriteHandler Text);
+ private TextRewriteHandler TextRewriteHandler => _textRewriteHandler!;
}
internal sealed class QueryExecution : QueryExecution