Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions src/AngleSharp.Js.Tests/DomTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,47 @@ public async Task NodeHasChildNodesWithChildren()
var result = await "new DOMParser().parseFromString(`<div><input/></div>`, 'text/html').body.firstChild.hasChildNodes()".EvalScriptAsync();
Assert.AreEqual("True", result);
}

[Test]
public async Task PrototypeChainOfElementIsBuiltCompletely()
{
var result = await "(function () { var p = Object.getPrototypeOf(document.createElement('div')), t = []; while (p) { t.push(p[Symbol.toStringTag]); p = Object.getPrototypeOf(p); } return t.join(); })()".EvalScriptAsync();
Assert.AreEqual("HTMLDivElement,HTMLElement,Element,Node,EventTarget,", result);
}

[Test]
public async Task ConstructorPropertyOfPrototypeRefersBackToTheConstructor()
{
var result = await "HTMLDivElement.prototype.constructor === HTMLDivElement".EvalScriptAsync();
Assert.AreEqual("True", result);
}

[Test]
public async Task NumericIndexerOfHtmlCollectionYieldsTheElement()
{
var result = await "document.getElementsByTagName('script')[0].nodeName".EvalScriptAsync();
Assert.AreEqual("SCRIPT", result);
}

[Test]
public async Task NumericIndexerOfHtmlCollectionOutOfRangeIsUndefined()
{
var result = await "typeof document.getElementsByTagName('script')[5]".EvalScriptAsync();
Assert.AreEqual("undefined", result);
}

[Test]
public async Task NumericIndexerOfNodeListYieldsTheNode()
{
var result = await "new DOMParser().parseFromString(`<div><input/></div>`, 'text/html').body.childNodes[0].nodeName".EvalScriptAsync();
Assert.AreEqual("DIV", result);
}

[Test]
public async Task NumericIndexerOfTokenListYieldsTheToken()
{
var result = await "new DOMParser().parseFromString(`<div class='a b'></div>`, 'text/html').body.firstChild.classList[1]".EvalScriptAsync();
Assert.AreEqual("b", result);
}
}
}
14 changes: 13 additions & 1 deletion src/AngleSharp.Js/Proxies/DomConstructorInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,19 @@ public DomConstructorInstance(EngineInstance engine, Type type)
_instance = engine;
FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true));
SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false));
_objectPrototype.FastSetProperty("constructor", new PropertyDescriptor(this, true, false, true));

var constructor = new PropertyDescriptor(this, true, false, true);

// Every exposed type gets a constructor, so writing this directly would make
// each of their prototypes register its members right away.
if (_objectPrototype is DomPrototypeInstance domPrototype)
{
domPrototype.DefineDeferredProperty("constructor", constructor);
}
else
{
_objectPrototype.FastSetProperty("constructor", constructor);
}
}

public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor)
Expand Down
122 changes: 105 additions & 17 deletions src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,37 +15,90 @@ sealed class DomPrototypeInstance : ObjectInstance
{
private readonly String _name;
private readonly EngineInstance _instance;
private readonly Type _type;
private readonly Type _baseType;

private PropertyInfo _numericIndexer;
private PropertyInfo _stringIndexer;
private List<KeyValuePair<String, PropertyDescriptor>> _deferred;
private Boolean _membersSet;
private MethodInfo _numericIndexer;
private MethodInfo _stringIndexer;

public DomPrototypeInstance(EngineInstance engine, Type type)
: base(engine.Jint)
{
var baseType = type.GetTypeInfo().BaseType ?? typeof(Object);
_name = type.GetOfficialName(baseType);
_baseType = type.GetTypeInfo().BaseType ?? typeof(Object);
_name = type.GetOfficialName(_baseType);
_instance = engine;
_type = type;
}

// A document uses a handful of the DOM types an assembly exposes, but a prototype
// is created for every one of them. Reflecting over the whole type tree is by far
// the most expensive part of that, so it waits until the prototype is looked at.
// Jint calls this before serving any property, and marks the instance initialized
// beforehand, so the registration below can use the object as usual.
protected override void Initialize()
{
_membersSet = true;

Set(GlobalSymbolRegistry.ToStringTag, _name);

SetAllMembers(type);
SetAllMembers(_type);
SetExtensionMembers();

// DOM objects can have properties added dynamically
Prototype = engine.GetDomPrototype(baseType);
Prototype = _instance.GetDomPrototype(_baseType);

if (_deferred != null)
{
foreach (var property in _deferred)
{
FastSetProperty(property.Key, property.Value);
}

_deferred = null;
}
}

// The prototype link is only established once the members are known, so reading it
// has to initialize as well - not every reader goes through a property lookup.
protected override ObjectInstance GetPrototypeOf()
{
EnsureInitialized();
return base.GetPrototypeOf();
}

/// <summary>
/// Defines a property on the prototype without forcing its members to be
/// registered. The property is applied after the members, so it keeps
/// overriding a member of the same name.
/// </summary>
public void DefineDeferredProperty(String name, PropertyDescriptor descriptor)
{
if (_membersSet)
{
FastSetProperty(name, descriptor);
}
else
{
_deferred = _deferred ?? new List<KeyValuePair<String, PropertyDescriptor>>();
_deferred.Add(new KeyValuePair<String, PropertyDescriptor>(name, descriptor));
}
}

public Boolean TryGetFromIndex(Object value, String index, out PropertyDescriptor result)
{
// If we have a numeric indexer and the property is numeric
result = default;

EnsureInitialized();

if (_numericIndexer != null && Int32.TryParse(index, out var numericIndex))
{
try
{
var args = new Object[] { numericIndex };
var orig = _numericIndexer.GetMethod.Invoke(value, args);
var orig = _numericIndexer.Invoke(value, args);
result = new PropertyDescriptor(orig.ToJsValue(_instance), false, false, false);
return true;
}
Expand All @@ -70,7 +123,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto
if (_stringIndexer != null && !HasProperty(index))
{
var args = new Object[] { index };
var valueAtIndex = _stringIndexer.GetMethod.Invoke(value, args);
var valueAtIndex = _stringIndexer.Invoke(value, args);

if (valueAtIndex == null)
{
Expand Down Expand Up @@ -229,19 +282,54 @@ private void SetProperty(String name, MethodInfo getter, MethodInfo setter, DomP

private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters)
{
if (indexParameters.Length == 1)
if (indexParameters.Length != 1)
{
if (indexParameters[0].ParameterType == typeof(Int32))
{
_numericIndexer = property;
}
else if (indexParameters[0].ParameterType == typeof(String))
{
_stringIndexer = property;
}
return;
}

var getter = ResolveAccessor(property.GetMethod);

if (getter == null)
{
return;
}

if (indexParameters[0].ParameterType == typeof(Int32))
{
_numericIndexer = getter;
}
else if (indexParameters[0].ParameterType == typeof(String))
{
_stringIndexer = getter;
}
}

private MethodInfo ResolveAccessor(MethodInfo accessor)
{
// An interface may re-implement a member of one of its own base interfaces
// explicitly, e.g. "T IReadOnlyList<T>.this[Int32 index]" declared on an
// IHtmlCollection<T>. Such a member is private and abstract - invoking it
// reflectively throws an EntryPointNotFoundException because the actual
// implementation lives in a different slot. Resolve it against the type the
// prototype was created for, which is where the implementation can be found.
if (accessor == null || accessor.IsPublic)
{
return accessor;
}

var name = accessor.Name;
var simpleName = name.Substring(name.LastIndexOf('.') + 1);
var parameters = accessor.GetParameters();
var parameterTypes = new Type[parameters.Length];

for (var i = 0; i < parameters.Length; i++)
{
parameterTypes[i] = parameters[i].ParameterType;
}

return _type.GetRuntimeMethod(simpleName, parameterTypes) ?? accessor;
}

private void SetMethod(String name, MethodInfo method)
{
//TODO Jint
Expand Down
Loading