diff --git a/src/AngleSharp.Js.Tests/EcmaTests.cs b/src/AngleSharp.Js.Tests/EcmaTests.cs
index 35c8929..4ca5cff 100644
--- a/src/AngleSharp.Js.Tests/EcmaTests.cs
+++ b/src/AngleSharp.Js.Tests/EcmaTests.cs
@@ -84,19 +84,40 @@ public async Task ModuleScriptWithScopedImportMapShouldRunCorrectScript()
{
{ "/example-module-1.js", "export function test() { document.getElementById('test1').remove(); }" },
{ "/example-module-2.js", "export function test() { document.getElementById('test2').remove(); }" },
+ { "/test.js", "import { test } from 'example-module'; test();" },
+ { "/test/test.js", "import { test } from 'example-module'; test();" }
}))
.WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });
var context = BrowsingContext.New(config);
- var html = "
Test
Test
";
- var document1 = await context.OpenAsync(r => r.Content(html));
+ var html1 = "Test
Test
";
+ var document1 = await context.OpenAsync(r => r.Content(html1));
Assert.IsNull(document1.GetElementById("test1"));
Assert.IsNotNull(document1.GetElementById("test2"));
- var document2 = await context.OpenAsync(r => r.Content(html).Address("http://localhost/test/"));
+ var html2 = "Test
Test
";
+ var document2 = await context.OpenAsync(r => r.Content(html2));
Assert.IsNull(document2.GetElementById("test2"));
Assert.IsNotNull(document2.GetElementById("test1"));
}
+
+ [Test]
+ public async Task ModuleScriptWithAbsoluteUrlImportMapShouldRun()
+ {
+ var config =
+ Configuration.Default
+ .WithJs()
+ .With(new MockHttpClientRequester(new Dictionary()
+ {
+ { "/jquery_4_0_0_esm.js", Constants.Jquery4_0_0_ESM }
+ }))
+ .WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true });
+
+ var context = BrowsingContext.New(config);
+ var html = "Test
";
+ var document = await context.OpenAsync(r => r.Content(html));
+ Assert.IsNull(document.GetElementById("test"));
+ }
}
}
diff --git a/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs
index 80413f0..65277d5 100644
--- a/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs
+++ b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs
@@ -1,14 +1,14 @@
-using AngleSharp.Io;
-using AngleSharp.Io.Network;
-using System.Collections.Generic;
-using System.IO;
-using System.Net;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
namespace AngleSharp.Js.Tests.Mocks
{
+ using AngleSharp.Io;
+ using AngleSharp.Io.Network;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Net;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+
///
/// Mock HttpClientRequester which returns content for a specific request from a local dictionary.
///
diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs
index cff0b6f..3848ebb 100644
--- a/src/AngleSharp.Js/EngineInstance.cs
+++ b/src/AngleSharp.Js/EngineInstance.cs
@@ -8,8 +8,6 @@ namespace AngleSharp.Js
using Jint.Native.Object;
using System;
using System.Collections.Generic;
- using System.IO;
- using System.Linq;
using System.Reflection;
sealed class EngineInstance
@@ -21,9 +19,7 @@ sealed class EngineInstance
private readonly ReferenceCache _references;
private readonly IEnumerable _libs;
private readonly DomNodeInstance _window;
- private readonly IResourceLoader _resourceLoader;
- private readonly IElement _scriptElement;
- private readonly string _documentUrl;
+ private readonly JsImportMap _importMap;
#endregion
@@ -31,15 +27,11 @@ sealed class EngineInstance
public EngineInstance(IWindow window, IDictionary assignments, IEnumerable libs)
{
- _resourceLoader = window.Document.Context.GetService();
-
- _scriptElement = window.Document.CreateElement(TagNames.Script);
-
- _documentUrl = window.Document.Url;
+ _importMap = new JsImportMap();
_engine = new Engine((options) =>
{
- options.EnableModules(new JsModuleLoader(this, _documentUrl, false));
+ options.EnableModules(new JsModuleLoader(this, window.Document, false));
});
_prototypes = new PrototypeCache(_engine);
_references = new ReferenceCache();
@@ -81,6 +73,8 @@ public EngineInstance(IWindow window, IDictionary assignments, I
public Engine Jint => _engine;
+ public JsImportMap ImportMap => _importMap;
+
#endregion
#region Methods
@@ -89,7 +83,7 @@ public EngineInstance(IWindow window, IDictionary assignments, I
public ObjectInstance GetDomPrototype(Type type) => _prototypes.GetOrCreate(type, CreatePrototype);
- public JsValue RunScript(String source, String type, JsValue context)
+ public JsValue RunScript(String source, String type, String sourceUrl, JsValue context)
{
if (string.IsNullOrEmpty(type))
{
@@ -109,7 +103,7 @@ public JsValue RunScript(String source, String type, JsValue context)
else if (type.Isi("module"))
{
// use a unique specifier to import the module into Jint
- var specifier = Guid.NewGuid().ToString();
+ var specifier = sourceUrl ?? Guid.NewGuid().ToString();
return ImportModule(specifier, source);
}
@@ -124,34 +118,34 @@ private JsValue LoadImportMap(String source)
{
var importMap = _engine.Evaluate($"JSON.parse('{source}')").AsObject();
- // get list of imports based on any scoped imports for the current document path, and any global imports
- var moduleImports = new Dictionary();
- var documentPathName = Url.Create(_documentUrl).PathName.ToLower();
-
if (importMap.TryGetValue("scopes", out var scopes))
{
var scopesObj = scopes.AsObject();
- var scopePaths = scopesObj.GetOwnPropertyKeys().Select(k => k.AsString()).OrderByDescending(k => k.Length);
-
- foreach (var scopePath in scopePaths)
+ foreach (var scopeProperty in scopesObj.GetOwnProperties())
{
- if (!documentPathName.Contains(scopePath.ToLower()))
+ var scopePath = scopeProperty.Key.AsString();
+
+ if (_importMap.Scopes.ContainsKey(scopePath))
{
continue;
}
- var scopeImports = scopesObj[scopePath].AsObject();
+ var scopeValue = new Dictionary();
- var scopeImportImportSpecifiers = scopeImports.GetOwnPropertyKeys().Select(k => k.AsString());
+ var scopeImports = scopesObj[scopePath].AsObject();
- foreach (var scopeImportSpecifier in scopeImportImportSpecifiers)
+ foreach (var scopeImportProperty in scopeImports.GetOwnProperties())
{
- if (!moduleImports.ContainsKey(scopeImportSpecifier))
+ var scopeImportSpecifier = scopeImportProperty.Key.AsString();
+
+ if (!scopeValue.ContainsKey(scopeImportSpecifier))
{
- moduleImports.Add(scopeImportSpecifier, scopeImports[scopeImportSpecifier].AsString());
+ scopeValue.Add(scopeImportSpecifier, new Uri(scopeImports[scopeImportSpecifier].AsString(), UriKind.RelativeOrAbsolute));
}
}
+
+ _importMap.Scopes.Add(scopePath, scopeValue);
}
}
@@ -159,24 +153,17 @@ private JsValue LoadImportMap(String source)
{
var importsObj = imports.AsObject();
- var importSpecifiers = importsObj.GetOwnPropertyKeys().Select(k => k.AsString());
-
- foreach (var importSpecifier in importSpecifiers)
+ foreach (var importProperty in importsObj.GetOwnProperties())
{
- if (!moduleImports.ContainsKey(importSpecifier))
+ var importSpecifier = importProperty.Key.AsString();
+
+ if (!_importMap.Imports.ContainsKey(importSpecifier))
{
- moduleImports.Add(importSpecifier, importsObj[importSpecifier].AsString());
+ _importMap.Imports.Add(importSpecifier, new Uri(importsObj[importSpecifier].AsString(), UriKind.RelativeOrAbsolute));
}
}
}
- foreach (var import in moduleImports)
- {
- var moduleContent = FetchModule(new Uri(import.Value, UriKind.RelativeOrAbsolute));
-
- ImportModule(import.Key, moduleContent);
- }
-
return JsValue.Undefined;
}
@@ -188,34 +175,6 @@ private JsValue ImportModule(String specifier, String source)
return JsValue.Undefined;
}
- public string FetchModule(Uri moduleUrl)
- {
- if (_resourceLoader == null)
- {
- return string.Empty;
- }
-
- if (!moduleUrl.IsAbsoluteUri)
- {
- moduleUrl = new Uri(new Uri(_documentUrl), moduleUrl);
- }
-
- var importUrl = Url.Convert(moduleUrl);
-
- var request = new ResourceRequest(_scriptElement, importUrl);
-
- var response = _resourceLoader.FetchAsync(request).Task.Result;
-
- string content;
-
- using (var streamReader = new StreamReader(response.Content))
- {
- content = streamReader.ReadToEnd();
- }
-
- return content;
- }
-
#endregion
#region Helpers
diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs
index 2b90984..12cf37e 100644
--- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs
+++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs
@@ -198,11 +198,11 @@ public static void AddInstance(this EngineInstance engine, ObjectInstance obj, T
apply.Invoke(engine, obj);
}
- public static JsValue RunScript(this EngineInstance engine, String source, String type) =>
- engine.RunScript(source, type, engine.Window);
+ public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl) =>
+ engine.RunScript(source, type, sourceUrl, engine.Window);
- public static JsValue RunScript(this EngineInstance engine, String source, String type, INode context) =>
- engine.RunScript(source, type, context.ToJsValue(engine));
+ public static JsValue RunScript(this EngineInstance engine, String source, String type, String sourceUrl, INode context) =>
+ engine.RunScript(source, type, sourceUrl, context.ToJsValue(engine));
public static JsValue Call(this EngineInstance instance, MethodInfo method, JsValue thisObject, JsValue[] arguments)
{
diff --git a/src/AngleSharp.Js/JsApiExtensions.cs b/src/AngleSharp.Js/JsApiExtensions.cs
index 12815ed..b055e11 100644
--- a/src/AngleSharp.Js/JsApiExtensions.cs
+++ b/src/AngleSharp.Js/JsApiExtensions.cs
@@ -16,14 +16,15 @@ public static class JsApiExtensions
/// The document as context.
/// The script to run.
/// The type of the script to run (defaults to "text/javascript").
+ /// The URL of the script.
/// The result of running the script, if any.
- public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null)
+ public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null, String sourceUrl = null)
{
if (document == null)
throw new ArgumentNullException(nameof(document));
var service = document?.Context.GetService();
- return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript);
+ return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript, sourceUrl);
}
}
}
diff --git a/src/AngleSharp.Js/JsImportMap.cs b/src/AngleSharp.Js/JsImportMap.cs
new file mode 100644
index 0000000..247c99b
--- /dev/null
+++ b/src/AngleSharp.Js/JsImportMap.cs
@@ -0,0 +1,29 @@
+namespace AngleSharp.Js
+{
+ using System;
+ using System.Collections.Generic;
+
+ ///
+ /// https://html.spec.whatwg.org/multipage/webappapis.html#import-map
+ ///
+ sealed class JsImportMap
+ {
+ public JsImportMap()
+ {
+ Imports = new Dictionary();
+ Scopes = new Dictionary>();
+ }
+
+ ///
+ /// Provides the mappings between module specifier text that might appear in an import statement or import() operator,
+ /// and the text that will replace it when the specifier is resolved.
+ ///
+ public Dictionary Imports { get; set; }
+
+ ///
+ /// Mappings that are only used if the script importing the module contains a particular URL path.
+ /// If the URL of the loading script matches the supplied path, the mapping associated with the scope will be used.
+ ///
+ public Dictionary> Scopes { get; set; }
+ }
+}
diff --git a/src/AngleSharp.Js/JsModuleLoader.cs b/src/AngleSharp.Js/JsModuleLoader.cs
index 7fa9a05..1863ebd 100644
--- a/src/AngleSharp.Js/JsModuleLoader.cs
+++ b/src/AngleSharp.Js/JsModuleLoader.cs
@@ -1,29 +1,106 @@
-using AngleSharp.Io;
-using AngleSharp.Text;
-using Jint;
-using Jint.Runtime.Modules;
-
namespace AngleSharp.Js
{
+ using AngleSharp.Dom;
+ using AngleSharp.Io;
+ using AngleSharp.Text;
+ using Jint;
+ using Jint.Runtime.Modules;
+ using System.IO;
+ using System;
+ using System.Linq;
+
internal class JsModuleLoader : DefaultModuleLoader
{
private readonly EngineInstance _instance;
+ private readonly IResourceLoader _resourceLoader;
+ private readonly IElement _scriptElement;
+ private readonly string _documentUrl;
- public JsModuleLoader(EngineInstance instance, string basePath, bool restrictToBasePath = true) : base (basePath, restrictToBasePath)
+ public JsModuleLoader(EngineInstance instance, IDocument document, bool restrictToBasePath = true) : base (document.Url, restrictToBasePath)
{
_instance = instance;
+ _resourceLoader = document.Context.GetService();
+ _scriptElement = document.CreateElement(TagNames.Script);
+ _documentUrl = document.Url;
+ }
+
+ public override ResolvedSpecifier Resolve(string referencingModuleLocation, ModuleRequest moduleRequest)
+ {
+ if (referencingModuleLocation != null && _instance.ImportMap.Scopes.Count > 0)
+ {
+ foreach (var scopePath in _instance.ImportMap.Scopes.Keys.OrderByDescending(k => k.Length))
+ {
+ if (referencingModuleLocation.Contains(scopePath))
+ {
+ var scopeImports = _instance.ImportMap.Scopes[scopePath];
+
+ if (scopeImports.TryGetValue(moduleRequest.Specifier, out var scopeModuleUrl))
+ {
+ if (!scopeModuleUrl.IsAbsoluteUri)
+ {
+ scopeModuleUrl = new Uri(new Uri(_documentUrl), scopeModuleUrl);
+ }
+
+ return new ResolvedSpecifier(
+ moduleRequest,
+ moduleRequest.Specifier,
+ scopeModuleUrl,
+ SpecifierType.RelativeOrAbsolute);
+ }
+ }
+ }
+ }
+
+ if (_instance.ImportMap.Imports.TryGetValue(moduleRequest.Specifier, out var moduleUrl))
+ {
+ if (!moduleUrl.IsAbsoluteUri)
+ {
+ moduleUrl = new Uri(new Uri(_documentUrl), moduleUrl);
+ }
+
+ return new ResolvedSpecifier(
+ moduleRequest,
+ moduleRequest.Specifier,
+ moduleUrl,
+ SpecifierType.RelativeOrAbsolute);
+ }
+
+ return base.Resolve(referencingModuleLocation, moduleRequest);
}
protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved)
{
if (resolved.Uri?.Scheme.IsOneOf(ProtocolNames.Http, ProtocolNames.Https) == true)
{
- return _instance.FetchModule(resolved.Uri);
+ return FetchModule(resolved.Uri);
}
else
{
return base.LoadModuleContents(engine, resolved);
}
}
+
+ private string FetchModule(Uri moduleUrl)
+ {
+ if (_resourceLoader == null)
+ {
+ return string.Empty;
+ }
+
+ var importUrl = Url.Convert(moduleUrl);
+
+ var request = new ResourceRequest(_scriptElement, importUrl);
+
+ var response = _resourceLoader.FetchAsync(request).Task.Result;
+
+ string content;
+
+ using (var streamReader = new StreamReader(response.Content))
+ {
+ content = streamReader.ReadToEnd();
+ }
+
+ return content;
+ }
}
}
diff --git a/src/AngleSharp.Js/JsScriptingService.cs b/src/AngleSharp.Js/JsScriptingService.cs
index 90eb989..add873e 100644
--- a/src/AngleSharp.Js/JsScriptingService.cs
+++ b/src/AngleSharp.Js/JsScriptingService.cs
@@ -84,7 +84,7 @@ public async Task EvaluateScriptAsync(IResponse response, ScriptOptions options,
{
var content = await reader.ReadToEndAsync().ConfigureAwait(false);
await options.EventLoop.EnqueueAsync(_ =>
- EvaluateScript(options.Document, content, options.Element?.Type), TaskPriority.Critical).ConfigureAwait(false);
+ EvaluateScript(options.Document, content, options.Element?.Type, options.Element?.Source), TaskPriority.Critical).ConfigureAwait(false);
}
}
@@ -94,11 +94,12 @@ await options.EventLoop.EnqueueAsync(_ =>
/// The context of the evaluation.
/// The source of the script.
/// The type of the script.
+ /// The URL of the script.
/// The result of the evaluation.
- public Object EvaluateScript(IDocument document, String source, String type)
+ public Object EvaluateScript(IDocument document, String source, String type, String sourceUrl)
{
document = document ?? throw new ArgumentNullException(nameof(document));
- return GetOrCreateInstance(document).RunScript(source, type).FromJsValue();
+ return GetOrCreateInstance(document).RunScript(source, type, sourceUrl).FromJsValue();
}
#endregion