bytes, int startOffset)
+ => BitConverter.ToInt32(bytes.Slice(startOffset, 4).ToArray(), 0);
+
+ public static uint ReadUnsignedLEB128(byte[] bytes, int startOffset, out int numBytesRead)
+ {
+ var result = (uint)0;
+ var shift = 0;
+ var currentByte = (byte)128;
+ numBytesRead = 0;
+
+ for (var count = 0; count < 4 && currentByte >= 128; count++)
+ {
+ currentByte = bytes[startOffset + count];
+ result += (uint)(currentByte & 0x7f) << shift;
+ shift += 7;
+ numBytesRead++;
+ }
+
+ return result;
+ }
+
+ class FakeComponent : IComponent
+ {
+ public void Configure(RenderHandle renderHandle)
+ => throw new NotImplementedException();
+
+ public Task SetParametersAsync(ParameterCollection parameters)
+ => throw new NotImplementedException();
+ }
+
+ class FakeRenderer : Renderer
+ {
+ public FakeRenderer()
+ : base(new ServiceCollection().BuildServiceProvider(), new RendererSynchronizationContext())
+ {
+ }
+
+ protected override void HandleException(Exception exception)
+ {
+ throw new NotImplementedException();
+ }
+
+ protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
+ => throw new NotImplementedException();
+ }
+ }
+}
diff --git a/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor b/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor
index ea87f6be2d..06a30e57f0 100644
--- a/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor
+++ b/src/Components/test/testassets/ComponentsApp.App/Pages/Counter.razor
@@ -4,7 +4,7 @@
Current count: @currentCount
-
+
@code {
int currentCount = 0;
diff --git a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj
index d5ac161574..7f9d81bfb8 100644
--- a/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj
+++ b/src/Components/test/testassets/ComponentsApp.Server/ComponentsApp.Server.csproj
@@ -1,4 +1,4 @@
-
+
netcoreapp3.0
diff --git a/src/Components/test/testassets/Ignitor/CommentNode.cs b/src/Components/test/testassets/Ignitor/CommentNode.cs
new file mode 100644
index 0000000000..9f273e5d61
--- /dev/null
+++ b/src/Components/test/testassets/Ignitor/CommentNode.cs
@@ -0,0 +1,9 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Ignitor
+{
+ internal class LogicalContainerNode : ContainerNode
+ {
+ }
+}
diff --git a/src/Components/test/testassets/Ignitor/ComponentNode.cs b/src/Components/test/testassets/Ignitor/ComponentNode.cs
new file mode 100644
index 0000000000..920733eeb7
--- /dev/null
+++ b/src/Components/test/testassets/Ignitor/ComponentNode.cs
@@ -0,0 +1,15 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Ignitor
+{
+ internal class ComponentNode : ContainerNode
+ {
+ private readonly int _componentId;
+
+ public ComponentNode(int componentId)
+ {
+ _componentId = componentId;
+ }
+ }
+}
diff --git a/src/Components/test/testassets/Ignitor/ContainerNode.cs b/src/Components/test/testassets/Ignitor/ContainerNode.cs
new file mode 100644
index 0000000000..3de2810039
--- /dev/null
+++ b/src/Components/test/testassets/Ignitor/ContainerNode.cs
@@ -0,0 +1,84 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+
+namespace Ignitor
+{
+ internal abstract class ContainerNode : Node
+ {
+ private readonly List _children;
+
+ protected ContainerNode()
+ {
+ _children = new List();
+ }
+
+ public IReadOnlyList Children => _children;
+
+ public void InsertLogicalChild(Node child, int childIndex)
+ {
+ if (child is LogicalContainerNode comment && comment.Children.Count > 0)
+ {
+ // There's nothing to stop us implementing support for this scenario, and it's not difficult
+ // (after inserting 'child' itself, also iterate through its logical children and physically
+ // put them as following-siblings in the DOM). However there's no scenario that requires it
+ // presently, so if we did implement it there'd be no good way to have tests for it.
+ throw new Exception("Not implemented: inserting non-empty logical container");
+ }
+
+ if (child.Parent != null)
+ {
+ // Likewise, we could easily support this scenario too (in this 'if' block, just splice
+ // out 'child' from the logical children array of its previous logical parent by using
+ // Array.prototype.indexOf to determine its previous sibling index).
+ // But again, since there's not currently any scenario that would use it, we would not
+ // have any test coverage for such an implementation.
+ throw new NotSupportedException("Not implemented: moving existing logical children");
+ }
+
+ if (childIndex < Children.Count)
+ {
+ // Insert
+ _children[childIndex] = child;
+ }
+ else
+ {
+ // Append
+ _children.Add(child);
+ }
+
+ child.Parent = this;
+ }
+
+ public ContainerNode CreateAndInsertContainer(int childIndex)
+ {
+ var containerElement = new LogicalContainerNode();
+ InsertLogicalChild(containerElement, childIndex);
+ return containerElement;
+ }
+
+ public ComponentNode CreateAndInsertComponent(int componentId, int childIndex)
+ {
+ var componentElement = new ComponentNode(componentId);
+ InsertLogicalChild(componentElement, childIndex);
+ return componentElement;
+ }
+
+ public void RemoveLogicalChild(int childIndex)
+ {
+ var childToRemove = Children[childIndex];
+ _children.RemoveAt(childIndex);
+
+ // If it's a logical container, also remove its descendants
+ if (childToRemove is LogicalContainerNode container)
+ {
+ while (container.Children.Count > 0)
+ {
+ container.RemoveLogicalChild(0);
+ }
+ }
+ }
+ }
+}
diff --git a/src/Components/test/testassets/Ignitor/ElementHive.cs b/src/Components/test/testassets/Ignitor/ElementHive.cs
new file mode 100644
index 0000000000..5abfb9f156
--- /dev/null
+++ b/src/Components/test/testassets/Ignitor/ElementHive.cs
@@ -0,0 +1,473 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNetCore.Components.Rendering;
+using Microsoft.AspNetCore.Components.RenderTree;
+
+namespace Ignitor
+{
+ internal class ElementHive
+ {
+ private const string SelectValuePropname = "_blazorSelectValue";
+
+ public Dictionary Components { get; } = new Dictionary();
+
+ public string SerializedValue => NodeSerializer.Serialize(this);
+
+ public void Update(RenderBatch batch)
+ {
+ for (var i = 0; i < batch.UpdatedComponents.Count; i++)
+ {
+ var diff = batch.UpdatedComponents.Array[i];
+ var componentId = diff.ComponentId;
+ var edits = diff.Edits;
+ UpdateComponent(batch, componentId, edits);
+ }
+
+ for (var i = 0; i < batch.DisposedComponentIDs.Count; i++)
+ {
+ DisposeComponent(batch.DisposedComponentIDs.Array[i]);
+ }
+
+ for (var i = 0; i < batch.DisposedEventHandlerIDs.Count; i++)
+ {
+ DisposeEventHandler(batch.DisposedEventHandlerIDs.Array[i]);
+ }
+ }
+
+ public bool TryFindElementById(string id, out ElementNode element)
+ {
+ foreach (var kvp in Components)
+ {
+ var component = kvp.Value;
+ if (TryGetElementFromChildren(component, out element))
+ {
+ return true;
+ }
+ }
+
+ element = null;
+ return false;
+
+ bool TryGetElementFromChildren(Node node, out ElementNode foundNode)
+ {
+ if (node is ElementNode elementNode &&
+ elementNode.Attributes.TryGetValue("id", out var elementId) &&
+ elementId?.ToString() == id)
+ {
+ foundNode = elementNode;
+ return true;
+ }
+
+ if (node is ContainerNode containerNode)
+ {
+ for (var i = 0; i < containerNode.Children.Count; i++)
+ {
+ if (TryGetElementFromChildren(containerNode.Children[i], out foundNode))
+ {
+ return true;
+ }
+ }
+ }
+
+ foundNode = null;
+ return false;
+ }
+ }
+
+ private void UpdateComponent(RenderBatch batch, int componentId, ArraySegment edits)
+ {
+ if (!Components.TryGetValue(componentId, out var component))
+ {
+ component = new ComponentNode(componentId);
+ Components.Add(componentId, component);
+ }
+
+ ApplyEdits(batch, component, 0, edits);
+ }
+
+ private void DisposeComponent(int componentId)
+ {
+
+ }
+
+ private void DisposeEventHandler(int eventHandlerId)
+ {
+
+ }
+
+ private void ApplyEdits(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment edits)
+ {
+ var currentDepth = 0;
+ var childIndexAtCurrentDepth = childIndex;
+ var permutations = new List();
+
+ for (var editIndex = edits.Offset; editIndex < edits.Offset + edits.Count; editIndex++)
+ {
+ var edit = edits.Array[editIndex];
+ switch (edit.Type)
+ {
+ case RenderTreeEditType.PrependFrame:
+ {
+ var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
+ var siblingIndex = edit.SiblingIndex;
+ InsertFrame(batch, parent, childIndexAtCurrentDepth + siblingIndex, batch.ReferenceFrames.Array, frame, edit.ReferenceFrameIndex);
+ break;
+ }
+
+ case RenderTreeEditType.RemoveFrame:
+ {
+ var siblingIndex = edit.SiblingIndex;
+ parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex);
+ break;
+ }
+
+ case RenderTreeEditType.SetAttribute:
+ {
+ var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
+ var siblingIndex = edit.SiblingIndex;
+ var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
+ if (node is ElementNode element)
+ {
+ ApplyAttribute(batch, element, frame);
+ }
+ else
+ {
+ throw new Exception("Cannot set attribute on non-element child");
+ }
+ break;
+ }
+
+ case RenderTreeEditType.RemoveAttribute:
+ {
+ // Note that we don't have to dispose the info we track about event handlers here, because the
+ // disposed event handler IDs are delivered separately (in the 'disposedEventHandlerIds' array)
+ var siblingIndex = edit.SiblingIndex;
+ var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
+ if (node is ElementNode element)
+ {
+ var attributeName = edit.RemovedAttributeName;
+
+ // First try to remove any special property we use for this attribute
+ if (!TryApplySpecialProperty(batch, element, attributeName, default))
+ {
+ // If that's not applicable, it's a regular DOM attribute so remove that
+ element.RemoveAttribute(attributeName);
+ }
+ }
+ else
+ {
+ throw new Exception("Cannot remove attribute from non-element child");
+ }
+ break;
+ }
+
+ case RenderTreeEditType.UpdateText:
+ {
+ var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
+ var siblingIndex = edit.SiblingIndex;
+ var node = parent.Children[childIndexAtCurrentDepth + siblingIndex];
+ if (node is TextNode textNode)
+ {
+ textNode.TextContent = frame.TextContent;
+ }
+ else
+ {
+ throw new Exception("Cannot set text content on non-text child");
+ }
+ break;
+ }
+
+
+ case RenderTreeEditType.UpdateMarkup:
+ {
+ var frame = batch.ReferenceFrames.Array[edit.ReferenceFrameIndex];
+ var siblingIndex = edit.SiblingIndex;
+ parent.RemoveLogicalChild(childIndexAtCurrentDepth + siblingIndex);
+ InsertMarkup(parent, childIndexAtCurrentDepth + siblingIndex, frame);
+ break;
+ }
+
+ case RenderTreeEditType.StepIn:
+ {
+ var siblingIndex = edit.SiblingIndex;
+ parent = (ContainerNode)parent.Children[childIndexAtCurrentDepth + siblingIndex];
+ currentDepth++;
+ childIndexAtCurrentDepth = 0;
+ break;
+ }
+
+ case RenderTreeEditType.StepOut:
+ {
+ parent = parent.Parent;
+ currentDepth--;
+ childIndexAtCurrentDepth = currentDepth == 0 ? childIndex : 0; // The childIndex is only ever nonzero at zero depth
+ break;
+ }
+
+ case RenderTreeEditType.PermutationListEntry:
+ {
+ permutations.Add(new PermutationListEntry(childIndexAtCurrentDepth + edit.SiblingIndex, childIndexAtCurrentDepth + edit.MoveToSiblingIndex));
+ break;
+ }
+
+ case RenderTreeEditType.PermutationListEnd:
+ {
+ throw new NotSupportedException();
+ //permuteLogicalChildren(parent, permutations!);
+ //permutations.Clear();
+ //break;
+ }
+
+ default:
+ {
+ throw new Exception($"Unknown edit type: '{edit.Type}'");
+ }
+ }
+ }
+ }
+
+ private int InsertFrame(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, RenderTreeFrame frame, int frameIndex)
+ {
+ switch (frame.FrameType)
+ {
+ case RenderTreeFrameType.Element:
+ {
+ InsertElement(batch, parent, childIndex, frames, frame, frameIndex);
+ return 1;
+ }
+
+ case RenderTreeFrameType.Text:
+ {
+ InsertText(parent, childIndex, frame);
+ return 1;
+ }
+
+ case RenderTreeFrameType.Attribute:
+ {
+ throw new Exception("Attribute frames should only be present as leading children of element frames.");
+ }
+
+ case RenderTreeFrameType.Component:
+ {
+ InsertComponent(parent, childIndex, frame);
+ return 1;
+ }
+
+ case RenderTreeFrameType.Region:
+ {
+ return InsertFrameRange(batch, parent, childIndex, frames, frameIndex + 1, frameIndex + CountDescendantFrames(frame));
+ }
+
+ case RenderTreeFrameType.ElementReferenceCapture:
+ {
+ // No action for reference captures.
+ break;
+ }
+
+ case RenderTreeFrameType.Markup:
+ {
+ InsertMarkup(parent, childIndex, frame);
+ return 1;
+ }
+
+ }
+
+ throw new Exception($"Unknown frame type: {frame.FrameType}");
+ }
+
+ private void InsertText(ContainerNode parent, int childIndex, RenderTreeFrame frame)
+ {
+ var textContent = frame.TextContent;
+ var newTextNode = new TextNode(textContent);
+ parent.InsertLogicalChild(newTextNode, childIndex);
+ }
+
+ private void InsertComponent(ContainerNode parent, int childIndex, RenderTreeFrame frame)
+ {
+ // All we have to do is associate the child component ID with its location. We don't actually
+ // do any rendering here, because the diff for the child will appear later in the render batch.
+ var childComponentId = frame.ComponentId;
+ var containerElement = parent.CreateAndInsertComponent(childComponentId, childIndex);
+
+ Components[childComponentId] = containerElement;
+ }
+
+ private int InsertFrameRange(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, int startIndex, int endIndexExcl)
+ {
+ var origChildIndex = childIndex;
+ for (var index = startIndex; index < endIndexExcl; index++)
+ {
+ var frame = batch.ReferenceFrames.Array[index];
+ var numChildrenInserted = InsertFrame(batch, parent, childIndex, frames, frame, index);
+ childIndex += numChildrenInserted;
+
+ // Skip over any descendants, since they are already dealt with recursively
+ index += CountDescendantFrames(frame);
+ }
+
+ return (childIndex - origChildIndex); // Total number of children inserted
+ }
+
+ private void InsertElement(RenderBatch batch, ContainerNode parent, int childIndex, ArraySegment frames, RenderTreeFrame frame, int frameIndex)
+ {
+ // Note: we don't handle SVG here
+ var newElement = new ElementNode(frame.ElementName);
+ parent.InsertLogicalChild(newElement, childIndex);
+
+ // Apply attributes
+ for (var i = frameIndex + 1; i < frameIndex + frame.ElementSubtreeLength; i++)
+ {
+ var descendantFrame = batch.ReferenceFrames.Array[i];
+ if (descendantFrame.FrameType == RenderTreeFrameType.Attribute)
+ {
+ ApplyAttribute(batch, newElement, descendantFrame);
+ }
+ else
+ {
+ // As soon as we see a non-attribute child, all the subsequent child frames are
+ // not attributes, so bail out and insert the remnants recursively
+ InsertFrameRange(batch, newElement, 0, frames, i, frameIndex + frame.ElementSubtreeLength);
+ break;
+ }
+ }
+ }
+
+ private void ApplyAttribute(RenderBatch batch, ElementNode elementNode, RenderTreeFrame attributeFrame)
+ {
+ var attributeName = attributeFrame.AttributeName;
+ var eventHandlerId = attributeFrame.AttributeEventHandlerId;
+
+ if (eventHandlerId != 0)
+ {
+ var firstTwoChars = attributeName.Substring(0, 2);
+ var eventName = attributeName.Substring(2);
+ if (firstTwoChars != "on" || string.IsNullOrEmpty(eventName))
+ {
+ throw new InvalidOperationException($"Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.");
+ }
+ var descriptor = new ElementNode.ElementEventDescriptor(eventName, eventHandlerId);
+ elementNode.SetEvent(eventName, descriptor);
+
+ return;
+ }
+
+ // First see if we have special handling for this attribute
+ if (!TryApplySpecialProperty(batch, elementNode, attributeName, attributeFrame))
+ {
+ // If not, treat it as a regular string-valued attribute
+ elementNode.SetAttribute(
+ attributeName,
+ attributeFrame.AttributeValue);
+ }
+ }
+
+ private bool TryApplySpecialProperty(RenderBatch batch, ElementNode element, string attributeName, RenderTreeFrame attributeFrame)
+ {
+ switch (attributeName)
+ {
+ case "value":
+ return TryApplyValueProperty(element, attributeFrame);
+ case "checked":
+ return TryApplyCheckedProperty(element, attributeFrame);
+ default:
+ return false;
+ }
+ }
+
+
+
+ private bool TryApplyValueProperty(ElementNode element, RenderTreeFrame attributeFrame)
+ {
+ // Certain elements have built-in behaviour for their 'value' property
+ switch (element.TagName)
+ {
+ case "INPUT":
+ case "SELECT":
+ case "TEXTAREA":
+ {
+ var value = attributeFrame.AttributeValue;
+ element.SetProperty("value", value);
+
+ if (element.TagName == "SELECT")
+ {
+ //