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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# 1.0.1

Released on Wednesday, August 5 2026.

- Fixed `AmbiguousMatchException` in case of multiple `DomName` attributes (#136)
- Added support for aliases with multiple `DomName` attributes

# 1.0.0

Released on Friday, July 31 2026.
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
[![GitHub Tag](https://img.shields.io/github/tag/AngleSharp/AngleSharp.Js.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Js/releases)
[![NuGet Count](https://img.shields.io/nuget/dt/AngleSharp.Js.svg?style=flat-square)](https://www.nuget.org/packages/AngleSharp.Js/)
[![Issues Open](https://img.shields.io/github/issues/AngleSharp/AngleSharp.Js.svg?style=flat-square)](https://github.com/AngleSharp/AngleSharp.Js/issues)
[![Gitter Chat](http://img.shields.io/badge/gitter-AngleSharp/AngleSharp-blue.svg?style=flat-square)](https://gitter.im/AngleSharp/AngleSharp)
[![StackOverflow Questions](https://img.shields.io/stackexchange/stackoverflow/t/anglesharp.svg?style=flat-square)](https://stackoverflow.com/tags/anglesharp)
[![CLA Assistant](https://cla-assistant.io/readme/badge/AngleSharp/AngleSharp.Js?style=flat-square)](https://cla-assistant.io/AngleSharp/AngleSharp.Js)

AngleSharp.Js extends the core AngleSharp library with a .NET-based JavaScript engine.
Expand Down
2 changes: 1 addition & 1 deletion src/AngleSharp.Js.Docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@anglesharp/js",
"version": "0.16.0",
"version": "1.0.1",
"preview": true,
"description": "The doclet for the AngleSharp.Js documentation.",
"keywords": [
Expand Down
141 changes: 141 additions & 0 deletions src/AngleSharp.Js.Tests/DomNameResolutionTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
namespace AngleSharp.Js.Tests
{
using AngleSharp.Attributes;
using AngleSharp.Js.Cache;
using NUnit.Framework;
using System;

[TestFixture]
public class DomNameResolutionTests
{
[Test]
public void TypeOfficialNameWithMultipleDomNamesUsesFirst()
{
var name = typeof(MultiNamedType).GetOfficialName(null);
Assert.AreEqual("PrimaryTypeName", name);
}

[Test]
public void TypeOfficialNamesWithMultipleDomNamesUsesAll()
{
var names = typeof(MultiNamedType).GetOfficialNames(null);
CollectionAssert.AreEqual(new[] { "PrimaryTypeName", "SecondaryTypeName" }, names);
}

[Test]
public void InterfaceOfficialNameWithMultipleDomNamesUsesFirst()
{
var name = typeof(MultiNamedImplementation).GetOfficialName(null);
Assert.AreEqual("PrimaryInterfaceName", name);
}

[Test]
public void EnumLiteralDefinitionWithMultipleDomNamesUsesFirst()
{
var definition = typeof(MultiNamedEnum).GetEnumLiteralDefinition();
Assert.NotNull(definition);
Assert.AreEqual("PrimaryEnumName", definition.Name);
Assert.AreEqual("PrimaryMemberName", definition.Members[0].Name);
}

[Test]
public void ConstructorDefinitionWithMultipleDomNamesUsesAll()
{
var definition = typeof(MultiNamedConstructorType).GetConstructorDefinition();

Assert.NotNull(definition);
CollectionAssert.AreEqual(new[] { "DOMRect", "SVGRect" }, definition.Names);
Assert.AreEqual("DOMRect", definition.Name);
}

[Test]
public void ConstructorSelectionPublishesAllAliases()
{
var selected = EngineExtensions.SelectConstructors(new[] { typeof(MultiNamedConstructorType) });

Assert.IsTrue(selected.ContainsKey("DOMRect"));
Assert.IsTrue(selected.ContainsKey("SVGRect"));
Assert.AreSame(selected["DOMRect"], selected["SVGRect"]);
Assert.AreEqual(typeof(MultiNamedConstructorType), selected["DOMRect"].Type);
}

[Test]
public void ConstructorSelectionPrefersFirstClassOverInterfacesForSharedName()
{
var selected = EngineExtensions.SelectConstructors(new[]
{
typeof(IFirstSharedName),
typeof(FirstSharedNameClass),
typeof(SecondSharedNameClass),
typeof(ISecondSharedName),
});

Assert.IsTrue(selected.ContainsKey("SharedName"));
Assert.AreEqual(typeof(FirstSharedNameClass), selected["SharedName"].Type);
}

[Test]
public void ConstructorSelectionUsesFirstInterfaceIfNoClassExists()
{
var selected = EngineExtensions.SelectConstructors(new[]
{
typeof(IFirstSharedName),
typeof(ISecondSharedName),
});

Assert.IsTrue(selected.ContainsKey("SharedName"));
Assert.AreEqual(typeof(IFirstSharedName), selected["SharedName"].Type);
}

[DomName("PrimaryTypeName")]
[DomName("SecondaryTypeName")]
private sealed class MultiNamedType
{
}

[DomName("PrimaryInterfaceName")]
[DomName("SecondaryInterfaceName")]
private interface IMultiNamedInterface
{
}

private sealed class MultiNamedImplementation : IMultiNamedInterface
{
}

[DomName("DOMRect")]
[DomName("SVGRect")]
private sealed class MultiNamedConstructorType
{
}

[DomName("SharedName")]
private interface IFirstSharedName
{
}

[DomName("SharedName")]
private interface ISecondSharedName
{
}

[DomName("SharedName")]
private sealed class FirstSharedNameClass
{
}

[DomName("SharedName")]
private sealed class SecondSharedNameClass
{
}

[DomName("PrimaryEnumName")]
[DomName("SecondaryEnumName")]
private enum MultiNamedEnum
{
[DomName("PrimaryMemberName")]
[DomName("SecondaryMemberName")]
Value = 1,
}
}
}
7 changes: 7 additions & 0 deletions src/AngleSharp.Js.Tests/ScriptEvalTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,13 @@ public async Task AccessUndefinedGlobalVariable()
Assert.AreEqual("", result);
}

[Test]
public async Task EvaluatingSimpleScriptShouldWorkWithMultipleDomNameAttributes_Issue136()
{
var result = await EvaluateComplexScriptAsync(SetResult("(2 + 3).toString()"));
Assert.AreEqual("5", result);
}

[Test]
public async Task AccessGlobalVariablesFromOtherScriptShouldWork()
{
Expand Down
30 changes: 19 additions & 11 deletions src/AngleSharp.Js/Cache/CreatorCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,16 @@ public static ConstructorDefinition GetConstructorDefinition(this Type type)
if (!_constructorDefinitions.TryGetValue(type, out var definition))
{
var ti = type.GetTypeInfo();
var names = ti.GetCustomAttributes<DomNameAttribute>();
var name = names.FirstOrDefault();
var names = ti.GetCustomAttributes<DomNameAttribute>()
.Select(m => m.OfficialName)
.Where(m => m != null)
.Distinct(StringComparer.Ordinal)
.ToArray();

if (name != null && !ti.IsEnum)
if (names.Length > 0 && !ti.IsEnum)
{
var info = ti.DeclaredConstructors.FirstOrDefault(m => m.GetCustomAttributes<DomConstructorAttribute>().Any());
definition = new ConstructorDefinition(type, name.OfficialName, info);
definition = new ConstructorDefinition(type, names, info);
}

_constructorDefinitions.TryAdd(type, definition);
Expand All @@ -54,7 +57,7 @@ public static EnumLiteralDefinition GetEnumLiteralDefinition(this Type type)

if (ti.IsEnum)
{
var name = ti.GetCustomAttribute<DomNameAttribute>(true)?.OfficialName;
var name = ti.GetCustomAttributes<DomNameAttribute>(true).FirstOrDefault()?.OfficialName;

if (name != null)
{
Expand All @@ -65,7 +68,7 @@ public static EnumLiteralDefinition GetEnumLiteralDefinition(this Type type)
var members = ti.DeclaredFields
.Where(m => m.IsLiteral)
.Select(m => new EnumLiteralMember(
m.GetCustomAttribute<DomNameAttribute>()?.OfficialName,
m.GetCustomAttributes<DomNameAttribute>().FirstOrDefault()?.OfficialName,
m.GetRawConstantValue()))
.Where(m => m.Name != null)
.ToArray();
Expand Down Expand Up @@ -99,7 +102,7 @@ private static ISet<String> GetNonEnumTypeNames(Assembly assembly)
continue;
}

var name = ti.GetCustomAttribute<DomNameAttribute>(true)?.OfficialName;
var name = ti.GetCustomAttributes<DomNameAttribute>(true).FirstOrDefault()?.OfficialName;

if (name != null)
{
Expand Down Expand Up @@ -216,10 +219,10 @@ public EnumLiteralMember(String name, Object value)
/// </summary>
sealed class ConstructorDefinition
{
public ConstructorDefinition(Type type, String name, ConstructorInfo info)
public ConstructorDefinition(Type type, String[] names, ConstructorInfo info)
{
Type = type;
Name = name;
Names = names;
Info = info;
}

Expand All @@ -229,9 +232,14 @@ public ConstructorDefinition(Type type, String name, ConstructorInfo info)
public Type Type { get; }

/// <summary>
/// Gets the name the constructor is exposed under.
/// Gets the names the constructor is exposed under.
/// </summary>
public String Name { get; }
public String[] Names { get; }

/// <summary>
/// Gets the primary name of the constructor.
/// </summary>
public String Name => Names[0];

/// <summary>
/// Gets the constructor to invoke, or null if the type cannot be constructed from
Expand Down
59 changes: 41 additions & 18 deletions src/AngleSharp.Js/Cache/PrototypeTypeCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,21 +157,23 @@ private static IReadOnlyDictionary<String, Type> CreateDefiningTypes(Assembly as
}

var baseType = typeInfo.BaseType;
var name = type.GetOfficialName(baseType);
var names = type.GetOfficialNames(baseType);

if (name == null || String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal))
foreach (var name in names)
{
// Either nothing to define, or the base type already defines the very same
// name - so this class is not the topmost one carrying it.
continue;
}
if (String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal))
{
// The base type already defines this very name, so this class is not
// the topmost one carrying it.
continue;
}

// A name may legitimately be defined twice (col and colgroup are both an
// HTMLTableColElement); share a prototype, but pick the same one every time.
if (!result.TryGetValue(name, out var existing) ||
String.CompareOrdinal(type.FullName, existing.FullName) < 0)
{
result[name] = type;
// A name may legitimately be defined twice (col and colgroup are both an
// HTMLTableColElement); share a prototype and keep the first class seen.
if (!result.ContainsKey(name))
{
result[name] = type;
}
}
}

Expand Down Expand Up @@ -202,20 +204,41 @@ private static IReadOnlyDictionary<String, Type> CreateExposedTypes(Assembly ass
continue;
}

var name = typeInfo.GetCustomAttributes<DomNameAttribute>().FirstOrDefault()?.OfficialName;
var names = typeInfo.GetCustomAttributes<DomNameAttribute>()
.Select(m => m.OfficialName)
.Where(m => m != null)
.Distinct(StringComparer.Ordinal);
var rank = GetExposureRank(type);

// An interface wins over a class: it is the DOM type, and the class is only
// one way of implementing it.
if (name != null && (!result.TryGetValue(name, out var existing) ||
(typeInfo.IsInterface && !existing.GetTypeInfo().IsInterface)))
foreach (var name in names)
{
result[name] = type;
if (!result.TryGetValue(name, out var existing) || rank > GetExposureRank(existing))
{
result[name] = type;
}
}
}

return result;
}

private static Int32 GetExposureRank(Type type)
{
var typeInfo = type.GetTypeInfo();

if (typeInfo.IsClass)
{
return 2;
}

if (typeInfo.IsInterface)
{
return 1;
}

return 0;
}

private static IEnumerable<Type> GetLoadableTypes(Assembly assembly)
{
try
Expand Down
Loading