From 088c45c593282a33653743ab2b63e470c27b22c7 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 25 Nov 2021 22:45:39 +0100 Subject: [PATCH 01/73] Updated CI/CD --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e224ca8..810411b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,6 +49,13 @@ jobs: steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 3.1.x + 5.0.x + 6.0.x - name: Build run: ./build.sh @@ -58,6 +65,13 @@ jobs: steps: - uses: actions/checkout@v2 + + - uses: actions/setup-dotnet@v1 + with: + dotnet-version: | + 3.1.x + 5.0.x + 6.0.x - name: Build run: | From ad44d9d82acb73352a5e49db03144273e2a04b16 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 25 Nov 2021 23:23:16 +0100 Subject: [PATCH 02/73] Updated to .NET Core 3.1 --- src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 167bca3..475e1ac 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -1,7 +1,7 @@ AngleSharp.Js.Tests - netcoreapp2.1 + netcoreapp3.1 true Key.snk false @@ -21,8 +21,8 @@ - + - + - \ No newline at end of file + From 457dad14f1149999b70b1f83c45d7030efbf5e82 Mon Sep 17 00:00:00 2001 From: Wayne Sebbens Date: Sun, 11 Jun 2023 09:50:24 +1000 Subject: [PATCH 03/73] Use DomName in enum JsValues --- src/AngleSharp.Js/Extensions/EngineExtensions.cs | 6 ++++++ .../Extensions/ReflectionExtensions.cs | 16 ++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index 37c7417..c8958eb 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -45,6 +45,12 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) } else if (obj is Enum) { + string name = ((Enum)obj).GetOfficialName(); + if (name != null) + { + return new JsValue(name); + } + return new JsValue(Convert.ToInt32(obj)); } diff --git a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs index 164068c..fded50b 100644 --- a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs +++ b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs @@ -55,8 +55,8 @@ public static MethodInfo PrepareConvert(this Type fromType, Type toType) public static String GetOfficialName(this MemberInfo member) { var names = member.GetCustomAttributes(); - var officalNameAttribute = names.FirstOrDefault(); - return officalNameAttribute?.OfficialName ?? member.Name; + var officialNameAttribute = names.FirstOrDefault(); + return officialNameAttribute?.OfficialName ?? member.Name; } public static String GetOfficialName(this Type currentType, Type baseType) @@ -87,6 +87,18 @@ public static String GetOfficialName(this Type currentType, Type baseType) return name; } + public static String GetOfficialName(this Enum value) + { + var enumType = value.GetType(); + var member = enumType.GetMember(value.ToString()).FirstOrDefault(); + + // if the enum value does not have a DomNameAttribute, calling member.GetOfficialName() would return the value name + // to allow previous behaviour to be preserved, if the DomNameAttribute is not present then null will be returned + IEnumerable names = member.GetCustomAttributes(); + var officialNameAttribute = names.FirstOrDefault(); + return officialNameAttribute?.OfficialName; + } + public static PropertyInfo GetInheritedProperty(this Type type, String propertyName, BindingFlags bindingAttr = BindingFlags.Public | BindingFlags.Instance) { if (type.GetTypeInfo().IsInterface) From 2829487ef774a0b8e7253e396fe842defad0f13b Mon Sep 17 00:00:00 2001 From: Wayne Sebbens Date: Sun, 11 Jun 2023 11:16:28 +1000 Subject: [PATCH 04/73] Only use DomName for certain enum types --- src/AngleSharp.Js/Extensions/EngineExtensions.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index c8958eb..f9c124c 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -45,12 +45,16 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) } else if (obj is Enum) { - string name = ((Enum)obj).GetOfficialName(); - if (name != null) + switch (obj) { - return new JsValue(name); + case DocumentReadyState _: + string name = ((Enum)obj).GetOfficialName(); + if (name != null) + { + return new JsValue(name); + } + break; } - return new JsValue(Convert.ToInt32(obj)); } From efb949ea33486d477b09c89368473a2b1a0ab4cd Mon Sep 17 00:00:00 2001 From: Wayne Sebbens Date: Sun, 11 Jun 2023 11:25:09 +1000 Subject: [PATCH 05/73] Updates to follow style conventions --- src/AngleSharp.Js/Extensions/EngineExtensions.cs | 2 +- src/AngleSharp.Js/Extensions/ReflectionExtensions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index f9c124c..180236a 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -48,7 +48,7 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) switch (obj) { case DocumentReadyState _: - string name = ((Enum)obj).GetOfficialName(); + var name = ((Enum)obj).GetOfficialName(); if (name != null) { return new JsValue(name); diff --git a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs index fded50b..3a309ea 100644 --- a/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs +++ b/src/AngleSharp.Js/Extensions/ReflectionExtensions.cs @@ -94,7 +94,7 @@ public static String GetOfficialName(this Enum value) // if the enum value does not have a DomNameAttribute, calling member.GetOfficialName() would return the value name // to allow previous behaviour to be preserved, if the DomNameAttribute is not present then null will be returned - IEnumerable names = member.GetCustomAttributes(); + var names = member.GetCustomAttributes(); var officialNameAttribute = names.FirstOrDefault(); return officialNameAttribute?.OfficialName; } From 3fb3c454d8527fb1558635c387030677d8151c99 Mon Sep 17 00:00:00 2001 From: Wayne Sebbens Date: Sun, 11 Jun 2023 11:56:41 +1000 Subject: [PATCH 06/73] Added unit test to check expected DocumentReadyState values --- src/AngleSharp.Js.Tests/FireEventTests.cs | 32 +++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/AngleSharp.Js.Tests/FireEventTests.cs b/src/AngleSharp.Js.Tests/FireEventTests.cs index e468a1f..eac505c 100644 --- a/src/AngleSharp.Js.Tests/FireEventTests.cs +++ b/src/AngleSharp.Js.Tests/FireEventTests.cs @@ -4,6 +4,9 @@ namespace AngleSharp.Js.Tests using AngleSharp.Dom.Events; using AngleSharp.Scripting; using NUnit.Framework; + + using System; + using System.Linq; using System.Threading.Tasks; [TestFixture] @@ -256,6 +259,35 @@ public async Task DocumentLoadEventIsFired_Issue42() Assert.AreEqual("Success!", div?.TextContent); } + [Test] + public async Task DocumentReadyStateIsComplete_Issue86() + { + var cfg = Configuration.Default.WithJs().WithEventLoop(); + var html = @" + + + +"; + var context = BrowsingContext.New(cfg); + var document = await context.OpenAsync(m => m.Content(html)) + .WhenStable(); + + var divs = document.GetElementsByTagName("div"); + + // expected value will vary depending on AngleSharp package version + // 1.0.2 and greater, expected value will be { "interactive", "complete" + // prior to 1.0.2, expected value will be { "1", "2" } + var expected = new[] { DocumentReadyState.Interactive, DocumentReadyState.Complete } + .Select(e => e.GetOfficialName() ?? Convert.ToInt32(e).ToString()); + CollectionAssert.AreEqual(expected, divs.Select(d => d.TextContent)); + } + [Test] public async Task SetTimeoutWithStringAsFunction() { From fc4da2bd89d6efd79e317cd339349945b584fa93 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Wed, 28 Feb 2024 14:33:49 +0000 Subject: [PATCH 07/73] Upgraded to Jint v3 --- src/AngleSharp.Js.Tests/ComponentTests.cs | 2 +- src/AngleSharp.Js.Tests/Constants.cs | 156 ++++++++++++++++ src/AngleSharp.Js.Tests/EcmaTests.cs | 21 +++ src/AngleSharp.Js.Tests/FireEventTests.cs | 5 +- src/AngleSharp.Js.Tests/InteractionTests.cs | 6 +- src/AngleSharp.Js.Tests/JqueryTests.cs | 4 +- .../Mocks/DataRequester.cs | 4 +- src/AngleSharp.Js/AngleSharp.Js.csproj | 6 +- src/AngleSharp.Js/Cache/PrototypeCache.cs | 2 +- .../Converters/DomTypeConverter.cs | 169 ------------------ .../Converters/SystemTypeConverter.cs | 56 ------ .../Converters/UnresolvedConverter.cs | 12 -- src/AngleSharp.Js/Dom/Console.cs | 35 ++++ src/AngleSharp.Js/Dom/DomParser.cs | 2 +- .../Dom/EventAttributeObserver.cs | 6 +- src/AngleSharp.Js/Dom/WindowExtensions.cs | 12 ++ src/AngleSharp.Js/EngineInstance.cs | 30 ++-- src/AngleSharp.Js/Extensions/DomDelegates.cs | 11 +- .../Extensions/EngineExtensions.cs | 47 ++--- .../Extensions/JsValueExtensions.cs | 9 +- src/AngleSharp.Js/JsApiExtensions.cs | 2 +- src/AngleSharp.Js/Proxies/ConsoleInstance.cs | 37 ---- .../Proxies/DomConstructorInstance.cs | 41 +++-- src/AngleSharp.Js/Proxies/DomConstructors.cs | 38 ---- .../Proxies/DomDelegateInstance.cs | 36 ---- src/AngleSharp.Js/Proxies/DomEventInstance.cs | 15 +- .../Proxies/DomFunctionInstance.cs | 35 ---- src/AngleSharp.Js/Proxies/DomNodeInstance.cs | 62 ++++++- .../Proxies/DomPrototypeInstance.cs | 29 +-- 29 files changed, 393 insertions(+), 497 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/EcmaTests.cs delete mode 100644 src/AngleSharp.Js/Converters/DomTypeConverter.cs delete mode 100644 src/AngleSharp.Js/Converters/SystemTypeConverter.cs delete mode 100644 src/AngleSharp.Js/Converters/UnresolvedConverter.cs create mode 100644 src/AngleSharp.Js/Dom/Console.cs delete mode 100644 src/AngleSharp.Js/Proxies/ConsoleInstance.cs delete mode 100644 src/AngleSharp.Js/Proxies/DomConstructors.cs delete mode 100644 src/AngleSharp.Js/Proxies/DomDelegateInstance.cs delete mode 100644 src/AngleSharp.Js/Proxies/DomFunctionInstance.cs diff --git a/src/AngleSharp.Js.Tests/ComponentTests.cs b/src/AngleSharp.Js.Tests/ComponentTests.cs index 5ba0c4a..0e0072d 100644 --- a/src/AngleSharp.Js.Tests/ComponentTests.cs +++ b/src/AngleSharp.Js.Tests/ComponentTests.cs @@ -1,7 +1,7 @@ namespace AngleSharp.Js.Tests { using AngleSharp.Scripting; - using AngleSharp.Xml; + using Jint; using NUnit.Framework; using System; using System.Threading.Tasks; diff --git a/src/AngleSharp.Js.Tests/Constants.cs b/src/AngleSharp.Js.Tests/Constants.cs index 5dc8d04..d9be29d 100644 --- a/src/AngleSharp.Js.Tests/Constants.cs +++ b/src/AngleSharp.Js.Tests/Constants.cs @@ -265,5 +265,161 @@ static class Constants b,c,d){ob(c)?void 0:n(""200"");null==a||void 0===a._reactInternalFiber?n(""38""):void 0;return Wc(a,b,c,!1,d)},unmountComponentAtNode:function(a){ob(a)?void 0:n(""40"");return a._reactRootContainer?($g(function(){Wc(null,null,a,!1,function(){a._reactRootContainer=null})}),!0):!1},unstable_createPortal:function(){return ch.apply(void 0,arguments)},unstable_batchedUpdates:Zg,unstable_interactiveUpdates:ah,flushSync:function(a,b){w?n(""187""):void 0;var c=z;z=!0;try{return Tg(a,b)}finally{z=c,Z(1073741823,!1)}}, unstable_createRoot:function(a,b){ob(a)?void 0:n(""299"",""unstable_createRoot"");return new nb(a,!0,null!=b&&!0===b.hydrate)},unstable_flushControlled:function(a){var b=z;z=!0;try{Tg(a)}finally{(z=b)||w||Z(1073741823,!1)}},__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED:{Events:[Je,Da,dd,Ae.injectEventPluginsByName,Zc,Qa,function(a){ad(a,xh)},Ve,We,oc,cd]}};(function(a){var b=a.findFiberByHostInstance;return ai(B({},a,{overrideProps:null,currentDispatcherRef:Ma.ReactCurrentDispatcher,findHostInstanceByFiber:function(a){a= tf(a);return null===a?null:a.stateNode},findFiberByHostInstance:function(a){return b?b(a):null}}))})({findFiberByHostInstance:dc,bundleType:0,version:""16.8.6"",rendererPackageName:""react-dom""});var ph={default:oh},qh=ph&&oh||ph;return qh.default||qh});"; + + public static readonly String Bootstrap_5_3_3 = @"/*! * Bootstrap v5.3.3 (https://getbootstrap.com/) * Copyright 2011-2024 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) */ !function(t,e){""object""==typeof exports&&""undefined""!=typeof module?module.exports=e():""function""==typeof define&&define.amd?define(e):(t=""undefined""!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,(function(){""use strict"";const t=new Map,e={set(e,i,n){t.has(e)||t.set(e +,new Map);const s=t.get(e);s.has(i)||0===s.size?s.set(i,n):console.error(`Bootstrap doesn't allow more than one instance per element. Bound instance: ${Array.from(s.keys())[0]}.`)},get:(e,i)=>t.has(e)&&t.get(e).get(i)||null,remove(e,i){if(!t.has(e))return;const n=t.get(e);n.delete(i),0===n.size&&t.delete(e)}},i=""transitionend"",n=t=>(t&&window.CSS&&window.CSS.escape&&(t=t.replace(/#([^\s""#']+)/g,((t,e)=>`#${CSS.escape(e)}`))),t),s=t=>{t.dispatchEvent(new Event(i))},o=t=>!(!t||""object""!=typeof t)&&(void + 0!==t.jquery&&(t=t[0]),void 0!==t.nodeType),r=t=>o(t)?t.jquery?t[0]:t:""string""==typeof t&&t.length>0?document.querySelector(n(t)):null,a=t=>{if(!o(t)||0===t.getClientRects().length)return!1;const e=""visible""===getComputedStyle(t).getPropertyValue(""visibility""),i=t.closest(""details:not([open])"");if(!i)return e;if(i!==t){const e=t.closest(""summary"");if(e&&e.parentNode!==i)return!1;if(null===e)return!1}return e},l=t=>!t||t.nodeType!==Node.ELEMENT_NODE||!!t.classList.contains(""disabled"")||(void 0!==t.disabled?t.disabled:t.hasAttribute(""disabled"")&&""false""!==t.getAttribute(""disabled"")) +,c=t=>{if(!document.documentElement.attachShadow)return null;if(""function""==typeof t.getRootNode){const e=t.getRootNode();return e instanceof ShadowRoot?e:null}return t instanceof ShadowRoot?t:t.parentNode?c(t.parentNode):null},h=()=>{},d=t=>{t.offsetHeight},u=()=>window.jQuery&&!document.body.hasAttribute(""data-bs-no-jquery"")?window.jQuery:null,f=[],p=()=>""rtl""===document.documentElement.dir,m=t=>{var e;e=()=>{const e=u();if(e){const i=t.NAME,n=e.fn[i];e.fn[i]=t.jQueryInterface,e.fn[i].Constructor=t +,e.fn[i].noConflict=()=>(e.fn[i]=n,t.jQueryInterface)}},""loading""===document.readyState?(f.length||document.addEventListener(""DOMContentLoaded"",(()=>{for(const t of f)t()})),f.push(e)):e()},g=(t,e=[],i=t)=>""function""==typeof t?t(...e):i,_=(t,e,n=!0)=>{if(!n)return void g(t);const o=(t=>{if(!t)return 0;let{transitionDuration:e,transitionDelay:i}=window.getComputedStyle(t);const n=Number.parseFloat(e),s=Number.parseFloat(i);return n||s?(e=e.split("","")[0],i=i.split("","")[0],1e3*(Number.parseFloat(e)+Number.parseFloat(i))):0})(e)+5 +;let r=!1;const a=({target:n})=>{n===e&&(r=!0,e.removeEventListener(i,a),g(t))};e.addEventListener(i,a),setTimeout((()=>{r||s(e)}),o)},b=(t,e,i,n)=>{const s=t.length;let o=t.indexOf(e);return-1===o?!i&&n?t[s-1]:t[0]:(o+=i?1:-1,n&&(o=(o+s)%s),t[Math.max(0,Math.min(o,s-1))])},v=/[^.]*(?=\..*)\.|.*/,y=/\..*/,w=/::\d+$/,A={};let E=1;const T={mouseenter:""mouseover"",mouseleave:""mouseout""},C=new Set([""click"",""dblclick"",""mouseup"",""mousedown"",""contextmenu"",""mousewheel"",""DOMMouseScroll"",""mouseover"",""mouseout"" +,""mousemove"",""selectstart"",""selectend"",""keydown"",""keypress"",""keyup"",""orientationchange"",""touchstart"",""touchmove"",""touchend"",""touchcancel"",""pointerdown"",""pointermove"",""pointerup"",""pointerleave"",""pointercancel"",""gesturestart"",""gesturechange"",""gestureend"",""focus"",""blur"",""change"",""reset"",""select"",""submit"",""focusin"",""focusout"",""load"",""unload"",""beforeunload"",""resize"",""move"",""DOMContentLoaded"",""readystatechange"",""error"",""abort"",""scroll""]);function O(t,e){return e&&`${e}::${E++}`||t.uidEvent||E++}function + x(t){const e=O(t);return t.uidEvent=e,A[e]=A[e]||{},A[e]}function k(t,e,i=null){return Object.values(t).find((t=>t.callable===e&&t.delegationSelector===i))}function L(t,e,i){const n=""string""==typeof e,s=n?i:e||i;let o=I(t);return C.has(o)||(o=t),[n,s,o]}function S(t,e,i,n,s){if(""string""!=typeof e||!t)return;let[o,r,a]=L(e,i,n);if(e in T){const t=t=>function(e){if(!e.relatedTarget||e.relatedTarget!==e.delegateTarget&&!e.delegateTarget.contains(e.relatedTarget))return t.call(this,e)};r=t(r)}const + l=x(t),c=l[a]||(l[a]={}),h=k(c,r,o?i:null);if(h)return void(h.oneOff=h.oneOff&&s);const d=O(r,e.replace(v,"""")),u=o?function(t,e,i){return function n(s){const o=t.querySelectorAll(e);for(let{target:r}=s;r&&r!==this;r=r.parentNode)for(const a of o)if(a===r)return P(s,{delegateTarget:r}),n.oneOff&&N.off(t,s.type,e,i),i.apply(r,[s])}}(t,i,r):function(t,e){return function i(n){return P(n,{delegateTarget:t}),i.oneOff&&N.off(t,n.type,e),e.apply(t,[n])}}(t,r);u.delegationSelector=o?i:null,u.callable=r,u.oneOff=s +,u.uidEvent=d,c[d]=u,t.addEventListener(a,u,o)}function D(t,e,i,n,s){const o=k(e[i],n,s);o&&(t.removeEventListener(i,o,Boolean(s)),delete e[i][o.uidEvent])}function $(t,e,i,n){const s=e[i]||{};for(const[o,r]of Object.entries(s))o.includes(n)&&D(t,e,i,r.callable,r.delegationSelector)}function I(t){return t=t.replace(y,""""),T[t]||t}const N={on(t,e,i,n){S(t,e,i,n,!1)},one(t,e,i,n){S(t,e,i,n,!0)},off(t,e,i,n){if(""string""!=typeof e||!t)return;const[s,o,r]=L(e,i,n),a=r!==e,l=x(t),c=l[r]||{},h=e.startsWith(""."") +;if(void 0===o){if(h)for(const i of Object.keys(l))$(t,l,i,e.slice(1));for(const[i,n]of Object.entries(c)){const s=i.replace(w,"""");a&&!e.includes(s)||D(t,l,r,n.callable,n.delegationSelector)}}else{if(!Object.keys(c).length)return;D(t,l,r,o,s?i:null)}},trigger(t,e,i){if(""string""!=typeof e||!t)return null;const n=u();let s=null,o=!0,r=!0,a=!1;e!==I(e)&&n&&(s=n.Event(e,i),n(t).trigger(s),o=!s.isPropagationStopped(),r=!s.isImmediatePropagationStopped(),a=s.isDefaultPrevented());const l=P(new Event(e +,{bubbles:o,cancelable:!0}),i);return a&&l.preventDefault(),r&&t.dispatchEvent(l),l.defaultPrevented&&s&&s.preventDefault(),l}};function P(t,e={}){for(const[i,n]of Object.entries(e))try{t[i]=n}catch(e){Object.defineProperty(t,i,{configurable:!0,get:()=>n})}return t}function j(t){if(""true""===t)return!0;if(""false""===t)return!1;if(t===Number(t).toString())return Number(t);if(""""===t||""null""===t)return null;if(""string""!=typeof t)return t;try{return JSON.parse(decodeURIComponent(t))}catch(e){return t}}function + M(t){return t.replace(/[A-Z]/g,(t=>`-${t.toLowerCase()}`))}const F={setDataAttribute(t,e,i){t.setAttribute(`data-bs-${M(e)}`,i)},removeDataAttribute(t,e){t.removeAttribute(`data-bs-${M(e)}`)},getDataAttributes(t){if(!t)return{};const e={},i=Object.keys(t.dataset).filter((t=>t.startsWith(""bs"")&&!t.startsWith(""bsConfig"")));for(const n of i){let i=n.replace(/^bs/,"""");i=i.charAt(0).toLowerCase()+i.slice(1,i.length),e[i]=j(t.dataset[n])}return e},getDataAttribute:(t,e)=>j(t.getAttribute(`data-bs-${M(e)}`))} +;class H{static get Default(){return{}}static get DefaultType(){return{}}static get NAME(){throw new Error('You have to implement the static method ""NAME"", for each component!')}_getConfig(t){return t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t}_mergeConfigObj(t,e){const i=o(e)?F.getDataAttribute(e,""config""):{};return{...this.constructor.Default,...""object""==typeof i?i:{},...o(e)?F.getDataAttributes(e):{},...""object""==typeof t?t:{}}}_typeCheckConfig(t +,e=this.constructor.DefaultType){for(const[n,s]of Object.entries(e)){const e=t[n],r=o(e)?""element"":null==(i=e)?`${i}`:Object.prototype.toString.call(i).match(/\s([a-z]+)/i)[1].toLowerCase();if(!new RegExp(s).test(r))throw new TypeError(`${this.constructor.NAME.toUpperCase()}: Option ""${n}"" provided type ""${r}"" but expected type ""${s}"".`)}var i}}class W extends H{constructor(t,i){super(),(t=r(t))&&(this._element=t,this._config=this._getConfig(i),e.set(this._element,this.constructor.DATA_KEY,this))}dispose(){e.remove(this._element +,this.constructor.DATA_KEY),N.off(this._element,this.constructor.EVENT_KEY);for(const t of Object.getOwnPropertyNames(this))this[t]=null}_queueCallback(t,e,i=!0){_(t,e,i)}_getConfig(t){return t=this._mergeConfigObj(t,this._element),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}static getInstance(t){return e.get(r(t),this.DATA_KEY)}static getOrCreateInstance(t,e={}){return this.getInstance(t)||new this(t,""object""==typeof e?e:null)}static get VERSION(){return""5.3.3""}static get DATA_KEY(){return`bs.${this.NAME}`}static + get EVENT_KEY(){return`.${this.DATA_KEY}`}static eventName(t){return`${t}${this.EVENT_KEY}`}}const B=t=>{let e=t.getAttribute(""data-bs-target"");if(!e||""#""===e){let i=t.getAttribute(""href"");if(!i||!i.includes(""#"")&&!i.startsWith("".""))return null;i.includes(""#"")&&!i.startsWith(""#"")&&(i=`#${i.split(""#"")[1]}`),e=i&&""#""!==i?i.trim():null}return e?e.split("","").map((t=>n(t))).join("",""):null},z={find:(t,e=document.documentElement)=>[].concat(...Element.prototype.querySelectorAll.call(e,t)),findOne:(t,e=document.documentElement)=>Element.prototype.querySelector.call(e +,t),children:(t,e)=>[].concat(...t.children).filter((t=>t.matches(e))),parents(t,e){const i=[];let n=t.parentNode.closest(e);for(;n;)i.push(n),n=n.parentNode.closest(e);return i},prev(t,e){let i=t.previousElementSibling;for(;i;){if(i.matches(e))return[i];i=i.previousElementSibling}return[]},next(t,e){let i=t.nextElementSibling;for(;i;){if(i.matches(e))return[i];i=i.nextElementSibling}return[]},focusableChildren(t){const e=[""a"",""button"",""input"",""textarea"",""select"",""details"",""[tabindex]"",'[contenteditable=""true""]'].map((t=>`${t}:not([tabindex^=""-""])`)) +.join("","");return this.find(e,t).filter((t=>!l(t)&&a(t)))},getSelectorFromElement(t){const e=B(t);return e&&z.findOne(e)?e:null},getElementFromSelector(t){const e=B(t);return e?z.findOne(e):null},getMultipleElementsFromSelector(t){const e=B(t);return e?z.find(e):[]}},R=(t,e=""hide"")=>{const i=`click.dismiss${t.EVENT_KEY}`,n=t.NAME;N.on(document,i,`[data-bs-dismiss=""${n}""]`,(function(i){if([""A"",""AREA""].includes(this.tagName)&&i.preventDefault(),l(this))return;const s=z.getElementFromSelector(this)||this.closest(`.${n}`) +;t.getOrCreateInstance(s)[e]()}))},q="".bs.alert"",V=`close${q}`,K=`closed${q}`;class Q extends W{static get NAME(){return""alert""}close(){if(N.trigger(this._element,V).defaultPrevented)return;this._element.classList.remove(""show"");const t=this._element.classList.contains(""fade"");this._queueCallback((()=>this._destroyElement()),this._element,t)}_destroyElement(){this._element.remove(),N.trigger(this._element,K),this.dispose()}static jQueryInterface(t){return this.each((function(){const e=Q.getOrCreateInstance(this) +;if(""string""==typeof t){if(void 0===e[t]||t.startsWith(""_"")||""constructor""===t)throw new TypeError(`No method named ""${t}""`);e[t](this)}}))}}R(Q,""close""),m(Q);const X='[data-bs-toggle=""button""]';class Y extends W{static get NAME(){return""button""}toggle(){this._element.setAttribute(""aria-pressed"",this._element.classList.toggle(""active""))}static jQueryInterface(t){return this.each((function(){const e=Y.getOrCreateInstance(this);""toggle""===t&&e[t]()}))}}N.on(document,""click.bs.button.data-api"",X,(t=>{t.preventDefault() +;const e=t.target.closest(X);Y.getOrCreateInstance(e).toggle()})),m(Y);const U="".bs.swipe"",G=`touchstart${U}`,J=`touchmove${U}`,Z=`touchend${U}`,tt=`pointerdown${U}`,et=`pointerup${U}`,it={endCallback:null,leftCallback:null,rightCallback:null},nt={endCallback:""(function|null)"",leftCallback:""(function|null)"",rightCallback:""(function|null)""};class st extends H{constructor(t,e){super(),this._element=t,t&&st.isSupported()&&(this._config=this._getConfig(e),this._deltaX=0,this._supportPointerEvents=Boolean(window.PointerEvent) +,this._initEvents())}static get Default(){return it}static get DefaultType(){return nt}static get NAME(){return""swipe""}dispose(){N.off(this._element,U)}_start(t){this._supportPointerEvents?this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX):this._deltaX=t.touches[0].clientX}_end(t){this._eventIsPointerPenTouch(t)&&(this._deltaX=t.clientX-this._deltaX),this._handleSwipe(),g(this._config.endCallback)}_move(t){this._deltaX=t.touches&&t.touches.length>1?0:t.touches[0].clientX-this._deltaX}_handleSwipe(){const + t=Math.abs(this._deltaX);if(t<=40)return;const e=t/this._deltaX;this._deltaX=0,e&&g(e>0?this._config.rightCallback:this._config.leftCallback)}_initEvents(){this._supportPointerEvents?(N.on(this._element,tt,(t=>this._start(t))),N.on(this._element,et,(t=>this._end(t))),this._element.classList.add(""pointer-event"")):(N.on(this._element,G,(t=>this._start(t))),N.on(this._element,J,(t=>this._move(t))),N.on(this._element,Z,(t=>this._end(t))))}_eventIsPointerPenTouch(t){return this._supportPointerEvents&&(""pen""===t.pointerType||""touch""===t.pointerType)}static + isSupported(){return""ontouchstart""in document.documentElement||navigator.maxTouchPoints>0}}const ot="".bs.carousel"",rt="".data-api"",at=""next"",lt=""prev"",ct=""left"",ht=""right"",dt=`slide${ot}`,ut=`slid${ot}`,ft=`keydown${ot}`,pt=`mouseenter${ot}`,mt=`mouseleave${ot}`,gt=`dragstart${ot}`,_t=`load${ot}${rt}`,bt=`click${ot}${rt}`,vt=""carousel"",yt=""active"",wt="".active"",At="".carousel-item"",Et=wt+At,Tt={ArrowLeft:ht,ArrowRight:ct},Ct={interval:5e3,keyboard:!0,pause:""hover"",ride:!1,touch:!0,wrap:!0},Ot={interval:""(number|boolean)"" +,keyboard:""boolean"",pause:""(string|boolean)"",ride:""(boolean|string)"",touch:""boolean"",wrap:""boolean""};class xt extends W{constructor(t,e){super(t,e),this._interval=null,this._activeElement=null,this._isSliding=!1,this.touchTimeout=null,this._swipeHelper=null,this._indicatorsElement=z.findOne("".carousel-indicators"",this._element),this._addEventListeners(),this._config.ride===vt&&this.cycle()}static get Default(){return Ct}static get DefaultType(){return Ot}static get NAME(){return""carousel""}next(){this._slide(at)}nextWhenVisible(){!document.hidden&&a(this._element)&&this.next()}prev(){this._slide(lt)}pause(){this._isSliding&&s(this._element) +,this._clearInterval()}cycle(){this._clearInterval(),this._updateInterval(),this._interval=setInterval((()=>this.nextWhenVisible()),this._config.interval)}_maybeEnableCycle(){this._config.ride&&(this._isSliding?N.one(this._element,ut,(()=>this.cycle())):this.cycle())}to(t){const e=this._getItems();if(t>e.length-1||t<0)return;if(this._isSliding)return void N.one(this._element,ut,(()=>this.to(t)));const i=this._getItemIndex(this._getActive());if(i===t)return;const n=t>i?at:lt;this._slide(n,e[t])}dispose(){this._swipeHelper&&this._swipeHelper.dispose() +,super.dispose()}_configAfterMerge(t){return t.defaultInterval=t.interval,t}_addEventListeners(){this._config.keyboard&&N.on(this._element,ft,(t=>this._keydown(t))),""hover""===this._config.pause&&(N.on(this._element,pt,(()=>this.pause())),N.on(this._element,mt,(()=>this._maybeEnableCycle()))),this._config.touch&&st.isSupported()&&this._addTouchEventListeners()}_addTouchEventListeners(){for(const t of z.find("".carousel-item img"",this._element))N.on(t,gt,(t=>t.preventDefault()));const t={leftCallback:()=>this._slide(this._directionToOrder(ct)) +,rightCallback:()=>this._slide(this._directionToOrder(ht)),endCallback:()=>{""hover""===this._config.pause&&(this.pause(),this.touchTimeout&&clearTimeout(this.touchTimeout),this.touchTimeout=setTimeout((()=>this._maybeEnableCycle()),500+this._config.interval))}};this._swipeHelper=new st(this._element,t)}_keydown(t){if(/input|textarea/i.test(t.target.tagName))return;const e=Tt[t.key];e&&(t.preventDefault(),this._slide(this._directionToOrder(e)))}_getItemIndex(t){return this._getItems().indexOf(t)}_setActiveIndicatorElement(t){if(!this._indicatorsElement)return +;const e=z.findOne(wt,this._indicatorsElement);e.classList.remove(yt),e.removeAttribute(""aria-current"");const i=z.findOne(`[data-bs-slide-to=""${t}""]`,this._indicatorsElement);i&&(i.classList.add(yt),i.setAttribute(""aria-current"",""true""))}_updateInterval(){const t=this._activeElement||this._getActive();if(!t)return;const e=Number.parseInt(t.getAttribute(""data-bs-interval""),10);this._config.interval=e||this._config.defaultInterval}_slide(t,e=null){if(this._isSliding)return;const i=this._getActive() +,n=t===at,s=e||b(this._getItems(),i,n,this._config.wrap);if(s===i)return;const o=this._getItemIndex(s),r=e=>N.trigger(this._element,e,{relatedTarget:s,direction:this._orderToDirection(t),from:this._getItemIndex(i),to:o});if(r(dt).defaultPrevented)return;if(!i||!s)return;const a=Boolean(this._interval);this.pause(),this._isSliding=!0,this._setActiveIndicatorElement(o),this._activeElement=s;const l=n?""carousel-item-start"":""carousel-item-end"",c=n?""carousel-item-next"":""carousel-item-prev"";s.classList.add(c) +,d(s),i.classList.add(l),s.classList.add(l),this._queueCallback((()=>{s.classList.remove(l,c),s.classList.add(yt),i.classList.remove(yt,c,l),this._isSliding=!1,r(ut)}),i,this._isAnimated()),a&&this.cycle()}_isAnimated(){return this._element.classList.contains(""slide"")}_getActive(){return z.findOne(Et,this._element)}_getItems(){return z.find(At,this._element)}_clearInterval(){this._interval&&(clearInterval(this._interval),this._interval=null)}_directionToOrder(t){return p()?t===ct?lt:at:t===ct?at:lt}_orderToDirection(t){return + p()?t===lt?ct:ht:t===lt?ht:ct}static jQueryInterface(t){return this.each((function(){const e=xt.getOrCreateInstance(this,t);if(""number""!=typeof t){if(""string""==typeof t){if(void 0===e[t]||t.startsWith(""_"")||""constructor""===t)throw new TypeError(`No method named ""${t}""`);e[t]()}}else e.to(t)}))}}N.on(document,bt,""[data-bs-slide], [data-bs-slide-to]"",(function(t){const e=z.getElementFromSelector(this);if(!e||!e.classList.contains(vt))return;t.preventDefault();const i=xt.getOrCreateInstance(e),n=this.getAttribute(""data-bs-slide-to"") +;return n?(i.to(n),void i._maybeEnableCycle()):""next""===F.getDataAttribute(this,""slide"")?(i.next(),void i._maybeEnableCycle()):(i.prev(),void i._maybeEnableCycle())})),N.on(window,_t,(()=>{const t=z.find('[data-bs-ride=""carousel""]');for(const e of t)xt.getOrCreateInstance(e)})),m(xt);const kt="".bs.collapse"",Lt=`show${kt}`,St=`shown${kt}`,Dt=`hide${kt}`,$t=`hidden${kt}`,It=`click${kt}.data-api`,Nt=""show"",Pt=""collapse"",jt=""collapsing"",Mt=`:scope .${Pt} .${Pt}`,Ft='[data-bs-toggle=""collapse""]',Ht={parent:null +,toggle:!0},Wt={parent:""(null|element)"",toggle:""boolean""};class Bt extends W{constructor(t,e){super(t,e),this._isTransitioning=!1,this._triggerArray=[];const i=z.find(Ft);for(const t of i){const e=z.getSelectorFromElement(t),i=z.find(e).filter((t=>t===this._element));null!==e&&i.length&&this._triggerArray.push(t)}this._initializeChildren(),this._config.parent||this._addAriaAndCollapsedClass(this._triggerArray,this._isShown()),this._config.toggle&&this.toggle()}static get Default(){return Ht}static + get DefaultType(){return Wt}static get NAME(){return""collapse""}toggle(){this._isShown()?this.hide():this.show()}show(){if(this._isTransitioning||this._isShown())return;let t=[];if(this._config.parent&&(t=this._getFirstLevelChildren("".collapse.show, .collapse.collapsing"").filter((t=>t!==this._element)).map((t=>Bt.getOrCreateInstance(t,{toggle:!1})))),t.length&&t[0]._isTransitioning)return;if(N.trigger(this._element,Lt).defaultPrevented)return;for(const e of t)e.hide();const e=this._getDimension() +;this._element.classList.remove(Pt),this._element.classList.add(jt),this._element.style[e]=0,this._addAriaAndCollapsedClass(this._triggerArray,!0),this._isTransitioning=!0;const i=`scroll${e[0].toUpperCase()+e.slice(1)}`;this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt),this._element.classList.add(Pt,Nt),this._element.style[e]="""",N.trigger(this._element,St)}),this._element,!0),this._element.style[e]=`${this._element[i]}px`}hide(){if(this._isTransitioning||!this._isShown())return +;if(N.trigger(this._element,Dt).defaultPrevented)return;const t=this._getDimension();this._element.style[t]=`${this._element.getBoundingClientRect()[t]}px`,d(this._element),this._element.classList.add(jt),this._element.classList.remove(Pt,Nt);for(const t of this._triggerArray){const e=z.getElementFromSelector(t);e&&!this._isShown(e)&&this._addAriaAndCollapsedClass([t],!1)}this._isTransitioning=!0,this._element.style[t]="""",this._queueCallback((()=>{this._isTransitioning=!1,this._element.classList.remove(jt) +,this._element.classList.add(Pt),N.trigger(this._element,$t)}),this._element,!0)}_isShown(t=this._element){return t.classList.contains(Nt)}_configAfterMerge(t){return t.toggle=Boolean(t.toggle),t.parent=r(t.parent),t}_getDimension(){return this._element.classList.contains(""collapse-horizontal"")?""width"":""height""}_initializeChildren(){if(!this._config.parent)return;const t=this._getFirstLevelChildren(Ft);for(const e of t){const t=z.getElementFromSelector(e);t&&this._addAriaAndCollapsedClass([e],this._isShown(t))}}_getFirstLevelChildren(t){const + e=z.find(Mt,this._config.parent);return z.find(t,this._config.parent).filter((t=>!e.includes(t)))}_addAriaAndCollapsedClass(t,e){if(t.length)for(const i of t)i.classList.toggle(""collapsed"",!e),i.setAttribute(""aria-expanded"",e)}static jQueryInterface(t){const e={};return""string""==typeof t&&/show|hide/.test(t)&&(e.toggle=!1),this.each((function(){const i=Bt.getOrCreateInstance(this,e);if(""string""==typeof t){if(void 0===i[t])throw new TypeError(`No method named ""${t}""`);i[t]()}}))}}N.on(document,It +,Ft,(function(t){(""A""===t.target.tagName||t.delegateTarget&&""A""===t.delegateTarget.tagName)&&t.preventDefault();for(const t of z.getMultipleElementsFromSelector(this))Bt.getOrCreateInstance(t,{toggle:!1}).toggle()})),m(Bt);var zt=""top"",Rt=""bottom"",qt=""right"",Vt=""left"",Kt=""auto"",Qt=[zt,Rt,qt,Vt],Xt=""start"",Yt=""end"",Ut=""clippingParents"",Gt=""viewport"",Jt=""popper"",Zt=""reference"",te=Qt.reduce((function(t,e){return t.concat([e+""-""+Xt,e+""-""+Yt])}),[]),ee=[].concat(Qt,[Kt]).reduce((function(t,e){return t.concat([e +,e+""-""+Xt,e+""-""+Yt])}),[]),ie=""beforeRead"",ne=""read"",se=""afterRead"",oe=""beforeMain"",re=""main"",ae=""afterMain"",le=""beforeWrite"",ce=""write"",he=""afterWrite"",de=[ie,ne,se,oe,re,ae,le,ce,he];function ue(t){return t?(t.nodeName||"""").toLowerCase():null}function fe(t){if(null==t)return window;if(""[object Window]""!==t.toString()){var e=t.ownerDocument;return e&&e.defaultView||window}return t}function pe(t){return t instanceof fe(t).Element||t instanceof Element}function me(t){return t instanceof fe(t).HTMLElement||t + instanceof HTMLElement}function ge(t){return""undefined""!=typeof ShadowRoot&&(t instanceof fe(t).ShadowRoot||t instanceof ShadowRoot)}const _e={name:""applyStyles"",enabled:!0,phase:""write"",fn:function(t){var e=t.state;Object.keys(e.elements).forEach((function(t){var i=e.styles[t]||{},n=e.attributes[t]||{},s=e.elements[t];me(s)&&ue(s)&&(Object.assign(s.style,i),Object.keys(n).forEach((function(t){var e=n[t];!1===e?s.removeAttribute(t):s.setAttribute(t,!0===e?"""":e)})))}))},effect:function(t){var e=t.state +,i={popper:{position:e.options.strategy,left:""0"",top:""0"",margin:""0""},arrow:{position:""absolute""},reference:{}};return Object.assign(e.elements.popper.style,i.popper),e.styles=i,e.elements.arrow&&Object.assign(e.elements.arrow.style,i.arrow),function(){Object.keys(e.elements).forEach((function(t){var n=e.elements[t],s=e.attributes[t]||{},o=Object.keys(e.styles.hasOwnProperty(t)?e.styles[t]:i[t]).reduce((function(t,e){return t[e]="""",t}),{});me(n)&&ue(n)&&(Object.assign(n.style,o),Object.keys(s).forEach((function(t){n.removeAttribute(t)})))}))}} +,requires:[""computeStyles""]};function be(t){return t.split(""-"")[0]}var ve=Math.max,ye=Math.min,we=Math.round;function Ae(){var t=navigator.userAgentData;return null!=t&&t.brands&&Array.isArray(t.brands)?t.brands.map((function(t){return t.brand+""/""+t.version})).join("" ""):navigator.userAgent}function Ee(){return!/^((?!chrome|android).)*safari/i.test(Ae())}function Te(t,e,i){void 0===e&&(e=!1),void 0===i&&(i=!1);var n=t.getBoundingClientRect(),s=1,o=1;e&&me(t)&&(s=t.offsetWidth>0&&we(n.width)/t.offsetWidth||1 +,o=t.offsetHeight>0&&we(n.height)/t.offsetHeight||1);var r=(pe(t)?fe(t):window).visualViewport,a=!Ee()&&i,l=(n.left+(a&&r?r.offsetLeft:0))/s,c=(n.top+(a&&r?r.offsetTop:0))/o,h=n.width/s,d=n.height/o;return{width:h,height:d,top:c,right:l+h,bottom:c+d,left:l,x:l,y:c}}function Ce(t){var e=Te(t),i=t.offsetWidth,n=t.offsetHeight;return Math.abs(e.width-i)<=1&&(i=e.width),Math.abs(e.height-n)<=1&&(n=e.height),{x:t.offsetLeft,y:t.offsetTop,width:i,height:n}}function Oe(t,e){var i=e.getRootNode&&e.getRootNode() +;if(t.contains(e))return!0;if(i&&ge(i)){var n=e;do{if(n&&t.isSameNode(n))return!0;n=n.parentNode||n.host}while(n)}return!1}function xe(t){return fe(t).getComputedStyle(t)}function ke(t){return[""table"",""td"",""th""].indexOf(ue(t))>=0}function Le(t){return((pe(t)?t.ownerDocument:t.document)||window.document).documentElement}function Se(t){return""html""===ue(t)?t:t.assignedSlot||t.parentNode||(ge(t)?t.host:null)||Le(t)}function De(t){return me(t)&&""fixed""!==xe(t).position?t.offsetParent:null}function $e(t){for(var + e=fe(t),i=De(t);i&&ke(i)&&""static""===xe(i).position;)i=De(i);return i&&(""html""===ue(i)||""body""===ue(i)&&""static""===xe(i).position)?e:i||function(t){var e=/firefox/i.test(Ae());if(/Trident/i.test(Ae())&&me(t)&&""fixed""===xe(t).position)return null;var i=Se(t);for(ge(i)&&(i=i.host);me(i)&&[""html"",""body""].indexOf(ue(i))<0;){var n=xe(i);if(""none""!==n.transform||""none""!==n.perspective||""paint""===n.contain||-1!==[""transform"",""perspective""].indexOf(n.willChange)||e&&""filter""===n.willChange||e&&n.filter&&""none""!==n.filter)return + i;i=i.parentNode}return null}(t)||e}function Ie(t){return[""top"",""bottom""].indexOf(t)>=0?""x"":""y""}function Ne(t,e,i){return ve(t,ye(e,i))}function Pe(t){return Object.assign({},{top:0,right:0,bottom:0,left:0},t)}function je(t,e){return e.reduce((function(e,i){return e[i]=t,e}),{})}const Me={name:""arrow"",enabled:!0,phase:""main"",fn:function(t){var e,i=t.state,n=t.name,s=t.options,o=i.elements.arrow,r=i.modifiersData.popperOffsets,a=be(i.placement),l=Ie(a),c=[Vt,qt].indexOf(a)>=0?""height"":""width"";if(o&&r){var + h=function(t,e){return Pe(""number""!=typeof(t=""function""==typeof t?t(Object.assign({},e.rects,{placement:e.placement})):t)?t:je(t,Qt))}(s.padding,i),d=Ce(o),u=""y""===l?zt:Vt,f=""y""===l?Rt:qt,p=i.rects.reference[c]+i.rects.reference[l]-r[l]-i.rects.popper[c],m=r[l]-i.rects.reference[l],g=$e(o),_=g?""y""===l?g.clientHeight||0:g.clientWidth||0:0,b=p/2-m/2,v=h[u],y=_-d[c]-h[f],w=_/2-d[c]/2+b,A=Ne(v,w,y),E=l;i.modifiersData[n]=((e={})[E]=A,e.centerOffset=A-w,e)}},effect:function(t){var e=t.state,i=t.options.element +,n=void 0===i?""[data-popper-arrow]"":i;null!=n&&(""string""!=typeof n||(n=e.elements.popper.querySelector(n)))&&Oe(e.elements.popper,n)&&(e.elements.arrow=n)},requires:[""popperOffsets""],requiresIfExists:[""preventOverflow""]};function Fe(t){return t.split(""-"")[1]}var He={top:""auto"",right:""auto"",bottom:""auto"",left:""auto""};function We(t){var e,i=t.popper,n=t.popperRect,s=t.placement,o=t.variation,r=t.offsets,a=t.position,l=t.gpuAcceleration,c=t.adaptive,h=t.roundOffsets,d=t.isFixed,u=r.x,f=void 0===u?0:u +,p=r.y,m=void 0===p?0:p,g=""function""==typeof h?h({x:f,y:m}):{x:f,y:m};f=g.x,m=g.y;var _=r.hasOwnProperty(""x""),b=r.hasOwnProperty(""y""),v=Vt,y=zt,w=window;if(c){var A=$e(i),E=""clientHeight"",T=""clientWidth"";A===fe(i)&&""static""!==xe(A=Le(i)).position&&""absolute""===a&&(E=""scrollHeight"",T=""scrollWidth""),(s===zt||(s===Vt||s===qt)&&o===Yt)&&(y=Rt,m-=(d&&A===w&&w.visualViewport?w.visualViewport.height:A[E])-n.height,m*=l?1:-1),s!==Vt&&(s!==zt&&s!==Rt||o!==Yt)||(v=qt,f-=(d&&A===w&&w.visualViewport?w.visualViewport.width:A[T])-n.width +,f*=l?1:-1)}var C,O=Object.assign({position:a},c&&He),x=!0===h?function(t,e){var i=t.x,n=t.y,s=e.devicePixelRatio||1;return{x:we(i*s)/s||0,y:we(n*s)/s||0}}({x:f,y:m},fe(i)):{x:f,y:m};return f=x.x,m=x.y,l?Object.assign({},O,((C={})[y]=b?""0"":"""",C[v]=_?""0"":"""",C.transform=(w.devicePixelRatio||1)<=1?""translate(""+f+""px, ""+m+""px)"":""translate3d(""+f+""px, ""+m+""px, 0)"",C)):Object.assign({},O,((e={})[y]=b?m+""px"":"""",e[v]=_?f+""px"":"""",e.transform="""",e))}const Be={name:""computeStyles"",enabled:!0,phase:""beforeWrite"" +,fn:function(t){var e=t.state,i=t.options,n=i.gpuAcceleration,s=void 0===n||n,o=i.adaptive,r=void 0===o||o,a=i.roundOffsets,l=void 0===a||a,c={placement:be(e.placement),variation:Fe(e.placement),popper:e.elements.popper,popperRect:e.rects.popper,gpuAcceleration:s,isFixed:""fixed""===e.options.strategy};null!=e.modifiersData.popperOffsets&&(e.styles.popper=Object.assign({},e.styles.popper,We(Object.assign({},c,{offsets:e.modifiersData.popperOffsets,position:e.options.strategy,adaptive:r,roundOffsets:l})))) +,null!=e.modifiersData.arrow&&(e.styles.arrow=Object.assign({},e.styles.arrow,We(Object.assign({},c,{offsets:e.modifiersData.arrow,position:""absolute"",adaptive:!1,roundOffsets:l})))),e.attributes.popper=Object.assign({},e.attributes.popper,{""data-popper-placement"":e.placement})},data:{}};var ze={passive:!0};const Re={name:""eventListeners"",enabled:!0,phase:""write"",fn:function(){},effect:function(t){var e=t.state,i=t.instance,n=t.options,s=n.scroll,o=void 0===s||s,r=n.resize,a=void 0===r||r,l=fe(e.elements.popper) +,c=[].concat(e.scrollParents.reference,e.scrollParents.popper);return o&&c.forEach((function(t){t.addEventListener(""scroll"",i.update,ze)})),a&&l.addEventListener(""resize"",i.update,ze),function(){o&&c.forEach((function(t){t.removeEventListener(""scroll"",i.update,ze)})),a&&l.removeEventListener(""resize"",i.update,ze)}},data:{}};var qe={left:""right"",right:""left"",bottom:""top"",top:""bottom""};function Ve(t){return t.replace(/left|right|bottom|top/g,(function(t){return qe[t]}))}var Ke={start:""end"",end:""start""} +;function Qe(t){return t.replace(/start|end/g,(function(t){return Ke[t]}))}function Xe(t){var e=fe(t);return{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Ye(t){return Te(Le(t)).left+Xe(t).scrollLeft}function Ue(t){var e=xe(t),i=e.overflow,n=e.overflowX,s=e.overflowY;return/auto|scroll|overlay|hidden/.test(i+s+n)}function Ge(t){return[""html"",""body"",""#document""].indexOf(ue(t))>=0?t.ownerDocument.body:me(t)&&Ue(t)?t:Ge(Se(t))}function Je(t,e){var i;void 0===e&&(e=[]);var n=Ge(t),s=n===(null==(i=t.ownerDocument)?void + 0:i.body),o=fe(n),r=s?[o].concat(o.visualViewport||[],Ue(n)?n:[]):n,a=e.concat(r);return s?a:a.concat(Je(Se(r)))}function Ze(t){return Object.assign({},t,{left:t.x,top:t.y,right:t.x+t.width,bottom:t.y+t.height})}function ti(t,e,i){return e===Gt?Ze(function(t,e){var i=fe(t),n=Le(t),s=i.visualViewport,o=n.clientWidth,r=n.clientHeight,a=0,l=0;if(s){o=s.width,r=s.height;var c=Ee();(c||!c&&""fixed""===e)&&(a=s.offsetLeft,l=s.offsetTop)}return{width:o,height:r,x:a+Ye(t),y:l}}(t,i)):pe(e)?function(t,e){var + i=Te(t,!1,""fixed""===e);return i.top=i.top+t.clientTop,i.left=i.left+t.clientLeft,i.bottom=i.top+t.clientHeight,i.right=i.left+t.clientWidth,i.width=t.clientWidth,i.height=t.clientHeight,i.x=i.left,i.y=i.top,i}(e,i):Ze(function(t){var e,i=Le(t),n=Xe(t),s=null==(e=t.ownerDocument)?void 0:e.body,o=ve(i.scrollWidth,i.clientWidth,s?s.scrollWidth:0,s?s.clientWidth:0),r=ve(i.scrollHeight,i.clientHeight,s?s.scrollHeight:0,s?s.clientHeight:0),a=-n.scrollLeft+Ye(t),l=-n.scrollTop;return""rtl""===xe(s||i).direction&&(a+=ve(i.clientWidth +,s?s.clientWidth:0)-o),{width:o,height:r,x:a,y:l}}(Le(t)))}function ei(t){var e,i=t.reference,n=t.element,s=t.placement,o=s?be(s):null,r=s?Fe(s):null,a=i.x+i.width/2-n.width/2,l=i.y+i.height/2-n.height/2;switch(o){case zt:e={x:a,y:i.y-n.height};break;case Rt:e={x:a,y:i.y+i.height};break;case qt:e={x:i.x+i.width,y:l};break;case Vt:e={x:i.x-n.width,y:l};break;default:e={x:i.x,y:i.y}}var c=o?Ie(o):null;if(null!=c){var h=""y""===c?""height"":""width"";switch(r){case Xt:e[c]=e[c]-(i[h]/2-n[h]/2);break;case + Yt:e[c]=e[c]+(i[h]/2-n[h]/2)}}return e}function ii(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=void 0===n?t.placement:n,o=i.strategy,r=void 0===o?t.strategy:o,a=i.boundary,l=void 0===a?Ut:a,c=i.rootBoundary,h=void 0===c?Gt:c,d=i.elementContext,u=void 0===d?Jt:d,f=i.altBoundary,p=void 0!==f&&f,m=i.padding,g=void 0===m?0:m,_=Pe(""number""!=typeof g?g:je(g,Qt)),b=u===Jt?Zt:Jt,v=t.rects.popper,y=t.elements[p?b:u],w=function(t,e,i,n){var s=""clippingParents""===e?function(t){var e=Je(Se(t)),i=[""absolute"" +,""fixed""].indexOf(xe(t).position)>=0&&me(t)?$e(t):t;return pe(i)?e.filter((function(t){return pe(t)&&Oe(t,i)&&""body""!==ue(t)})):[]}(t):[].concat(e),o=[].concat(s,[i]),r=o[0],a=o.reduce((function(e,i){var s=ti(t,i,n);return e.top=ve(s.top,e.top),e.right=ye(s.right,e.right),e.bottom=ye(s.bottom,e.bottom),e.left=ve(s.left,e.left),e}),ti(t,r,n));return a.width=a.right-a.left,a.height=a.bottom-a.top,a.x=a.left,a.y=a.top,a}(pe(y)?y:y.contextElement||Le(t.elements.popper),l,h,r),A=Te(t.elements.reference) +,E=ei({reference:A,element:v,strategy:""absolute"",placement:s}),T=Ze(Object.assign({},v,E)),C=u===Jt?T:A,O={top:w.top-C.top+_.top,bottom:C.bottom-w.bottom+_.bottom,left:w.left-C.left+_.left,right:C.right-w.right+_.right},x=t.modifiersData.offset;if(u===Jt&&x){var k=x[s];Object.keys(O).forEach((function(t){var e=[qt,Rt].indexOf(t)>=0?1:-1,i=[zt,Rt].indexOf(t)>=0?""y"":""x"";O[t]+=k[i]*e}))}return O}function ni(t,e){void 0===e&&(e={});var i=e,n=i.placement,s=i.boundary,o=i.rootBoundary,r=i.padding,a=i.flipVariations +,l=i.allowedAutoPlacements,c=void 0===l?ee:l,h=Fe(n),d=h?a?te:te.filter((function(t){return Fe(t)===h})):Qt,u=d.filter((function(t){return c.indexOf(t)>=0}));0===u.length&&(u=d);var f=u.reduce((function(e,i){return e[i]=ii(t,{placement:i,boundary:s,rootBoundary:o,padding:r})[be(i)],e}),{});return Object.keys(f).sort((function(t,e){return f[t]-f[e]}))}const si={name:""flip"",enabled:!0,phase:""main"",fn:function(t){var e=t.state,i=t.options,n=t.name;if(!e.modifiersData[n]._skip){for(var s=i.mainAxis,o=void + 0===s||s,r=i.altAxis,a=void 0===r||r,l=i.fallbackPlacements,c=i.padding,h=i.boundary,d=i.rootBoundary,u=i.altBoundary,f=i.flipVariations,p=void 0===f||f,m=i.allowedAutoPlacements,g=e.options.placement,_=be(g),b=l||(_!==g&&p?function(t){if(be(t)===Kt)return[];var e=Ve(t);return[Qe(t),e,Qe(e)]}(g):[Ve(g)]),v=[g].concat(b).reduce((function(t,i){return t.concat(be(i)===Kt?ni(e,{placement:i,boundary:h,rootBoundary:d,padding:c,flipVariations:p,allowedAutoPlacements:m}):i)}),[]),y=e.rects.reference,w=e.rects.popper +,A=new Map,E=!0,T=v[0],C=0;C=0,S=L?""width"":""height"",D=ii(e,{placement:O,boundary:h,rootBoundary:d,altBoundary:u,padding:c}),$=L?k?qt:Vt:k?Rt:zt;y[S]>w[S]&&($=Ve($));var I=Ve($),N=[];if(o&&N.push(D[x]<=0),a&&N.push(D[$]<=0,D[I]<=0),N.every((function(t){return t}))){T=O,E=!1;break}A.set(O,N)}if(E)for(var P=function(t){var e=v.find((function(e){var i=A.get(e);if(i)return i.slice(0,t).every((function(t){return t}))}));if(e)return T=e +,""break""},j=p?3:1;j>0&&""break""!==P(j);j--);e.placement!==T&&(e.modifiersData[n]._skip=!0,e.placement=T,e.reset=!0)}},requiresIfExists:[""offset""],data:{_skip:!1}};function oi(t,e,i){return void 0===i&&(i={x:0,y:0}),{top:t.top-e.height-i.y,right:t.right-e.width+i.x,bottom:t.bottom-e.height+i.y,left:t.left-e.width-i.x}}function ri(t){return[zt,qt,Rt,Vt].some((function(e){return t[e]>=0}))}const ai={name:""hide"",enabled:!0,phase:""main"",requiresIfExists:[""preventOverflow""],fn:function(t){var e=t.state +,i=t.name,n=e.rects.reference,s=e.rects.popper,o=e.modifiersData.preventOverflow,r=ii(e,{elementContext:""reference""}),a=ii(e,{altBoundary:!0}),l=oi(r,n),c=oi(a,s,o),h=ri(l),d=ri(c);e.modifiersData[i]={referenceClippingOffsets:l,popperEscapeOffsets:c,isReferenceHidden:h,hasPopperEscaped:d},e.attributes.popper=Object.assign({},e.attributes.popper,{""data-popper-reference-hidden"":h,""data-popper-escaped"":d})}},li={name:""offset"",enabled:!0,phase:""main"",requires:[""popperOffsets""],fn:function(t){var e=t.state +,i=t.options,n=t.name,s=i.offset,o=void 0===s?[0,0]:s,r=ee.reduce((function(t,i){return t[i]=function(t,e,i){var n=be(t),s=[Vt,zt].indexOf(n)>=0?-1:1,o=""function""==typeof i?i(Object.assign({},e,{placement:t})):i,r=o[0],a=o[1];return r=r||0,a=(a||0)*s,[Vt,qt].indexOf(n)>=0?{x:a,y:r}:{x:r,y:a}}(i,e.rects,o),t}),{}),a=r[e.placement],l=a.x,c=a.y;null!=e.modifiersData.popperOffsets&&(e.modifiersData.popperOffsets.x+=l,e.modifiersData.popperOffsets.y+=c),e.modifiersData[n]=r}},ci={name:""popperOffsets"" +,enabled:!0,phase:""read"",fn:function(t){var e=t.state,i=t.name;e.modifiersData[i]=ei({reference:e.rects.reference,element:e.rects.popper,strategy:""absolute"",placement:e.placement})},data:{}},hi={name:""preventOverflow"",enabled:!0,phase:""main"",fn:function(t){var e=t.state,i=t.options,n=t.name,s=i.mainAxis,o=void 0===s||s,r=i.altAxis,a=void 0!==r&&r,l=i.boundary,c=i.rootBoundary,h=i.altBoundary,d=i.padding,u=i.tether,f=void 0===u||u,p=i.tetherOffset,m=void 0===p?0:p,g=ii(e,{boundary:l,rootBoundary:c +,padding:d,altBoundary:h}),_=be(e.placement),b=Fe(e.placement),v=!b,y=Ie(_),w=""x""===y?""y"":""x"",A=e.modifiersData.popperOffsets,E=e.rects.reference,T=e.rects.popper,C=""function""==typeof m?m(Object.assign({},e.rects,{placement:e.placement})):m,O=""number""==typeof C?{mainAxis:C,altAxis:C}:Object.assign({mainAxis:0,altAxis:0},C),x=e.modifiersData.offset?e.modifiersData.offset[e.placement]:null,k={x:0,y:0};if(A){if(o){var L,S=""y""===y?zt:Vt,D=""y""===y?Rt:qt,$=""y""===y?""height"":""width"",I=A[y],N=I+g[S],P=I-g[D] +,j=f?-T[$]/2:0,M=b===Xt?E[$]:T[$],F=b===Xt?-T[$]:-E[$],H=e.elements.arrow,W=f&&H?Ce(H):{width:0,height:0},B=e.modifiersData[""arrow#persistent""]?e.modifiersData[""arrow#persistent""].padding:{top:0,right:0,bottom:0,left:0},z=B[S],R=B[D],q=Ne(0,E[$],W[$]),V=v?E[$]/2-j-q-z-O.mainAxis:M-q-z-O.mainAxis,K=v?-E[$]/2+j+q+R+O.mainAxis:F+q+R+O.mainAxis,Q=e.elements.arrow&&$e(e.elements.arrow),X=Q?""y""===y?Q.clientTop||0:Q.clientLeft||0:0,Y=null!=(L=null==x?void 0:x[y])?L:0,U=I+K-Y,G=Ne(f?ye(N,I+V-Y-X):N,I,f?ve(P +,U):P);A[y]=G,k[y]=G-I}if(a){var J,Z=""x""===y?zt:Vt,tt=""x""===y?Rt:qt,et=A[w],it=""y""===w?""height"":""width"",nt=et+g[Z],st=et-g[tt],ot=-1!==[zt,Vt].indexOf(_),rt=null!=(J=null==x?void 0:x[w])?J:0,at=ot?nt:et-E[it]-T[it]-rt+O.altAxis,lt=ot?et+E[it]+T[it]-rt-O.altAxis:st,ct=f&&ot?function(t,e,i){var n=Ne(t,e,i);return n>i?i:n}(at,et,lt):Ne(f?at:nt,et,f?lt:st);A[w]=ct,k[w]=ct-et}e.modifiersData[n]=k}},requiresIfExists:[""offset""]};function di(t,e,i){void 0===i&&(i=!1);var n,s,o=me(e),r=me(e)&&function(t){var + e=t.getBoundingClientRect(),i=we(e.width)/t.offsetWidth||1,n=we(e.height)/t.offsetHeight||1;return 1!==i||1!==n}(e),a=Le(e),l=Te(t,r,i),c={scrollLeft:0,scrollTop:0},h={x:0,y:0};return(o||!o&&!i)&&((""body""!==ue(e)||Ue(a))&&(c=(n=e)!==fe(n)&&me(n)?{scrollLeft:(s=n).scrollLeft,scrollTop:s.scrollTop}:Xe(n)),me(e)?((h=Te(e,!0)).x+=e.clientLeft,h.y+=e.clientTop):a&&(h.x=Ye(a))),{x:l.left+c.scrollLeft-h.x,y:l.top+c.scrollTop-h.y,width:l.width,height:l.height}}function ui(t){var e=new Map,i=new Set,n=[] +;function s(t){i.add(t.name),[].concat(t.requires||[],t.requiresIfExists||[]).forEach((function(t){if(!i.has(t)){var n=e.get(t);n&&s(n)}})),n.push(t)}return t.forEach((function(t){e.set(t.name,t)})),t.forEach((function(t){i.has(t.name)||s(t)})),n}var fi={placement:""bottom"",modifiers:[],strategy:""absolute""};function pi(){for(var t=arguments.length,e=new Array(t),i=0;iNumber.parseInt(t,10))):""function""==typeof t?e=>t(e,this._element):t}_getPopperConfig(){const t={placement:this._getPlacement(),modifiers:[{name:""preventOverflow"",options:{boundary:this._config.boundary}},{name:""offset"",options:{offset:this._getOffset()}}]};return(this._inNavbar||""static""===this._config.display)&&(F.setDataAttribute(this._menu +,""popper"",""static""),t.modifiers=[{name:""applyStyles"",enabled:!1}]),{...t,...g(this._config.popperConfig,[t])}}_selectMenuItem({key:t,target:e}){const i=z.find("".dropdown-menu .dropdown-item:not(.disabled):not(:disabled)"",this._menu).filter((t=>a(t)));i.length&&b(i,e,t===Ti,!i.includes(e)).focus()}static jQueryInterface(t){return this.each((function(){const e=qi.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t])throw new TypeError(`No method named ""${t}""`);e[t]()}}))}static clearMenus(t){if(2===t.button||""keyup""===t.type&&""Tab""!==t.key)return +;const e=z.find(Ni);for(const i of e){const e=qi.getInstance(i);if(!e||!1===e._config.autoClose)continue;const n=t.composedPath(),s=n.includes(e._menu);if(n.includes(e._element)||""inside""===e._config.autoClose&&!s||""outside""===e._config.autoClose&&s)continue;if(e._menu.contains(t.target)&&(""keyup""===t.type&&""Tab""===t.key||/input|select|option|textarea|form/i.test(t.target.tagName)))continue;const o={relatedTarget:e._element};""click""===t.type&&(o.clickEvent=t),e._completeHide(o)}}static dataApiKeydownHandler(t){const + e=/input|textarea/i.test(t.target.tagName),i=""Escape""===t.key,n=[Ei,Ti].includes(t.key);if(!n&&!i)return;if(e&&!i)return;t.preventDefault();const s=this.matches(Ii)?this:z.prev(this,Ii)[0]||z.next(this,Ii)[0]||z.findOne(Ii,t.delegateTarget.parentNode),o=qi.getOrCreateInstance(s);if(n)return t.stopPropagation(),o.show(),void o._selectMenuItem(t);o._isShown()&&(t.stopPropagation(),o.hide(),s.focus())}}N.on(document,Si,Ii,qi.dataApiKeydownHandler),N.on(document,Si,Pi,qi.dataApiKeydownHandler),N.on(document +,Li,qi.clearMenus),N.on(document,Di,qi.clearMenus),N.on(document,Li,Ii,(function(t){t.preventDefault(),qi.getOrCreateInstance(this).toggle()})),m(qi);const Vi=""backdrop"",Ki=""show"",Qi=`mousedown.bs.${Vi}`,Xi={className:""modal-backdrop"",clickCallback:null,isAnimated:!1,isVisible:!0,rootElement:""body""},Yi={className:""string"",clickCallback:""(function|null)"",isAnimated:""boolean"",isVisible:""boolean"",rootElement:""(element|string)""};class Ui extends H{constructor(t){super(),this._config=this._getConfig(t) +,this._isAppended=!1,this._element=null}static get Default(){return Xi}static get DefaultType(){return Yi}static get NAME(){return Vi}show(t){if(!this._config.isVisible)return void g(t);this._append();const e=this._getElement();this._config.isAnimated&&d(e),e.classList.add(Ki),this._emulateAnimation((()=>{g(t)}))}hide(t){this._config.isVisible?(this._getElement().classList.remove(Ki),this._emulateAnimation((()=>{this.dispose(),g(t)}))):g(t)}dispose(){this._isAppended&&(N.off(this._element,Qi),this._element.remove() +,this._isAppended=!1)}_getElement(){if(!this._element){const t=document.createElement(""div"");t.className=this._config.className,this._config.isAnimated&&t.classList.add(""fade""),this._element=t}return this._element}_configAfterMerge(t){return t.rootElement=r(t.rootElement),t}_append(){if(this._isAppended)return;const t=this._getElement();this._config.rootElement.append(t),N.on(t,Qi,(()=>{g(this._config.clickCallback)})),this._isAppended=!0}_emulateAnimation(t){_(t,this._getElement(),this._config.isAnimated)}}const + Gi="".bs.focustrap"",Ji=`focusin${Gi}`,Zi=`keydown.tab${Gi}`,tn=""backward"",en={autofocus:!0,trapElement:null},nn={autofocus:""boolean"",trapElement:""element""};class sn extends H{constructor(t){super(),this._config=this._getConfig(t),this._isActive=!1,this._lastTabNavDirection=null}static get Default(){return en}static get DefaultType(){return nn}static get NAME(){return""focustrap""}activate(){this._isActive||(this._config.autofocus&&this._config.trapElement.focus(),N.off(document,Gi),N.on(document,Ji +,(t=>this._handleFocusin(t))),N.on(document,Zi,(t=>this._handleKeydown(t))),this._isActive=!0)}deactivate(){this._isActive&&(this._isActive=!1,N.off(document,Gi))}_handleFocusin(t){const{trapElement:e}=this._config;if(t.target===document||t.target===e||e.contains(t.target))return;const i=z.focusableChildren(e);0===i.length?e.focus():this._lastTabNavDirection===tn?i[i.length-1].focus():i[0].focus()}_handleKeydown(t){""Tab""===t.key&&(this._lastTabNavDirection=t.shiftKey?tn:""forward"")}}const +on="".fixed-top, .fixed-bottom, .is-fixed, .sticky-top"",rn="".sticky-top"",an=""padding-right"",ln=""margin-right"";class cn{constructor(){this._element=document.body}getWidth(){const t=document.documentElement.clientWidth;return Math.abs(window.innerWidth-t)}hide(){const t=this.getWidth();this._disableOverFlow(),this._setElementAttributes(this._element,an,(e=>e+t)),this._setElementAttributes(on,an,(e=>e+t)),this._setElementAttributes(rn,ln,(e=>e-t))}reset(){this._resetElementAttributes(this._element,""overflow""),this._resetElementAttributes(this._element +,an),this._resetElementAttributes(on,an),this._resetElementAttributes(rn,ln)}isOverflowing(){return this.getWidth()>0}_disableOverFlow(){this._saveInitialAttribute(this._element,""overflow""),this._element.style.overflow=""hidden""}_setElementAttributes(t,e,i){const n=this.getWidth();this._applyManipulationCallback(t,(t=>{if(t!==this._element&&window.innerWidth>t.clientWidth+n)return;this._saveInitialAttribute(t,e);const s=window.getComputedStyle(t).getPropertyValue(e);t.style.setProperty(e,`${i(Number.parseFloat(s))}px`)}))}_saveInitialAttribute(t +,e){const i=t.style.getPropertyValue(e);i&&F.setDataAttribute(t,e,i)}_resetElementAttributes(t,e){this._applyManipulationCallback(t,(t=>{const i=F.getDataAttribute(t,e);null!==i?(F.removeDataAttribute(t,e),t.style.setProperty(e,i)):t.style.removeProperty(e)}))}_applyManipulationCallback(t,e){if(o(t))e(t);else for(const i of z.find(t,this._element))e(i)}}const hn="".bs.modal"",dn=`hide${hn}`,un=`hidePrevented${hn}`,fn=`hidden${hn}`,pn=`show${hn}`,mn=`shown${hn}`,gn=`resize${hn}`,_n=`click.dismiss${hn}` +,bn=`mousedown.dismiss${hn}`,vn=`keydown.dismiss${hn}`,yn=`click${hn}.data-api`,wn=""modal-open"",An=""show"",En=""modal-static"",Tn={backdrop:!0,focus:!0,keyboard:!0},Cn={backdrop:""(boolean|string)"",focus:""boolean"",keyboard:""boolean""};class On extends W{constructor(t,e){super(t,e),this._dialog=z.findOne("".modal-dialog"",this._element),this._backdrop=this._initializeBackDrop(),this._focustrap=this._initializeFocusTrap(),this._isShown=!1,this._isTransitioning=!1,this._scrollBar=new cn,this._addEventListeners()}static + get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){return""modal""}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||this._isTransitioning||N.trigger(this._element,pn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._isTransitioning=!0,this._scrollBar.hide(),document.body.classList.add(wn),this._adjustDialog(),this._backdrop.show((()=>this._showElement(t))))}hide(){this._isShown&&!this._isTransitioning&&(N.trigger(this._element,dn).defaultPrevented||(this._isShown=!1 +,this._isTransitioning=!0,this._focustrap.deactivate(),this._element.classList.remove(An),this._queueCallback((()=>this._hideModal()),this._element,this._isAnimated())))}dispose(){N.off(window,hn),N.off(this._dialog,hn),this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}handleUpdate(){this._adjustDialog()}_initializeBackDrop(){return new Ui({isVisible:Boolean(this._config.backdrop),isAnimated:this._isAnimated()})}_initializeFocusTrap(){return new sn({trapElement:this._element})}_showElement(t){document.body.contains(this._element)||document.body.append(this._element) +,this._element.style.display=""block"",this._element.removeAttribute(""aria-hidden""),this._element.setAttribute(""aria-modal"",!0),this._element.setAttribute(""role"",""dialog""),this._element.scrollTop=0;const e=z.findOne("".modal-body"",this._dialog);e&&(e.scrollTop=0),d(this._element),this._element.classList.add(An),this._queueCallback((()=>{this._config.focus&&this._focustrap.activate(),this._isTransitioning=!1,N.trigger(this._element,mn,{relatedTarget:t})}),this._dialog,this._isAnimated())}_addEventListeners(){N.on(this._element +,vn,(t=>{""Escape""===t.key&&(this._config.keyboard?this.hide():this._triggerBackdropTransition())})),N.on(window,gn,(()=>{this._isShown&&!this._isTransitioning&&this._adjustDialog()})),N.on(this._element,bn,(t=>{N.one(this._element,_n,(e=>{this._element===t.target&&this._element===e.target&&(""static""!==this._config.backdrop?this._config.backdrop&&this.hide():this._triggerBackdropTransition())}))}))}_hideModal(){this._element.style.display=""none"",this._element.setAttribute(""aria-hidden"",!0),this._element.removeAttribute(""aria-modal"") +,this._element.removeAttribute(""role""),this._isTransitioning=!1,this._backdrop.hide((()=>{document.body.classList.remove(wn),this._resetAdjustments(),this._scrollBar.reset(),N.trigger(this._element,fn)}))}_isAnimated(){return this._element.classList.contains(""fade"")}_triggerBackdropTransition(){if(N.trigger(this._element,un).defaultPrevented)return;const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._element.style.overflowY;""hidden""===e||this._element.classList.contains(En)||(t||(this._element.style.overflowY=""hidden"") +,this._element.classList.add(En),this._queueCallback((()=>{this._element.classList.remove(En),this._queueCallback((()=>{this._element.style.overflowY=e}),this._dialog)}),this._dialog),this._element.focus())}_adjustDialog(){const t=this._element.scrollHeight>document.documentElement.clientHeight,e=this._scrollBar.getWidth(),i=e>0;if(i&&!t){const t=p()?""paddingLeft"":""paddingRight"";this._element.style[t]=`${e}px`}if(!i&&t){const t=p()?""paddingRight"":""paddingLeft"";this._element.style[t]=`${e}px`}}_resetAdjustments(){this._element.style.paddingLeft="""" +,this._element.style.paddingRight=""""}static jQueryInterface(t,e){return this.each((function(){const i=On.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===i[t])throw new TypeError(`No method named ""${t}""`);i[t](e)}}))}}N.on(document,yn,'[data-bs-toggle=""modal""]',(function(t){const e=z.getElementFromSelector(this);[""A"",""AREA""].includes(this.tagName)&&t.preventDefault(),N.one(e,pn,(t=>{t.defaultPrevented||N.one(e,fn,(()=>{a(this)&&this.focus()}))}));const i=z.findOne("".modal.show"");i&&On.getInstance(i).hide() +,On.getOrCreateInstance(e).toggle(this)})),R(On),m(On);const xn="".bs.offcanvas"",kn="".data-api"",Ln=`load${xn}${kn}`,Sn=""show"",Dn=""showing"",$n=""hiding"",In="".offcanvas.show"",Nn=`show${xn}`,Pn=`shown${xn}`,jn=`hide${xn}`,Mn=`hidePrevented${xn}`,Fn=`hidden${xn}`,Hn=`resize${xn}`,Wn=`click${xn}${kn}`,Bn=`keydown.dismiss${xn}`,zn={backdrop:!0,keyboard:!0,scroll:!1},Rn={backdrop:""(boolean|string)"",keyboard:""boolean"",scroll:""boolean""};class qn extends W{constructor(t,e){super(t,e),this._isShown=!1,this._backdrop=this._initializeBackDrop() +,this._focustrap=this._initializeFocusTrap(),this._addEventListeners()}static get Default(){return zn}static get DefaultType(){return Rn}static get NAME(){return""offcanvas""}toggle(t){return this._isShown?this.hide():this.show(t)}show(t){this._isShown||N.trigger(this._element,Nn,{relatedTarget:t}).defaultPrevented||(this._isShown=!0,this._backdrop.show(),this._config.scroll||(new cn).hide(),this._element.setAttribute(""aria-modal"",!0),this._element.setAttribute(""role"",""dialog""),this._element.classList.add(Dn) +,this._queueCallback((()=>{this._config.scroll&&!this._config.backdrop||this._focustrap.activate(),this._element.classList.add(Sn),this._element.classList.remove(Dn),N.trigger(this._element,Pn,{relatedTarget:t})}),this._element,!0))}hide(){this._isShown&&(N.trigger(this._element,jn).defaultPrevented||(this._focustrap.deactivate(),this._element.blur(),this._isShown=!1,this._element.classList.add($n),this._backdrop.hide(),this._queueCallback((()=>{this._element.classList.remove(Sn,$n),this._element.removeAttribute(""aria-modal"") +,this._element.removeAttribute(""role""),this._config.scroll||(new cn).reset(),N.trigger(this._element,Fn)}),this._element,!0)))}dispose(){this._backdrop.dispose(),this._focustrap.deactivate(),super.dispose()}_initializeBackDrop(){const t=Boolean(this._config.backdrop);return new Ui({className:""offcanvas-backdrop"",isVisible:t,isAnimated:!0,rootElement:this._element.parentNode,clickCallback:t?()=>{""static""!==this._config.backdrop?this.hide():N.trigger(this._element,Mn)}:null})}_initializeFocusTrap(){return + new sn({trapElement:this._element})}_addEventListeners(){N.on(this._element,Bn,(t=>{""Escape""===t.key&&(this._config.keyboard?this.hide():N.trigger(this._element,Mn))}))}static jQueryInterface(t){return this.each((function(){const e=qn.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t]||t.startsWith(""_"")||""constructor""===t)throw new TypeError(`No method named ""${t}""`);e[t](this)}}))}}N.on(document,Wn,'[data-bs-toggle=""offcanvas""]',(function(t){const e=z.getElementFromSelector(this) +;if([""A"",""AREA""].includes(this.tagName)&&t.preventDefault(),l(this))return;N.one(e,Fn,(()=>{a(this)&&this.focus()}));const i=z.findOne(In);i&&i!==e&&qn.getInstance(i).hide(),qn.getOrCreateInstance(e).toggle(this)})),N.on(window,Ln,(()=>{for(const t of z.find(In))qn.getOrCreateInstance(t).show()})),N.on(window,Hn,(()=>{for(const t of z.find(""[aria-modal][class*=show][class*=offcanvas-]""))""fixed""!==getComputedStyle(t).position&&qn.getOrCreateInstance(t).hide()})),R(qn),m(qn);const Vn={""*"":[""class"" +,""dir"",""id"",""lang"",""role"",/^aria-[\w-]*$/i],a:[""target"",""href"",""title"",""rel""],area:[],b:[],br:[],col:[],code:[],dd:[],div:[],dl:[],dt:[],em:[],hr:[],h1:[],h2:[],h3:[],h4:[],h5:[],h6:[],i:[],img:[""src"",""srcset"",""alt"",""title"",""width"",""height""],li:[],ol:[],p:[],pre:[],s:[],small:[],span:[],sub:[],sup:[],strong:[],u:[],ul:[]},Kn=new Set([""background"",""cite"",""href"",""itemtype"",""longdesc"",""poster"",""src"",""xlink:href""]),Qn=/^(?!javascript:)(?:[a-z0-9+.-]+:|[^&:/?#]*(?:[/?#]|$))/i,Xn=(t,e)=>{const i=t.nodeName.toLowerCase() +;return e.includes(i)?!Kn.has(i)||Boolean(Qn.test(t.nodeValue)):e.filter((t=>t instanceof RegExp)).some((t=>t.test(i)))},Yn={allowList:Vn,content:{},extraClass:"""",html:!1,sanitize:!0,sanitizeFn:null,template:""
""},Un={allowList:""object"",content:""object"",extraClass:""(string|function)"",html:""boolean"",sanitize:""boolean"",sanitizeFn:""(null|function)"",template:""string""},Gn={entry:""(string|element|function|null)"",selector:""(string|element)""};class Jn extends H{constructor(t){super(),this._config=this._getConfig(t)}static + get Default(){return Yn}static get DefaultType(){return Un}static get NAME(){return""TemplateFactory""}getContent(){return Object.values(this._config.content).map((t=>this._resolvePossibleFunction(t))).filter(Boolean)}hasContent(){return this.getContent().length>0}changeContent(t){return this._checkContent(t),this._config.content={...this._config.content,...t},this}toHtml(){const t=document.createElement(""div"");t.innerHTML=this._maybeSanitize(this._config.template);for(const[e,i]of Object.entries(this._config.content))this._setContent(t +,i,e);const e=t.children[0],i=this._resolvePossibleFunction(this._config.extraClass);return i&&e.classList.add(...i.split("" "")),e}_typeCheckConfig(t){super._typeCheckConfig(t),this._checkContent(t.content)}_checkContent(t){for(const[e,i]of Object.entries(t))super._typeCheckConfig({selector:e,entry:i},Gn)}_setContent(t,e,i){const n=z.findOne(i,t);n&&((e=this._resolvePossibleFunction(e))?o(e)?this._putElementInTemplate(r(e),n):this._config.html?n.innerHTML=this._maybeSanitize(e):n.textContent=e:n.remove())}_maybeSanitize(t){return + this._config.sanitize?function(t,e,i){if(!t.length)return t;if(i&&""function""==typeof i)return i(t);const n=(new window.DOMParser).parseFromString(t,""text/html""),s=[].concat(...n.body.querySelectorAll(""*""));for(const t of s){const i=t.nodeName.toLowerCase();if(!Object.keys(e).includes(i)){t.remove();continue}const n=[].concat(...t.attributes),s=[].concat(e[""*""]||[],e[i]||[]);for(const e of n)Xn(e,s)||t.removeAttribute(e.nodeName)}return n.body.innerHTML}(t,this._config.allowList,this._config.sanitizeFn):t}_resolvePossibleFunction(t){return + g(t,[this])}_putElementInTemplate(t,e){if(this._config.html)return e.innerHTML="""",void e.append(t);e.textContent=t.textContent}}const Zn=new Set([""sanitize"",""allowList"",""sanitizeFn""]),ts=""fade"",es=""show"",is="".modal"",ns=""hide.bs.modal"",ss=""hover"",os=""focus"",rs={AUTO:""auto"",TOP:""top"",RIGHT:p()?""left"":""right"",BOTTOM:""bottom"",LEFT:p()?""right"":""left""},as={allowList:Vn,animation:!0,boundary:""clippingParents"",container:!1,customClass:"""",delay:0,fallbackPlacements:[""top"",""right"",""bottom"",""left""],html:!1 +,offset:[0,6],placement:""top"",popperConfig:null,sanitize:!0,sanitizeFn:null,selector:!1,template:'
',title:"""",trigger:""hover focus""},ls={allowList:""object"",animation:""boolean"",boundary:""(string|element)"",container:""(string|element|boolean)"",customClass:""(string|function)"",delay:""(number|object)"",fallbackPlacements:""array"",html:""boolean"",offset:""(array|string|function)"",placement:""(string|function)"" +,popperConfig:""(null|object|function)"",sanitize:""boolean"",sanitizeFn:""(null|function)"",selector:""(string|boolean)"",template:""string"",title:""(string|element|function)"",trigger:""string""};class cs extends W{constructor(t,e){if(void 0===vi)throw new TypeError(""Bootstrap's tooltips require Popper (https://popper.js.org)"");super(t,e),this._isEnabled=!0,this._timeout=0,this._isHovered=null,this._activeTrigger={},this._popper=null,this._templateFactory=null,this._newContent=null,this.tip=null,this._setListeners() +,this._config.selector||this._fixTitle()}static get Default(){return as}static get DefaultType(){return ls}static get NAME(){return""tooltip""}enable(){this._isEnabled=!0}disable(){this._isEnabled=!1}toggleEnabled(){this._isEnabled=!this._isEnabled}toggle(){this._isEnabled&&(this._activeTrigger.click=!this._activeTrigger.click,this._isShown()?this._leave():this._enter())}dispose(){clearTimeout(this._timeout),N.off(this._element.closest(is),ns,this._hideModalHandler),this._element.getAttribute(""data-bs-original-title"")&&this._element.setAttribute(""title"" +,this._element.getAttribute(""data-bs-original-title"")),this._disposePopper(),super.dispose()}show(){if(""none""===this._element.style.display)throw new Error(""Please use show on visible elements"");if(!this._isWithContent()||!this._isEnabled)return;const t=N.trigger(this._element,this.constructor.eventName(""show"")),e=(c(this._element)||this._element.ownerDocument.documentElement).contains(this._element);if(t.defaultPrevented||!e)return;this._disposePopper();const i=this._getTipElement();this._element.setAttribute(""aria-describedby"" +,i.getAttribute(""id""));const{container:n}=this._config;if(this._element.ownerDocument.documentElement.contains(this.tip)||(n.append(i),N.trigger(this._element,this.constructor.eventName(""inserted""))),this._popper=this._createPopper(i),i.classList.add(es),""ontouchstart""in document.documentElement)for(const t of[].concat(...document.body.children))N.on(t,""mouseover"",h);this._queueCallback((()=>{N.trigger(this._element,this.constructor.eventName(""shown"")),!1===this._isHovered&&this._leave(),this._isHovered=!1}) +,this.tip,this._isAnimated())}hide(){if(this._isShown()&&!N.trigger(this._element,this.constructor.eventName(""hide"")).defaultPrevented){if(this._getTipElement().classList.remove(es),""ontouchstart""in document.documentElement)for(const t of[].concat(...document.body.children))N.off(t,""mouseover"",h);this._activeTrigger.click=!1,this._activeTrigger[os]=!1,this._activeTrigger[ss]=!1,this._isHovered=null,this._queueCallback((()=>{this._isWithActiveTrigger()||(this._isHovered||this._disposePopper(),this._element.removeAttribute(""aria-describedby"") +,N.trigger(this._element,this.constructor.eventName(""hidden"")))}),this.tip,this._isAnimated())}}update(){this._popper&&this._popper.update()}_isWithContent(){return Boolean(this._getTitle())}_getTipElement(){return this.tip||(this.tip=this._createTipElement(this._newContent||this._getContentForTemplate())),this.tip}_createTipElement(t){const e=this._getTemplateFactory(t).toHtml();if(!e)return null;e.classList.remove(ts,es),e.classList.add(`bs-${this.constructor.NAME}-auto`);const i=(t=>{do{t+=Math.floor(1e6*Math.random())}while(document.getElementById(t)) +;return t})(this.constructor.NAME).toString();return e.setAttribute(""id"",i),this._isAnimated()&&e.classList.add(ts),e}setContent(t){this._newContent=t,this._isShown()&&(this._disposePopper(),this.show())}_getTemplateFactory(t){return this._templateFactory?this._templateFactory.changeContent(t):this._templateFactory=new Jn({...this._config,content:t,extraClass:this._resolvePossibleFunction(this._config.customClass)}),this._templateFactory}_getContentForTemplate(){return{"".tooltip-inner"":this._getTitle()}}_getTitle(){return + this._resolvePossibleFunction(this._config.title)||this._element.getAttribute(""data-bs-original-title"")}_initializeOnDelegatedTarget(t){return this.constructor.getOrCreateInstance(t.delegateTarget,this._getDelegateConfig())}_isAnimated(){return this._config.animation||this.tip&&this.tip.classList.contains(ts)}_isShown(){return this.tip&&this.tip.classList.contains(es)}_createPopper(t){const e=g(this._config.placement,[this,t,this._element]),i=rs[e.toUpperCase()];return bi(this._element,t,this._getPopperConfig(i))}_getOffset(){const{offset:t}=this._config +;return""string""==typeof t?t.split("","").map((t=>Number.parseInt(t,10))):""function""==typeof t?e=>t(e,this._element):t}_resolvePossibleFunction(t){return g(t,[this._element])}_getPopperConfig(t){const e={placement:t,modifiers:[{name:""flip"",options:{fallbackPlacements:this._config.fallbackPlacements}},{name:""offset"",options:{offset:this._getOffset()}},{name:""preventOverflow"",options:{boundary:this._config.boundary}},{name:""arrow"",options:{element:`.${this.constructor.NAME}-arrow`}},{name:""preSetPlacement"" +,enabled:!0,phase:""beforeMain"",fn:t=>{this._getTipElement().setAttribute(""data-popper-placement"",t.state.placement)}}]};return{...e,...g(this._config.popperConfig,[e])}}_setListeners(){const t=this._config.trigger.split("" "");for(const e of t)if(""click""===e)N.on(this._element,this.constructor.eventName(""click""),this._config.selector,(t=>{this._initializeOnDelegatedTarget(t).toggle()}));else if(""manual""!==e){const t=e===ss?this.constructor.eventName(""mouseenter""):this.constructor.eventName(""focusin"") +,i=e===ss?this.constructor.eventName(""mouseleave""):this.constructor.eventName(""focusout"");N.on(this._element,t,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[""focusin""===t.type?os:ss]=!0,e._enter()})),N.on(this._element,i,this._config.selector,(t=>{const e=this._initializeOnDelegatedTarget(t);e._activeTrigger[""focusout""===t.type?os:ss]=e._element.contains(t.relatedTarget),e._leave()}))}this._hideModalHandler=()=>{this._element&&this.hide()},N.on(this._element.closest(is) +,ns,this._hideModalHandler)}_fixTitle(){const t=this._element.getAttribute(""title"");t&&(this._element.getAttribute(""aria-label"")||this._element.textContent.trim()||this._element.setAttribute(""aria-label"",t),this._element.setAttribute(""data-bs-original-title"",t),this._element.removeAttribute(""title""))}_enter(){this._isShown()||this._isHovered?this._isHovered=!0:(this._isHovered=!0,this._setTimeout((()=>{this._isHovered&&this.show()}),this._config.delay.show))}_leave(){this._isWithActiveTrigger()||(this._isHovered=!1 +,this._setTimeout((()=>{this._isHovered||this.hide()}),this._config.delay.hide))}_setTimeout(t,e){clearTimeout(this._timeout),this._timeout=setTimeout(t,e)}_isWithActiveTrigger(){return Object.values(this._activeTrigger).includes(!0)}_getConfig(t){const e=F.getDataAttributes(this._element);for(const t of Object.keys(e))Zn.has(t)&&delete e[t];return t={...e,...""object""==typeof t&&t?t:{}},t=this._mergeConfigObj(t),t=this._configAfterMerge(t),this._typeCheckConfig(t),t}_configAfterMerge(t){return t.container=!1===t.container?document.body:r(t.container) +,""number""==typeof t.delay&&(t.delay={show:t.delay,hide:t.delay}),""number""==typeof t.title&&(t.title=t.title.toString()),""number""==typeof t.content&&(t.content=t.content.toString()),t}_getDelegateConfig(){const t={};for(const[e,i]of Object.entries(this._config))this.constructor.Default[e]!==i&&(t[e]=i);return t.selector=!1,t.trigger=""manual"",t}_disposePopper(){this._popper&&(this._popper.destroy(),this._popper=null),this.tip&&(this.tip.remove(),this.tip=null)}static jQueryInterface(t){return this.each((function(){const + e=cs.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t])throw new TypeError(`No method named ""${t}""`);e[t]()}}))}}m(cs);const hs={...cs.Default,content:"""",offset:[0,8],placement:""right"",template:'

',trigger:""click""},ds={...cs.DefaultType,content:""(null|string|element|function)""};class us extends cs{static get Default(){return hs}static get DefaultType(){return + ds}static get NAME(){return""popover""}_isWithContent(){return this._getTitle()||this._getContent()}_getContentForTemplate(){return{"".popover-header"":this._getTitle(),"".popover-body"":this._getContent()}}_getContent(){return this._resolvePossibleFunction(this._config.content)}static jQueryInterface(t){return this.each((function(){const e=us.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t])throw new TypeError(`No method named ""${t}""`);e[t]()}}))}}m(us);const fs="".bs.scrollspy"",ps=`activate${fs}` +,ms=`click${fs}`,gs=`load${fs}.data-api`,_s=""active"",bs=""[href]"",vs="".nav-link"",ys=`${vs}, .nav-item > ${vs}, .list-group-item`,ws={offset:null,rootMargin:""0px 0px -25%"",smoothScroll:!1,target:null,threshold:[.1,.5,1]},As={offset:""(number|null)"",rootMargin:""string"",smoothScroll:""boolean"",target:""element"",threshold:""array""};class Es extends W{constructor(t,e){super(t,e),this._targetLinks=new Map,this._observableSections=new Map,this._rootElement=""visible""===getComputedStyle(this._element).overflowY?null:this._element +,this._activeTarget=null,this._observer=null,this._previousScrollData={visibleEntryTop:0,parentScrollTop:0},this.refresh()}static get Default(){return ws}static get DefaultType(){return As}static get NAME(){return""scrollspy""}refresh(){this._initializeTargetsAndObservables(),this._maybeEnableSmoothScroll(),this._observer?this._observer.disconnect():this._observer=this._getNewObserver();for(const t of this._observableSections.values())this._observer.observe(t)}dispose(){this._observer.disconnect() +,super.dispose()}_configAfterMerge(t){return t.target=r(t.target)||document.body,t.rootMargin=t.offset?`${t.offset}px 0px -30%`:t.rootMargin,""string""==typeof t.threshold&&(t.threshold=t.threshold.split("","").map((t=>Number.parseFloat(t)))),t}_maybeEnableSmoothScroll(){this._config.smoothScroll&&(N.off(this._config.target,ms),N.on(this._config.target,ms,bs,(t=>{const e=this._observableSections.get(t.target.hash);if(e){t.preventDefault();const i=this._rootElement||window,n=e.offsetTop-this._element.offsetTop +;if(i.scrollTo)return void i.scrollTo({top:n,behavior:""smooth""});i.scrollTop=n}})))}_getNewObserver(){const t={root:this._rootElement,threshold:this._config.threshold,rootMargin:this._config.rootMargin};return new IntersectionObserver((t=>this._observerCallback(t)),t)}_observerCallback(t){const e=t=>this._targetLinks.get(`#${t.target.id}`),i=t=>{this._previousScrollData.visibleEntryTop=t.target.offsetTop,this._process(e(t))},n=(this._rootElement||document.documentElement).scrollTop,s=n>=this._previousScrollData.parentScrollTop +;this._previousScrollData.parentScrollTop=n;for(const o of t){if(!o.isIntersecting){this._activeTarget=null,this._clearActiveClass(e(o));continue}const t=o.target.offsetTop>=this._previousScrollData.visibleEntryTop;if(s&&t){if(i(o),!n)return}else s||t||i(o)}}_initializeTargetsAndObservables(){this._targetLinks=new Map,this._observableSections=new Map;const t=z.find(bs,this._config.target);for(const e of t){if(!e.hash||l(e))continue;const t=z.findOne(decodeURI(e.hash),this._element);a(t)&&(this._targetLinks.set(decodeURI(e.hash) +,e),this._observableSections.set(e.hash,t))}}_process(t){this._activeTarget!==t&&(this._clearActiveClass(this._config.target),this._activeTarget=t,t.classList.add(_s),this._activateParents(t),N.trigger(this._element,ps,{relatedTarget:t}))}_activateParents(t){if(t.classList.contains(""dropdown-item""))z.findOne("".dropdown-toggle"",t.closest("".dropdown"")).classList.add(_s);else for(const e of z.parents(t,"".nav, .list-group""))for(const t of z.prev(e,ys))t.classList.add(_s)}_clearActiveClass(t){t.classList.remove(_s) +;const e=z.find(`${bs}.${_s}`,t);for(const t of e)t.classList.remove(_s)}static jQueryInterface(t){return this.each((function(){const e=Es.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t]||t.startsWith(""_"")||""constructor""===t)throw new TypeError(`No method named ""${t}""`);e[t]()}}))}}N.on(window,gs,(()=>{for(const t of z.find('[data-bs-spy=""scroll""]'))Es.getOrCreateInstance(t)})),m(Es);const Ts="".bs.tab"",Cs=`hide${Ts}`,Os=`hidden${Ts}`,xs=`show${Ts}`,ks=`shown${Ts}`,Ls=`click${Ts}` +,Ss=`keydown${Ts}`,Ds=`load${Ts}`,$s=""ArrowLeft"",Is=""ArrowRight"",Ns=""ArrowUp"",Ps=""ArrowDown"",js=""Home"",Ms=""End"",Fs=""active"",Hs=""fade"",Ws=""show"",Bs="".dropdown-toggle"",zs=`:not(${Bs})`,Rs='[data-bs-toggle=""tab""], [data-bs-toggle=""pill""], [data-bs-toggle=""list""]',qs=`.nav-link${zs}, .list-group-item${zs}, [role=""tab""]${zs}, ${Rs}`,Vs=`.${Fs}[data-bs-toggle=""tab""], .${Fs}[data-bs-toggle=""pill""], .${Fs}[data-bs-toggle=""list""]`;class Ks extends W{constructor(t){super(t),this._parent= +this._element.closest('.list-group, .nav, [role=""tablist""]'),this._parent&&(this._setInitialAttributes(this._parent,this._getChildren()),N.on(this._element,Ss,(t=>this._keydown(t))))}static get NAME(){return""tab""}show(){const t=this._element;if(this._elemIsActive(t))return;const e=this._getActiveElem(),i=e?N.trigger(e,Cs,{relatedTarget:t}):null;N.trigger(t,xs,{relatedTarget:e}).defaultPrevented||i&&i.defaultPrevented||(this._deactivate(e,t),this._activate(t,e))}_activate(t,e){t&&(t.classList.add(Fs),this._activate(z.getElementFromSelector(t)) +,this._queueCallback((()=>{""tab""===t.getAttribute(""role"")?(t.removeAttribute(""tabindex""),t.setAttribute(""aria-selected"",!0),this._toggleDropDown(t,!0),N.trigger(t,ks,{relatedTarget:e})):t.classList.add(Ws)}),t,t.classList.contains(Hs)))}_deactivate(t,e){t&&(t.classList.remove(Fs),t.blur(),this._deactivate(z.getElementFromSelector(t)),this._queueCallback((()=>{""tab""===t.getAttribute(""role"")?(t.setAttribute(""aria-selected"",!1),t.setAttribute(""tabindex"",""-1""),this._toggleDropDown(t,!1),N.trigger(t,Os +,{relatedTarget:e})):t.classList.remove(Ws)}),t,t.classList.contains(Hs)))}_keydown(t){if(![$s,Is,Ns,Ps,js,Ms].includes(t.key))return;t.stopPropagation(),t.preventDefault();const e=this._getChildren().filter((t=>!l(t)));let i;if([js,Ms].includes(t.key))i=e[t.key===js?0:e.length-1];else{const n=[Is,Ps].includes(t.key);i=b(e,t.target,n,!0)}i&&(i.focus({preventScroll:!0}),Ks.getOrCreateInstance(i).show())}_getChildren(){return z.find(qs,this._parent)}_getActiveElem(){return this._getChildren().find((t=>this._elemIsActive(t)))||null}_setInitialAttributes(t +,e){this._setAttributeIfNotExists(t,""role"",""tablist"");for(const t of e)this._setInitialAttributesOnChild(t)}_setInitialAttributesOnChild(t){t=this._getInnerElement(t);const e=this._elemIsActive(t),i=this._getOuterElement(t);t.setAttribute(""aria-selected"",e),i!==t&&this._setAttributeIfNotExists(i,""role"",""presentation""),e||t.setAttribute(""tabindex"",""-1""),this._setAttributeIfNotExists(t,""role"",""tab""),this._setInitialAttributesOnTargetPanel(t)}_setInitialAttributesOnTargetPanel(t){const e=z.getElementFromSelector(t) +;e&&(this._setAttributeIfNotExists(e,""role"",""tabpanel""),t.id&&this._setAttributeIfNotExists(e,""aria-labelledby"",`${t.id}`))}_toggleDropDown(t,e){const i=this._getOuterElement(t);if(!i.classList.contains(""dropdown""))return;const n=(t,n)=>{const s=z.findOne(t,i);s&&s.classList.toggle(n,e)};n(Bs,Fs),n("".dropdown-menu"",Ws),i.setAttribute(""aria-expanded"",e)}_setAttributeIfNotExists(t,e,i){t.hasAttribute(e)||t.setAttribute(e,i)}_elemIsActive(t){return t.classList.contains(Fs)}_getInnerElement(t){return + t.matches(qs)?t:z.findOne(qs,t)}_getOuterElement(t){return t.closest("".nav-item, .list-group-item"")||t}static jQueryInterface(t){return this.each((function(){const e=Ks.getOrCreateInstance(this);if(""string""==typeof t){if(void 0===e[t]||t.startsWith(""_"")||""constructor""===t)throw new TypeError(`No method named ""${t}""`);e[t]()}}))}}N.on(document,Ls,Rs,(function(t){[""A"",""AREA""].includes(this.tagName)&&t.preventDefault(),l(this)||Ks.getOrCreateInstance(this).show()})),N.on(window,Ds,(()=>{for(const t + of z.find(Vs))Ks.getOrCreateInstance(t)})),m(Ks);const Qs="".bs.toast"",Xs=`mouseover${Qs}`,Ys=`mouseout${Qs}`,Us=`focusin${Qs}`,Gs=`focusout${Qs}`,Js=`hide${Qs}`,Zs=`hidden${Qs}`,to=`show${Qs}`,eo=`shown${Qs}`,io=""hide"",no=""show"",so=""showing"",oo={animation:""boolean"",autohide:""boolean"",delay:""number""},ro={animation:!0,autohide:!0,delay:5e3};class ao extends W{constructor(t,e){super(t,e),this._timeout=null,this._hasMouseInteraction=!1,this._hasKeyboardInteraction=!1,this._setListeners()}static get + Default(){return ro}static get DefaultType(){return oo}static get NAME(){return""toast""}show(){N.trigger(this._element,to).defaultPrevented||(this._clearTimeout(),this._config.animation&&this._element.classList.add(""fade""),this._element.classList.remove(io),d(this._element),this._element.classList.add(no,so),this._queueCallback((()=>{this._element.classList.remove(so),N.trigger(this._element,eo),this._maybeScheduleHide()}),this._element,this._config.animation))}hide(){this.isShown()&&(N.trigger(this._element +,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}) +,this._config.delay)))}_onInteraction(t,e){switch(t.type){case""mouseover"":case""mouseout"":this._hasMouseInteraction=e;break;case""focusin"":case""focusout"":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element +,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t])throw new TypeError(`No method named ""${t}""`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); //# sourceMappingURL=bootstrap.bundle.min.js.map"; } } diff --git a/src/AngleSharp.Js.Tests/EcmaTests.cs b/src/AngleSharp.Js.Tests/EcmaTests.cs new file mode 100644 index 0000000..244d7bb --- /dev/null +++ b/src/AngleSharp.Js.Tests/EcmaTests.cs @@ -0,0 +1,21 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System; + using System.Threading.Tasks; + + [TestFixture] + public class EcmaTests + { + private static String SetResult(String eval) => + $"document.querySelector('#result').textContent = {eval};"; + + [Test] + public async Task BootstrapVersionFive() + { + var result = await (new[] { Constants.Bootstrap_5_3_3, SetResult("bootstrap.toString()") }).EvalScriptsAsync() + .ConfigureAwait(false); + Assert.AreNotEqual("", result); + } + } +} diff --git a/src/AngleSharp.Js.Tests/FireEventTests.cs b/src/AngleSharp.Js.Tests/FireEventTests.cs index eac505c..cddaf59 100644 --- a/src/AngleSharp.Js.Tests/FireEventTests.cs +++ b/src/AngleSharp.Js.Tests/FireEventTests.cs @@ -3,6 +3,7 @@ namespace AngleSharp.Js.Tests using AngleSharp.Dom; using AngleSharp.Dom.Events; using AngleSharp.Scripting; + using Jint; using NUnit.Framework; using System; @@ -60,11 +61,11 @@ public async Task InvokeLoadEventFromJsAndCustomEventFromJsAndCs() document.AddEventListener("hello", (s, ev) => { - log.Put(log.Get("length").AsNumber().ToString(), "d", false); + log.Set(log.Get("length").AsNumber(), "d", false); }); document.Dispatch(new Event("hello")); - + Assert.AreEqual(4.0, log.Get("length").AsNumber()); Assert.AreEqual("a", log.Get("0").AsString()); Assert.AreEqual("b", log.Get("1").AsString()); diff --git a/src/AngleSharp.Js.Tests/InteractionTests.cs b/src/AngleSharp.Js.Tests/InteractionTests.cs index 8afcdf7..eff1a33 100644 --- a/src/AngleSharp.Js.Tests/InteractionTests.cs +++ b/src/AngleSharp.Js.Tests/InteractionTests.cs @@ -3,6 +3,7 @@ namespace AngleSharp.Js.Tests using AngleSharp.Dom; using AngleSharp.Html.Dom; using AngleSharp.Scripting; + using Jint; using Jint.Runtime; using NUnit.Framework; using System; @@ -30,8 +31,9 @@ public async Task RunJavaScriptFunctionFromCSharp() var cfg = Configuration.Default.With(service); var html = ""; var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)); - var square = service.GetOrCreateJint(document).GetValue("square"); - var result = square.Invoke(4); + var engine = service.GetOrCreateJint(document); + var square = engine.GetValue("square"); + var result = engine.Invoke(square, 4); Assert.AreEqual(Types.Number, result.Type); Assert.AreEqual(16.0, result.AsNumber()); } diff --git a/src/AngleSharp.Js.Tests/JqueryTests.cs b/src/AngleSharp.Js.Tests/JqueryTests.cs index 3110025..6627c7c 100644 --- a/src/AngleSharp.Js.Tests/JqueryTests.cs +++ b/src/AngleSharp.Js.Tests/JqueryTests.cs @@ -66,7 +66,7 @@ public async Task JqueryWithAjaxToDelayedResponse() var message = "Hi!"; var req = new DelayedRequester(10, message); var cfg = Configuration.Default.WithJs().WithEventLoop().With(req).WithDefaultLoader(); - var sources = new [] { Constants.Jquery2_1_4, @" + var sources = new[] { Constants.Jquery2_1_4, @" $.ajax('http://example.com/', { success: function (data, status, xhr) { var res = document.querySelector('#result'); @@ -87,7 +87,7 @@ public async Task JqueryWithAjaxToDelayedResponse() [Test] public async Task JqueryVersionOne() { - var result = await (new [] { Constants.Jquery1_11_2, SetResult("$.toString()") }).EvalScriptsAsync() + var result = await (new[] { Constants.Jquery1_11_2, SetResult("$.toString()") }).EvalScriptsAsync() .ConfigureAwait(false); Assert.AreNotEqual("", result); } diff --git a/src/AngleSharp.Js.Tests/Mocks/DataRequester.cs b/src/AngleSharp.Js.Tests/Mocks/DataRequester.cs index 75d64c7..19e26c9 100644 --- a/src/AngleSharp.Js.Tests/Mocks/DataRequester.cs +++ b/src/AngleSharp.Js.Tests/Mocks/DataRequester.cs @@ -11,12 +11,12 @@ sealed class DataRequester : BaseRequester { private static readonly String Base64Section = ";base64"; - + public override Boolean SupportsProtocol(String protocol) { return protocol.Is(ProtocolNames.Data); } - + protected override Task PerformRequestAsync(Request request, CancellationToken cancel) { var content = new MemoryStream(); diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index 85e35c1..51d4a50 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -1,9 +1,9 @@ - + AngleSharp.Js AngleSharp.Js netstandard2.0 - netstandard2.0;net46;net461;net472 + netstandard2.0;net462 true Key.snk true @@ -22,7 +22,7 @@ - + diff --git a/src/AngleSharp.Js/Cache/PrototypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeCache.cs index 52d2136..ffcc2fd 100644 --- a/src/AngleSharp.Js/Cache/PrototypeCache.cs +++ b/src/AngleSharp.Js/Cache/PrototypeCache.cs @@ -14,7 +14,7 @@ public PrototypeCache(Engine engine) { _prototypes = new ConcurrentDictionary { - [typeof(Object)] = engine.Object.PrototypeObject, + [typeof(Object)] = engine.Intrinsics.Object.PrototypeObject, }; _engine = engine; } diff --git a/src/AngleSharp.Js/Converters/DomTypeConverter.cs b/src/AngleSharp.Js/Converters/DomTypeConverter.cs deleted file mode 100644 index 29889fa..0000000 --- a/src/AngleSharp.Js/Converters/DomTypeConverter.cs +++ /dev/null @@ -1,169 +0,0 @@ -namespace AngleSharp.Js -{ - using AngleSharp.Dom; - using AngleSharp.Dom.Events; - using AngleSharp.Html.Dom; - using AngleSharp.Html.Dom.Events; - using AngleSharp.Media.Dom; - using Jint.Native; - using System; - - static class DomTypeConverter - { - public static DomEventHandler ToEventHandler(JsValue arg) - { - return null; - } - - public static MutationCallback ToMutationCallback(JsValue arg) - { - return null; - } - - public static NodeFilter ToNodeFilter(JsValue arg) - { - return null; - } - - public static IAttr ToAttr(JsValue arg) - { - return null; - } - - public static IElement ToElement(JsValue arg) - { - return null; - } - - public static IHtmlElement ToHtmlElement(JsValue arg) - { - return null; - } - - public static IHtmlOptionElement ToOptionElement(JsValue arg) - { - return null; - } - - public static IHtmlTableCaptionElement ToTableCaptionElement(JsValue arg) - { - return null; - } - - public static IHtmlTableSectionElement ToTableSectionElement(JsValue arg) - { - return null; - } - - public static IHtmlMenuElement ToMenuElement(JsValue arg) - { - return null; - } - - public static IHtmlOptionsGroupElement ToOptionsGroupElement(JsValue arg) - { - return null; - } - - public static IEventTarget ToEventTarget(JsValue arg) - { - return null; - } - - public static INode ToNode(JsValue arg) - { - return null; - } - - public static Action ToTimer(JsValue arg) - { - return null; - } - - public static IRenderingContext ToRenderingContext(JsValue arg) - { - return null; - } - - public static IWindow ToWindow(JsValue arg) - { - return null; - } - - public static IDocumentType ToDoctype(JsValue arg) - { - return null; - } - - public static IMessagePort ToMessagePort(JsValue arg) - { - return null; - } - - public static DomException ToDomException(JsValue arg) - { - return null; - } - - public static ITouchList ToTouchList(JsValue arg) - { - return null; - } - - public static ITextTrackCue ToTextTrackCue(JsValue arg) - { - return null; - } - - public static Event ToEvent(JsValue arg) - { - return null; - } - - public static IRange ToRange(JsValue arg) - { - return null; - } - - public static AdjacentPosition ToAdjacentPosition(JsValue arg) - { - return ToEnum(arg); - } - - public static RangeType ToRangeType(JsValue arg) - { - return ToEnum(arg); - } - - public static WheelMode ToWheelMode(JsValue arg) - { - return ToEnum(arg); - } - - public static MouseButton ToMouseButton(JsValue arg) - { - return ToEnum(arg); - } - - public static KeyboardLocation ToKeyboardLocation(JsValue arg) - { - return ToEnum(arg); - } - - public static TextTrackMode ToTextTrackMode(JsValue arg) - { - return ToEnum(arg); - } - - public static FilterSettings ToFilterSettings(JsValue arg) - { - return ToEnum(arg); - } - - public static T ToEnum(JsValue arg) - where T : struct, IComparable - { - return default(T); - } - } -} diff --git a/src/AngleSharp.Js/Converters/SystemTypeConverter.cs b/src/AngleSharp.Js/Converters/SystemTypeConverter.cs deleted file mode 100644 index 76fb70d..0000000 --- a/src/AngleSharp.Js/Converters/SystemTypeConverter.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint.Native; - using Jint.Runtime; - using System; - using System.Collections.Generic; - using System.IO; - - static class SystemTypeConverter - { - public static IDictionary ToObjBag(JsValue arg) - { - var obj = arg.AsObject(); - var dict = new Dictionary(); - var properties = obj.GetOwnProperties(); - - foreach (var property in properties) - { - var value = property.Value.Value.Clr(); - dict.Add(property.Key,value); - } - - return dict; - } - - public static Action ToStreamTask(JsValue arg) - { - return null; - } - - public static Nullable ToOptionalInt32(JsValue arg) - { - if (arg.IsNumber()) - { - return TypeConverter.ToInt32(arg); - } - - return null; - } - - public static Nullable ToOptionalDateTime(JsValue arg) - { - return null; - } - - public static Object ToObject(JsValue arg) - { - return arg.ToObject(); - } - - static Object Clr(this JsValue arg) - { - return arg?.ToObject(); - } - } -} diff --git a/src/AngleSharp.Js/Converters/UnresolvedConverter.cs b/src/AngleSharp.Js/Converters/UnresolvedConverter.cs deleted file mode 100644 index ff3b49f..0000000 --- a/src/AngleSharp.Js/Converters/UnresolvedConverter.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint.Native; - - static class UnresolvedConverter - { - public static T To(JsValue arg) - { - return default(T); - } - } -} diff --git a/src/AngleSharp.Js/Dom/Console.cs b/src/AngleSharp.Js/Dom/Console.cs new file mode 100644 index 0000000..5dc3321 --- /dev/null +++ b/src/AngleSharp.Js/Dom/Console.cs @@ -0,0 +1,35 @@ +using AngleSharp.Attributes; +using AngleSharp.Dom; + +namespace AngleSharp.Js.Dom +{ + /// + /// Defines the console. + /// + [DomName("Console")] + public sealed class Console + { + private readonly IConsoleLogger _logger; + + /// + /// Creates a new console. + /// + /// + [DomConstructor] + public Console(IWindow window) + { + var context = window.Document.Context; + _logger = context.GetService(); + } + + /// + /// Outputs a message to the console. + /// + /// + [DomName("log")] + public void Log(params object[] objs) + { + _logger?.Log(objs); + } + } +} diff --git a/src/AngleSharp.Js/Dom/DomParser.cs b/src/AngleSharp.Js/Dom/DomParser.cs index ff7800b..882ed45 100644 --- a/src/AngleSharp.Js/Dom/DomParser.cs +++ b/src/AngleSharp.Js/Dom/DomParser.cs @@ -40,7 +40,7 @@ public IDocument Parse(String str, String type) { var ctx = _window?.Document.Context; var factory = ctx?.GetService() ?? throw new DomException(DomError.NotSupported); - + using (var content = new MemoryStream(TextEncoding.Utf8.GetBytes(str))) { var response = new DefaultResponse diff --git a/src/AngleSharp.Js/Dom/EventAttributeObserver.cs b/src/AngleSharp.Js/Dom/EventAttributeObserver.cs index 3c299e9..55928c4 100644 --- a/src/AngleSharp.Js/Dom/EventAttributeObserver.cs +++ b/src/AngleSharp.Js/Dom/EventAttributeObserver.cs @@ -98,11 +98,9 @@ private void RegisterEventCallback(String eventName) var document = element.Owner; var engine = _service.GetOrCreateInstance(document); var jint = engine.Jint; - jint.EnterExecutionContext(engine.Lexicals, engine.Variables, engine.Window); - var instance = jint.Function.Construct(new JsValue[] { "event", value }); - jint.LeaveExecutionContext(); + var instance = jint.Intrinsics.Function.Construct(new JsValue[] { "event", value }, JsValue.Undefined); - if (instance is FunctionInstance functor) + if (instance is Function functor) { element.AddEventListener(eventName, functor.ToListener(engine)); } diff --git a/src/AngleSharp.Js/Dom/WindowExtensions.cs b/src/AngleSharp.Js/Dom/WindowExtensions.cs index e0c7977..5fec386 100644 --- a/src/AngleSharp.Js/Dom/WindowExtensions.cs +++ b/src/AngleSharp.Js/Dom/WindowExtensions.cs @@ -30,5 +30,17 @@ public static void PostMessage(this IWindow window, String message, String targe [DomName("top")] [DomAccessor(Accessors.Getter)] public static IWindow Top(this IWindow window) => window.Document.Context?.Creator?.DefaultView; + + /// + /// Gets the console instance. + /// + /// + /// + [DomName("console")] + [DomAccessor(Accessors.Getter)] + public static Console Console(this IWindow window) + { + return new Console(window); + } } } diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 7d111ab..2767e3d 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -4,7 +4,6 @@ namespace AngleSharp.Js using Jint; using Jint.Native; using Jint.Native.Object; - using Jint.Runtime.Environments; using System; using System.Collections.Generic; using System.Reflection; @@ -17,8 +16,6 @@ sealed class EngineInstance private readonly PrototypeCache _prototypes; private readonly ReferenceCache _references; private readonly IEnumerable _libs; - private readonly LexicalEnvironment _lexicals; - private readonly LexicalEnvironment _variables; private readonly DomNodeInstance _window; #endregion @@ -27,13 +24,10 @@ sealed class EngineInstance public EngineInstance(IWindow window, IDictionary assignments, IEnumerable libs) { - var context = window.Document.Context; - var logger = context.GetService(); _engine = new Engine(); _prototypes = new PrototypeCache(_engine); _references = new ReferenceCache(); _libs = libs; - _engine.SetValue("console", new ConsoleInstance(_engine, logger)); foreach (var assignment in assignments) { @@ -41,15 +35,24 @@ public EngineInstance(IWindow window, IDictionary assignments, I } _window = GetDomNode(window); - _lexicals = LexicalEnvironment.NewObjectEnvironment(_engine, _window, _engine.ExecutionContext.LexicalEnvironment, true); - _variables = LexicalEnvironment.NewObjectEnvironment(_engine, _engine.Global, null, false); foreach (var lib in libs) { - this.AddConstructors(_window, lib); this.AddConstructors(_window, lib); this.AddInstances(_window, lib); } + + foreach (var property in _window.GetOwnProperties()) + { + _engine.Global.FastSetProperty(property.Key.ToString(), property.Value); + } + + foreach (var prototypeProperty in Window.Prototype.GetOwnProperties()) + { + _engine.Global.FastSetProperty(prototypeProperty.Key.ToString(), prototypeProperty.Value); + } + + _engine.Global.Prototype = _window.Prototype; } #endregion @@ -60,10 +63,6 @@ public EngineInstance(IWindow window, IDictionary assignments, I public DomNodeInstance Window => _window; - public LexicalEnvironment Lexicals => _lexicals; - - public LexicalEnvironment Variables => _variables; - public Engine Jint => _engine; #endregion @@ -78,10 +77,7 @@ public JsValue RunScript(String source, JsValue context) { lock (_engine) { - _engine.EnterExecutionContext(Lexicals, Variables, context); - _engine.Execute(source); - _engine.LeaveExecutionContext(); - return _engine.GetCompletionValue(); + return _engine.Evaluate(source); } } diff --git a/src/AngleSharp.Js/Extensions/DomDelegates.cs b/src/AngleSharp.Js/Extensions/DomDelegates.cs index 9294476..5c95086 100644 --- a/src/AngleSharp.Js/Extensions/DomDelegates.cs +++ b/src/AngleSharp.Js/Extensions/DomDelegates.cs @@ -2,6 +2,7 @@ namespace AngleSharp.Js { using AngleSharp.Dom; using AngleSharp.Dom.Events; + using Jint; using Jint.Native; using Jint.Native.Function; using Jint.Runtime; @@ -12,10 +13,10 @@ namespace AngleSharp.Js static class DomDelegates { - private static readonly Type[] ToCallbackSignature = new[] { typeof(FunctionInstance), typeof(EngineInstance) }; + private static readonly Type[] ToCallbackSignature = new[] { typeof(Function), typeof(EngineInstance) }; private static readonly Type[] ToJsValueSignature = new[] { typeof(Object), typeof(EngineInstance) }; - public static Delegate ToDelegate(this Type type, FunctionInstance function, EngineInstance engine) + public static Delegate ToDelegate(this Type type, Function function, EngineInstance engine) { if (type != typeof(DomEventHandler)) { @@ -26,7 +27,7 @@ public static Delegate ToDelegate(this Type type, FunctionInstance function, Eng return function.ToListener(engine); } - public static DomEventHandler ToListener(this FunctionInstance function, EngineInstance engine) => (obj, ev) => + public static DomEventHandler ToListener(this Function function, EngineInstance engine) => (obj, ev) => { var objAsJs = obj.ToJsValue(engine); var evAsJs = ev.ToJsValue(engine); @@ -38,11 +39,11 @@ public static DomEventHandler ToListener(this FunctionInstance function, EngineI catch (JavaScriptException jsException) { var window = (IWindow)engine.Window.Value; - window.Fire(e => e.Init(null, jsException.LineNumber, jsException.Column, jsException)); + window.Fire(e => e.Init(null, jsException.Location.Start.Line, jsException.Location.Start.Column, jsException)); } }; - public static T ToCallback(this FunctionInstance function, EngineInstance engine) + public static T ToCallback(this Function function, EngineInstance engine) { var methodInfo = typeof(T).GetRuntimeMethods().First(m => m.Name == "Invoke"); var convert = typeof(EngineExtensions).GetRuntimeMethod("ToJsValue", ToJsValueSignature); diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index 180236a..2200831 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -21,27 +21,27 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) { if (obj is String) { - return new JsValue((String)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(String)); } else if (obj is Int32) { - return new JsValue((Int32)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(Int32)); } else if (obj is UInt32) { - return new JsValue((UInt32)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(UInt32)); } else if (obj is Double) { - return new JsValue((Double)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(Double)); } else if (obj is Single) { - return new JsValue((Single)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(Single)); } else if (obj is Boolean) { - return new JsValue((Boolean)obj); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(Boolean)); } else if (obj is Enum) { @@ -55,7 +55,7 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) } break; } - return new JsValue(Convert.ToInt32(obj)); + return JsValue.FromObjectWithType(engine.Jint, obj, typeof(Enum)); } return engine.GetDomNode(obj); @@ -64,17 +64,11 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) return JsValue.Null; } - public static ClrFunctionInstance AsValue(this Engine engine, Func func) => - new ClrFunctionInstance(engine, func); + public static ClrFunction AsValue(this Engine engine, string name, Func func) => + new ClrFunction(engine, name, func); - public static PropertyDescriptor AsProperty(this Engine engine, Func getter, Action setter) => - new PropertyDescriptor(new GetterFunctionInstance(engine, getter), new SetterFunctionInstance(engine, setter), true, true); - - public static PropertyDescriptor AsProperty(this Engine engine, Func getter) => - new PropertyDescriptor(new GetterFunctionInstance(engine, getter), null, true, false); - - public static PropertyDescriptor AsProperty(this Engine engine, Action setter) => - new PropertyDescriptor(null, new SetterFunctionInstance(engine, setter), true, false); + public static PropertyDescriptor AsProperty(this Engine engine, JsValue getter = null, JsValue setter = null) => + new GetSetPropertyDescriptor(getter, setter, true, getter != null && setter != null); public static Object[] BuildArgs(this EngineInstance context, MethodBase method, JsValue[] arguments) { @@ -212,15 +206,26 @@ public static JsValue RunScript(this EngineInstance engine, String source, INode public static JsValue Call(this EngineInstance instance, MethodInfo method, JsValue thisObject, JsValue[] arguments) { - if (method != null && thisObject.Type == Types.Object && thisObject.AsObject() is DomNodeInstance node) + if (method != null) { + DomNodeInstance nodeInstance; + + if (thisObject.Type == Types.Object && thisObject.AsObject() is DomNodeInstance node) + { + nodeInstance = node; + } + else + { + nodeInstance = instance.Window; + } + try { if (method.IsStatic) { var newArgs = new List { - thisObject, + nodeInstance, }; newArgs.AddRange(arguments); var parameters = instance.BuildArgs(method, newArgs.ToArray()); @@ -229,12 +234,12 @@ public static JsValue Call(this EngineInstance instance, MethodInfo method, JsVa else { var parameters = instance.BuildArgs(method, arguments); - return method.Invoke(node.Value, parameters).ToJsValue(instance); + return method.Invoke(nodeInstance.Value, parameters).ToJsValue(instance); } } catch (TargetInvocationException) { - throw new JavaScriptException(instance.Jint.Error); + throw new JavaScriptException(instance.Jint.Intrinsics.Error); } } diff --git a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs index b786214..70e4c04 100644 --- a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs +++ b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs @@ -1,5 +1,6 @@ namespace AngleSharp.Js { + using Jint; using Jint.Native; using Jint.Native.Function; using Jint.Runtime; @@ -20,13 +21,13 @@ private static Object AsComplex(this JsValue value, Type targetType, EngineInsta } else if (targetType.GetTypeInfo().IsSubclassOf(typeof(Delegate))) { - var f = obj as FunctionInstance; + var f = obj as Function; if (f == null && obj is String b) { var e = engine.Jint; - var p = new[] { new JsValue(b) }; - f = new ClrFunctionInstance(e, (_this, args) => e.Eval.Call(_this, p)); + var p = new[] { JsValue.FromObjectWithType(e, b, typeof(String)) }; + f = new ClrFunction(e, "AsComplex", (_this, args) => e.Intrinsics.Eval.Call(_this, p)); } if (f != null) @@ -56,7 +57,7 @@ public static Object FromJsValue(this JsValue val) var node = obj as DomNodeInstance; return node != null ? node.Value : obj; case Types.Undefined: - return Undefined.Text; + return JsValue.Undefined.ToString(); case Types.Null: return null; } diff --git a/src/AngleSharp.Js/JsApiExtensions.cs b/src/AngleSharp.Js/JsApiExtensions.cs index 53ee910..d5e8cbf 100644 --- a/src/AngleSharp.Js/JsApiExtensions.cs +++ b/src/AngleSharp.Js/JsApiExtensions.cs @@ -19,7 +19,7 @@ public static Object ExecuteScript(this IDocument document, String scriptCode) { if (document == null) throw new ArgumentNullException(nameof(document)); - + var service = document?.Context.GetService(); return service?.EvaluateScript(document, scriptCode); } diff --git a/src/AngleSharp.Js/Proxies/ConsoleInstance.cs b/src/AngleSharp.Js/Proxies/ConsoleInstance.cs deleted file mode 100644 index a75086e..0000000 --- a/src/AngleSharp.Js/Proxies/ConsoleInstance.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint; - using Jint.Native; - using Jint.Native.Object; - using Jint.Runtime.Interop; - using System; - - sealed class ConsoleInstance : ObjectInstance - { - private readonly IConsoleLogger _logger; - - public ConsoleInstance(Engine engine, IConsoleLogger logger) - : base(engine) - { - _logger = logger; - FastAddProperty("log", new ClrFunctionInstance(engine, Log), false, false, false); - } - - private JsValue Log(JsValue ctx, JsValue[] args) - { - if (_logger != null) - { - var objs = new Object[args.Length]; - - for (var i = 0; i < args.Length; i++) - { - objs[i] = args[i].FromJsValue(); - } - - _logger.Log(objs); - } - - return JsValue.Undefined; - } - } -} diff --git a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs index 9534e0f..64bd0d3 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs @@ -1,28 +1,28 @@ namespace AngleSharp.Js { using Jint.Native; - using Jint.Native.Function; using Jint.Native.Object; using Jint.Runtime; + using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using System; using System.Reflection; - sealed class DomConstructorInstance : FunctionInstance, IConstructor + sealed class DomConstructorInstance : Constructor { private readonly ConstructorInfo _constructor; private readonly EngineInstance _instance; private readonly ObjectInstance _objectPrototype; public DomConstructorInstance(EngineInstance engine, Type type) - : base(engine.Jint, null, null, false) + : base(engine.Jint, type.GetOfficialName()) { - var toString = new ClrFunctionInstance(Engine, ToString); + var toString = new ClrFunction(Engine, "toString", ToString); _objectPrototype = engine.GetDomPrototype(type); _instance = engine; - FastAddProperty("toString", toString, true, false, true); - FastAddProperty("prototype", _objectPrototype, false, false, false); - _objectPrototype.FastAddProperty("constructor", this, true, false, true); + 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)); } public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor) @@ -31,17 +31,7 @@ public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor _constructor = constructor; } - public override JsValue Call(JsValue thisObject, JsValue[] arguments) - { - if (_constructor != null) - { - throw new JavaScriptException("Only call the constructor with the new keyword."); - } - - return ((IConstructor)this).Construct(arguments); - } - - ObjectInstance IConstructor.Construct(JsValue[] arguments) + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { if (_constructor == null) { @@ -56,11 +46,20 @@ ObjectInstance IConstructor.Construct(JsValue[] arguments) } catch { - throw new JavaScriptException(_instance.Jint.Error); + throw new JavaScriptException(_instance.Jint.Intrinsics.Error); } } - private JsValue ToString(JsValue thisObj, JsValue[] arguments) => - $"function {_objectPrototype.Class}() {{ [native code] }}"; + protected override JsValue Call(JsValue thisObject, JsValue[] arguments) + { + if (_constructor != null) + { + throw new JavaScriptException("Only call the constructor with the new keyword."); + } + + return Construct(arguments, null); + } + + private JsValue ToString(JsValue thisObj, JsValue[] arguments) => ToString(); } } diff --git a/src/AngleSharp.Js/Proxies/DomConstructors.cs b/src/AngleSharp.Js/Proxies/DomConstructors.cs deleted file mode 100644 index cf55349..0000000 --- a/src/AngleSharp.Js/Proxies/DomConstructors.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint.Native.Function; - using Jint.Native.Object; - using System.Reflection; - - partial class DomConstructors - { - private readonly EngineInstance _engine; - - public DomConstructors(EngineInstance engine) - { - Object = engine.Jint.Object; - _engine = engine; - } - - public ObjectConstructor Object - { - get; - private set; - } - - public void Configure() => Setup(_engine); - - public void AttachConstructors(ObjectInstance obj) - { - var properties = GetType().GetTypeInfo().DeclaredProperties; - - foreach (var property in properties) - { - var func = property.GetValue(this) as FunctionInstance; - obj.FastAddProperty(property.Name, func, true, false, true); - } - } - - partial void Setup(EngineInstance engine); - } -} diff --git a/src/AngleSharp.Js/Proxies/DomDelegateInstance.cs b/src/AngleSharp.Js/Proxies/DomDelegateInstance.cs deleted file mode 100644 index 741df11..0000000 --- a/src/AngleSharp.Js/Proxies/DomDelegateInstance.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint.Native; - using Jint.Native.Function; - using Jint.Runtime; - using Jint.Runtime.Interop; - using System; - - sealed class DomDelegateInstance : FunctionInstance - { - private readonly EngineInstance _instance; - private readonly Func _func; - private readonly String _officialName; - - public DomDelegateInstance(EngineInstance engine, String officialName, Func func) - : base(engine.Jint, Array.Empty(), null, false) - { - var toString = new ClrFunctionInstance(Engine, ToString); - _instance = engine; - _func = func; - _officialName = officialName; - FastAddProperty("toString", toString, true, false, true); - } - - public override JsValue Call(JsValue thisObject, JsValue[] arguments) => - _func.Invoke(thisObject, arguments); - - private JsValue ToString(JsValue thisObj, JsValue[] arguments) - { - var func = thisObj.TryCast() ?? - throw new JavaScriptException(Engine.TypeError, "Function object expected."); - - return $"function {_officialName}() {{ [native code] }}"; - } - } -} diff --git a/src/AngleSharp.Js/Proxies/DomEventInstance.cs b/src/AngleSharp.Js/Proxies/DomEventInstance.cs index 41d7e57..3eb9d23 100644 --- a/src/AngleSharp.Js/Proxies/DomEventInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomEventInstance.cs @@ -1,6 +1,7 @@ namespace AngleSharp.Js { using AngleSharp.Dom; + using Jint; using Jint.Native; using Jint.Native.Function; using Jint.Runtime.Interop; @@ -13,20 +14,20 @@ sealed class DomEventInstance private readonly MethodInfo _addHandler; private readonly MethodInfo _removeHandler; private DomEventHandler _handler; - private FunctionInstance _function; + private Function _function; public DomEventInstance(EngineInstance engine, MethodInfo addHandler, MethodInfo removeHandler) { _engine = engine; _addHandler = addHandler; _removeHandler = removeHandler; - Getter = new ClrFunctionInstance(engine.Jint, GetEventHandler); - Setter = new ClrFunctionInstance(engine.Jint, SetEventHandler); + Getter = new ClrFunction(engine.Jint, "get", GetEventHandler); + Setter = new ClrFunction(engine.Jint, "set", SetEventHandler); } - public ClrFunctionInstance Getter { get; } + public ClrFunction Getter { get; } - public ClrFunctionInstance Setter { get; } + public ClrFunction Setter { get; } private JsValue GetEventHandler(JsValue thisObject, JsValue[] arguments) => _function ?? JsValue.Null; @@ -44,9 +45,9 @@ private JsValue SetEventHandler(JsValue thisObject, JsValue[] arguments) _function = null; } - if (arguments[0].Is()) + if (arguments[0] is Function) { - _function = arguments[0].As(); + _function = arguments[0].As(); _handler = (s, ev) => { var sender = s.ToJsValue(_engine); diff --git a/src/AngleSharp.Js/Proxies/DomFunctionInstance.cs b/src/AngleSharp.Js/Proxies/DomFunctionInstance.cs deleted file mode 100644 index 263060b..0000000 --- a/src/AngleSharp.Js/Proxies/DomFunctionInstance.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace AngleSharp.Js -{ - using Jint.Native; - using Jint.Native.Function; - using Jint.Runtime; - using Jint.Runtime.Interop; - using System.Reflection; - - sealed class DomFunctionInstance : FunctionInstance - { - private readonly EngineInstance _instance; - private readonly MethodInfo _method; - - public DomFunctionInstance(EngineInstance engine, MethodInfo method) - : base(engine.Jint, method.GetParameterNames(), null, false) - { - var toString = new ClrFunctionInstance(Engine, ToString); - _instance = engine; - _method = method; - FastAddProperty("toString", toString, true, false, true); - } - - public override JsValue Call(JsValue thisObject, JsValue[] arguments) => - _instance.Call(_method, thisObject, arguments); - - private JsValue ToString(JsValue thisObj, JsValue[] arguments) - { - var func = thisObj.TryCast() ?? - throw new JavaScriptException(Engine.TypeError, "Function object expected."); - - var officialName = _method.GetOfficialName(); - return $"function {officialName}() {{ [native code] }}"; - } - } -} diff --git a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs index 8ade45d..3705b46 100644 --- a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs @@ -1,5 +1,7 @@ namespace AngleSharp.Js { + using AngleSharp.Dom; + using Jint.Native; using Jint.Native.Object; using Jint.Runtime.Descriptors; using System; @@ -14,23 +16,69 @@ public DomNodeInstance(EngineInstance engine, Object value) { _instance = engine; _value = value; - - Extensible = true; + Prototype = engine.GetDomPrototype(value.GetType()); } public Object Value => _value; - public override String Class => Prototype.Class; + public override object ToObject() => _value; - public override PropertyDescriptor GetOwnProperty(String propertyName) + public override PropertyDescriptor GetOwnProperty(JsValue property) { - if (Prototype is DomPrototypeInstance prototype && prototype.TryGetFromIndex(_value, propertyName, out var descriptor)) + if (Prototype is DomPrototypeInstance prototype) { - return descriptor; + if (prototype.TryGetFromIndex(_value, property.ToString(), out var descriptor)) + { + return descriptor; + } + else if (prototype.HasProperty(property)) + { + return prototype.GetProperty(property); + } } - return base.GetOwnProperty(propertyName); + return base.GetOwnProperty(property); + } + + protected override void SetOwnProperty(JsValue property, PropertyDescriptor desc) + { + base.SetOwnProperty(property, desc); + + if (_value is IWindow) + { + _instance.Jint.Global.FastSetProperty(property, desc); + } + } + + public new void FastSetProperty(string name, PropertyDescriptor value) + { + base.FastSetProperty(name, value); + + if (_value is IWindow) + { + _instance.Jint.Global.FastSetProperty(name, value); + } + } + + public new void FastSetProperty(JsValue property, PropertyDescriptor value) + { + base.FastSetProperty(property, value); + + if (_value is IWindow) + { + _instance.Jint.Global.FastSetProperty(property, value); + } + } + + public new void FastSetDataProperty(string name, JsValue value) + { + base.FastSetDataProperty(name, value); + + if (_value is IWindow) + { + _instance.Jint.Global.FastSetProperty(name, new PropertyDescriptor(value, true, true, true)); + } } } } diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index ec64833..183439e 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -5,7 +5,9 @@ namespace AngleSharp.Js using AngleSharp.Text; using Jint.Native; using Jint.Native.Object; + using Jint.Native.Symbol; using Jint.Runtime.Descriptors; + using Jint.Runtime.Interop; using System; using System.Collections.Generic; using System.Linq; @@ -26,16 +28,15 @@ public DomPrototypeInstance(EngineInstance engine, Type type) _name = type.GetOfficialName(baseType); _instance = engine; + Set(GlobalSymbolRegistry.ToStringTag, _name); + SetAllMembers(type); SetExtensionMembers(); // DOM objects can have properties added dynamically - Extensible = true; Prototype = engine.GetDomPrototype(baseType); } - public override String Class => _name; - public Boolean TryGetFromIndex(Object value, String index, out PropertyDescriptor result) { // If we have a numeric indexer and the property is numeric @@ -68,7 +69,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto // Eg. object.callMethod1() vs object['callMethod1'] is not necessarily the same if the object has a string indexer?? (I'm not an ECMA expert!) // node.attributes is one such object - has both a string and numeric indexer // This GetOwnProperty override might need an additional parameter to let us know this was called via an indexer - if (_stringIndexer != null && !Properties.ContainsKey(index)) + if (_stringIndexer != null && !HasProperty(index)) { var args = new Object[] { index }; var prop = _stringIndexer.GetMethod.Invoke(value, args).ToJsValue(_instance); @@ -120,7 +121,7 @@ private void SetExtensionMethods(IEnumerable methods) var name = entry.Key; var value = entry.Value; - if (Properties.ContainsKey(name)) + if (HasProperty(name)) { } else if (value.Adder != null && value.Remover != null) @@ -178,20 +179,20 @@ private void SetNormalMethods(IEnumerable methods) private void SetEvent(String name, MethodInfo adder, MethodInfo remover) { var eventInstance = new DomEventInstance(_instance, adder, remover); - FastSetProperty(name, new PropertyDescriptor(eventInstance.Getter, eventInstance.Setter, false, false)); + FastSetProperty(name, new GetSetPropertyDescriptor(eventInstance.Getter, eventInstance.Setter, false, false)); } private void SetProperty(String name, MethodInfo getter, MethodInfo setter, DomPutForwardsAttribute putsForward) { - FastSetProperty(name, new PropertyDescriptor( - new DomDelegateInstance(_instance, name, (obj, values) => + FastSetProperty(name, new GetSetPropertyDescriptor( + new ClrFunction(_instance.Jint, name, (obj, values) => _instance.Call(getter, obj, values)), - new DomDelegateInstance(_instance, name, (obj, values) => + new ClrFunction(_instance.Jint, name, (obj, values) => { if (putsForward != null) { var ep = Array.Empty(); - var that = obj.AsObject() as DomNodeInstance; + var that = obj as DomNodeInstance; var target = getter.Invoke(that.Value, ep); var propName = putsForward.PropertyName; var prop = getter.ReturnType @@ -227,10 +228,12 @@ private void SetMethod(String name, MethodInfo method) // If it already has a property with the given name (usually another method), // then convert that method to a two-layer method, which decides which one // to pick depending on the number (and probably types) of arguments. - if (!Properties.ContainsKey(name)) + if (!HasProperty(name)) { - var func = new DomFunctionInstance(_instance, method); - FastAddProperty(name, func, false, false, false); + FastSetProperty(name, new PropertyDescriptor( + new ClrFunction(_instance.Jint, name, (obj, values) => + _instance.Call(method, obj, values) + ), false, false, false)); } } } From cf57e5a0834c5346ba9d3bace6cce3fa5a97bfd0 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Thu, 29 Feb 2024 13:14:25 +0000 Subject: [PATCH 08/73] Upgraded AngleSharp to v1.1 --- .../AngleSharp.Js.Tests.csproj | 17 ++++++++++------- src/AngleSharp.Js/AngleSharp.Js.csproj | 6 +++--- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 475e1ac..84dd112 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -17,12 +17,15 @@ - - - - - - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index 51d4a50..e63eda6 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -17,12 +17,12 @@ - + - - + + From 5fde3fe6e0672a08dd561174281d548acc218dfc Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Thu, 29 Feb 2024 13:30:18 +0000 Subject: [PATCH 09/73] Migrated build to Nuke --- .gitignore | 3 + .nuke/build.schema.json | 134 ++++++++++ .nuke/parameters.json | 4 + build.cake | 14 -- build.cmd | 7 + build.ps1 | 114 ++++----- build.sh | 129 ++++------ nuke/.editorconfig | 11 + nuke/Build.cs | 298 ++++++++++++++++++++++ nuke/Configuration.cs | 16 ++ nuke/Directory.Build.props | 8 + nuke/Directory.Build.targets | 8 + nuke/Extensions/StringExtensions.cs | 80 ++++++ nuke/ReleaseNotes.cs | 81 ++++++ nuke/ReleaseNotesParser.cs | 146 +++++++++++ nuke/SemVersion.cs | 378 ++++++++++++++++++++++++++++ nuke/_build.csproj | 21 ++ 17 files changed, 1295 insertions(+), 157 deletions(-) create mode 100644 .nuke/build.schema.json create mode 100644 .nuke/parameters.json delete mode 100644 build.cake create mode 100644 build.cmd create mode 100644 nuke/.editorconfig create mode 100644 nuke/Build.cs create mode 100644 nuke/Configuration.cs create mode 100644 nuke/Directory.Build.props create mode 100644 nuke/Directory.Build.targets create mode 100644 nuke/Extensions/StringExtensions.cs create mode 100644 nuke/ReleaseNotes.cs create mode 100644 nuke/ReleaseNotesParser.cs create mode 100644 nuke/SemVersion.cs create mode 100644 nuke/_build.csproj diff --git a/.gitignore b/.gitignore index 39a64c7..a43fa81 100644 --- a/.gitignore +++ b/.gitignore @@ -194,3 +194,6 @@ pip-log.txt # Mac crap .DS_Store + +# Nuke build tool +.nuke/temp \ No newline at end of file diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json new file mode 100644 index 0000000..c1999a9 --- /dev/null +++ b/.nuke/build.schema.json @@ -0,0 +1,134 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "title": "Build Schema", + "$ref": "#/definitions/build", + "definitions": { + "build": { + "type": "object", + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", + "enum": [ + "Debug", + "Release" + ] + }, + "Continue": { + "type": "boolean", + "description": "Indicates to continue a previously failed build attempt" + }, + "Help": { + "type": "boolean", + "description": "Shows the help text for this build assembly" + }, + "Host": { + "type": "string", + "description": "Host for execution. Default is 'automatic'", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "NoLogo": { + "type": "boolean", + "description": "Disables displaying the NUKE logo" + }, + "Partition": { + "type": "string", + "description": "Partition to use on CI" + }, + "Plan": { + "type": "boolean", + "description": "Shows the execution plan (HTML)" + }, + "Profile": { + "type": "array", + "description": "Defines the profiles to load", + "items": { + "type": "string" + } + }, + "ReleaseNotesFilePath": { + "type": "string", + "description": "ReleaseNotesFilePath - To determine the SemanticVersion" + }, + "Root": { + "type": "string", + "description": "Root directory during build execution" + }, + "Skip": { + "type": "array", + "description": "List of targets to be skipped. Empty list skips all dependencies", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CopyFiles", + "CreatePackage", + "Default", + "Package", + "PrePublish", + "Publish", + "PublishPackage", + "PublishPreRelease", + "PublishRelease", + "Restore", + "RunUnitTests" + ] + } + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" + }, + "Target": { + "type": "array", + "description": "List of targets to be invoked. Default is '{default_target}'", + "items": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CopyFiles", + "CreatePackage", + "Default", + "Package", + "PrePublish", + "Publish", + "PublishPackage", + "PublishPreRelease", + "PublishRelease", + "Restore", + "RunUnitTests" + ] + } + }, + "Verbosity": { + "type": "string", + "description": "Logging verbosity during build execution. Default is 'Normal'", + "enum": [ + "Minimal", + "Normal", + "Quiet", + "Verbose" + ] + } + } + } + } +} \ No newline at end of file diff --git a/.nuke/parameters.json b/.nuke/parameters.json new file mode 100644 index 0000000..4dcccaa --- /dev/null +++ b/.nuke/parameters.json @@ -0,0 +1,4 @@ +{ + "$schema": "./build.schema.json", + "Solution": "src/AngleSharp.Js.sln" +} \ No newline at end of file diff --git a/build.cake b/build.cake deleted file mode 100644 index 33ee873..0000000 --- a/build.cake +++ /dev/null @@ -1,14 +0,0 @@ -var target = Argument("target", "Default"); -var projectName = "AngleSharp.Js"; -var solutionName = "AngleSharp.Js"; -var frameworks = new Dictionary -{ - { "net46", "net46" }, - { "net461", "net461" }, - { "net472", "net472" }, - { "netstandard2.0", "netstandard2.0" }, -}; - -#load tools/anglesharp.cake - -RunTarget(target); diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000..b08cc59 --- /dev/null +++ b/build.cmd @@ -0,0 +1,7 @@ +:; set -eo pipefail +:; SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) +:; ${SCRIPT_DIR}/build.sh "$@" +:; exit $? + +@ECHO OFF +powershell -ExecutionPolicy ByPass -NoProfile -File "%~dp0build.ps1" %* diff --git a/build.ps1 b/build.ps1 index 6f27bfd..1f264e7 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,81 +1,69 @@ +[CmdletBinding()] Param( - [string]$Script = "build.cake", - [string]$Target = "Default", - [ValidateSet("Release", "Debug")] - [string]$Configuration = "Release", - [ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")] - [string]$Verbosity = "Verbose", - [switch]$Experimental, - [switch]$WhatIf, - [switch]$Mono, - [switch]$SkipToolPackageRestore, [Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)] - [string[]]$ScriptArgs + [string[]]$BuildArguments ) -$PSScriptRoot = split-path -parent $MyInvocation.MyCommand.Definition; -$UseDryRun = ""; -$UseMono = ""; -$TOOLS_DIR = Join-Path $PSScriptRoot "tools" -$NUGET_EXE = Join-Path $TOOLS_DIR "nuget.exe" -$NUGET_OLD_EXE = Join-Path $TOOLS_DIR "nuget_old.exe" -$CAKE_EXE = Join-Path $TOOLS_DIR "Cake/Cake.exe" -$NUGET_URL = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" -$NUGET_OLD_URL = "https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe" +Write-Output "PowerShell $($PSVersionTable.PSEdition) version $($PSVersionTable.PSVersion)" -# Should we use experimental build of Roslyn? -$UseExperimental = ""; -if ($Experimental.IsPresent) { - $UseExperimental = "--experimental" -} +Set-StrictMode -Version 2.0; $ErrorActionPreference = "Stop"; $ConfirmPreference = "None"; trap { Write-Error $_ -ErrorAction Continue; exit 1 } +$PSScriptRoot = Split-Path $MyInvocation.MyCommand.Path -Parent -# Is this a dry run? -if ($WhatIf.IsPresent) { - $UseDryRun = "--dryrun" -} +########################################################################### +# CONFIGURATION +########################################################################### -# Should we use mono? -if ($Mono.IsPresent) { - $UseMono = "--mono" -} +$BuildProjectFile = "$PSScriptRoot\nuke\_build.csproj" +$TempDirectory = "$PSScriptRoot\\.nuke\temp" -# Try download NuGet.exe if do not exist. -if (!(Test-Path $NUGET_EXE)) { - (New-Object System.Net.WebClient).DownloadFile($NUGET_URL, $NUGET_EXE) -} +$DotNetGlobalFile = "$PSScriptRoot\\global.json" +$DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" +$DotNetChannel = "Current" -# Try download NuGet.exe if do not exist. -if (!(Test-Path $NUGET_OLD_URL)) { - (New-Object System.Net.WebClient).DownloadFile($NUGET_OLD_URL, $NUGET_OLD_EXE) -} +$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 +$env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 +$env:DOTNET_MULTILEVEL_LOOKUP = 0 -# Make sure NuGet (latest) exists where we expect it. -if (!(Test-Path $NUGET_EXE)) { - Throw "Could not find nuget.exe" +########################################################################### +# EXECUTION +########################################################################### + +function ExecSafe([scriptblock] $cmd) { + & $cmd + if ($LASTEXITCODE) { exit $LASTEXITCODE } } -# Make sure NuGet (v3.5.0) exists where we expect it. -if (!(Test-Path $NUGET_OLD_EXE)) { - Throw "Could not find nuget_old.exe" +# If dotnet CLI is installed globally and it matches requested version, use for execution +if ($null -ne (Get-Command "dotnet" -ErrorAction SilentlyContinue) -and ` + $(dotnet --version) -and $LASTEXITCODE -eq 0) { + $env:DOTNET_EXE = (Get-Command "dotnet").Path } +else { + # Download install script + $DotNetInstallFile = "$TempDirectory\dotnet-install.ps1" + New-Item -ItemType Directory -Path $TempDirectory -Force | Out-Null + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + (New-Object System.Net.WebClient).DownloadFile($DotNetInstallUrl, $DotNetInstallFile) -# Restore tools from NuGet? -if (-Not $SkipToolPackageRestore.IsPresent) -{ - Push-Location - Set-Location $TOOLS_DIR - Invoke-Expression "$NUGET_EXE install -ExcludeVersion" - Pop-Location - if ($LASTEXITCODE -ne 0) { - exit $LASTEXITCODE + # If global.json exists, load expected version + if (Test-Path $DotNetGlobalFile) { + $DotNetGlobal = $(Get-Content $DotNetGlobalFile | Out-String | ConvertFrom-Json) + if ($DotNetGlobal.PSObject.Properties["sdk"] -and $DotNetGlobal.sdk.PSObject.Properties["version"]) { + $DotNetVersion = $DotNetGlobal.sdk.version + } } -} -# Make sure that Cake has been installed. -if (!(Test-Path $CAKE_EXE)) { - Throw "Could not find Cake.exe" + # Install by channel or version + $DotNetDirectory = "$TempDirectory\dotnet-win" + if (!(Test-Path variable:DotNetVersion)) { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Channel $DotNetChannel -NoPath } + } else { + ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } + } + $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" } -# Start Cake -Invoke-Expression "$CAKE_EXE `"$Script`" --target=`"$Target`" --configuration=`"$Configuration`" --verbosity=`"$Verbosity`" $UseMono $UseDryRun $UseExperimental $ScriptArgs" -exit $LASTEXITCODE \ No newline at end of file +Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" + +ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } +ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index c62ce5c..7534123 100755 --- a/build.sh +++ b/build.sh @@ -1,93 +1,62 @@ #!/usr/bin/env bash -############################################################### -# This is the Cake bootstrapper script that is responsible for -# downloading Cake and all specified tools from NuGet. -############################################################### -# Define directories. -SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) -TOOLS_DIR=$SCRIPT_DIR/tools -NUGET_EXE=$TOOLS_DIR/nuget.exe -NUGET_OLD_EXE=$TOOLS_DIR/nuget_old.exe -CAKE_EXE=$TOOLS_DIR/Cake/Cake.exe +bash --version 2>&1 | head -n 1 -# Define default arguments. -SCRIPT="build.cake" -TARGET="Default" -CONFIGURATION="Release" -VERBOSITY="verbose" -DRYRUN= -SHOW_VERSION=false -SCRIPT_ARGUMENTS=() +set -eo pipefail +SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd) -# Parse arguments. -for i in "$@"; do - case $1 in - -s|--script) SCRIPT="$2"; shift ;; - -t|--target) TARGET="$2"; shift ;; - -c|--configuration) CONFIGURATION="$2"; shift ;; - -v|--verbosity) VERBOSITY="$2"; shift ;; - -d|--dryrun) DRYRUN="--dryrun" ;; - --version) SHOW_VERSION=true ;; - --) shift; SCRIPT_ARGUMENTS+=("$@"); break ;; - *) SCRIPT_ARGUMENTS+=("$1") ;; - esac - shift -done +########################################################################### +# CONFIGURATION +########################################################################### -# Make sure the tools folder exist. -if [ ! -d $TOOLS_DIR ]; then - mkdir $TOOLS_DIR -fi +BUILD_PROJECT_FILE="$SCRIPT_DIR/nuke/_build.csproj" +TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" -# Make sure that packages.config exist. -if [ ! -f $TOOLS_DIR/packages.config ]; then - echo "Downloading packages.config..." - curl -Lsfo $TOOLS_DIR/packages.config http://cakebuild.net/bootstrapper/packages - if [ $? -ne 0 ]; then - echo "An error occured while downloading packages.config." - exit 1 - fi -fi +DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" +DOTNET_CHANNEL="Current" -# Download NuGet (v3.5.0) if it does not exist. -if [ ! -f $NUGET_OLD_EXE ]; then - echo "Downloading NuGet..." - curl -Lsfo $NUGET_OLD_EXE https://dist.nuget.org/win-x86-commandline/v3.5.0/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occured while downloading nuget.exe." - exit 1 - fi -fi +export DOTNET_CLI_TELEMETRY_OPTOUT=1 +export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 +export DOTNET_MULTILEVEL_LOOKUP=0 + +########################################################################### +# EXECUTION +########################################################################### -# Download NuGet (latest) if it does not exist. -if [ ! -f $NUGET_EXE ]; then - echo "Downloading NuGet..." - curl -Lsfo $NUGET_EXE https://dist.nuget.org/win-x86-commandline/latest/nuget.exe - if [ $? -ne 0 ]; then - echo "An error occured while downloading nuget.exe." - exit 1 +function FirstJsonValue { + perl -nle 'print $1 if m{"'"$1"'": "([^"]+)",?}' <<< "${@:2}" +} + +# If dotnet CLI is installed globally and it matches requested version, use for execution +if [ -x "$(command -v dotnet)" ] && dotnet --version &>/dev/null; then + export DOTNET_EXE="$(command -v dotnet)" +else + # Download install script + DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh" + mkdir -p "$TEMP_DIRECTORY" + curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL" + chmod +x "$DOTNET_INSTALL_FILE" + + # If global.json exists, load expected version + if [[ -f "$DOTNET_GLOBAL_FILE" ]]; then + DOTNET_VERSION=$(FirstJsonValue "version" "$(cat "$DOTNET_GLOBAL_FILE")") + if [[ "$DOTNET_VERSION" == "" ]]; then + unset DOTNET_VERSION + fi fi -fi -# Restore tools from NuGet. -pushd $TOOLS_DIR >/dev/null -mono $NUGET_EXE install -ExcludeVersion -if [ $? -ne 0 ]; then - echo "Could not restore NuGet packages." - exit 1 + # Install by channel or version + DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix" + if [[ -z ${DOTNET_VERSION+x} ]]; then + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path + else + "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path + fi + export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" fi -popd >/dev/null -# Make sure that Cake has been installed. -if [ ! -f $CAKE_EXE ]; then - echo "Could not find Cake.exe at '$CAKE_EXE'." - exit 1 -fi +echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" -# Start Cake -if $SHOW_VERSION; then - exec mono $CAKE_EXE --version -else - exec mono $CAKE_EXE $SCRIPT --verbosity=$VERBOSITY --configuration=$CONFIGURATION --target=$TARGET $DRYRUN "${SCRIPT_ARGUMENTS[@]}" -fi \ No newline at end of file +"$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet +"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/nuke/.editorconfig b/nuke/.editorconfig new file mode 100644 index 0000000..31e43dc --- /dev/null +++ b/nuke/.editorconfig @@ -0,0 +1,11 @@ +[*.cs] +dotnet_style_qualification_for_field = false:warning +dotnet_style_qualification_for_property = false:warning +dotnet_style_qualification_for_method = false:warning +dotnet_style_qualification_for_event = false:warning +dotnet_style_require_accessibility_modifiers = never:warning + +csharp_style_expression_bodied_methods = true:silent +csharp_style_expression_bodied_properties = true:warning +csharp_style_expression_bodied_indexers = true:warning +csharp_style_expression_bodied_accessors = true:warning diff --git a/nuke/Build.cs b/nuke/Build.cs new file mode 100644 index 0000000..43ecd98 --- /dev/null +++ b/nuke/Build.cs @@ -0,0 +1,298 @@ +using Microsoft.Build.Exceptions; +using Nuke.Common; +using Nuke.Common.CI.GitHubActions; +using Nuke.Common.IO; +using Nuke.Common.ProjectModel; +using Nuke.Common.Tools.DotNet; +using Nuke.Common.Tools.GitHub; +using Nuke.Common.Tools.NuGet; +using Nuke.Common.Utilities.Collections; +using Octokit; +using Octokit.Internal; +using Serilog; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Nuke.Common.Tooling; +using static Nuke.Common.IO.FileSystemTasks; +using static Nuke.Common.IO.PathConstruction; +using static Nuke.Common.Tools.DotNet.DotNetTasks; +using static Nuke.Common.Tools.NuGet.NuGetTasks; +using Project = Nuke.Common.ProjectModel.Project; + +class Build : NukeBuild +{ + /// Support plugins are available for: + /// - JetBrains ReSharper https://nuke.build/resharper + /// - JetBrains Rider https://nuke.build/rider + /// - Microsoft VisualStudio https://nuke.build/visualstudio + /// - Microsoft VSCode https://nuke.build/vscode + + public static int Main () => Execute(x => x.RunUnitTests); + + [Nuke.Common.Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")] + readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release; + + [Nuke.Common.Parameter("ReleaseNotesFilePath - To determine the SemanticVersion")] + readonly AbsolutePath ReleaseNotesFilePath = RootDirectory / "CHANGELOG.md"; + + [Solution] + readonly Solution Solution; + + string TargetProjectName => "AngleSharp.Js"; + + string TargetLibName => TargetProjectName; + + AbsolutePath SourceDirectory => RootDirectory / "src"; + + AbsolutePath BuildDirectory => SourceDirectory / TargetProjectName / "bin" / Configuration; + + AbsolutePath ResultDirectory => RootDirectory / "bin" / Version; + + AbsolutePath NugetDirectory => ResultDirectory / "nuget"; + + GitHubActions GitHubActions => GitHubActions.Instance; + + Project TargetProject { get; set; } + + // Note: The ChangeLogTasks from Nuke itself look buggy. So using the Cake source code. + IReadOnlyList ChangeLog { get; set; } + + ReleaseNotes LatestReleaseNotes { get; set; } + + SemVersion SemVersion { get; set; } + + string Version { get; set; } + + IReadOnlyCollection TargetFrameworks { get; set; } + + protected override void OnBuildInitialized() + { + var parser = new ReleaseNotesParser(); + + Log.Debug("Reading ChangeLog {FilePath}...", ReleaseNotesFilePath); + ChangeLog = parser.Parse(File.ReadAllText(ReleaseNotesFilePath)); + ChangeLog.NotNull("ChangeLog / ReleaseNotes could not be read!"); + + LatestReleaseNotes = ChangeLog.First(); + LatestReleaseNotes.NotNull("LatestVersion could not be read!"); + + Log.Debug("Using LastestVersion from ChangeLog: {LatestVersion}", LatestReleaseNotes.Version); + SemVersion = LatestReleaseNotes.SemVersion; + Version = LatestReleaseNotes.Version.ToString(); + + if (GitHubActions != null) + { + Log.Debug("Add Version Postfix if under CI - GithubAction(s)..."); + + var buildNumber = GitHubActions.RunNumber; + + if (ScheduledTargets.Contains(Default)) + { + Version = $"{Version}-ci.{buildNumber}"; + } + else if (ScheduledTargets.Contains(PrePublish)) + { + Version = $"{Version}-beta.{buildNumber}"; + } + } + + Log.Information("Building version: {Version}", Version); + + TargetProject = Solution.GetProject(SourceDirectory / TargetProjectName / $"{TargetLibName}.csproj" ); + TargetProject.NotNull("TargetProject could not be loaded!"); + + TargetFrameworks = TargetProject.GetTargetFrameworks(); + TargetFrameworks.NotNull("No TargetFramework(s) found to build for!"); + + Log.Information("Target Framework(s): {Frameworks}", TargetFrameworks); + } + + Target Clean => _ => _ + .Before(Restore) + .Executes(() => + { + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + }); + + Target Restore => _ => _ + .Executes(() => + { + DotNetRestore(s => s + .SetProjectFile(Solution)); + }); + + Target Compile => _ => _ + .DependsOn(Restore) + .Executes(() => + { + DotNetBuild(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore()); + }); + + Target RunUnitTests => _ => _ + .DependsOn(Compile) + .Executes(() => + { + DotNetTest(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore() + .EnableNoBuild() + .SetProcessEnvironmentVariable("prefetched", "false") + ); + + DotNetTest(s => s + .SetProjectFile(Solution) + .SetConfiguration(Configuration) + .EnableNoRestore() + .EnableNoBuild() + .SetProcessEnvironmentVariable("prefetched", "true") + ); + }); + + Target CopyFiles => _ => _ + .DependsOn(Compile) + .Executes(() => + { + foreach (var item in TargetFrameworks) + { + var targetDir = NugetDirectory / "lib" / item; + var srcDir = BuildDirectory / item; + + CopyFile(srcDir / $"{TargetProjectName}.dll", targetDir / $"{TargetProjectName}.dll", FileExistsPolicy.OverwriteIfNewer); + CopyFile(srcDir / $"{TargetProjectName}.pdb", targetDir / $"{TargetProjectName}.pdb", FileExistsPolicy.OverwriteIfNewer); + CopyFile(srcDir / $"{TargetProjectName}.xml", targetDir / $"{TargetProjectName}.xml", FileExistsPolicy.OverwriteIfNewer); + } + + CopyFile(SourceDirectory / $"{TargetProjectName}.nuspec", NugetDirectory / $"{TargetProjectName}.nuspec", FileExistsPolicy.OverwriteIfNewer); + CopyFile(RootDirectory / "logo.png", NugetDirectory / "logo.png", FileExistsPolicy.OverwriteIfNewer); + CopyFile(RootDirectory / "README.md", NugetDirectory / "README.md", FileExistsPolicy.OverwriteIfNewer); + }); + + Target CreatePackage => _ => _ + .DependsOn(CopyFiles) + .Executes(() => + { + var nuspec = NugetDirectory / $"{TargetProjectName}.nuspec"; + + NuGetPack(_ => _ + .SetTargetPath(nuspec) + .SetVersion(Version) + .SetOutputDirectory(NugetDirectory) + .SetSymbols(true) + .SetSymbolPackageFormat("snupkg") + .AddProperty("Configuration", Configuration) + ); + }); + + Target PublishPackage => _ => _ + .DependsOn(CreatePackage) + .DependsOn(RunUnitTests) + .Executes(() => + { + var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY"); + + + if (apiKey.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve the NuGet API key."); + } + + foreach (var nupkg in GlobFiles(NugetDirectory, "*.nupkg")) + { + NuGetPush(s => s + .SetTargetPath(nupkg) + .SetSource("https://api.nuget.org/v3/index.json") + .SetApiKey(apiKey)); + } + }); + + Target PublishPreRelease => _ => _ + .DependsOn(PublishPackage) + .Executes(() => + { + string gitHubToken; + + if (GitHubActions != null) + { + gitHubToken = GitHubActions.Token; + } + else + { + gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + } + + if (gitHubToken.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve GitHub token."); + } + + var credentials = new Credentials(gitHubToken); + + GitHubTasks.GitHubClient = new GitHubClient( + new ProductHeaderValue(nameof(NukeBuild)), + new InMemoryCredentialStore(credentials)); + + GitHubTasks.GitHubClient.Repository.Release + .Create("AngleSharp", TargetProjectName, new NewRelease(Version) + { + Name = Version, + Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes), + Prerelease = true, + TargetCommitish = "devel", + }); + }); + + Target PublishRelease => _ => _ + .DependsOn(PublishPackage) + .Executes(() => + { + string gitHubToken; + + if (GitHubActions != null) + { + gitHubToken = GitHubActions.Token; + } + else + { + gitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + } + + if (gitHubToken.IsNullOrEmpty()) + { + throw new BuildAbortedException("Could not resolve GitHub token."); + } + + var credentials = new Credentials(gitHubToken); + + GitHubTasks.GitHubClient = new GitHubClient( + new ProductHeaderValue(nameof(NukeBuild)), + new InMemoryCredentialStore(credentials)); + + GitHubTasks.GitHubClient.Repository.Release + .Create("AngleSharp", TargetProjectName, new NewRelease(Version) + { + Name = Version, + Body = String.Join(Environment.NewLine, LatestReleaseNotes.Notes), + Prerelease = false, + TargetCommitish = "main", + }); + }); + + Target Package => _ => _ + .DependsOn(RunUnitTests) + .DependsOn(CreatePackage); + + Target Default => _ => _ + .DependsOn(Package); + + Target Publish => _ => _ + .DependsOn(PublishRelease); + + Target PrePublish => _ => _ + .DependsOn(PublishPreRelease); +} diff --git a/nuke/Configuration.cs b/nuke/Configuration.cs new file mode 100644 index 0000000..9c08b1a --- /dev/null +++ b/nuke/Configuration.cs @@ -0,0 +1,16 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Nuke.Common.Tooling; + +[TypeConverter(typeof(TypeConverter))] +public class Configuration : Enumeration +{ + public static Configuration Debug = new Configuration { Value = nameof(Debug) }; + public static Configuration Release = new Configuration { Value = nameof(Release) }; + + public static implicit operator string(Configuration configuration) + { + return configuration.Value; + } +} diff --git a/nuke/Directory.Build.props b/nuke/Directory.Build.props new file mode 100644 index 0000000..e147d63 --- /dev/null +++ b/nuke/Directory.Build.props @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/nuke/Directory.Build.targets b/nuke/Directory.Build.targets new file mode 100644 index 0000000..2532609 --- /dev/null +++ b/nuke/Directory.Build.targets @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/nuke/Extensions/StringExtensions.cs b/nuke/Extensions/StringExtensions.cs new file mode 100644 index 0000000..5dd4b54 --- /dev/null +++ b/nuke/Extensions/StringExtensions.cs @@ -0,0 +1,80 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +// ReSharper disable once CheckNamespace +/// +/// Contains extension methods for . +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Core/Extensions/StringExtensions.cs +/// +public static class StringExtensions +{ + /// + /// Quotes the specified . + /// + /// The string to quote. + /// A quoted string. + public static string Quote(this string value) + { + if (!IsQuoted(value)) + { + value = string.Concat("\"", value, "\""); + } + + return value; + } + + /// + /// Unquote the specified . + /// + /// The string to unquote. + /// An unquoted string. + public static string UnQuote(this string value) + { + if (IsQuoted(value)) + { + value = value.Trim('"'); + } + + return value; + } + + /// + /// Splits the into lines. + /// + /// The string to split. + /// The lines making up the provided string. + public static string[] SplitLines(this string content) + { + content = NormalizeLineEndings(content); + return content.Split(new[] { "\r\n" }, StringSplitOptions.None); + } + + /// + /// Normalizes the line endings in a . + /// + /// The string to normalize line endings in. + /// A with normalized line endings. + public static string NormalizeLineEndings(this string value) + { + if (value != null) + { + value = value.Replace("\r\n", "\n"); + value = value.Replace("\r", string.Empty); + return value.Replace("\n", "\r\n"); + } + + return string.Empty; + } + + private static bool IsQuoted(this string value) + { + return value.StartsWith("\"", StringComparison.OrdinalIgnoreCase) + && value.EndsWith("\"", StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/nuke/ReleaseNotes.cs b/nuke/ReleaseNotes.cs new file mode 100644 index 0000000..01dcdad --- /dev/null +++ b/nuke/ReleaseNotes.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; + +/// +/// Represent release notes. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotes.cs +/// +public sealed class ReleaseNotes +{ + private readonly List _notes; + + /// + /// Gets the version. + /// + /// The version. + public SemVersion SemVersion { get; } + + /// + /// Gets the version. + /// + /// The version. + public Version Version { get; } + + /// + /// Gets the release notes. + /// + /// The release notes. + public IReadOnlyList Notes => _notes; + + /// + /// Gets the raw text of the line that was extracted from. + /// + /// The raw text of the Version line. + public string RawVersionLine { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The semantic version. + /// The notes. + /// The raw text of the version line. + public ReleaseNotes(SemVersion semVersion, IEnumerable notes, string rawVersionLine) + : this( + semVersion?.AssemblyVersion ?? throw new ArgumentNullException(nameof(semVersion)), + semVersion, + notes, + rawVersionLine) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The version. + /// The notes. + /// The raw text of the version line. + public ReleaseNotes(Version version, IEnumerable notes, string rawVersionLine) + : this( + version ?? throw new ArgumentNullException(nameof(version)), + new SemVersion(version.Major, version.Minor, version.Build), + notes, + rawVersionLine) + { + } + + private ReleaseNotes(Version version, SemVersion semVersion, IEnumerable notes, string rawVersionLine) + { + Version = version ?? throw new ArgumentNullException(nameof(version)); + SemVersion = semVersion ?? throw new ArgumentNullException(nameof(semVersion)); + RawVersionLine = rawVersionLine; + _notes = new List(notes ?? Enumerable.Empty()); + } +} \ No newline at end of file diff --git a/nuke/ReleaseNotesParser.cs b/nuke/ReleaseNotesParser.cs new file mode 100644 index 0000000..d911c90 --- /dev/null +++ b/nuke/ReleaseNotesParser.cs @@ -0,0 +1,146 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.RegularExpressions; +using Microsoft.Build.Exceptions; + +/// +/// The release notes parser. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/ReleaseNotesParser.cs +/// +public sealed class ReleaseNotesParser +{ + /// + /// Parses all release notes. + /// + /// The content. + /// All release notes. + public IReadOnlyList Parse(string content) + { + if (content == null) + { + throw new ArgumentNullException(nameof(content)); + } + + var lines = content.SplitLines(); + if (lines.Length > 0) + { + var line = lines[0].Trim(); + + if (line.StartsWith("#", StringComparison.OrdinalIgnoreCase)) + { + return ParseComplexFormat(lines); + } + + if (line.StartsWith("*", StringComparison.OrdinalIgnoreCase)) + { + return ParseSimpleFormat(lines); + } + } + + throw new BuildAbortedException("Unknown release notes format."); + } + + private IReadOnlyList ParseComplexFormat(string[] lines) + { + var lineIndex = 0; + var result = new List(); + + while (true) + { + if (lineIndex >= lines.Length) + { + break; + } + + // Create release notes. + var semVer = SemVersion.Zero; + var version = SemVersion.TryParse(lines[lineIndex], out semVer); + if (!version) + { + throw new BuildAbortedException("Could not parse version from release notes header."); + } + + var rawVersionLine = lines[lineIndex]; + + // Increase the line index. + lineIndex++; + + // Parse content. + var notes = new List(); + while (true) + { + // Sanity checks. + if (lineIndex >= lines.Length) + { + break; + } + + if (lines[lineIndex].StartsWith("#", StringComparison.OrdinalIgnoreCase)) + { + break; + } + + // Get the current line. + var line = (lines[lineIndex] ?? string.Empty).Trim('*').Trim(); + if (!string.IsNullOrWhiteSpace(line)) + { + notes.Add(line); + } + + lineIndex++; + } + + result.Add(new ReleaseNotes(semVer, notes, rawVersionLine)); + } + + return result.OrderByDescending(x => x.SemVersion).ToArray(); + } + + private IReadOnlyList ParseSimpleFormat(string[] lines) + { + var lineIndex = 0; + var result = new List(); + + while (true) + { + if (lineIndex >= lines.Length) + { + break; + } + + // Trim the current line. + var line = (lines[lineIndex] ?? string.Empty).Trim('*', ' '); + if (string.IsNullOrWhiteSpace(line)) + { + lineIndex++; + continue; + } + + // Parse header. + var semVer = SemVersion.Zero; + var version = SemVersion.TryParse(lines[lineIndex], out semVer); + if (!version) + { + throw new BuildAbortedException("Could not parse version from release notes header."); + } + + // Parse the description. + line = line.Substring(semVer.ToString().Length).Trim('-', ' '); + + // Add the release notes to the result. + result.Add(new ReleaseNotes(semVer, new[] { line }, line)); + + lineIndex++; + } + + return result.OrderByDescending(x => x.SemVersion).ToArray(); + } +} \ No newline at end of file diff --git a/nuke/SemVersion.cs b/nuke/SemVersion.cs new file mode 100644 index 0000000..4a9a715 --- /dev/null +++ b/nuke/SemVersion.cs @@ -0,0 +1,378 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Globalization; +using System.Text; +using System.Text.RegularExpressions; + +/// +/// Class for representing semantic versions. +/// +/// +/// Original from Cake build tool source: +/// https://github.com/cake-build/cake/blob/9828d7b246d332054896e52ba56983822feb3f05/src/Cake.Common/SemanticVersion.cs +/// +public class SemVersion : IComparable, IComparable, IEquatable +{ + /// + /// Gets the default version of a SemanticVersion. + /// + public static SemVersion Zero { get; } = new SemVersion(0, 0, 0, null, null, "0.0.0"); + + /// + /// Regex property for parsing a semantic version number. + /// + public static readonly Regex SemVerRegex = + new Regex( + @"(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*))(?:\.(?0|(?:[1-9]\d*)))?(?:\-(?[0-9A-Z\.-]+))?(?:\+(?[0-9A-Z\.-]+))?)?", + RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase); + + /// + /// Gets the major number of the version. + /// + public int Major { get; } + + /// + /// Gets the minor number of the version. + /// + public int Minor { get; } + + /// + /// Gets the patch number of the version. + /// + public int Patch { get; } + + /// + /// Gets the prerelease of the version. + /// + public string PreRelease { get; } + + /// + /// Gets the meta of the version. + /// + public string Meta { get; } + + /// + /// Gets a value indicating whether semantic version is a prerelease or not. + /// + public bool IsPreRelease { get; } + + /// + /// Gets a value indicating whether semantic version has meta or not. + /// + public bool HasMeta { get; } + + /// + /// Gets the VersionString of the semantic version. + /// + public string VersionString { get; } + + /// + /// Gets the AssemblyVersion of the semantic version. + /// + public Version AssemblyVersion { get; } + + /// + /// Initializes a new instance of the class. + /// + /// Major number. + /// Minor number. + /// Patch number. + /// Prerelease string. + /// Meta string. + public SemVersion(int major, int minor, int patch, string preRelease = null, string meta = null) : this(major, + minor, patch, preRelease, meta, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// Major number. + /// Minor number. + /// Patch number. + /// Prerelease string. + /// Meta string. + /// The complete version number. + public SemVersion(int major, int minor, int patch, string preRelease, string meta, string versionString) + { + Major = major; + Minor = minor; + Patch = patch; + AssemblyVersion = new Version(major, minor, patch); + IsPreRelease = !string.IsNullOrEmpty(preRelease); + HasMeta = !string.IsNullOrEmpty(meta); + PreRelease = IsPreRelease ? preRelease : null; + Meta = HasMeta ? meta : null; + + if (!string.IsNullOrEmpty(versionString)) + { + VersionString = versionString; + } + else + { + var sb = new StringBuilder(); + sb.AppendFormat(CultureInfo.InvariantCulture, "{0}.{1}.{2}", Major, Minor, Patch); + + if (IsPreRelease) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "-{0}", PreRelease); + } + + if (HasMeta) + { + sb.AppendFormat(CultureInfo.InvariantCulture, "+{0}", Meta); + } + + VersionString = sb.ToString(); + } + } + + /// + /// Method which tries to parse a semantic version string. + /// + /// the version that should be parsed. + /// the out parameter the parsed version should be stored in. + /// Returns a boolean indicating if the parse was successful. + public static bool TryParse(string version, + out SemVersion semVersion) + { + semVersion = Zero; + + if (string.IsNullOrEmpty(version)) + { + return false; + } + + var match = SemVerRegex.Match(version); + if (!match.Success) + { + return false; + } + + if (!int.TryParse( + match.Groups["Major"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var major) || + !int.TryParse( + match.Groups["Minor"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var minor) || + !int.TryParse( + match.Groups["Patch"].Value, + NumberStyles.Integer, + CultureInfo.InvariantCulture, + out var patch)) + { + return false; + } + + semVersion = new SemVersion( + major, + minor, + patch, + match.Groups["PreRelease"]?.Value, + match.Groups["Meta"]?.Value, + version); + + return true; + } + + /// + /// Checks if two SemVersion objects are equal. + /// + /// the other SemVersion want to test equality to. + /// A boolean indicating whether the objecst we're equal or not. + public bool Equals(SemVersion other) + { + return other is object + && Major == other.Major + && Minor == other.Minor + && Patch == other.Patch + && string.Equals(PreRelease, other.PreRelease, StringComparison.OrdinalIgnoreCase) + && string.Equals(Meta, other.Meta, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Compares to SemVersion objects to and another. + /// + /// The SemVersion object we compare with. + /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older. + public int CompareTo(SemVersion other) + { + if (other is null) + { + return 1; + } + + if (Equals(other)) + { + return 0; + } + + if (Major > other.Major) + { + return 1; + } + + if (Major < other.Major) + { + return -1; + } + + if (Minor > other.Minor) + { + return 1; + } + + if (Minor < other.Minor) + { + return -1; + } + + if (Patch > other.Patch) + { + return 1; + } + + if (Patch < other.Patch) + { + return -1; + } + + if (IsPreRelease != other.IsPreRelease) + { + return other.IsPreRelease ? 1 : -1; + } + + switch (StringComparer.InvariantCultureIgnoreCase.Compare(PreRelease, other.PreRelease)) + { + case 1: + return 1; + + case -1: + return -1; + + default: + { + return (string.IsNullOrEmpty(Meta) != string.IsNullOrEmpty(other.Meta)) + ? string.IsNullOrEmpty(Meta) ? 1 : -1 + : StringComparer.InvariantCultureIgnoreCase.Compare(Meta, other.Meta); + } + } + } + + /// + /// Compares to SemVersion objects to and another. + /// + /// The object we compare with. + /// Return 0 if the objects are identical, 1 if the version is newer and -1 if the version is older. + public int CompareTo(object obj) + { + return (obj is SemVersion semVersion) + ? CompareTo(semVersion) + : -1; + } + + /// + /// Equals-method for the SemVersion class. + /// + /// the other SemVersion want to test equality to. + /// A boolean indicating whether the objecst we're equal or not. + public override bool Equals(object obj) + { + return (obj is SemVersion semVersion) + && Equals(semVersion); + } + + /// + /// Method for getting the hashcode of the SemVersion object. + /// + /// The hashcode of the SemVersion object. + public override int GetHashCode() + { + unchecked + { + var hashCode = Major; + hashCode = (hashCode * 397) ^ Minor; + hashCode = (hashCode * 397) ^ Patch; + hashCode = (hashCode * 397) ^ + (PreRelease != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(PreRelease) : 0); + hashCode = (hashCode * 397) ^ (Meta != null ? StringComparer.OrdinalIgnoreCase.GetHashCode(Meta) : 0); + return hashCode; + } + } + + /// + /// Returns the string representation of an SemVersion object. + /// + /// The string representation of the object. + public override string ToString() + { + int[] verParts = { Major, Minor, Patch }; + string ver = string.Join(".", verParts); + return $"{ver}{(IsPreRelease ? "-" : string.Empty)}{PreRelease}{Meta}"; + } + + /// + /// The greater than-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was greater than operand2. + public static bool operator >(SemVersion operand1, SemVersion operand2) + => operand1 is { } && operand1.CompareTo(operand2) == 1; + + /// + /// The less than-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was less than operand2. + public static bool operator <(SemVersion operand1, SemVersion operand2) + => operand1 is { } + ? operand1.CompareTo(operand2) == -1 + : operand2 is { }; + + /// + /// The greater than or equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was greater than or equal to operand2. + public static bool operator >=(SemVersion operand1, SemVersion operand2) + => operand1 is { } + ? operand1.CompareTo(operand2) >= 0 + : operand2 is null; + + /// + /// The lesser than or equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was lesser than or equal to operand2. + public static bool operator <=(SemVersion operand1, SemVersion operand2) + => operand1 is null || operand1.CompareTo(operand2) <= 0; + + /// + /// The equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was equal to operand2. + public static bool operator ==(SemVersion operand1, SemVersion operand2) + => operand1?.Equals(operand2) ?? operand2 is null; + + /// + /// The not equal to-operator for the SemVersion class. + /// + /// first SemVersion. + /// second. SemVersion. + /// A value indicating if the operand1 was not equal to operand2. + public static bool operator !=(SemVersion operand1, SemVersion operand2) + => !(operand1?.Equals(operand2) ?? operand2 is null); +} \ No newline at end of file diff --git a/nuke/_build.csproj b/nuke/_build.csproj new file mode 100644 index 0000000..7521e1b --- /dev/null +++ b/nuke/_build.csproj @@ -0,0 +1,21 @@ + + + + Exe + net6.0 + + CS0649;CS0169 + .. + .. + 1 + + + + + + + + + + + From 7634f874c40020f55e7cfbca9169b79cffe2ab9e Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Fri, 1 Mar 2024 09:43:30 +0000 Subject: [PATCH 10/73] Fix after rebase --- src/AngleSharp.Js/Extensions/EngineExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index 2200831..599d4f8 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -51,7 +51,7 @@ public static JsValue ToJsValue(this Object obj, EngineInstance engine) var name = ((Enum)obj).GetOfficialName(); if (name != null) { - return new JsValue(name); + return JsValue.FromObjectWithType(engine.Jint, name, typeof(String)); } break; } From 2f711af0591756342feb0dc579469565f2ac4ed2 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Fri, 1 Mar 2024 10:06:41 +0000 Subject: [PATCH 11/73] Updated target frameworks --- src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj | 3 ++- src/AngleSharp.Js/AngleSharp.Js.csproj | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 84dd112..035fc32 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -1,7 +1,8 @@ AngleSharp.Js.Tests - netcoreapp3.1 + net6.0;net7.0;net8.0 + net462;net472;net6.0;net7.0;net8.0 true Key.snk false diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index e63eda6..8ac2c94 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -1,9 +1,9 @@ - + AngleSharp.Js AngleSharp.Js - netstandard2.0 - netstandard2.0;net462 + netstandard2.0;net6.0;net7.0;net8.0 + netstandard2.0;net462;net472;net6.0;net7.0;net8.0 true Key.snk true From 415344f6f57577fe38d4b6bcb838dae06118ca7d Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Fri, 1 Mar 2024 11:20:42 +0100 Subject: [PATCH 12/73] Update CI --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 810411b..2218216 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,9 +53,9 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x - 5.0.x 6.0.x + 7.0.x + 8.0.x - name: Build run: ./build.sh @@ -69,9 +69,9 @@ jobs: - uses: actions/setup-dotnet@v1 with: dotnet-version: | - 3.1.x - 5.0.x 6.0.x + 7.0.x + 8.0.x - name: Build run: | From d66bcb2c921b4569687f13640406147007a8d500 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Fri, 1 Mar 2024 11:37:11 +0100 Subject: [PATCH 13/73] Cosmetics for the repo --- CHANGELOG.md | 26 +++-- CONTRIBUTORS.md | 2 + LICENSE | 2 +- README.md | 4 +- src/AngleSharp.Js.nuspec | 34 ++++--- src/Directory.Build.props | 5 +- src/Key.snk | Bin 0 -> 596 bytes tools/anglesharp.cake | 199 -------------------------------------- 8 files changed, 48 insertions(+), 224 deletions(-) create mode 100644 src/Key.snk delete mode 100644 tools/anglesharp.cake diff --git a/CHANGELOG.md b/CHANGELOG.md index c5429e3..f3b1f32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +# 1.0.0 + +Released on ?. + +- Fixed usage of document ready state (#87) @Sebbs128 +- Updated to use AngleSharp v1 +- Updated for Jint v3 (#89) @tomvanenckevort + # 0.15.0 Released on Sunday, June 12 2021. @@ -16,12 +24,12 @@ Released on Tuesday, March 31 2020. Released on Friday, September 6 2019. -- Added thread-based event loop implementation `JsEventLoop` -- Included new `WithEventLoop` configuration extensions +- Fixed missing `btoa` and `atob` functions (#55) +- Added `javascript:` URL handler (#47) +- Added new `WithEventLoop` configuration extensions +- Added support for .NET Standard 1.3 (#58) - Added constructors to `window` (#12) -- Fixed `btoa` and `atob` missing (#55) -- Included `javascript:` URL handler (#47) -- Included support for .NET Standard 1.3 (#58) +- Added thread-based event loop implementation `JsEventLoop` # 0.12.1 @@ -34,13 +42,13 @@ Released on Wednesday, May 15 2019. Released on Tuesday, May 14 2019. - Properly forward setting window.location (#31) -- Respect window.onload event (#42) -- Support for more APIs to enable jQuery (#43) -- Respect DOMContentLoaded event (#50) - Restored compatibility with AngleSharp v0.12 (#51) - Renamed to `AngleSharp.Js` (focus only on JavaScript) (#51) - Renamed the `WithJavaScript` extension method to `WithJs` -- Changed the namespace from `AngleSharp.Scripting.JavaScript` to `AngleSharp.Js` +- Updated the namespace from `AngleSharp.Scripting.JavaScript` to `AngleSharp.Js` +- Added support for window.onload event (#42) +- Added support for more APIs to enable jQuery (#43) +- Added support for DOMContentLoaded event (#50) # 0.5.1 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 0850c88..19e5b64 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -8,6 +8,8 @@ AngleSharp.Js contains code written by (in order of first pull request / commit) * [Georgios Diamantopoulos](https://github.com/georgiosd) * [miroslav22](https://github.com/miroslav22) * [doominator42](https://github.com/doominator42) +* [Tom van Enckevort](https://github.com/tomvanenckevort) +* [Wayne Sebbens](https://github.com/Sebbs128) Without these awesome people AngleSharp.Js could not exist. Thanks to everyone for your contributions! :beers: diff --git a/LICENSE b/LICENSE index fddded2..10b5b1c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 - 2021 AngleSharp +Copyright (c) 2013 - 2024 AngleSharp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index af0ffe6..2e010e0 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ # AngleSharp.Js -[![Build Status](https://img.shields.io/appveyor/ci/FlorianRappl/AngleSharp-Scripting.svg?style=flat-square)](https://ci.appveyor.com/project/FlorianRappl/AngleSharp-Scripting) +[![CI](https://github.com/AngleSharp/AngleSharp.Js/actions/workflows/ci.yml/badge.svg)](https://github.com/AngleSharp/AngleSharp.Js/actions/workflows/ci.yml) [![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) @@ -83,7 +83,7 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) The MIT License (MIT) -Copyright (c) 2015 - 2020 AngleSharp +Copyright (c) 2015 - 2024 AngleSharp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/src/AngleSharp.Js.nuspec b/src/AngleSharp.Js.nuspec index 29ddcdd..0b8f08e 100644 --- a/src/AngleSharp.Js.nuspec +++ b/src/AngleSharp.Js.nuspec @@ -6,29 +6,39 @@ AngleSharp Florian Rappl MIT + https://anglesharp.github.io logo.png + README.md false Integrates a JavaScript engine to AngleSharp. https://github.com/AngleSharp/AngleSharp.Js/blob/master/CHANGELOG.md - Copyright 2017-2021, AngleSharp + Copyright 2017-2024s, AngleSharp html html5 css css3 dom javascript scripting library js scripts runtime jint anglesharp angle - - + + - - - - - - - + + + - - + + + + + + + + + + + + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d5d4785..f3d6f9d 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,9 @@ Integrates a JavaScript engine to AngleSharp. AngleSharp.Js - 0.15.0 + 1.0.0 + latest + true + $(MSBuildThisFileDirectory)\Key.snk \ No newline at end of file diff --git a/src/Key.snk b/src/Key.snk new file mode 100644 index 0000000000000000000000000000000000000000..d43ce417f37cbcb4f6f018dc1d617a7ab11da228 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa500962t@3pGD>qRz=;T$6U~d`^mi5gHYnhi1 ze9oAMM{)_*!k>XoB?#EPB_`>fjM8(x5m=*`Pada8Fziz)r+jbn0ij;rxU*`oMyt`h z;t~Fhy9aH3fNw^zTm7U>?*!`5W|?v6Y+q(mMr?S>H@F&}kvez15mHx^5_$!9!WV(jypeSq+n+yxVU z&A7+kLMZ`Yx;5y0&W2uDRgQV%p?ex -{ - Information($"Building version {version} of {projectName}."); - Information("For the publish target the following environment variables need to be set:"); - Information("- NUGET_API_KEY"); - Information("- GITHUB_API_TOKEN"); -}); - -// Tasks -// ---------------------------------------- - -Task("Clean") - .Does(() => - { - CleanDirectories(new DirectoryPath[] { buildDir, buildResultDir, nugetRoot }); - }); - -Task("Restore-Packages") - .IsDependentOn("Clean") - .Does(() => - { - NuGetRestore($"./src/{solutionName}.sln", new NuGetRestoreSettings - { - ToolPath = "tools/nuget.exe", - }); - }); - -Task("Build") - .IsDependentOn("Restore-Packages") - .Does(() => - { - ReplaceRegexInFiles("./src/Directory.Build.props", "(?<=)(.+?)(?=)", version); - DotNetCoreBuild($"./src/{solutionName}.sln", new DotNetCoreBuildSettings - { - Configuration = configuration, - }); - }); - -Task("Run-Unit-Tests") - .IsDependentOn("Build") - .Does(() => - { - var settings = new DotNetCoreTestSettings - { - Configuration = configuration, - }; - - if (isRunningOnGitHubActions) - { - settings.Loggers.Add("GitHubActions"); - } - - DotNetCoreTest($"./src/{solutionName}.Tests/", settings); - }); - -Task("Copy-Files") - .IsDependentOn("Build") - .Does(() => - { - foreach (var item in frameworks) - { - var targetDir = nugetRoot + Directory("lib") + Directory(item.Key); - CreateDirectory(targetDir); - CopyFiles(new FilePath[] - { - buildDir + Directory(item.Value) + File($"{projectName}.dll"), - buildDir + Directory(item.Value) + File($"{projectName}.xml"), - }, targetDir); - } - - CopyFiles(new FilePath[] { - $"src/{projectName}.nuspec", - "logo.png" - }, nugetRoot); - }); - -Task("Create-Package") - .IsDependentOn("Copy-Files") - .Does(() => - { - var nugetExe = GetFiles("./tools/**/nuget.exe").FirstOrDefault() - ?? throw new InvalidOperationException("Could not find nuget.exe."); - - var nuspec = nugetRoot + File($"{projectName}.nuspec"); - - NuGetPack(nuspec, new NuGetPackSettings - { - Version = version, - OutputDirectory = nugetRoot, - Symbols = false, - Properties = new Dictionary - { - { "Configuration", configuration }, - }, - }); - }); - -Task("Publish-Package") - .IsDependentOn("Create-Package") - .IsDependentOn("Run-Unit-Tests") - .Does(() => - { - var apiKey = EnvironmentVariable("NUGET_API_KEY"); - - if (String.IsNullOrEmpty(apiKey)) - { - throw new InvalidOperationException("Could not resolve the NuGet API key."); - } - - foreach (var nupkg in GetFiles(nugetRoot.Path.FullPath + "/*.nupkg")) - { - NuGetPush(nupkg, new NuGetPushSettings - { - Source = "https://nuget.org/api/v2/package", - ApiKey = apiKey, - }); - } - }); - -Task("Publish-Release") - .IsDependentOn("Publish-Package") - .IsDependentOn("Run-Unit-Tests") - .Does(() => - { - var githubToken = EnvironmentVariable("GITHUB_TOKEN"); - - if (String.IsNullOrEmpty(githubToken)) - { - throw new InvalidOperationException("Could not resolve GitHub token."); - } - - var github = new GitHubClient(new ProductHeaderValue("AngleSharpCakeBuild")) - { - Credentials = new Credentials(githubToken), - }; - - var newRelease = github.Repository.Release; - newRelease.Create("AngleSharp", projectName, new NewRelease("v" + version) - { - Name = version, - Body = String.Join(Environment.NewLine, releaseNotes.Notes), - Prerelease = false, - TargetCommitish = "main", - }).Wait(); - }); - -// Targets -// ---------------------------------------- - -Task("Package") - .IsDependentOn("Run-Unit-Tests") - .IsDependentOn("Create-Package"); - -Task("Default") - .IsDependentOn("Package"); - -Task("Publish") - .IsDependentOn("Publish-Release"); - -Task("PrePublish") - .IsDependentOn("Publish-Package"); From e93f3f97db2e1ba31a6dd6399862edc21a90b305 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Fri, 8 Mar 2024 10:31:01 +0000 Subject: [PATCH 14/73] Added support for JS modules and importmaps --- .../AngleSharp.Js.Tests.csproj | 1 + src/AngleSharp.Js.Tests/Constants.cs | 159 ++++++++++++++++++ src/AngleSharp.Js.Tests/EcmaTests.cs | 81 +++++++++ .../Mocks/MockHttpClientRequester.cs | 42 +++++ src/AngleSharp.Js/AngleSharp.Js.csproj | 1 + src/AngleSharp.Js/EngineInstance.cs | 143 +++++++++++++++- .../Extensions/EngineExtensions.cs | 8 +- src/AngleSharp.Js/JsApiExtensions.cs | 6 +- src/AngleSharp.Js/JsImportMap.cs | 26 +++ src/AngleSharp.Js/JsModuleLoader.cs | 29 ++++ src/AngleSharp.Js/JsScriptingService.cs | 12 +- 11 files changed, 495 insertions(+), 13 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs create mode 100644 src/AngleSharp.Js/JsImportMap.cs create mode 100644 src/AngleSharp.Js/JsModuleLoader.cs diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 035fc32..01bfbe1 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -18,6 +18,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AngleSharp.Js.Tests/Constants.cs b/src/AngleSharp.Js.Tests/Constants.cs index d9be29d..81fa93d 100644 --- a/src/AngleSharp.Js.Tests/Constants.cs +++ b/src/AngleSharp.Js.Tests/Constants.cs @@ -421,5 +421,164 @@ get Default(){return Tn}static get DefaultType(){return Cn}static get NAME(){ret ,Js).defaultPrevented||(this._element.classList.add(so),this._queueCallback((()=>{this._element.classList.add(io),this._element.classList.remove(so,no),N.trigger(this._element,Zs)}),this._element,this._config.animation)))}dispose(){this._clearTimeout(),this.isShown()&&this._element.classList.remove(no),super.dispose()}isShown(){return this._element.classList.contains(no)}_maybeScheduleHide(){this._config.autohide&&(this._hasMouseInteraction||this._hasKeyboardInteraction||(this._timeout=setTimeout((()=>{this.hide()}) ,this._config.delay)))}_onInteraction(t,e){switch(t.type){case""mouseover"":case""mouseout"":this._hasMouseInteraction=e;break;case""focusin"":case""focusout"":this._hasKeyboardInteraction=e}if(e)return void this._clearTimeout();const i=t.relatedTarget;this._element===i||this._element.contains(i)||this._maybeScheduleHide()}_setListeners(){N.on(this._element,Xs,(t=>this._onInteraction(t,!0))),N.on(this._element,Ys,(t=>this._onInteraction(t,!1))),N.on(this._element,Us,(t=>this._onInteraction(t,!0))),N.on(this._element ,Gs,(t=>this._onInteraction(t,!1)))}_clearTimeout(){clearTimeout(this._timeout),this._timeout=null}static jQueryInterface(t){return this.each((function(){const e=ao.getOrCreateInstance(this,t);if(""string""==typeof t){if(void 0===e[t])throw new TypeError(`No method named ""${t}""`);e[t](this)}}))}}return R(ao),m(ao),{Alert:Q,Button:Y,Carousel:xt,Collapse:Bt,Dropdown:qi,Modal:On,Offcanvas:qn,Popover:us,ScrollSpy:Es,Tab:Ks,Toast:ao,Tooltip:cs}})); //# sourceMappingURL=bootstrap.bundle.min.js.map"; + + public static readonly string Jquery4_0_0_ESM = @"/*! jQuery v4.0.0-beta | (c) OpenJS Foundation and other contributors | jquery.org/license */function e(e,t){if(void 0===e||!e.document)throw Error(""jQuery requires a window with a document"");var n=[],r=Object.getPrototypeOf,i=n.slice,o=n.flat?function(e){return n.flat.call(e)}:function(e){return n.concat.apply([],e)},a=n.push,s=n.indexOf,u={},l=u.toString,c=u.hasOwnProperty,f=c.toString,p=f.call(Object),d={};function h(e){return null==e?e+"""":""object""==typeof e?u[l.call(e)]||""object"":typeof e} +function g(e){return null!=e&&e===e.window}function v(e){var t=!!e&&e.length,n=h(e);return!(""function""==typeof e||g(e))&&(""array""===n||0===t||""number""==typeof t&&t>0&&t-1 in e)}var y=e.document,m={type:!0,src:!0,nonce:!0,noModule:!0};function x(e,t,n){var r,i=(n=n||y).createElement(""script"");for(r in i.text=e,m)t&&t[r]&&(i[r]=t[r]);n.head.appendChild(i).parentNode&&i.parentNode.removeChild(i)}var b=""4.0.0-beta"",w=/HTML$/i,T=function(e,t){return new T.fn.init(e,t)};function C(e,t){return + e.nodeName&&e.nodeName.toLowerCase()===t.toLowerCase()}T.fn=T.prototype={jquery:b,constructor:T,length:0,toArray:function(){return i.call(this)},get:function(e){return null==e?i.call(this):e<0?this[e+this.length]:this[e]},pushStack:function(e){var t=T.merge(this.constructor(),e);return t.prevObject=this,t},each:function(e){return T.each(this,e)},map:function(e){return this.pushStack(T.map(this,(function(t,n){return e.call(t,n,t)})))},slice:function(){return this.pushStack(i.apply(this, + arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(T.grep(this,(function(e,t){return(t+1)%2})))},odd:function(){return this.pushStack(T.grep(this,(function(e,t){return t%2})))},eq:function(e){var t=this.length,n=+e+(e<0?t:0);return this.pushStack(n>=0&&n+~]|""+E+"")""+E+""*""),N=RegExp(E+""|>""),O=/[+~]/,H=y.documentElement,L=H.matches||H.msMatchesSelector;function P(){var e=[];function t(n,r){return e.push(n+"" "")>T.expr.cacheLength&&delete t[e.shift()],t[n+"" ""]=r}return t}function R(e){return e&&void 0!==e.getElementsByTagName&&e}var M=""\\[""+E+""*(""+A+"")(?:""+E+""*([*^$|!~]?=)""+E+""*(?:'((?:\\\\.|[^\\\\'])*)'|\""((?:\\\\.|[^\\\\\""])*)\""|(""+A+""))|)""+E+ + ""*\\]"",W="":(""+A+"")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\""((?:\\\\.|[^\\\\\""])*)\"")|((?:\\\\.|[^\\\\()[\\]]|""+M+"")*)|.*)\\)|)"",I={ID:RegExp(""^#(""+A+"")""),CLASS:RegExp(""^\\.(""+A+"")""),TAG:RegExp(""^(""+A+""|[*])""),ATTR:RegExp(""^""+M),PSEUDO:RegExp(""^""+W),CHILD:RegExp(""^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(""+E+""*(even|odd|(([+-]|)(\\d*)n|)""+E+""*(?:([+-]|)""+E+""*(\\d+)|))""+E+""*\\)|)"",""i"")},$=new RegExp(W),F=RegExp(""\\\\[\\da-fA-F]{1,6}""+E+""?|\\\\([^\\r\\n\\f])"",""g""),B=function(e,t){var n=""0x""+ + e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))};function _(e){return e.replace(F,B)}function U(e){T.error(""Syntax error, unrecognized expression: ""+e)}var X=RegExp(""^""+E+""*,""+E+""*""),z=P();function V(e,t){var n,r,i,o,a,s,u,l=z[e+"" ""];if(l)return t?0:l.slice(0);a=e,s=[],u=T.expr.preFilter;while(a){for(o in(!n||(r=X.exec(a)))&&(r&&(a=a.slice(r[0].length)||a),s.push(i=[])),n=!1,(r=q.exec(a))&&(n=r.shift(),i.push({value:n,type:r[0].replace( + D,"" "")}),a=a.slice(n.length)),I)(r=T.expr.match[o].exec(a))&&(!u[o]||(r=u[o](r)))&&(n=r.shift(),i.push({value:n,type:o,matches:r}),a=a.slice(n.length));if(!n)break}return t?a.length:a?U(e):z(e,s).slice(0)}function Y(e){for(var t=0,n=e.length,r="""";t1)},removeAttr:function(e){return this.each((function(){T.removeAttr(this,e)}))}}),T.extend({attr:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o){if(void 0===e.getAttribute)return T.prop(e,t,n);if(1===o&&T.isXMLDoc(e)||(i=T.attrHooks[t.toLowerCase()]),void 0!==n){if( + null===n){T.removeAttr(e,t);return}return i&&""set""in i&&void 0!==(r=i.set(e,n,t))?r:(e.setAttribute(t,n),n)}return i&&""get""in i&&null!==(r=i.get(e,t))?r:null==(r=e.getAttribute(t))?void 0:r}},attrHooks:{},removeAttr:function(e,t){var n,r=0,i=t&&t.match(Q);if(i&&1===e.nodeType)while(n=i[r++])e.removeAttribute(n)}}),k&&(T.attrHooks.type={set:function(e,t){if(""radio""===t&&C(e,""input"")){var n=e.value;return e.setAttribute(""type"",t),n&&(e.value=n),t}}}),T.each(""checked selected async autofocus autoplay controls defer disabled hidden ismap loop multiple open readonly required scoped"".split("" ""),(function(e,t){T.attrHooks[t]={get:function(e){return null!=e.getAttribute(t)?t.toLowerCase():null},set:function(e,t,n){return!1===t?T.removeAttr(e,n):e.setAttribute(n,n),n}}}));var J=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\x80-\uFFFF\w-]/g;function K(e,t){return t?""\0""===e?"" "":e.slice(0,-1)+""\\""+e.charCodeAt(e.length-1).toString(16)+"" "":""\\""+e}T.escapeSelector=function(e){return(e+"""").replace(J,K)};var + Z=n.sort,ee=n.splice;function te(e,t){if(e===t)return ne=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)?e==y||e.ownerDocument==y&&T.contains(y,e)?-1:t==y||t.ownerDocument==y&&T.contains(y,t)?1:0:4&n?-1:1)}T.uniqueSort=function(e){var t,n=[],r=0,i=0;if(ne=!1,Z.call(e,te),ne){while(t=e[i++])t===e[i]&&(r=n.push(i));while(r--)ee.call(e,n[r],1)}return e},T.fn.uniqueSort=function(){return + this.pushStack(T.uniqueSort(i.apply(this)))};var ne,re,ie,oe,ae,se,ue=0,le=0,ce=P(),fe=P(),pe=P(),de=RegExp(E+""+"",""g""),he=RegExp(""^""+A+""$""),ge=T.extend({needsContext:RegExp(""^""+E+""*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(""+E+""*((?:-\\d)?\\d*)""+E+""*\\)|)(?=[^-]|$)"",""i"")},I),ve=/^(?:input|select|textarea|button)$/i,ye=/^h\d$/i,me=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,xe=function(){Ee()},be=Se((function(e){return!0===e.disabled&&C(e,""fieldset"")}),{dir:""parentNode"",next:""legend""});function we(e, + t,n,r){var i,o,s,u,l,c,f,p=t&&t.ownerDocument,d=t?t.nodeType:9;if(n=n||[],""string""!=typeof e||!e||1!==d&&9!==d&&11!==d)return n;if(!r&&(Ee(t),t=t||oe,se)){if(11!==d&&(l=me.exec(e))){if(i=l[1]){if(9===d)return(s=t.getElementById(i))&&a.call(n,s),n;if(p&&(s=p.getElementById(i))&&T.contains(t,s))return a.call(n,s),n}else if(l[2])return a.apply(n,t.getElementsByTagName(e)),n;else if((i=l[3])&&t.getElementsByClassName)return a.apply(n,t.getElementsByClassName(i)),n}if(!pe[e+"" ""]&&(!S||!S.test(e))){ + if(f=e,p=t,1===d&&(N.test(e)||q.test(e))){((p=O.test(e)&&R(t.parentNode)||t)!=t||k)&&((u=t.getAttribute(""id""))?u=T.escapeSelector(u):t.setAttribute(""id"",u=T.expando)),o=(c=V(e)).length;while(o--)c[o]=(u?""#""+u:"":scope"")+"" ""+Y(c[o]);f=c.join("","")}try{return a.apply(n,p.querySelectorAll(f)),n}catch(t){pe(e,!0)}finally{u===T.expando&&t.removeAttribute(""id"")}}}return Ne(e.replace(D,""$1""),t,n,r)}function Te(e){return e[T.expando]=!0,e}function Ce(e){return function(t){if(""form""in t)return + t.parentNode&&!1===t.disabled?""label""in t?""label""in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||!e!==t.isDisabled&&be(t)===e:t.disabled===e;return""label""in t&&t.disabled===e}}function je(e){return Te((function(t){return t=+t,Te((function(n,r){var i,o=e([],n.length,t),a=o.length;while(a--)n[i=o[a]]&&(n[i]=!(r[i]=n[i]))}))}))}function Ee(e){var t,n=e?e.ownerDocument||e:y;n!=oe&&9===n.nodeType&&(ae=(oe=n).documentElement,se=!T.isXMLDoc(oe),k&&y!=oe&&(t=oe.defaultView) + &&t.top!==t&&t.addEventListener(""unload"",xe))}for(re in we.matches=function(e,t){return we(e,null,null,t)},we.matchesSelector=function(e,t){if(Ee(e),se&&!pe[t+"" ""]&&(!S||!S.test(t)))try{return L.call(e,t)}catch(e){pe(t,!0)}return we(t,oe,null,[e]).length>0},T.expr={cacheLength:50,createPseudo:Te,match:ge,find:{ID:function(e,t){if(void 0!==t.getElementById&&se){var n=t.getElementById(e);return n?[n]:[]}},TAG:function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e): + t.querySelectorAll(e)},CLASS:function(e,t){if(void 0!==t.getElementsByClassName&&se)return t.getElementsByClassName(e)}},relative:{"">"":{dir:""parentNode"",first:!0},"" "":{dir:""parentNode""},""+"":{dir:""previousSibling"",first:!0},""~"":{dir:""previousSibling""}},preFilter:{ATTR:function(e){return e[1]=_(e[1]),e[3]=_(e[3]||e[4]||e[5]||""""),""~=""===e[2]&&(e[3]="" ""+e[3]+"" ""),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),""nth""===e[1].slice(0,3)?(e[3]||U(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*( + ""even""===e[3]||""odd""===e[3])),e[5]=+(e[7]+e[8]||""odd""===e[3])):e[3]&&U(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return I.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"""":n&&$.test(n)&&(t=V(n,!0))&&(t=n.indexOf("")"",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{ID:function(e){var t=_(e);return function(e){return e.getAttribute(""id"")===t}},TAG:function(e){var t=_(e).toLowerCase();return""*""===e?function(){return!0}:function(e){return C(e,t)}},CLASS: + function(e){var t=ce[e+"" ""];return t||(t=RegExp(""(^|""+E+"")""+e+""(""+E+""|$)""),ce(e,(function(e){return t.test(""string""==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute(""class"")||"""")})))},ATTR:function(e,t,n){return function(r){var i=T.attr(r,e);return null==i?""!=""===t:!t||((i+="""",""=""===t)?i===n:""!=""===t?i!==n:""^=""===t?n&&0===i.indexOf(n):""*=""===t?n&&i.indexOf(n)>-1:""$=""===t?n&&i.slice(-n.length)===n:""~=""===t?("" ""+i.replace(de,"" "")+"" "").indexOf(n)>-1:""|=""===t&&(i===n|| + i.slice(0,n.length+1)===n+""-""))}},CHILD:function(e,t,n,r,i){var o=""nth""!==e.slice(0,3),a=""last""!==e.slice(-4),s=""of-type""===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,u){var l,c,f,p,d,h=o!==a?""nextSibling"":""previousSibling"",g=t.parentNode,v=s&&t.nodeName.toLowerCase(),y=!u&&!s,m=!1;if(g){if(o){while(h){f=t;while(f=f[h])if(s?C(f,v):1===f.nodeType)return!1;d=h=""only""===e&&!d&&""nextSibling""}return!0}if(d=[a?g.firstChild:g.lastChild],a&&y){m=(p=(l=(c=g[T.expando]||(g[ + T.expando]={}))[e]||[])[0]===ue&&l[1])&&l[2],f=p&&g.childNodes[p];while(f=++p&&f&&f[h]||(m=p=0)||d.pop())if(1===f.nodeType&&++m&&f===t){c[e]=[ue,p,m];break}}else if(y&&(m=p=(l=(c=t[T.expando]||(t[T.expando]={}))[e]||[])[0]===ue&&l[1]),!1===m){while(f=++p&&f&&f[h]||(m=p=0)||d.pop())if((s?C(f,v):1===f.nodeType)&&++m&&(y&&((c=f[T.expando]||(f[T.expando]={}))[e]=[ue,m]),f===t))break}return(m-=i)===r||m%r==0&&m/r>=0}}},PSEUDO:function(e,t){var n=T.expr.pseudos[e]||T.expr.setFilters[e.toLowerCase()]|| + U(""unsupported pseudo: ""+e);return n[T.expando]?n(t):n}},pseudos:{not:Te((function(e){var t=[],n=[],r=qe(e.replace(D,""$1""));return r[T.expando]?Te((function(e,t,n,i){var o,a=r(e,null,i,[]),s=e.length;while(s--)(o=a[s])&&(e[s]=!(t[s]=o))})):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}})),has:Te((function(e){return function(t){return we(e,t).length>0}})),contains:Te((function(e){return e=_(e),function(t){return(t.textContent||T.text(t)).indexOf(e)>-1}})),lang:Te((function(e){ + return he.test(e||"""")||U(""unsupported lang: ""+e),e=_(e).toLowerCase(),function(t){var n;do{if(n=se?t.lang:t.getAttribute(""xml:lang"")||t.getAttribute(""lang""))return(n=n.toLowerCase())===e||0===n.indexOf(e+""-"")}while((t=t.parentNode)&&1===t.nodeType);return!1}})),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===ae},focus:function(e){return e===oe.activeElement&&oe.hasFocus()&&!!(e.type||e.href||~e.tabIndex)},enabled:Ce(!1),disabled:Ce( + !0),checked:function(e){return C(e,""input"")&&!!e.checked||C(e,""option"")&&!!e.selected},selected:function(e){return k&&e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!T.expr.pseudos.empty(e)},header:function(e){return ye.test(e.nodeName)},input:function(e){return ve.test(e.nodeName)},button:function(e){return C(e,""input"")&&""button""===e.type||C(e,""button"")},text:function( + e){return C(e,""input"")&&""text""===e.type},first:je((function(){return[0]})),last:je((function(e,t){return[t-1]})),eq:je((function(e,t,n){return[n<0?n+t:n]})),even:je((function(e,t){for(var n=0;nt?t:n;--r>=0;)e.push(r);return e})),gt:je((function(e,t,n){for(var r=n<0?n+t:n;++r1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function Ae(e,t,n,r,i){for(var o,a=[],s=0,u=e.length,l=null!=t;s-1}),l,!0),d=[function(e,t,r){var i=!u&&(r||t!=ie)||((n=t).nodeType?f(e,t,r):p(e,t,r));return n=null,i}];c-1&&(e[f]=!(u[f]=d))}}else h=Ae(h===u?h.splice(y,h.length):h),o?o(null,u,h,c):a.apply(u,h)}))}(c>1&&De(d),c>1&&Y(t.slice(0,c-1).concat({value:"" ""===t[c-2].type?""*"":""""})).replace(D,""$1""),r,c0,r=l.length>0,i=function(e,t,i,o,s){var c,f,p,d=0,h=""0"",g=e&&[],v=[],y=ie,m=e|| + r&&T.expr.find.TAG(""*"",s),x=ue+=null==y?1:Math.random()||.1;for(s&&(ie=t==oe||t||s);null!=(c=m[h]);h++){if(r&&c){f=0,t||c.ownerDocument==oe||(Ee(c),i=!se);while(p=l[f++])if(p(c,t||oe,i)){a.call(o,c);break}s&&(ue=x)}n&&((c=!p&&c)&&d--,e&&g.push(c))}if(d+=h,n&&h!==d){f=0;while(p=u[f++])p(g,v,t,i);if(e){if(d>0)while(h--)g[h]||v[h]||(v[h]=j.call(o));v=Ae(v)}a.apply(o,v),s&&!e&&v.length>0&&d+u.length>1&&T.uniqueSort(o)}return s&&(ue=x,ie=y),g},n?Te(i):i))).selector=e}return c}function Ne(e,t,n,r){ + var i,o,s,u,l,c=""function""==typeof e&&e,f=!r&&V(e=c.selector||e);if(n=n||[],1===f.length){if((o=f[0]=f[0].slice(0)).length>2&&""ID""===(s=o[0]).type&&9===t.nodeType&&se&&T.expr.relative[o[1].type]){if(!(t=(T.expr.find.ID(_(s.matches[0]),t)||[])[0]))return n;c&&(t=t.parentNode),e=e.slice(o.shift().value.length)}i=ge.needsContext.test(e)?0:o.length;while(i--){if(s=o[i],T.expr.relative[u=s.type])break;if((l=T.expr.find[u])&&(r=l(_(s.matches[0]),O.test(o[0].type)&&R(t.parentNode)||t))){if(o.splice(i, + 1),!(e=r.length&&Y(o)))return a.apply(n,r),n;break}}}return(c||qe(e,f))(r,t,!se,n,!t||O.test(e)&&R(t.parentNode)||t),n}function Oe(e,t,n){var r=[],i=void 0!==n;while((e=e[t])&&9!==e.nodeType)if(1===e.nodeType){if(i&&T(e).is(n))break;r.push(e)}return r}function He(e,t){for(var n=[];e;e=e.nextSibling)1===e.nodeType&&e!==t&&n.push(e);return n}ke.prototype=T.expr.filters=T.expr.pseudos,T.expr.setFilters=new ke,Ee(),T.find=we,we.compile=qe,we.select=Ne,we.setDocument=Ee,we.tokenize=V;var + Le=T.expr.match.needsContext,Pe=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function Re(e){return""<""===e[0]&&"">""===e[e.length-1]&&e.length>=3}function Me(e,t,n){return""function""==typeof t?T.grep(e,(function(e,r){return!!t.call(e,r,e)!==n})):t.nodeType?T.grep(e,(function(e){return e===t!==n})):""string""!=typeof t?T.grep(e,(function(e){return s.call(t,e)>-1!==n})):T.filter(t,e,n)}T.filter=function(e,t,n){var r=t[0];return(n&&(e="":not(""+e+"")""),1===t.length&&1===r.nodeType) + ?T.find.matchesSelector(r,e)?[r]:[]:T.find.matches(e,T.grep(t,(function(e){return 1===e.nodeType})))},T.fn.extend({find:function(e){var t,n,r=this.length,i=this;if(""string""!=typeof e)return this.pushStack(T(e).filter((function(){for(t=0;t1?T.uniqueSort(n):n},filter:function(e){return this.pushStack(Me(this,e||[],!1))},not:function(e){return this.pushStack(Me(this,e||[],!0))},is:function( + e){return!!Me(this,""string""==typeof e&&Le.test(e)?T(e):e||[],!1).length}});var We,Ie=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(T.fn.init=function(e,t){var n,r;if(!e)return this;if(e.nodeType)return this[0]=e,this.length=1,this;if(""function""==typeof e)return void 0!==We.ready?We.ready(e):e(T);if(Re(n=e+""""))n=[null,e,null];else{if(""string""!=typeof e)return T.makeArray(e,this);n=Ie.exec(e)}if(n&&(n[1]||!t)){if(!n[1])return(r=y.getElementById(n[2]))&&(this[0]=r,this.length=1),this;if(t=t instanceof + T?t[0]:t,T.merge(this,T.parseHTML(n[1],t&&t.nodeType?t.ownerDocument||t:y,!0)),Pe.test(n[1])&&T.isPlainObject(t))for(n in t)""function""==typeof this[n]?this[n](t[n]):this.attr(n,t[n]);return this}return!t||t.jquery?(t||We).find(e):this.constructor(t).find(e)}).prototype=T.fn,We=T(y);var $e=/^(?:parents|prev(?:Until|All))/,Fe={children:!0,contents:!0,next:!0,prev:!0};function Be(e,t){while((e=e[t])&&1!==e.nodeType);return e}function _e(e){return e}function Ue(e){throw e}function Xe(e,t,n,r){var i; + try{e&&""function""==typeof(i=e.promise)?i.call(e).done(t).fail(n):e&&""function""==typeof(i=e.then)?i.call(e,t,n):t.apply(void 0,[e].slice(r))}catch(e){n(e)}}T.fn.extend({has:function(e){var t=T(e,this),n=t.length;return this.filter((function(){for(var e=0;e-1: + 1===n.nodeType&&T.find.matchesSelector(n,e))){o.push(n);break}}return this.pushStack(o.length>1?T.uniqueSort(o):o)},index:function(e){return e?""string""==typeof e?s.call(T(e),this[0]):s.call(this,e.jquery?e[0]:e):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(e,t){return this.pushStack(T.uniqueSort(T.merge(this.get(),T(e,t))))},addBack:function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}}),T.each({parent:function(e){var t=e.parentNode;return + t&&11!==t.nodeType?t:null},parents:function(e){return Oe(e,""parentNode"")},parentsUntil:function(e,t,n){return Oe(e,""parentNode"",n)},next:function(e){return Be(e,""nextSibling"")},prev:function(e){return Be(e,""previousSibling"")},nextAll:function(e){return Oe(e,""nextSibling"")},prevAll:function(e){return Oe(e,""previousSibling"")},nextUntil:function(e,t,n){return Oe(e,""nextSibling"",n)},prevUntil:function(e,t,n){return Oe(e,""previousSibling"",n)},siblings:function(e){return He((e.parentNode||{}) + .firstChild,e)},children:function(e){return He(e.firstChild)},contents:function(e){return null!=e.contentDocument&&r(e.contentDocument)?e.contentDocument:(C(e,""template"")&&(e=e.content||e),T.merge([],e.childNodes))}},(function(e,t){T.fn[e]=function(n,r){var i=T.map(this,t,n);return""Until""!==e.slice(-5)&&(r=n),r&&""string""==typeof r&&(i=T.filter(r,i)),this.length>1&&(Fe[e]||T.uniqueSort(i),$e.test(e)&&i.reverse()),this.pushStack(i)}})),T.Callbacks=function(e){e=""string""==typeof e?(t=e,n={},T.each( + t.match(Q)||[],(function(e,t){n[t]=!0})),n):T.extend({},e);var t,n,r,i,o,a,s=[],u=[],l=-1,c=function(){for(a=a||e.once,o=r=!0;u.length;l=-1){i=u.shift();while(++l-1)s.splice(n,1),n<=l&&l--})),this},has:function(e){return e?T.inArray(e,s)>-1:s.length>0},empty:function(){return s&&(s=[]),this},disable:function(){return a=u=[],s=i="""",this},disabled:function(){return!s},lock:function(){return a=u=[],i||r||(s=i=""""),this},locked:function(){return!!a},fireWith:function(e,t){return a||(t=[e,(t=t||[]).slice?t.slice():t],u.push(t),r||c()),this},fire:function(){return f.fireWith( + this,arguments),this},fired:function(){return!!o}};return f},T.extend({Deferred:function(t){var n=[[""notify"",""progress"",T.Callbacks(""memory""),T.Callbacks(""memory""),2],[""resolve"",""done"",T.Callbacks(""once memory""),T.Callbacks(""once memory""),0,""resolved""],[""reject"",""fail"",T.Callbacks(""once memory""),T.Callbacks(""once memory""),1,""rejected""]],r=""pending"",i={state:function(){return r},always:function(){return o.done(arguments).fail(arguments),this},catch:function(e){return i.then(null,e)},pipe: + function(){var e=arguments;return T.Deferred((function(t){T.each(n,(function(n,r){var i=""function""==typeof e[r[4]]&&e[r[4]];o[r[1]]((function(){var e=i&&i.apply(this,arguments);e&&""function""==typeof e.promise?e.promise().progress(t.notify).done(t.resolve).fail(t.reject):t[r[0]+""With""](this,i?[e]:arguments)}))})),e=null})).promise()},then:function(t,r,i){var o=0;function a(t,n,r,i){return function(){var s=this,u=arguments,l=function(){var e,l;if(!(t=o&&(r!==Ue&&(s=void 0,u=[e]),n.rejectWith(s,u))}};t?c():(T.Deferred.getErrorHook&&(c.error=T.Deferred.getErrorHook()),e.setTimeout( + c))}}return T.Deferred((function(e){n[0][3].add(a(0,e,""function""==typeof i?i:_e,e.notifyWith)),n[1][3].add(a(0,e,""function""==typeof t?t:_e)),n[2][3].add(a(0,e,""function""==typeof r?r:Ue))})).promise()},promise:function(e){return null!=e?T.extend(e,i):i}},o={};return T.each(n,(function(e,t){var a=t[2],s=t[5];i[t[1]]=a.add,s&&a.add((function(){r=s}),n[3-e][2].disable,n[3-e][3].disable,n[0][2].lock,n[0][3].lock),a.add(t[3].fire),o[t[0]]=function(){return o[t[0]+""With""](this===o?void 0:this, + arguments),this},o[t[0]+""With""]=a.fireWith})),i.promise(o),t&&t.call(o,o),o},when:function(e){var t=arguments.length,n=t,r=Array(n),o=i.call(arguments),a=T.Deferred(),s=function(e){return function(n){r[e]=this,o[e]=arguments.length>1?i.call(arguments):n,--t||a.resolveWith(r,o)}};if(t<=1&&(Xe(e,a.done(s(n)).resolve,a.reject,!t),""pending""===a.state()||""function""==typeof(o[n]&&o[n].then)))return a.then();while(n--)Xe(o[n],s(n),a.reject);return a.promise()}});var ze= + /^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;T.Deferred.exceptionHook=function(t,n){t&&ze.test(t.name)&&e.console.warn(""jQuery.Deferred exception"",t,n)},T.readyException=function(t){e.setTimeout((function(){throw t}))};var Ve=T.Deferred();function Ye(){y.removeEventListener(""DOMContentLoaded"",Ye),e.removeEventListener(""load"",Ye),T.ready()}T.fn.ready=function(e){return Ve.then(e).catch((function(e){T.readyException(e)})),this},T.extend({isReady:!1,readyWait:1,ready:function(e){!(!0===e?--T.readyWait:T.isReady)&&( + T.isReady=!0,!0!==e&&--T.readyWait>0||Ve.resolveWith(y,[T]))}}),T.ready.then=Ve.then,""loading""!==y.readyState?e.setTimeout(T.ready):(y.addEventListener(""DOMContentLoaded"",Ye),e.addEventListener(""load"",Ye));var Ge=/-([a-z])/g;function Qe(e,t){return t.toUpperCase()}function Je(e){return e.replace(Ge,Qe)}function Ke(e){return 1===e.nodeType||9===e.nodeType||!+e.nodeType}function Ze(){this.expando=T.expando+Ze.uid++}Ze.uid=1,Ze.prototype={cache:function(e){var t=e[this.expando];return!t&&( + t=Object.create(null),Ke(e)&&(e.nodeType?e[this.expando]=t:Object.defineProperty(e,this.expando,{value:t,configurable:!0}))),t},set:function(e,t,n){var r,i=this.cache(e);if(""string""==typeof t)i[Je(t)]=n;else for(r in t)i[Je(r)]=t[r];return n},get:function(e,t){return void 0===t?this.cache(e):e[this.expando]&&e[this.expando][Je(t)]},access:function(e,t,n){return void 0===t||t&&""string""==typeof t&&void 0===n?this.get(e,t):(this.set(e,t,n),void 0!==n?n:t)},remove:function(e,t){var n,r=e[ + this.expando];if(void 0!==r){if(void 0!==t){n=(t=Array.isArray(t)?t.map(Je):(t=Je(t))in r?[t]:t.match(Q)||[]).length;while(n--)delete r[t[n]]}(void 0===t||T.isEmptyObject(r))&&(e.nodeType?e[this.expando]=void 0:delete e[this.expando])}},hasData:function(e){var t=e[this.expando];return void 0!==t&&!T.isEmptyObject(t)}};var et=new Ze,tt=new Ze,nt=/^(?:\{[\w\W]*\}|\[[\w\W]*\])$/,rt=/[A-Z]/g;function it(e,t,n){var r,i;if(void 0===n&&1===e.nodeType){if(r=""data-""+t.replace(rt,""-$&"").toLowerCase(), + ""string""==typeof(n=e.getAttribute(r))){try{i=n,n=""true""===i||""false""!==i&&(""null""===i?null:i===+i+""""?+i:nt.test(i)?JSON.parse(i):i)}catch(e){}tt.set(e,t,n)}else n=void 0}return n}T.extend({hasData:function(e){return tt.hasData(e)||et.hasData(e)},data:function(e,t,n){return tt.access(e,t,n)},removeData:function(e,t){tt.remove(e,t)},_data:function(e,t,n){return et.access(e,t,n)},_removeData:function(e,t){et.remove(e,t)}}),T.fn.extend({data:function(e,t){var n,r,i,o=this[0],a=o&&o.attributes;if( + void 0===e){if(this.length&&(i=tt.get(o),1===o.nodeType&&!et.get(o,""hasDataAttrs""))){n=a.length;while(n--)a[n]&&0===(r=a[n].name).indexOf(""data-"")&&it(o,r=Je(r.slice(5)),i[r]);et.set(o,""hasDataAttrs"",!0)}return i}return""object""==typeof e?this.each((function(){tt.set(this,e)})):G(this,(function(t){var n;if(o&&void 0===t)return void 0!==(n=tt.get(o,e))||void 0!==(n=it(o,e))?n:void 0;this.each((function(){tt.set(this,e,t)}))}),null,t,arguments.length>1,null,!0)},removeData:function(e){return + this.each((function(){tt.remove(this,e)}))}}),T.extend({queue:function(e,t,n){var r;if(e)return t=(t||""fx"")+""queue"",r=et.get(e,t),n&&(!r||Array.isArray(n)?r=et.set(e,t,T.makeArray(n)):r.push(n)),r||[]},dequeue:function(e,t){t=t||""fx"";var n=T.queue(e,t),r=n.length,i=n.shift(),o=T._queueHooks(e,t);""inprogress""===i&&(i=n.shift(),r--),i&&(""fx""===t&&n.unshift(""inprogress""),delete o.stop,i.call(e,(function(){T.dequeue(e,t)}),o)),!r&&o&&o.empty.fire()},_queueHooks:function(e,t){var n=t+""queueHooks""; + return et.get(e,n)||et.set(e,n,{empty:T.Callbacks(""once memory"").add((function(){et.remove(e,[t+""queue"",n])}))})}}),T.fn.extend({queue:function(e,t){var n=2;return(""string""!=typeof e&&(t=e,e=""fx"",n--),arguments.length\x20\t\r\n\f]*)/i,bt={thead:[""table""],col:[""colgroup"",""table""],tr:[""tbody"",""table""],td:[""tr"",""tbody"",""table""]};function wt(e,t){var n;return(n=void 0!==e.getElementsByTagName?e.getElementsByTagName(t||""*""):void 0!==e.querySelectorAll?e.querySelectorAll(t||""*""):[],void 0===t||t&&C(e,t))?T.merge([e],n):n} + bt.tbody=bt.tfoot=bt.colgroup=bt.caption=bt.thead,bt.th=bt.td;var Tt=/^$|^module$|\/(?:java|ecma)script/i;function Ct(e,t){for(var n=0,r=e.length;n-1)s=s.appendChild(t.createElement(u[c]));s.innerHTML=T.htmlPrefilter(a),T.merge(p,s.childNodes),(s=f.firstChild).textContent=""""}else p.push(t.createTextNode(a))}f.textContent="""",d=0;while(a=p[d++]){if(i&&T.inArray(a,i)>-1){o&&o.push(a);continue}if(l=yt(a),s=wt(f.appendChild(a),""script""),l&&Ct(s),r){c=0;while(a=s[c++])Tt.test(a.type||"""")&&r.push(a)}}return f}function kt(e){return e.type=(null!==e.getAttribute(""type""))+""/""+e.type,e}function St(e){ + return""true/""===(e.type||"""").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute(""type""),e}function Dt(e,t,n,r){t=o(t);var i,a,s,u,l,c,f=0,p=e.length,d=p-1,h=t[0];if(""function""==typeof h)return e.each((function(i){var o=e.eq(i);t[0]=h.call(this,i,o.html()),Dt(o,t,n,r)}));if(p&&(a=(i=Et(t,e[0].ownerDocument,!1,e,r)).firstChild,1===i.childNodes.length&&(i=a),a||r)){for(u=(s=T.map(wt(i,""script""),kt)).length;f=1)){for(;l!==this; + l=l.parentNode||this)if(1===l.nodeType&&!(""click""===e.type&&!0===l.disabled)){for(n=0,o=[],a={};n-1:T.find(i,this,null,[l]).length),a[i]&&o.push(r);o.length&&s.push({elem:l,handlers:o})}}return l=this,u0&&Ct(a,!u&&wt(e,""script"")),s},cleanData:function(e){for(var t,n,r,i=T.event.special,o=0;void 0!==(n=e[o]);o++)if(Ke(n)){if(t=n[et.expando]){ + if(t.events)for(r in t.events)i[r]?T.event.remove(n,r):T.removeEvent(n,r,t.handle);n[et.expando]=void 0}n[tt.expando]&&(n[tt.expando]=void 0)}}}),T.fn.extend({detach:function(e){return Wt(this,e,!0)},remove:function(e){return Wt(this,e)},text:function(e){return G(this,(function(e){return void 0===e?T.text(this):this.empty().each((function(){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&(this.textContent=e)}))}),null,e,arguments.length)},append:function(){return Dt(this,arguments,( + function(e){(1===this.nodeType||11===this.nodeType||9===this.nodeType)&&Rt(this,e).appendChild(e)}))},prepend:function(){return Dt(this,arguments,(function(e){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var t=Rt(this,e);t.insertBefore(e,t.firstChild)}}))},before:function(){return Dt(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e,this)}))},after:function(){return Dt(this,arguments,(function(e){this.parentNode&&this.parentNode.insertBefore(e, + this.nextSibling)}))},empty:function(){for(var e,t=0;null!=(e=this[t]);t++)1===e.nodeType&&(T.cleanData(wt(e,!1)),e.textContent="""");return this},clone:function(e,t){return e=null!=e&&e,t=null==t?e:t,this.map((function(){return T.clone(this,e,t)}))},html:function(e){return G(this,(function(e){var t=this[0]||{},n=0,r=this.length;if(void 0===e&&1===t.nodeType)return t.innerHTML;if(""string""==typeof e&&!Pt.test(e)&&!bt[(xt.exec(e)||["""",""""])[1].toLowerCase()]){e=T.htmlPrefilter(e);try{for(;nT.inArray(this,e)&&(T.cleanData(wt(this)),n&&n.replaceChild(t,this))}),e)}}),T.each({appendTo:""append"",prependTo:""prepend"",insertBefore:""before"",insertAfter:""after"",replaceAll:""replaceWith""},(function(e,t){T.fn[e]=function(e){for(var n,r=[],i=T(e),o=i.length-1,s=0;s<=o; + s++)n=s===o?this:this.clone(!0),T(i[s])[t](n),a.apply(r,n);return this.pushStack(r)}}));var It=RegExp(""^(""+ot+"")(?!px)[a-z%]+$"",""i""),$t=/^--/;function Ft(t){var n=t.ownerDocument.defaultView;return n||(n=e),n.getComputedStyle(t)}function Bt(e,t,n){var r,i=$t.test(t);return(n=n||Ft(e))&&(r=n.getPropertyValue(t)||n[t],i&&r&&(r=r.replace(D,""$1"")||void 0),""""!==r||yt(e)||(r=T.style(e,t))),void 0!==r?r+"""":r}var _t=[""Webkit"",""Moz"",""ms""],Ut=y.createElement(""div"").style,Xt={};function zt(e){return Xt[ + e]||(e in Ut?e:Xt[e]=function(e){var t=e[0].toUpperCase()+e.slice(1),n=_t.length;while(n--)if((e=_t[n]+t)in Ut)return e}(e)||e)}(tn=y.createElement(""div"")).style&&(d.reliableTrDimensions=function(){var t,n,r;if(null==en){if(t=y.createElement(""table""),n=y.createElement(""tr""),t.style.cssText=""position:absolute;left:-11111px;border-collapse:separate"",n.style.cssText=""box-sizing:content-box;border:1px solid"",n.style.height=""1px"",tn.style.height=""9px"",tn.style.display=""block"",H.appendChild(t) + .appendChild(n).appendChild(tn),0===t.offsetWidth){H.removeChild(t);return}en=parseInt((r=e.getComputedStyle(n)).height,10)+parseInt(r.borderTopWidth,10)+parseInt(r.borderBottomWidth,10)===n.offsetHeight,H.removeChild(t)}return en});var Vt=/^(none|table(?!-c[ea]).+)/,Yt={position:""absolute"",visibility:""hidden"",display:""block""},Gt={letterSpacing:""0"",fontWeight:""400""};function Qt(e,t,n){var r=at.exec(t);return r?Math.max(0,r[2]-(n||0))+(r[3]||""px""):t}function Jt(e,t,n,r,i,o){var a=""width""===t?1:0, + s=0,u=0,l=0;if(n===(r?""border"":""content""))return 0;for(;a<4;a+=2)""margin""===n&&(l+=T.css(e,n+st[a],!0,i)),r?(""content""===n&&(u-=T.css(e,""padding""+st[a],!0,i)),""margin""!==n&&(u-=T.css(e,""border""+st[a]+""Width"",!0,i))):(u+=T.css(e,""padding""+st[a],!0,i),""padding""!==n?u+=T.css(e,""border""+st[a]+""Width"",!0,i):s+=T.css(e,""border""+st[a]+""Width"",!0,i));return!r&&o>=0&&(u+=Math.max(0,Math.ceil(e[""offset""+t[0].toUpperCase()+t.slice(1)]-o-u-s-.5))||0),u+l}function Kt(e,t,n){var r=Ft(e),i=(k||n)&&""border-box""=== + T.css(e,""boxSizing"",!1,r),o=i,a=Bt(e,t,r),s=""offset""+t[0].toUpperCase()+t.slice(1);if(It.test(a)){if(!n)return a;a=""auto""}return(""auto""===a||k&&i||!d.reliableTrDimensions()&&C(e,""tr""))&&e.getClientRects().length&&(i=""border-box""===T.css(e,""boxSizing"",!1,r),(o=s in e)&&(a=e[s])),(a=parseFloat(a)||0)+Jt(e,t,n||(i?""border"":""content""),o,r,a)+""px""}function Zt(e,t,n,r,i){return new Zt.prototype.init(e,t,n,r,i)}T.extend({cssHooks:{},style:function(e,t,n,r){if( + e&&3!==e.nodeType&&8!==e.nodeType&&e.style){var i,o,a,s=ht(t),u=$t.test(t),l=e.style;if(u||(t=zt(s)),a=T.cssHooks[t]||T.cssHooks[s],void 0===n)return a&&""get""in a&&void 0!==(i=a.get(e,!1,r))?i:l[t];""string""==(o=typeof n)&&(i=at.exec(n))&&i[1]&&(n=pt(e,t,i),o=""number""),null!=n&&n==n&&(""number""===o&&(n+=i&&i[3]||(ft(s)?""px"":"""")),k&&""""===n&&0===t.indexOf(""background"")&&(l[t]=""inherit""),a&&""set""in a&&void 0===(n=a.set(e,n,r))||(u?l.setProperty(t,n):l[t]=n))}},css:function(e,t,n,r){var i,o,a,s=ht(t); + return($t.test(t)||(t=zt(s)),(a=T.cssHooks[t]||T.cssHooks[s])&&""get""in a&&(i=a.get(e,!0,n)),void 0===i&&(i=Bt(e,t,r)),""normal""===i&&t in Gt&&(i=Gt[t]),""""===n||n)?(o=parseFloat(i),!0===n||isFinite(o)?o||0:i):i}}),T.each([""height"",""width""],(function(e,t){T.cssHooks[t]={get:function(e,n,r){if(n)return!Vt.test(T.css(e,""display""))||e.getClientRects().length&&e.getBoundingClientRect().width?Kt(e,t,r):function(e,t,n){var r,i,o={};for(i in t)o[i]=e.style[i],e.style[i]=t[i];for(i in r=n.call(e),t) + e.style[i]=o[i];return r}(e,Yt,(function(){return Kt(e,t,r)}))},set:function(e,n,r){var i,o=Ft(e),a=r&&""border-box""===T.css(e,""boxSizing"",!1,o),s=r?Jt(e,t,r,a,o):0;return s&&(i=at.exec(n))&&""px""!==(i[3]||""px"")&&(e.style[t]=n,n=T.css(e,t)),Qt(e,n,s)}}})),T.each({margin:"""",padding:"""",border:""Width""},(function(e,t){T.cssHooks[e+t]={expand:function(n){for(var r=0,i={},o=""string""==typeof n?n.split("" ""):[n];r<4;r++)i[e+st[r]+t]=o[r]||o[r-2]||o[0];return i}},""margin""!==e&&(T.cssHooks[e+t].set=Qt)})), + T.fn.extend({css:function(e,t){return G(this,(function(e,t,n){var r,i,o={},a=0;if(Array.isArray(t)){for(r=Ft(e),i=t.length;a1)}}),T.Tween=Zt,Zt.prototype={constructor:Zt,init:function(e,t,n,r,i,o){this.elem=e,this.prop=n,this.easing=i||T.easing._default,this.options=t,this.start=this.now=this.cur(),this.end=r,this.unit=o||(ft(n)?""px"":"""")},cur:function(){var e=Zt.propHooks[this.prop]; + return e&&e.get?e.get(this):Zt.propHooks._default.get(this)},run:function(e){var t,n=Zt.propHooks[this.prop];return this.options.duration?this.pos=t=T.easing[this.easing](e,this.options.duration*e,0,1,this.options.duration):this.pos=t=e,this.now=(this.end-this.start)*t+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):Zt.propHooks._default.set(this),this}},Zt.prototype.init.prototype=Zt.prototype,Zt.propHooks={_default:{get:function(e){var t; + return 1!==e.elem.nodeType||null!=e.elem[e.prop]&&null==e.elem.style[e.prop]?e.elem[e.prop]:(t=T.css(e.elem,e.prop,""""))&&""auto""!==t?t:0},set:function(e){T.fx.step[e.prop]?T.fx.step[e.prop](e):1===e.elem.nodeType&&(T.cssHooks[e.prop]||null!=e.elem.style[zt(e.prop)])?T.style(e.elem,e.prop,e.now+e.unit):e.elem[e.prop]=e.now}}},T.easing={linear:function(e){return e},swing:function(e){return.5-Math.cos(e*Math.PI)/2},_default:""swing""},T.fx=Zt.prototype.init,T.fx.step={};var en,tn,nn,rn,on= + /^(?:toggle|show|hide)$/,an=/queueHooks$/;function sn(){return e.setTimeout((function(){nn=void 0})),nn=Date.now()}function un(e,t){var n,r=0,i={height:e};for(t=t?1:0;r<4;r+=2-t)i[""margin""+(n=st[r])]=i[""padding""+n]=e;return t&&(i.opacity=i.width=e),i}function ln(e,t,n){for(var r,i=(cn.tweeners[t]||[]).concat(cn.tweeners[""*""]),o=0,a=i.length;o1)},removeProp:function(e){return this.each((function(){delete this[T.propFix[e]||e]}))}}),T.extend({prop:function(e,t,n){var r,i,o=e.nodeType;if(3!==o&&8!==o&&2!==o)return(1===o&&T.isXMLDoc(e)||(t=T.propFix[t]||t,i=T.propHooks[t]),void 0!==n)?i&&""set""in i&&void 0!==(r=i.set(e,n,t))?r:e[t]=n:i&&""get""in + i&&null!==(r=i.get(e,t))?r:e[t]},propHooks:{tabIndex:{get:function(e){var t=e.getAttribute(""tabindex"");return t?parseInt(t,10):fn.test(e.nodeName)||pn.test(e.nodeName)&&e.href?0:-1}}},propFix:{for:""htmlFor"",class:""className""}}),k&&(T.propHooks.selected={get:function(e){var t=e.parentNode;return t&&t.parentNode&&t.parentNode.selectedIndex,null},set:function(e){var t=e.parentNode;t&&(t.selectedIndex,t.parentNode&&t.parentNode.selectedIndex)}}),T.each([""tabIndex"",""readOnly"",""maxLength"", + ""cellSpacing"",""cellPadding"",""rowSpan"",""colSpan"",""useMap"",""frameBorder"",""contentEditable""],(function(){T.propFix[this.toLowerCase()]=this})),T.fn.extend({addClass:function(e){var t,n,r,i,o,a;return""function""==typeof e?this.each((function(t){T(this).addClass(e.call(this,t,hn(this)))})):(t=gn(e)).length?this.each((function(){if(r=hn(this),n=1===this.nodeType&&"" ""+dn(r)+"" ""){for(o=0;on.indexOf("" ""+i+"" "")&&(n+=i+"" "");r!==(a=dn(n))&&this.setAttribute(""class"",a)}})):this}, + removeClass:function(e){var t,n,r,i,o,a;return""function""==typeof e?this.each((function(t){T(this).removeClass(e.call(this,t,hn(this)))})):arguments.length?(t=gn(e)).length?this.each((function(){if(r=hn(this),n=1===this.nodeType&&"" ""+dn(r)+"" ""){for(o=0;o-1)n=n.replace("" ""+i+"" "","" "")}r!==(a=dn(n))&&this.setAttribute(""class"",a)}})):this:this.attr(""class"","""")},toggleClass:function(e,t){var n,r,i,o;return""function""==typeof e?this.each((function(n){T( + this).toggleClass(e.call(this,n,hn(this),t),t)})):""boolean""==typeof t?t?this.addClass(e):this.removeClass(e):(n=gn(e)).length?this.each((function(){for(i=0,o=T(this);i-1)return!0;return!1}}),T.fn.extend({val:function(e){var t,n,r,i=this[0];return arguments.length?(r=""function""==typeof e,this.each((function(n){ + var i;1===this.nodeType&&(null==(i=r?e.call(this,n,T(this).val()):e)?i="""":""number""==typeof i?i+="""":Array.isArray(i)&&(i=T.map(i,(function(e){return null==e?"""":e+""""}))),(t=T.valHooks[this.type]||T.valHooks[this.nodeName.toLowerCase()])&&""set""in t&&void 0!==t.set(this,i,""value"")||(this.value=i))}))):i?(t=T.valHooks[i.type]||T.valHooks[i.nodeName.toLowerCase()])&&""get""in t&&void 0!==(n=t.get(i,""value""))?n:null==(n=i.value)?"""":n:void 0}}),T.extend({valHooks:{select:{get:function(e){var t,n,r, + i=e.options,o=e.selectedIndex,a=""select-one""===e.type,s=a?null:[],u=a?o+1:i.length;for(r=o<0?u:a?o:0;r-1)&&(n=!0);return n||(e.selectedIndex=-1),o}}}}),k&&(T.valHooks.option={get:function(e){var t=e.getAttribute(""value"");return + null!=t?t:dn(T.text(e))}}),T.each([""radio"",""checkbox""],(function(){T.valHooks[this]={set:function(e,t){if(Array.isArray(t))return e.checked=T.inArray(T(e).val(),t)>-1}}}));var vn=/^(?:focusinfocus|focusoutblur)$/,yn=function(e){e.stopPropagation()};T.extend(T.event,{trigger:function(t,n,r,i){var o,a,s,u,l,f,p,d,h=[r||y],v=c.call(t,""type"")?t.type:t,m=c.call(t,""namespace"")?t.namespace.split("".""):[];if(a=d=s=r=r||y,!(3===r.nodeType||8===r.nodeType||vn.test(v+T.event.triggered))&&(v.indexOf(""."")>- + 1&&(v=(m=v.split(""."")).shift(),m.sort()),l=0>v.indexOf("":"")&&""on""+v,(t=t[T.expando]?t:new T.Event(v,""object""==typeof t&&t)).isTrigger=i?2:3,t.namespace=m.join("".""),t.rnamespace=t.namespace?RegExp(""(^|\\.)""+m.join(""\\.(?:.*\\.|)"")+""(\\.|$)""):null,t.result=void 0,t.target||(t.target=r),n=null==n?[t]:T.makeArray(n,[t]),p=T.event.special[v]||{},i||!p.trigger||!1!==p.trigger.apply(r,n))){if(!i&&!p.noBubble&&!g(r)){for(u=p.delegateType||v,vn.test(u+v)||(a=a.parentNode);a;a=a.parentNode)h.push(a),s=a; + s===(r.ownerDocument||y)&&h.push(s.defaultView||s.parentWindow||e)}o=0;while((a=h[o++])&&!t.isPropagationStopped())d=a,t.type=o>1?u:p.bindType||v,(f=(et.get(a,""events"")||Object.create(null))[t.type]&&et.get(a,""handle""))&&f.apply(a,n),(f=l&&a[l])&&f.apply&&Ke(a)&&(t.result=f.apply(a,n),!1===t.result&&t.preventDefault());return t.type=v,!i&&!t.isDefaultPrevented()&&(!p._default||!1===p._default.apply(h.pop(),n))&&Ke(r)&&l&&""function""==typeof r[v]&&!g(r)&&((s=r[l])&&(r[l]=null),T.event.triggered=v, + t.isPropagationStopped()&&d.addEventListener(v,yn),r[v](),t.isPropagationStopped()&&d.removeEventListener(v,yn),T.event.triggered=void 0,s&&(r[l]=s)),t.result}},simulate:function(e,t,n){var r=T.extend(new T.Event,n,{type:e,isSimulated:!0});T.event.trigger(r,null,t)}}),T.fn.extend({trigger:function(e,t){return this.each((function(){T.event.trigger(e,t,this)}))},triggerHandler:function(e,t){var n=this[0];if(n)return T.event.trigger(e,t,n,!0)}});var mn=e.location,xn={guid:Date.now()},bn=/\?/; + T.parseXML=function(t){var n,r;if(!t||""string""!=typeof t)return null;try{n=(new e.DOMParser).parseFromString(t,""text/xml"")}catch(e){}return r=n&&n.getElementsByTagName(""parsererror"")[0],(!n||r)&&T.error(""Invalid XML: ""+(r?T.map(r.childNodes,(function(e){return e.textContent})).join(""\n""):t)),n};var wn=/\[\]$/,Tn=/\r?\n/g,Cn=/^(?:submit|button|image|reset|file)$/i,jn=/^(?:input|select|textarea|keygen)/i;T.param=function(e,t){var n,r=[],i=function(e,t){var n=""function""==typeof t?t():t;r[r.length] + =encodeURIComponent(e)+""=""+encodeURIComponent(null==n?"""":n)};if(null==e)return"""";if(Array.isArray(e)||e.jquery&&!T.isPlainObject(e))T.each(e,(function(){i(this.name,this.value)}));else for(n in e)!function e(t,n,r,i){var o;if(Array.isArray(n))T.each(n,(function(n,o){r||wn.test(t)?i(t,o):e(t+""[""+(""object""==typeof o&&null!=o?n:"""")+""]"",o,r,i)}));else if(r||""object""!==h(n))i(t,n);else for(o in n)e(t+""[""+o+""]"",n[o],r,i)}(n,e[n],t,i);return r.join(""&"")},T.fn.extend({serialize:function(){return + T.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var e=T.prop(this,""elements"");return e?T.makeArray(e):this})).filter((function(){var e=this.type;return this.name&&!T(this).is("":disabled"")&&jn.test(this.nodeName)&&!Cn.test(e)&&(this.checked||!At.test(e))})).map((function(e,t){var n=T(this).val();return null==n?null:Array.isArray(n)?T.map(n,(function(e){return{name:t.name,value:e.replace(Tn,""\r\n"")}})):{name:t.name,value:n.replace(Tn,""\r\n"")}})).get()}});var + En=/%20/g,kn=/#.*$/,Sn=/([?&])_=[^&]*/,Dn=/^(.*?):[ \t]*([^\r\n]*)$/gm,An=/^(?:GET|HEAD)$/,qn=/^\/\//,Nn={},On={},Hn=""*/"".concat(""*""),Ln=y.createElement(""a"");function Pn(e){return function(t,n){""string""!=typeof t&&(n=t,t=""*"");var r,i=0,o=t.toLowerCase().match(Q)||[];if(""function""==typeof n)while(r=o[i++])""+""===r[0]?(e[r=r.slice(1)||""*""]=e[r]||[]).unshift(n):(e[r]=e[r]||[]).push(n)}}function Rn(e,t,n,r){var i={},o=e===On;function a(s){var u;return i[s]=!0,T.each(e[s]||[],(function(e,s){var l=s(t, + n,r);return""string""!=typeof l||o||i[l]?o?!(u=l):void 0:(t.dataTypes.unshift(l),a(l),!1)})),u}return a(t.dataTypes[0])||!i[""*""]&&a(""*"")}function Mn(e,t){var n,r,i=T.ajaxSettings.flatOptions||{};for(n in t)void 0!==t[n]&&((i[n]?e:r||(r={}))[n]=t[n]);return r&&T.extend(!0,e,r),e}Ln.href=mn.href,T.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:mn.href,type:""GET"",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(mn.protocol),global:!0,processData:!0,async:!0, + contentType:""application/x-www-form-urlencoded; charset=UTF-8"",accepts:{""*"":Hn,text:""text/plain"",html:""text/html"",xml:""application/xml, text/xml"",json:""application/json, text/javascript""},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:""responseXML"",text:""responseText"",json:""responseJSON""},converters:{""* text"":String,""text html"":!0,""text json"":JSON.parse,""text xml"":T.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(e,t){return t?Mn(Mn(e,T.ajaxSettings),t): + Mn(T.ajaxSettings,e)},ajaxPrefilter:Pn(Nn),ajaxTransport:Pn(On),ajax:function(t,n){""object""==typeof t&&(n=t,t=void 0),n=n||{};var r,i,o,a,s,u,l,c,f,p,d=T.ajaxSetup({},n),h=d.context||d,g=d.context&&(h.nodeType||h.jquery)?T(h):T.event,v=T.Deferred(),m=T.Callbacks(""once memory""),x=d.statusCode||{},b={},w={},C=""canceled"",j={readyState:0,getResponseHeader:function(e){var t;if(l){if(!a){a={};while(t=Dn.exec(o))a[t[1].toLowerCase()+"" ""]=(a[t[1].toLowerCase()+"" ""]||[]).concat(t[2])}t=a[e.toLowerCase()+ + "" ""]}return null==t?null:t.join("", "")},getAllResponseHeaders:function(){return l?o:null},setRequestHeader:function(e,t){return null==l&&(b[e=w[e.toLowerCase()]=w[e.toLowerCase()]||e]=t),this},overrideMimeType:function(e){return null==l&&(d.mimeType=e),this},statusCode:function(e){var t;if(e){if(l)j.always(e[j.status]);else for(t in e)x[t]=[x[t],e[t]]}return this},abort:function(e){var t=e||C;return r&&r.abort(t),E(0,t),this}};if(v.promise(j),d.url=((t||d.url||mn.href)+"""").replace(qn,mn.protocol+ + ""//""),d.type=n.method||n.type||d.method||d.type,d.dataTypes=(d.dataType||""*"").toLowerCase().match(Q)||[""""],null==d.crossDomain){u=y.createElement(""a"");try{u.href=d.url,u.href=u.href,d.crossDomain=Ln.protocol+""//""+Ln.host!=u.protocol+""//""+u.host}catch(e){d.crossDomain=!0}}if(Rn(Nn,d,n,j),d.data&&d.processData&&""string""!=typeof d.data&&(d.data=T.param(d.data,d.traditional)),l)return j;for(f in(c=T.event&&d.global)&&0==T.active++&&T.event.trigger(""ajaxStart""),d.type=d.type.toUpperCase(), + d.hasContent=!An.test(d.type),i=d.url.replace(kn,""""),d.hasContent?d.data&&d.processData&&0===(d.contentType||"""").indexOf(""application/x-www-form-urlencoded"")&&(d.data=d.data.replace(En,""+"")):(p=d.url.slice(i.length),d.data&&(d.processData||""string""==typeof d.data)&&(i+=(bn.test(i)?""&"":""?"")+d.data,delete d.data),!1===d.cache&&(i=i.replace(Sn,""$1""),p=(bn.test(i)?""&"":""?"")+""_=""+xn.guid+++p),d.url=i+p),d.ifModified&&(T.lastModified[i]&&j.setRequestHeader(""If-Modified-Since"",T.lastModified[i]),T.etag[ + i]&&j.setRequestHeader(""If-None-Match"",T.etag[i])),(d.data&&d.hasContent&&!1!==d.contentType||n.contentType)&&j.setRequestHeader(""Content-Type"",d.contentType),j.setRequestHeader(""Accept"",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(""*""!==d.dataTypes[0]?"", ""+Hn+""; q=0.01"":""""):d.accepts[""*""]),d.headers)j.setRequestHeader(f,d.headers[f]);if(d.beforeSend&&(!1===d.beforeSend.call(h,j,d)||l))return j.abort();if(C=""abort"",m.add(d.complete),j.done(d.success),j.fail(d.error),r=Rn( + On,d,n,j)){if(j.readyState=1,c&&g.trigger(""ajaxSend"",[j,d]),l)return j;d.async&&d.timeout>0&&(s=e.setTimeout((function(){j.abort(""timeout"")}),d.timeout));try{l=!1,r.send(b,E)}catch(e){if(l)throw e;E(-1,e)}}else E(-1,""No Transport"");function E(t,n,a,u){var f,p,y,b,w,C=n;!l&&(l=!0,s&&e.clearTimeout(s),r=void 0,o=u||"""",j.readyState=t>0?4:0,f=t>=200&&t<300||304===t,a&&(b=function(e,t,n){var r,i,o,a,s=e.contents,u=e.dataTypes;while(""*""===u[0])u.shift(),void 0===r&&(r=e.mimeType||t.getResponseHeader( + ""Content-Type""));if(r){for(i in s)if(s[i]&&s[i].test(r)){u.unshift(i);break}}if(u[0]in n)o=u[0];else{for(i in n){if(!u[0]||e.converters[i+"" ""+u[0]]){o=i;break}a||(a=i)}o=o||a}if(o)return o!==u[0]&&u.unshift(o),n[o]}(d,j,a)),!f&&T.inArray(""script"",d.dataTypes)>-1&&0>T.inArray(""json"",d.dataTypes)&&(d.converters[""text script""]=function(){}),b=function(e,t,n,r){var i,o,a,s,u,l={},c=e.dataTypes.slice();if(c[1])for(a in e.converters)l[a.toLowerCase()]=e.converters[a];o=c.shift();while(o)if( + e.responseFields[o]&&(n[e.responseFields[o]]=t),!u&&r&&e.dataFilter&&(t=e.dataFilter(t,e.dataType)),u=o,o=c.shift()){if(""*""===o)o=u;else if(""*""!==u&&u!==o){if(!(a=l[u+"" ""+o]||l[""* ""+o])){for(i in l)if((s=i.split("" ""))[1]===o&&(a=l[u+"" ""+s[0]]||l[""* ""+s[0]])){!0===a?a=l[i]:!0!==l[i]&&(o=s[0],c.unshift(s[1]));break}}if(!0!==a){if(a&&e.throws)t=a(t);else try{t=a(t)}catch(e){return{state:""parsererror"",error:a?e:""No conversion from ""+u+"" to ""+o}}}}}return{state:""success"",data:t}}(d,b,j,f),f?( + d.ifModified&&((w=j.getResponseHeader(""Last-Modified""))&&(T.lastModified[i]=w),(w=j.getResponseHeader(""etag""))&&(T.etag[i]=w)),204===t||""HEAD""===d.type?C=""nocontent"":304===t?C=""notmodified"":(C=b.state,p=b.data,f=!(y=b.error))):(y=C,(t||!C)&&(C=""error"",t<0&&(t=0))),j.status=t,j.statusText=(n||C)+"""",f?v.resolveWith(h,[p,C,j]):v.rejectWith(h,[j,C,y]),j.statusCode(x),x=void 0,c&&g.trigger(f?""ajaxSuccess"":""ajaxError"",[j,d,f?p:y]),m.fireWith(h,[j,C]),!c||(g.trigger(""ajaxComplete"",[j,d]),--T.active|| + T.event.trigger(""ajaxStop"")))}return j},getJSON:function(e,t,n){return T.get(e,t,n,""json"")},getScript:function(e,t){return T.get(e,void 0,t,""script"")}}),T.each([""get"",""post""],(function(e,t){T[t]=function(e,n,r,i){return(""function""==typeof n||null===n)&&(i=i||r,r=n,n=void 0),T.ajax(T.extend({url:e,type:t,dataType:i,data:n,success:r},T.isPlainObject(e)&&e))}})),T.ajaxPrefilter((function(e){var t;for(t in e.headers)""content-type""===t.toLowerCase()&&(e.contentType=e.headers[t]||"""")})), + T._evalUrl=function(e,t,n){return T.ajax({url:e,type:""GET"",dataType:""script"",cache:!0,async:!1,global:!1,scriptAttrs:t.crossOrigin?{crossOrigin:t.crossOrigin}:void 0,converters:{""text script"":function(){}},dataFilter:function(e){T.globalEval(e,t,n)}})},T.fn.extend({wrapAll:function(e){var t;return this[0]&&(""function""==typeof e&&(e=e.call(this[0])),t=T(e,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&t.insertBefore(this[0]),t.map((function(){var e=this;while(e.firstElementChild) + e=e.firstElementChild;return e})).append(this)),this},wrapInner:function(e){return""function""==typeof e?this.each((function(t){T(this).wrapInner(e.call(this,t))})):this.each((function(){var t=T(this),n=t.contents();n.length?n.wrapAll(e):t.append(e)}))},wrap:function(e){var t=""function""==typeof e;return this.each((function(n){T(this).wrapAll(t?e.call(this,n):e)}))},unwrap:function(e){return this.parent(e).not(""body"").each((function(){T(this).replaceWith(this.childNodes)})),this}}), + T.expr.pseudos.hidden=function(e){return!T.expr.pseudos.visible(e)},T.expr.pseudos.visible=function(e){return!!(e.offsetWidth||e.offsetHeight||e.getClientRects().length)},T.ajaxSettings.xhr=function(){return new e.XMLHttpRequest};var Wn={0:200};function In(e){return e.scriptAttrs||!e.headers&&(e.crossDomain||e.async&&0>T.inArray(""json"",e.dataTypes))}T.ajaxTransport((function(e){var t;return{send:function(n,r){var i,o=e.xhr();if(o.open(e.type,e.url,e.async,e.username,e.password),e.xhrFields)for( + i in e.xhrFields)o[i]=e.xhrFields[i];for(i in e.mimeType&&o.overrideMimeType&&o.overrideMimeType(e.mimeType),e.crossDomain||n[""X-Requested-With""]||(n[""X-Requested-With""]=""XMLHttpRequest""),n)o.setRequestHeader(i,n[i]);t=function(e){return function(){t&&(t=o.onload=o.onerror=o.onabort=o.ontimeout=null,""abort""===e?o.abort():""error""===e?r(o.status,o.statusText):r(Wn[o.status]||o.status,o.statusText,""text""===(o.responseType||""text"")?{text:o.responseText}:{binary:o.response}, + o.getAllResponseHeaders()))}},o.onload=t(),o.onabort=o.onerror=o.ontimeout=t(""error""),t=t(""abort"");try{o.send(e.hasContent&&e.data||null)}catch(e){if(t)throw e}},abort:function(){t&&t()}}})),T.ajaxSetup({accepts:{script:""text/javascript, application/javascript, application/ecmascript, application/x-ecmascript""},converters:{""text script"":function(e){return T.globalEval(e),e}}}),T.ajaxPrefilter(""script"",(function(e){void 0===e.cache&&(e.cache=!1),In(e)&&(e.type=""GET"")})),T.ajaxTransport(""script"",( + function(e){if(In(e)){var t,n;return{send:function(r,i){t=T("""; + var document = await context.OpenAsync(r => r.Content(html)); + Assert.IsNull(document.GetElementById("test")); + } + + [Test] + public async Task InlineModuleScriptShouldRun() + { + 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")); + } + + [Test] + public async Task ModuleScriptWithImportMapShouldRun() + { + 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")); + } + + [Test] + public async Task ModuleScriptWithScopedImportMapShouldRunCorrectScript() + { + var config = + Configuration.Default + .WithJs() + .With(new MockHttpClientRequester(new Dictionary() + { + { "/example-module-1.js", "export function test() { document.getElementById('test1').remove(); }" }, + { "/example-module-2.js", "export function test() { document.getElementById('test2').remove(); }" }, + })) + .WithDefaultLoader(new LoaderOptions() { IsResourceLoadingEnabled = true }); + + var context = BrowsingContext.New(config); + var html = "
Test
Test
"; + + var document1 = await context.OpenAsync(r => r.Content(html)); + Assert.IsNull(document1.GetElementById("test1")); + Assert.IsNotNull(document1.GetElementById("test2")); + + var document2 = await context.OpenAsync(r => r.Content(html).Address("http://localhost/test/")); + Assert.IsNull(document2.GetElementById("test2")); + Assert.IsNotNull(document2.GetElementById("test1")); + } } } diff --git a/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs new file mode 100644 index 0000000..80413f0 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs @@ -0,0 +1,42 @@ +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 +{ + /// + /// Mock HttpClientRequester which returns content for a specific request from a local dictionary. + /// + internal class MockHttpClientRequester : HttpClientRequester + { + private readonly Dictionary _mockResponses; + + public MockHttpClientRequester(Dictionary mockResponses) : base() + { + _mockResponses = mockResponses; + } + + protected override async Task PerformRequestAsync(Request request, CancellationToken cancel) + { + var response = new DefaultResponse(); + + if (_mockResponses.TryGetValue(request.Address.PathName, out var responseContent)) + { + response.StatusCode = HttpStatusCode.OK; + response.Content = new MemoryStream(Encoding.UTF8.GetBytes(responseContent)); + } + else + { + response.StatusCode = HttpStatusCode.NotFound; + response.Content = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)); + } + + return response; + } + } +} diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index 8ac2c94..b4bc1c6 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -23,6 +23,7 @@ + diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 2767e3d..419f796 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -1,12 +1,17 @@ namespace AngleSharp.Js { using AngleSharp.Dom; + using AngleSharp.Io; + using AngleSharp.Text; using Jint; using Jint.Native; using Jint.Native.Object; using System; using System.Collections.Generic; + using System.IO; + using System.Linq; using System.Reflection; + using System.Text.Json; sealed class EngineInstance { @@ -17,6 +22,9 @@ 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; #endregion @@ -24,7 +32,16 @@ sealed class EngineInstance public EngineInstance(IWindow window, IDictionary assignments, IEnumerable libs) { - _engine = new Engine(); + _resourceLoader = window.Document.Context.GetService(); + + _scriptElement = window.Document.CreateElement(TagNames.Script); + + _documentUrl = window.Document.Url; + + _engine = new Engine((options) => + { + options.EnableModules(new JsModuleLoader(this, _documentUrl, false)); + }); _prototypes = new PrototypeCache(_engine); _references = new ReferenceCache(); _libs = libs; @@ -73,12 +90,132 @@ public EngineInstance(IWindow window, IDictionary assignments, I public ObjectInstance GetDomPrototype(Type type) => _prototypes.GetOrCreate(type, CreatePrototype); - public JsValue RunScript(String source, JsValue context) + public JsValue RunScript(String source, String type, JsValue context) { + if (string.IsNullOrEmpty(type)) + { + type = MimeTypeNames.DefaultJavaScript; + } + lock (_engine) { - return _engine.Evaluate(source); + if (MimeTypeNames.IsJavaScript(type)) + { + return _engine.Evaluate(source); + } + else if (type.Isi("importmap")) + { + return LoadImportMap(source); + } + else if (type.Isi("module")) + { + return RunModule(source); + } + else + { + return JsValue.Undefined; + } + } + } + + private JsValue LoadImportMap(String source) + { + JsImportMap importMap; + + try + { + importMap = JsonSerializer.Deserialize(source); + } + catch (JsonException) + { + importMap = null; + } + + // get list of imports based on any scoped imports for the current document path, and any global imports + var imports = new Dictionary(); + var documentPathName = Url.Create(_documentUrl).PathName.ToLower(); + + if (importMap?.Scopes?.Count > 0) + { + var scopePaths = importMap.Scopes.Keys.OrderByDescending(k => k.Length); + + foreach (var scopePath in scopePaths) + { + if (!documentPathName.Contains(scopePath.ToLower())) + { + continue; + } + + var scopeImports = importMap.Scopes[scopePath]; + + foreach (var scopeImport in scopeImports) + { + if (!imports.ContainsKey(scopeImport.Key)) + { + imports.Add(scopeImport.Key, scopeImport.Value); + } + } + } + } + + if (importMap?.Imports?.Count > 0) + { + foreach (var globalImport in importMap.Imports) + { + if (!imports.ContainsKey(globalImport.Key)) + { + imports.Add(globalImport.Key, globalImport.Value); + } + } + } + + foreach (var import in imports) + { + var moduleContent = FetchModule(import.Value); + + _engine.Modules.Add(import.Key, moduleContent); + _engine.Modules.Import(import.Key); + } + + return JsValue.Undefined; + } + + private JsValue RunModule(String source) + { + var moduleIdentifier = Guid.NewGuid().ToString(); + + _engine.Modules.Add(moduleIdentifier, source); + _engine.Modules.Import(moduleIdentifier); + + 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 diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index 599d4f8..2b90984 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) => - engine.RunScript(source, engine.Window); + 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, INode context) => - engine.RunScript(source, context.ToJsValue(engine)); + public static JsValue RunScript(this EngineInstance engine, String source, String type, INode context) => + engine.RunScript(source, type, 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 d5e8cbf..12815ed 100644 --- a/src/AngleSharp.Js/JsApiExtensions.cs +++ b/src/AngleSharp.Js/JsApiExtensions.cs @@ -1,6 +1,7 @@ namespace AngleSharp.Js { using AngleSharp.Dom; + using AngleSharp.Io; using AngleSharp.Scripting; using System; @@ -14,14 +15,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 result of running the script, if any. - public static Object ExecuteScript(this IDocument document, String scriptCode) + public static Object ExecuteScript(this IDocument document, String scriptCode, String scriptType = null) { if (document == null) throw new ArgumentNullException(nameof(document)); var service = document?.Context.GetService(); - return service?.EvaluateScript(document, scriptCode); + return service?.EvaluateScript(document, scriptCode, scriptType ?? MimeTypeNames.DefaultJavaScript); } } } diff --git a/src/AngleSharp.Js/JsImportMap.cs b/src/AngleSharp.Js/JsImportMap.cs new file mode 100644 index 0000000..50267bd --- /dev/null +++ b/src/AngleSharp.Js/JsImportMap.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; + +namespace AngleSharp.Js +{ + /// + /// https://html.spec.whatwg.org/multipage/webappapis.html#import-map + /// + sealed class JsImportMap + { + /// + /// 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. + /// + [JsonPropertyName("imports")] + 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. + /// + [JsonPropertyName("scopes")] + public Dictionary> Scopes { get; set; } + } +} diff --git a/src/AngleSharp.Js/JsModuleLoader.cs b/src/AngleSharp.Js/JsModuleLoader.cs new file mode 100644 index 0000000..7fa9a05 --- /dev/null +++ b/src/AngleSharp.Js/JsModuleLoader.cs @@ -0,0 +1,29 @@ +using AngleSharp.Io; +using AngleSharp.Text; +using Jint; +using Jint.Runtime.Modules; + +namespace AngleSharp.Js +{ + internal class JsModuleLoader : DefaultModuleLoader + { + private readonly EngineInstance _instance; + + public JsModuleLoader(EngineInstance instance, string basePath, bool restrictToBasePath = true) : base (basePath, restrictToBasePath) + { + _instance = instance; + } + + protected override string LoadModuleContents(Engine engine, ResolvedSpecifier resolved) + { + if (resolved.Uri?.Scheme.IsOneOf(ProtocolNames.Http, ProtocolNames.Https) == true) + { + return _instance.FetchModule(resolved.Uri); + } + else + { + return base.LoadModuleContents(engine, resolved); + } + } + } +} diff --git a/src/AngleSharp.Js/JsScriptingService.cs b/src/AngleSharp.Js/JsScriptingService.cs index 3f50b6e..90eb989 100644 --- a/src/AngleSharp.Js/JsScriptingService.cs +++ b/src/AngleSharp.Js/JsScriptingService.cs @@ -4,6 +4,7 @@ namespace AngleSharp.Scripting using AngleSharp.Dom; using AngleSharp.Io; using AngleSharp.Js; + using AngleSharp.Text; using Jint; using System; using System.Collections.Generic; @@ -57,7 +58,9 @@ public JsScriptingService() #region Methods Boolean IScriptingService.SupportsType(String mimeType) => - MimeTypeNames.IsJavaScript(mimeType); + MimeTypeNames.IsJavaScript(mimeType) || + mimeType.Isi("module") || + mimeType.Isi("importmap"); /// /// Gets the associated Jint engine or creates it. @@ -81,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), TaskPriority.Critical).ConfigureAwait(false); + EvaluateScript(options.Document, content, options.Element?.Type), TaskPriority.Critical).ConfigureAwait(false); } } @@ -90,11 +93,12 @@ await options.EventLoop.EnqueueAsync(_ => /// /// The context of the evaluation. /// The source of the script. + /// The type of the script. /// The result of the evaluation. - public Object EvaluateScript(IDocument document, String source) + public Object EvaluateScript(IDocument document, String source, String type) { document = document ?? throw new ArgumentNullException(nameof(document)); - return GetOrCreateInstance(document).RunScript(source).FromJsValue(); + return GetOrCreateInstance(document).RunScript(source, type).FromJsValue(); } #endregion From d326c4f6bde0c16e0c581a55862438c12abe3167 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Fri, 8 Mar 2024 10:42:05 +0000 Subject: [PATCH 15/73] Refactored module import --- src/AngleSharp.Js/EngineInstance.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 419f796..253cb95 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -109,7 +109,10 @@ public JsValue RunScript(String source, String type, JsValue context) } else if (type.Isi("module")) { - return RunModule(source); + // use a unique specifier to import the module into Jint + var specifier = Guid.NewGuid().ToString(); + + return ImportModule(specifier, source); } else { @@ -173,19 +176,16 @@ private JsValue LoadImportMap(String source) { var moduleContent = FetchModule(import.Value); - _engine.Modules.Add(import.Key, moduleContent); - _engine.Modules.Import(import.Key); + ImportModule(import.Key, moduleContent); } return JsValue.Undefined; } - private JsValue RunModule(String source) + private JsValue ImportModule(String specifier, String source) { - var moduleIdentifier = Guid.NewGuid().ToString(); - - _engine.Modules.Add(moduleIdentifier, source); - _engine.Modules.Import(moduleIdentifier); + _engine.Modules.Add(specifier, source); + _engine.Modules.Import(specifier); return JsValue.Undefined; } From 9b59f357a5d5dda3cedbdb5806a071378e8f58e5 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Mon, 11 Mar 2024 09:24:26 +0000 Subject: [PATCH 16/73] Replaced System.Text.Json with JSON.parse --- src/AngleSharp.Js/AngleSharp.Js.csproj | 1 - src/AngleSharp.Js/EngineInstance.cs | 46 ++++++++++++-------------- src/AngleSharp.Js/JsImportMap.cs | 26 --------------- 3 files changed, 22 insertions(+), 51 deletions(-) delete mode 100644 src/AngleSharp.Js/JsImportMap.cs diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index b4bc1c6..8ac2c94 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -23,7 +23,6 @@ - diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 253cb95..cff0b6f 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -11,7 +11,6 @@ namespace AngleSharp.Js using System.IO; using System.Linq; using System.Reflection; - using System.Text.Json; sealed class EngineInstance { @@ -123,24 +122,17 @@ public JsValue RunScript(String source, String type, JsValue context) private JsValue LoadImportMap(String source) { - JsImportMap importMap; - - try - { - importMap = JsonSerializer.Deserialize(source); - } - catch (JsonException) - { - importMap = null; - } + 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 imports = new Dictionary(); + var moduleImports = new Dictionary(); var documentPathName = Url.Create(_documentUrl).PathName.ToLower(); - if (importMap?.Scopes?.Count > 0) + if (importMap.TryGetValue("scopes", out var scopes)) { - var scopePaths = importMap.Scopes.Keys.OrderByDescending(k => k.Length); + var scopesObj = scopes.AsObject(); + + var scopePaths = scopesObj.GetOwnPropertyKeys().Select(k => k.AsString()).OrderByDescending(k => k.Length); foreach (var scopePath in scopePaths) { @@ -149,32 +141,38 @@ private JsValue LoadImportMap(String source) continue; } - var scopeImports = importMap.Scopes[scopePath]; + var scopeImports = scopesObj[scopePath].AsObject(); + + var scopeImportImportSpecifiers = scopeImports.GetOwnPropertyKeys().Select(k => k.AsString()); - foreach (var scopeImport in scopeImports) + foreach (var scopeImportSpecifier in scopeImportImportSpecifiers) { - if (!imports.ContainsKey(scopeImport.Key)) + if (!moduleImports.ContainsKey(scopeImportSpecifier)) { - imports.Add(scopeImport.Key, scopeImport.Value); + moduleImports.Add(scopeImportSpecifier, scopeImports[scopeImportSpecifier].AsString()); } } } } - if (importMap?.Imports?.Count > 0) + if (importMap.TryGetValue("imports", out var imports)) { - foreach (var globalImport in importMap.Imports) + var importsObj = imports.AsObject(); + + var importSpecifiers = importsObj.GetOwnPropertyKeys().Select(k => k.AsString()); + + foreach (var importSpecifier in importSpecifiers) { - if (!imports.ContainsKey(globalImport.Key)) + if (!moduleImports.ContainsKey(importSpecifier)) { - imports.Add(globalImport.Key, globalImport.Value); + moduleImports.Add(importSpecifier, importsObj[importSpecifier].AsString()); } } } - foreach (var import in imports) + foreach (var import in moduleImports) { - var moduleContent = FetchModule(import.Value); + var moduleContent = FetchModule(new Uri(import.Value, UriKind.RelativeOrAbsolute)); ImportModule(import.Key, moduleContent); } diff --git a/src/AngleSharp.Js/JsImportMap.cs b/src/AngleSharp.Js/JsImportMap.cs deleted file mode 100644 index 50267bd..0000000 --- a/src/AngleSharp.Js/JsImportMap.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text.Json.Serialization; - -namespace AngleSharp.Js -{ - /// - /// https://html.spec.whatwg.org/multipage/webappapis.html#import-map - /// - sealed class JsImportMap - { - /// - /// 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. - /// - [JsonPropertyName("imports")] - 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. - /// - [JsonPropertyName("scopes")] - public Dictionary> Scopes { get; set; } - } -} From 11078c2bbf101e925220639ae57df80ab45a6901 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Mon, 18 Mar 2024 09:01:32 +0000 Subject: [PATCH 17/73] Refactored importmap scopes --- src/AngleSharp.Js.Tests/EcmaTests.cs | 27 +++++- .../Mocks/MockHttpClientRequester.cs | 18 ++-- src/AngleSharp.Js/EngineInstance.cs | 91 +++++-------------- .../Extensions/EngineExtensions.cs | 8 +- src/AngleSharp.Js/JsApiExtensions.cs | 5 +- src/AngleSharp.Js/JsImportMap.cs | 29 ++++++ src/AngleSharp.Js/JsModuleLoader.cs | 91 +++++++++++++++++-- src/AngleSharp.Js/JsScriptingService.cs | 7 +- 8 files changed, 182 insertions(+), 94 deletions(-) create mode 100644 src/AngleSharp.Js/JsImportMap.cs 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 From 8aba70bd3c2110cdef688312d6cccbe896740d5e Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Mon, 18 Mar 2024 15:13:35 +0000 Subject: [PATCH 18/73] Fixed absolute path issue on Linux --- src/AngleSharp.Js/JsModuleLoader.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/AngleSharp.Js/JsModuleLoader.cs b/src/AngleSharp.Js/JsModuleLoader.cs index 1863ebd..ec547c4 100644 --- a/src/AngleSharp.Js/JsModuleLoader.cs +++ b/src/AngleSharp.Js/JsModuleLoader.cs @@ -64,6 +64,18 @@ public override ResolvedSpecifier Resolve(string referencingModuleLocation, Modu moduleUrl, SpecifierType.RelativeOrAbsolute); } + + // Before passing to the base default module loader, make sure any relative paths are prepended with a dot + // as Jint will otherwise resolve them as absolute file paths when running on Linux + if (moduleRequest.Specifier.StartsWith("/")) + { + moduleRequest = new ModuleRequest($".{moduleRequest.Specifier}", moduleRequest.Attributes); + } + + if (referencingModuleLocation?.StartsWith("/") == true) + { + referencingModuleLocation = $".{referencingModuleLocation}"; + } return base.Resolve(referencingModuleLocation, moduleRequest); } From 06d9a5188cc0b5fb8a4ba58512b2862391e5f7cc Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Mon, 25 Mar 2024 13:54:01 +0000 Subject: [PATCH 19/73] Added Image constructor support --- src/AngleSharp.Js.Tests/ScriptEvalTests.cs | 7 ++++ .../DomConstructorFunctionAttribute.cs | 28 +++++++++++++++ src/AngleSharp.Js/Cache/CreatorCache.cs | 35 +++++++++++++++++++ src/AngleSharp.Js/Dom/WindowExtensions.cs | 27 ++++++++++++++ src/AngleSharp.Js/EngineInstance.cs | 1 + .../Extensions/EngineExtensions.cs | 14 ++++++++ .../Extensions/JsValueExtensions.cs | 11 ++++++ .../Proxies/DomConstructorFunctionInstance.cs | 31 ++++++++++++++++ 8 files changed, 154 insertions(+) create mode 100644 src/AngleSharp.Js/Attributes/DomConstructorFunctionAttribute.cs create mode 100644 src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs diff --git a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs index 28e60af..82575fe 100644 --- a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs +++ b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs @@ -75,6 +75,13 @@ public async Task CreateXmlHttpRequestShouldWork() Assert.AreEqual("1", result); } + [Test] + public async Task CreateImageShouldWork() + { + var result = await EvaluateComplexScriptAsync("var img = new Image(400, 200); img.src = '/image.jpg';", SetResult("img.width")); + Assert.AreEqual("400", result); + } + [Test] public async Task PerformXmlHttpRequestSynchronousToDataUrlShouldWork() { diff --git a/src/AngleSharp.Js/Attributes/DomConstructorFunctionAttribute.cs b/src/AngleSharp.Js/Attributes/DomConstructorFunctionAttribute.cs new file mode 100644 index 0000000..6e6b44b --- /dev/null +++ b/src/AngleSharp.Js/Attributes/DomConstructorFunctionAttribute.cs @@ -0,0 +1,28 @@ +namespace AngleSharp.Js.Attributes +{ + using System; + + /// + /// This attribute is used to mark a method to be uses as a + /// constructor function from scripts. + /// + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + public sealed class DomConstructorFunctionAttribute : Attribute + { + /// + /// Creates a new DomConstructorFunctionAttribute. + /// + /// + /// The official name of the decorated method. + /// + public DomConstructorFunctionAttribute(String officialName) + { + OfficialName = officialName; + } + + /// + /// Gets the official name of the given class. + /// + public String OfficialName { get; } + } +} diff --git a/src/AngleSharp.Js/Cache/CreatorCache.cs b/src/AngleSharp.Js/Cache/CreatorCache.cs index fe26650..5003d93 100644 --- a/src/AngleSharp.Js/Cache/CreatorCache.cs +++ b/src/AngleSharp.Js/Cache/CreatorCache.cs @@ -1,4 +1,6 @@ using AngleSharp.Attributes; +using AngleSharp.Js.Attributes; +using AngleSharp.Js.Proxies; using Jint.Native.Object; using Jint.Runtime.Descriptors; using System; @@ -40,6 +42,39 @@ public static Action GetConstructorAction(this T return action; } + private static readonly Dictionary> _constructorFunctionActions = new Dictionary>(); + + public static Action GetConstructorFunctionAction(this Type type) + { + if (!_constructorFunctionActions.TryGetValue(type, out var action)) + { + var constructorFunctions = type.GetTypeInfo().GetMethods().Where(m => m.GetCustomAttributes().Any()); + + if (constructorFunctions.Any()) + { + action = (engine, obj) => + { + foreach (var constructorFunction in constructorFunctions) + { + var attribute = constructorFunction.GetCustomAttribute(); + + var constructorFunctionInstance = new DomConstructorFunctionInstance(engine, constructorFunction, attribute.OfficialName); + + obj.FastSetProperty(attribute.OfficialName, new PropertyDescriptor(constructorFunctionInstance, false, true, false)); + } + }; + } + else + { + action = (e, o) => { }; + } + + _constructorFunctionActions.Add(type, action); + } + + return action; + } + private static readonly Dictionary> _instanceActions = new Dictionary>(); public static Action GetInstanceAction(this Type type) diff --git a/src/AngleSharp.Js/Dom/WindowExtensions.cs b/src/AngleSharp.Js/Dom/WindowExtensions.cs index 5fec386..27835b9 100644 --- a/src/AngleSharp.Js/Dom/WindowExtensions.cs +++ b/src/AngleSharp.Js/Dom/WindowExtensions.cs @@ -4,6 +4,8 @@ namespace AngleSharp.Js.Dom using AngleSharp.Browser; using AngleSharp.Dom; using AngleSharp.Dom.Events; + using AngleSharp.Html.Dom; + using AngleSharp.Js.Attributes; using System; /// @@ -42,5 +44,30 @@ public static Console Console(this IWindow window) { return new Console(window); } + + /// + /// Creates a new IHtmlImageElement instance. + /// + /// + /// + /// + /// + [DomConstructorFunction("Image")] + public static IHtmlImageElement Image(this IWindow window, int? width = null, int? height = null) + { + var imageElement = window.Document.CreateElement(TagNames.Img) as IHtmlImageElement; + + if (width.HasValue) + { + imageElement.DisplayWidth = width.Value; + } + + if (height.HasValue) + { + imageElement.DisplayHeight = height.Value; + } + + return imageElement; + } } } diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 3848ebb..fe3d678 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -47,6 +47,7 @@ public EngineInstance(IWindow window, IDictionary assignments, I foreach (var lib in libs) { this.AddConstructors(_window, lib); + this.AddConstructorFunctions(_window, lib); this.AddInstances(_window, lib); } diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index 12cf37e..a28f7c9 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -178,6 +178,14 @@ public static void AddConstructors(this EngineInstance engine, ObjectInstance ct } } + public static void AddConstructorFunctions(this EngineInstance engine, ObjectInstance ctx, Assembly assembly) + { + foreach (var exportedType in assembly.ExportedTypes) + { + engine.AddConstructorFunction(ctx, exportedType); + } + } + public static void AddInstances(this EngineInstance engine, ObjectInstance obj, Assembly assembly) { foreach (var exportedType in assembly.ExportedTypes) @@ -192,6 +200,12 @@ public static void AddConstructor(this EngineInstance engine, ObjectInstance obj apply.Invoke(engine, obj); } + public static void AddConstructorFunction(this EngineInstance engine, ObjectInstance obj, Type type) + { + var apply = type.GetConstructorFunctionAction(); + apply.Invoke(engine, obj); + } + public static void AddInstance(this EngineInstance engine, ObjectInstance obj, Type type) { var apply = type.GetInstanceAction(); diff --git a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs index 70e4c04..26779df 100644 --- a/src/AngleSharp.Js/Extensions/JsValueExtensions.cs +++ b/src/AngleSharp.Js/Extensions/JsValueExtensions.cs @@ -73,6 +73,17 @@ public static Object As(this JsValue value, Type targetType, EngineInstance engi { return TypeConverter.ToInt32(value); } + else if (targetType == typeof(Nullable)) + { + if (value.IsUndefined()) + { + return null; + } + else + { + return TypeConverter.ToInt32(value); + } + } else if (targetType == typeof(Double)) { return TypeConverter.ToNumber(value); diff --git a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs new file mode 100644 index 0000000..0285cd9 --- /dev/null +++ b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs @@ -0,0 +1,31 @@ +namespace AngleSharp.Js.Proxies +{ + using Jint.Native; + using Jint.Native.Object; + using Jint.Runtime; + using System.Reflection; + + sealed class DomConstructorFunctionInstance : Constructor + { + private readonly EngineInstance _instance; + private readonly MethodInfo _constructorFunction; + + public DomConstructorFunctionInstance(EngineInstance instance, MethodInfo constructorFunction, string name) : base(instance.Jint, name) + { + _instance = instance; + _constructorFunction = constructorFunction; + } + + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) + { + try + { + return _instance.Call(_constructorFunction, _instance.Window, arguments) as DomNodeInstance; + } + catch + { + throw new JavaScriptException(_instance.Jint.Intrinsics.Error); + } + } + } +} From f019708a2cdaba57bcac0fbe9b3571b18ff02320 Mon Sep 17 00:00:00 2001 From: Tom van Enckevort Date: Mon, 25 Mar 2024 14:05:45 +0000 Subject: [PATCH 20/73] Use more generic type-cast --- src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs index 0285cd9..348ab1a 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs @@ -20,7 +20,7 @@ public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { try { - return _instance.Call(_constructorFunction, _instance.Window, arguments) as DomNodeInstance; + return _instance.Call(_constructorFunction, _instance.Window, arguments) as ObjectInstance; } catch { From 37d4fa3b629ab2bb605b5078a980ff4cff6fb396 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 13 Apr 2024 11:16:59 +0300 Subject: [PATCH 21/73] Upgrade to Jint 3.1.0 * small cleanup to API usage * add missing license expression * treat warnings as errors to catch obsolete members --- .../AngleSharp.Js.Tests.csproj | 5 ---- .../Mocks/MockHttpClientRequester.cs | 4 ++-- src/AngleSharp.Js/AngleSharp.Js.csproj | 16 +++++-------- .../Extensions/EngineExtensions.cs | 23 ++++++++---------- src/AngleSharp.Js/Proxies/DomNodeInstance.cs | 6 +++-- .../Proxies/DomPrototypeInstance.cs | 24 +++++++------------ src/Directory.Build.props | 1 + 7 files changed, 32 insertions(+), 47 deletions(-) diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 01bfbe1..0c9e57d 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -1,13 +1,8 @@ - AngleSharp.Js.Tests net6.0;net7.0;net8.0 net462;net472;net6.0;net7.0;net8.0 - true - Key.snk false - 7.1 - AngleSharp.Js.Tests true diff --git a/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs index 65277d5..a2355cf 100644 --- a/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs +++ b/src/AngleSharp.Js.Tests/Mocks/MockHttpClientRequester.cs @@ -21,7 +21,7 @@ public MockHttpClientRequester(Dictionary mockResponses) : base( _mockResponses = mockResponses; } - protected override async Task PerformRequestAsync(Request request, CancellationToken cancel) + protected override Task PerformRequestAsync(Request request, CancellationToken cancel) { var response = new DefaultResponse(); @@ -36,7 +36,7 @@ protected override async Task PerformRequestAsync(Request request, Ca response.Content = new MemoryStream(Encoding.UTF8.GetBytes(string.Empty)); } - return response; + return Task.FromResult(response); } } } diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index 8ac2c94..67bb351 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -1,28 +1,24 @@ - AngleSharp.Js - AngleSharp.Js - netstandard2.0;net6.0;net7.0;net8.0 - netstandard2.0;net462;net472;net6.0;net7.0;net8.0 - true - Key.snk + netstandard2.0;net6.0;net7.0;net8.0 + $(TargetFrameworks);net462;net472 true - 7.1 https://github.com/AngleSharp/AngleSharp.Js git true true true snupkg + MIT - + - - + + diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index a28f7c9..e84e3d2 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -148,21 +148,18 @@ private static JsValue[] ExpandInitDict(JsValue[] arguments, ParameterInfo[] par newArgs[i] = arguments[i]; } - if (obj != null) + for (var i = end + offset; i < max; i++) { - for (var i = end + offset; i < max; i++) - { - var p = parameters[i]; - var name = p.Name; + var p = parameters[i]; + var name = p.Name; - if (obj.HasProperty(name)) - { - newArgs[i - offset] = obj.GetProperty(name).Value; - } - else - { - newArgs[i - offset] = JsValue.Undefined; - } + if (obj.HasProperty(name)) + { + newArgs[i - offset] = obj.Get(name); + } + else + { + newArgs[i - offset] = JsValue.Undefined; } } diff --git a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs index 3705b46..2d4dec8 100644 --- a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs @@ -32,9 +32,11 @@ public override PropertyDescriptor GetOwnProperty(JsValue property) { return descriptor; } - else if (prototype.HasProperty(property)) + + var prototypeProperty = prototype.GetOwnProperty(property); + if (prototypeProperty != PropertyDescriptor.Undefined) { - return prototype.GetProperty(property); + return prototypeProperty; } } diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index 183439e..1967c15 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -1,9 +1,7 @@ namespace AngleSharp.Js { using AngleSharp.Attributes; - using AngleSharp.Dom; using AngleSharp.Text; - using Jint.Native; using Jint.Native.Object; using Jint.Native.Symbol; using Jint.Runtime.Descriptors; @@ -55,7 +53,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto { if (ex.InnerException is ArgumentOutOfRangeException) { - result = new PropertyDescriptor(JsValue.Undefined, false, false, false); + result = PropertyDescriptor.Undefined; return true; } @@ -104,12 +102,9 @@ private void SetNormalEvents(IEnumerable eventInfos) { foreach (var eventInfo in eventInfos) { - var names = eventInfo.GetCustomAttributes() - .Select(m => m.OfficialName); - - foreach (var name in names) + foreach (var m in eventInfo.GetCustomAttributes()) { - SetEvent(name, eventInfo.AddMethod, eventInfo.RemoveMethod); + SetEvent(m.OfficialName, eventInfo.AddMethod, eventInfo.RemoveMethod); } } } @@ -123,6 +118,7 @@ private void SetExtensionMethods(IEnumerable methods) if (HasProperty(name)) { + // skip } else if (value.Adder != null && value.Remover != null) { @@ -148,9 +144,10 @@ private void SetNormalProperties(IEnumerable properties) var putsForward = property.GetCustomAttribute(); var names = property .GetCustomAttributes() - .Select(m => m.OfficialName); + .Select(m => m.OfficialName) + .ToArray(); - if (index != null || names.Any(m => m.Is("item"))) + if (index != null || Array.Exists(names, m => m.Is("item"))) { SetIndexer(property, indexParameters); } @@ -166,12 +163,9 @@ private void SetNormalMethods(IEnumerable methods) { foreach (var method in methods) { - var names = method.GetCustomAttributes() - .Select(m => m.OfficialName); - - foreach (var name in names) + foreach (var m in method.GetCustomAttributes()) { - SetMethod(name, method); + SetMethod(m.OfficialName, method); } } } diff --git a/src/Directory.Build.props b/src/Directory.Build.props index f3d6f9d..f727b1f 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,6 +5,7 @@ 1.0.0 latest true + true $(MSBuildThisFileDirectory)\Key.snk \ No newline at end of file From 2c68f60d1fdd4b6539964223fecb1c74d4ba6caa Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sat, 13 Apr 2024 11:55:09 +0300 Subject: [PATCH 22/73] Upgrade to NUKE 8 * enable deterministic build --- .nuke/build.schema.json | 4 ++-- build.cmd | 0 build.ps1 | 11 ++++++++--- build.sh | 11 ++++++++--- nuke/Build.cs | 16 +++++++++------- nuke/_build.csproj | 4 ++-- src/AngleSharp.Js.sln | 2 ++ 7 files changed, 31 insertions(+), 17 deletions(-) mode change 100644 => 100755 build.cmd diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index c1999a9..7616e4d 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "title": "Build Schema", "$ref": "#/definitions/build", + "title": "Build Schema", "definitions": { "build": { "type": "object", @@ -131,4 +131,4 @@ } } } -} \ No newline at end of file +} diff --git a/build.cmd b/build.cmd old mode 100644 new mode 100755 diff --git a/build.ps1 b/build.ps1 index 1f264e7..3cca4ef 100644 --- a/build.ps1 +++ b/build.ps1 @@ -18,11 +18,10 @@ $TempDirectory = "$PSScriptRoot\\.nuke\temp" $DotNetGlobalFile = "$PSScriptRoot\\global.json" $DotNetInstallUrl = "https://dot.net/v1/dotnet-install.ps1" -$DotNetChannel = "Current" +$DotNetChannel = "STS" -$env:DOTNET_SKIP_FIRST_TIME_EXPERIENCE = 1 $env:DOTNET_CLI_TELEMETRY_OPTOUT = 1 -$env:DOTNET_MULTILEVEL_LOOKUP = 0 +$env:DOTNET_NOLOGO = 1 ########################################################################### # EXECUTION @@ -61,9 +60,15 @@ else { ExecSafe { & powershell $DotNetInstallFile -InstallDir $DotNetDirectory -Version $DotNetVersion -NoPath } } $env:DOTNET_EXE = "$DotNetDirectory\dotnet.exe" + $env:PATH = "$DotNetDirectory;$env:PATH" } Write-Output "Microsoft (R) .NET SDK version $(& $env:DOTNET_EXE --version)" +if (Test-Path env:NUKE_ENTERPRISE_TOKEN) { + & $env:DOTNET_EXE nuget remove source "nuke-enterprise" > $null + & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null +} + ExecSafe { & $env:DOTNET_EXE build $BuildProjectFile /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet } ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments } diff --git a/build.sh b/build.sh index 7534123..a226b96 100755 --- a/build.sh +++ b/build.sh @@ -14,11 +14,10 @@ TEMP_DIRECTORY="$SCRIPT_DIR//.nuke/temp" DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json" DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" -DOTNET_CHANNEL="Current" +DOTNET_CHANNEL="STS" export DOTNET_CLI_TELEMETRY_OPTOUT=1 -export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1 -export DOTNET_MULTILEVEL_LOOKUP=0 +export DOTNET_NOLOGO=1 ########################################################################### # EXECUTION @@ -54,9 +53,15 @@ else "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path fi export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet" + export PATH="$DOTNET_DIRECTORY:$PATH" fi echo "Microsoft (R) .NET SDK version $("$DOTNET_EXE" --version)" +if [[ ! -z ${NUKE_ENTERPRISE_TOKEN+x} && "$NUKE_ENTERPRISE_TOKEN" != "" ]]; then + "$DOTNET_EXE" nuget remove source "nuke-enterprise" &>/dev/null || true + "$DOTNET_EXE" nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password "$NUKE_ENTERPRISE_TOKEN" --store-password-in-clear-text &>/dev/null || true +fi + "$DOTNET_EXE" build "$BUILD_PROJECT_FILE" /nodeReuse:false /p:UseSharedCompilation=false -nologo -clp:NoSummary --verbosity quiet "$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@" diff --git a/nuke/Build.cs b/nuke/Build.cs index 43ecd98..d8cd73a 100644 --- a/nuke/Build.cs +++ b/nuke/Build.cs @@ -16,7 +16,6 @@ using System.Linq; using Nuke.Common.Tooling; using static Nuke.Common.IO.FileSystemTasks; -using static Nuke.Common.IO.PathConstruction; using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.NuGet.NuGetTasks; using Project = Nuke.Common.ProjectModel.Project; @@ -100,7 +99,7 @@ protected override void OnBuildInitialized() Log.Information("Building version: {Version}", Version); - TargetProject = Solution.GetProject(SourceDirectory / TargetProjectName / $"{TargetLibName}.csproj" ); + TargetProject = Solution.GetProject(TargetLibName); TargetProject.NotNull("TargetProject could not be loaded!"); TargetFrameworks = TargetProject.GetTargetFrameworks(); @@ -113,7 +112,7 @@ protected override void OnBuildInitialized() .Before(Restore) .Executes(() => { - SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(DeleteDirectory); + SourceDirectory.GlobDirectories("**/bin", "**/obj").ForEach(x => x.DeleteDirectory()); }); Target Restore => _ => _ @@ -130,6 +129,7 @@ protected override void OnBuildInitialized() DotNetBuild(s => s .SetProjectFile(Solution) .SetConfiguration(Configuration) + .SetContinuousIntegrationBuild(IsServerBuild) .EnableNoRestore()); }); @@ -143,6 +143,7 @@ protected override void OnBuildInitialized() .EnableNoRestore() .EnableNoBuild() .SetProcessEnvironmentVariable("prefetched", "false") + .When(GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) ); DotNetTest(s => s @@ -151,6 +152,7 @@ protected override void OnBuildInitialized() .EnableNoRestore() .EnableNoBuild() .SetProcessEnvironmentVariable("prefetched", "true") + .When(GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) ); }); @@ -183,9 +185,9 @@ protected override void OnBuildInitialized() .SetTargetPath(nuspec) .SetVersion(Version) .SetOutputDirectory(NugetDirectory) - .SetSymbols(true) - .SetSymbolPackageFormat("snupkg") - .AddProperty("Configuration", Configuration) + .EnableSymbols() + .SetSymbolPackageFormat(NuGetSymbolPackageFormat.snupkg) + .SetConfiguration(Configuration) ); }); @@ -202,7 +204,7 @@ protected override void OnBuildInitialized() throw new BuildAbortedException("Could not resolve the NuGet API key."); } - foreach (var nupkg in GlobFiles(NugetDirectory, "*.nupkg")) + foreach (var nupkg in NugetDirectory.GlobFiles("*.nupkg")) { NuGetPush(s => s .SetTargetPath(nupkg) diff --git a/nuke/_build.csproj b/nuke/_build.csproj index 7521e1b..a71fa1b 100644 --- a/nuke/_build.csproj +++ b/nuke/_build.csproj @@ -2,7 +2,7 @@ Exe - net6.0 + net8.0 CS0649;CS0169 .. @@ -11,7 +11,7 @@ - + diff --git a/src/AngleSharp.Js.sln b/src/AngleSharp.Js.sln index 46555c8..4ef0c1c 100644 --- a/src/AngleSharp.Js.sln +++ b/src/AngleSharp.Js.sln @@ -7,6 +7,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AngleSharp.Js", "AngleSharp EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AngleSharp.Js.Tests", "AngleSharp.Js.Tests\AngleSharp.Js.Tests.csproj", "{18B0B97B-8795-4DC2-A1E7-8070255BE718}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "_build", "..\nuke\_build.csproj", "{07DA52AA-8F6F-4F43-B09E-6AE899E95E43}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU From 520533f06d9f37a71bc5299aa1d574a5bb7aab34 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Wed, 24 Jul 2024 14:02:48 +0300 Subject: [PATCH 23/73] Upgrade to Jint 4 --- src/AngleSharp.Js/AngleSharp.Js.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index 67bb351..f0fd16d 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -18,7 +18,7 @@ - + From b90ef8aa512718e8a196659c9ebef5add2fe4eea Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jan 2025 21:11:49 +0200 Subject: [PATCH 24/73] Upgrade dependencies and remove obsolete NET 6 and 7 --- .nuke/build.schema.json | 153 +++++++++--------- nuke/Build.cs | 19 +-- nuke/_build.csproj | 4 +- .../AngleSharp.Js.Tests.csproj | 10 +- src/AngleSharp.Js/AngleSharp.Js.csproj | 4 +- 5 files changed, 96 insertions(+), 94 deletions(-) diff --git a/.nuke/build.schema.json b/.nuke/build.schema.json index 7616e4d..3166f3e 100644 --- a/.nuke/build.schema.json +++ b/.nuke/build.schema.json @@ -1,19 +1,56 @@ { "$schema": "http://json-schema.org/draft-04/schema#", - "$ref": "#/definitions/build", - "title": "Build Schema", "definitions": { - "build": { - "type": "object", + "Host": { + "type": "string", + "enum": [ + "AppVeyor", + "AzurePipelines", + "Bamboo", + "Bitbucket", + "Bitrise", + "GitHubActions", + "GitLab", + "Jenkins", + "Rider", + "SpaceAutomation", + "TeamCity", + "Terminal", + "TravisCI", + "VisualStudio", + "VSCode" + ] + }, + "ExecutableTarget": { + "type": "string", + "enum": [ + "Clean", + "Compile", + "CopyFiles", + "CreatePackage", + "Default", + "Package", + "PrePublish", + "Publish", + "PublishPackage", + "PublishPreRelease", + "PublishRelease", + "Restore", + "RunUnitTests" + ] + }, + "Verbosity": { + "type": "string", + "description": "", + "enum": [ + "Verbose", + "Normal", + "Minimal", + "Quiet" + ] + }, + "NukeBuild": { "properties": { - "Configuration": { - "type": "string", - "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", - "enum": [ - "Debug", - "Release" - ] - }, "Continue": { "type": "boolean", "description": "Indicates to continue a previously failed build attempt" @@ -23,25 +60,8 @@ "description": "Shows the help text for this build assembly" }, "Host": { - "type": "string", "description": "Host for execution. Default is 'automatic'", - "enum": [ - "AppVeyor", - "AzurePipelines", - "Bamboo", - "Bitbucket", - "Bitrise", - "GitHubActions", - "GitLab", - "Jenkins", - "Rider", - "SpaceAutomation", - "TeamCity", - "Terminal", - "TravisCI", - "VisualStudio", - "VSCode" - ] + "$ref": "#/definitions/Host" }, "NoLogo": { "type": "boolean", @@ -62,10 +82,6 @@ "type": "string" } }, - "ReleaseNotesFilePath": { - "type": "string", - "description": "ReleaseNotesFilePath - To determine the SemanticVersion" - }, "Root": { "type": "string", "description": "Root directory during build execution" @@ -74,61 +90,46 @@ "type": "array", "description": "List of targets to be skipped. Empty list skips all dependencies", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "CopyFiles", - "CreatePackage", - "Default", - "Package", - "PrePublish", - "Publish", - "PublishPackage", - "PublishPreRelease", - "PublishRelease", - "Restore", - "RunUnitTests" - ] + "$ref": "#/definitions/ExecutableTarget" } }, - "Solution": { - "type": "string", - "description": "Path to a solution file that is automatically loaded" - }, "Target": { "type": "array", "description": "List of targets to be invoked. Default is '{default_target}'", "items": { - "type": "string", - "enum": [ - "Clean", - "Compile", - "CopyFiles", - "CreatePackage", - "Default", - "Package", - "PrePublish", - "Publish", - "PublishPackage", - "PublishPreRelease", - "PublishRelease", - "Restore", - "RunUnitTests" - ] + "$ref": "#/definitions/ExecutableTarget" } }, "Verbosity": { - "type": "string", "description": "Logging verbosity during build execution. Default is 'Normal'", + "$ref": "#/definitions/Verbosity" + } + } + } + }, + "allOf": [ + { + "properties": { + "Configuration": { + "type": "string", + "description": "Configuration to build - Default is 'Debug' (local) or 'Release' (server)", "enum": [ - "Minimal", - "Normal", - "Quiet", - "Verbose" + "Debug", + "Release" ] + }, + "ReleaseNotesFilePath": { + "type": "string", + "description": "ReleaseNotesFilePath - To determine the SemanticVersion" + }, + "Solution": { + "type": "string", + "description": "Path to a solution file that is automatically loaded" } } + }, + { + "$ref": "#/definitions/NukeBuild" } - } + ] } diff --git a/nuke/Build.cs b/nuke/Build.cs index d8cd73a..cc011ea 100644 --- a/nuke/Build.cs +++ b/nuke/Build.cs @@ -15,9 +15,10 @@ using System.IO; using System.Linq; using Nuke.Common.Tooling; -using static Nuke.Common.IO.FileSystemTasks; + using static Nuke.Common.Tools.DotNet.DotNetTasks; using static Nuke.Common.Tools.NuGet.NuGetTasks; + using Project = Nuke.Common.ProjectModel.Project; class Build : NukeBuild @@ -143,7 +144,7 @@ protected override void OnBuildInitialized() .EnableNoRestore() .EnableNoBuild() .SetProcessEnvironmentVariable("prefetched", "false") - .When(GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) + .When(_ => GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) ); DotNetTest(s => s @@ -152,7 +153,7 @@ protected override void OnBuildInitialized() .EnableNoRestore() .EnableNoBuild() .SetProcessEnvironmentVariable("prefetched", "true") - .When(GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) + .When(_ => GitHubActions.Instance is not null, x => x.SetLoggers("GitHubActions")) ); }); @@ -165,14 +166,14 @@ protected override void OnBuildInitialized() var targetDir = NugetDirectory / "lib" / item; var srcDir = BuildDirectory / item; - CopyFile(srcDir / $"{TargetProjectName}.dll", targetDir / $"{TargetProjectName}.dll", FileExistsPolicy.OverwriteIfNewer); - CopyFile(srcDir / $"{TargetProjectName}.pdb", targetDir / $"{TargetProjectName}.pdb", FileExistsPolicy.OverwriteIfNewer); - CopyFile(srcDir / $"{TargetProjectName}.xml", targetDir / $"{TargetProjectName}.xml", FileExistsPolicy.OverwriteIfNewer); + (srcDir / $"{TargetProjectName}.dll").Copy(targetDir / $"{TargetProjectName}.dll", policy: ExistsPolicy.FileOverwriteIfNewer); + (srcDir / $"{TargetProjectName}.pdb").Copy(targetDir / $"{TargetProjectName}.pdb", policy: ExistsPolicy.FileOverwriteIfNewer); + (srcDir / $"{TargetProjectName}.xml").Copy(targetDir / $"{TargetProjectName}.xml", policy: ExistsPolicy.FileOverwriteIfNewer); } - CopyFile(SourceDirectory / $"{TargetProjectName}.nuspec", NugetDirectory / $"{TargetProjectName}.nuspec", FileExistsPolicy.OverwriteIfNewer); - CopyFile(RootDirectory / "logo.png", NugetDirectory / "logo.png", FileExistsPolicy.OverwriteIfNewer); - CopyFile(RootDirectory / "README.md", NugetDirectory / "README.md", FileExistsPolicy.OverwriteIfNewer); + (SourceDirectory / $"{TargetProjectName}.nuspec").Copy(NugetDirectory / $"{TargetProjectName}.nuspec", policy: ExistsPolicy.FileOverwriteIfNewer); + (RootDirectory / "logo.png").Copy(NugetDirectory / "logo.png", policy: ExistsPolicy.FileOverwriteIfNewer); + (RootDirectory / "README.md").Copy(NugetDirectory / "README.md", policy: ExistsPolicy.FileOverwriteIfNewer); }); Target CreatePackage => _ => _ diff --git a/nuke/_build.csproj b/nuke/_build.csproj index a71fa1b..3258f31 100644 --- a/nuke/_build.csproj +++ b/nuke/_build.csproj @@ -11,11 +11,11 @@ - + - + diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 0c9e57d..9a61a14 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -1,7 +1,7 @@ - net6.0;net7.0;net8.0 - net462;net472;net6.0;net7.0;net8.0 + net8.0 + $(TargetFrameworks);net462;net472 false true @@ -14,7 +14,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive @@ -22,7 +22,7 @@ - - + + diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index f0fd16d..e2a7010 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net6.0;net7.0;net8.0 + netstandard2.0;net8.0 $(TargetFrameworks);net462;net472 true https://github.com/AngleSharp/AngleSharp.Js @@ -18,7 +18,7 @@ - + From 510544062da7b1cc23dfd68f9f96812cd87b2b2f Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sun, 26 Jan 2025 21:46:50 +0100 Subject: [PATCH 25/73] Updated to 2025 --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 10b5b1c..b64c5a7 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 - 2024 AngleSharp +Copyright (c) 2013 - 2025 AngleSharp Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From c4bae863fa2f53760e207ea0334605b9d92d696b Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sun, 26 Jan 2025 21:47:08 +0100 Subject: [PATCH 26/73] Updated year --- src/AngleSharp.Js.nuspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleSharp.Js.nuspec b/src/AngleSharp.Js.nuspec index 0b8f08e..55f68cd 100644 --- a/src/AngleSharp.Js.nuspec +++ b/src/AngleSharp.Js.nuspec @@ -13,7 +13,7 @@ false Integrates a JavaScript engine to AngleSharp. https://github.com/AngleSharp/AngleSharp.Js/blob/master/CHANGELOG.md - Copyright 2017-2024s, AngleSharp + Copyright 2017-2025, AngleSharp html html5 css css3 dom javascript scripting library js scripts runtime jint anglesharp angle From 78fd7cbe1c493fa49c31b0b9005100d07dd40475 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sun, 26 Jan 2025 21:48:05 +0100 Subject: [PATCH 27/73] Updated license ref --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 2e010e0..00bd372 100644 --- a/README.md +++ b/README.md @@ -81,12 +81,4 @@ This project is supported by the [.NET Foundation](https://dotnetfoundation.org) ## License -The MIT License (MIT) - -Copyright (c) 2015 - 2024 AngleSharp - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +AngleSharp.Js is released using the MIT license. For more information see the [license file](./LICENSE). From 6eaa0e9c30b0e1b846a89368695234b537cbc5ae Mon Sep 17 00:00:00 2001 From: arekdygas Date: Mon, 14 Apr 2025 13:13:18 +0200 Subject: [PATCH 28/73] IN operator now returns false if string indexer is used to search for property and returns null --- src/AngleSharp.Js.Tests/OperatorsTests.cs | 47 +++++++++++++++++++ .../Proxies/DomPrototypeInstance.cs | 10 +++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/AngleSharp.Js.Tests/OperatorsTests.cs diff --git a/src/AngleSharp.Js.Tests/OperatorsTests.cs b/src/AngleSharp.Js.Tests/OperatorsTests.cs new file mode 100644 index 0000000..ada2f53 --- /dev/null +++ b/src/AngleSharp.Js.Tests/OperatorsTests.cs @@ -0,0 +1,47 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System.Threading.Tasks; + + [TestFixture] + public class OperatorsTests + { + [Test] + public async Task InOperatorExistingAttribute() + { + + var result = await "'action' in document.createElement('form')".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task InOperatorNonExistingAttribute() + { + + var result = await "'action' in document.createElement('div')".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task InOperatorInputWithinForm() + { + var result = await "'input1' in new DOMParser().parseFromString(`
`, 'text/html').body.firstChild".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task InOperatorInputWithinDiv() + { + var result = await "'input1' in new DOMParser().parseFromString(`
`, 'text/html').body.firstChild".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task InOperator_Issue104() + { + var result = await "'somethingThatDoesntExist' in document.createElement('form')".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + } +} diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index 1967c15..a6d2fd1 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -70,7 +70,15 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto if (_stringIndexer != null && !HasProperty(index)) { var args = new Object[] { index }; - var prop = _stringIndexer.GetMethod.Invoke(value, args).ToJsValue(_instance); + var valueAtIndex = _stringIndexer.GetMethod.Invoke(value, args); + + if (valueAtIndex == null) + { + result = PropertyDescriptor.Undefined; + return false; + } + + var prop = valueAtIndex.ToJsValue(_instance); result = new PropertyDescriptor(prop, false, false, false); return true; } From 5d08351c2f005c6d95825755352103288869439e Mon Sep 17 00:00:00 2001 From: arekdygas Date: Thu, 17 Apr 2025 13:15:09 +0200 Subject: [PATCH 29/73] Handling of new accessor: Method --- src/AngleSharp.Js/Extensions/Extensibility.cs | 3 +++ .../Proxies/DomPrototypeInstance.cs | 22 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/AngleSharp.Js/Extensions/Extensibility.cs b/src/AngleSharp.Js/Extensions/Extensibility.cs index 97c9d06..970c562 100644 --- a/src/AngleSharp.Js/Extensions/Extensibility.cs +++ b/src/AngleSharp.Js/Extensions/Extensibility.cs @@ -49,6 +49,9 @@ public static IDictionary GetExtensions(this IEnumerable case Accessors.Adder: entry.Adder = method; break; + case Accessors.Method: + entry.Other = method; + break; } } else diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index a6d2fd1..df226b9 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -148,14 +148,32 @@ private void SetNormalProperties(IEnumerable properties) foreach (var property in properties) { var indexParameters = property.GetIndexParameters(); - var index = property.GetCustomAttribute(); + var accessor = property.GetCustomAttribute()?.Type; var putsForward = property.GetCustomAttribute(); var names = property .GetCustomAttributes() .Select(m => m.OfficialName) .ToArray(); - if (index != null || Array.Exists(names, m => m.Is("item"))) + if (accessor == Accessors.Method) + { + // property decorated with Method accessor, so we need to treat it as a method, not a property + + if (property.GetMethod == null) + { + throw new InvalidOperationException("Getter not found."); + } + + foreach (var name in names) + { + SetMethod(name, property.GetMethod); + } + + // methods were set, so finish processing + return; + } + + if (accessor == Accessors.Getter || accessor == Accessors.Setter || Array.Exists(names, m => m.Is("item"))) { SetIndexer(property, indexParameters); } From 75264e73d26536774ce445abe9613d1031570ef2 Mon Sep 17 00:00:00 2001 From: arekdygas Date: Thu, 17 Apr 2025 13:16:01 +0200 Subject: [PATCH 30/73] Tests added for checking if hasChildNodes is a method and if it works as expected --- src/AngleSharp.Js.Tests/DomTests.cs | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/AngleSharp.Js.Tests/DomTests.cs diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs new file mode 100644 index 0000000..5d1b975 --- /dev/null +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -0,0 +1,30 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System.Threading.Tasks; + + [TestFixture] + public class DomTests + { + [Test] + public async Task NodeHasChildNodesIsAFunction() + { + var result = await "document.createElement('div').hasChildNodes".EvalScriptAsync(); + Assert.AreEqual("function hasChildNodes() { [native code] }", result); + } + + [Test] + public async Task NodeHasChildNodesWithoutChildren() + { + var result = await "document.createElement('div').hasChildNodes()".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task NodeHasChildNodesWithChildren() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.hasChildNodes()".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + } +} From 8681b4a1a2e52b095ab76a9b36f13a2ac00824ea Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Thu, 30 Oct 2025 13:50:53 +0100 Subject: [PATCH 31/73] Update Jint dependency version range to [4.0.0,5.0.0)Updated constraints --- src/AngleSharp.Js.nuspec | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AngleSharp.Js.nuspec b/src/AngleSharp.Js.nuspec index 55f68cd..b813ebd 100644 --- a/src/AngleSharp.Js.nuspec +++ b/src/AngleSharp.Js.nuspec @@ -18,27 +18,27 @@ - + - + - + - + - + - + From dda4f8baf5e3da233e9ea86f5915ca6a70d84942 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 30 Nov 2025 15:33:14 +0200 Subject: [PATCH 32/73] Upgrade to NUKE 10 and use latest GH Actions steps --- .github/workflows/ci.yml | 22 +++++++++------------- nuke/_build.csproj | 4 ++-- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2218216..566307c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,12 +26,12 @@ jobs: if: needs.can_document.outputs.value == 'true' steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - name: Use Node.js - uses: actions/setup-node@v1 + uses: actions/setup-node@v5 with: - node-version: "14.x" + node-version: "20.x" registry-url: 'https://registry.npmjs.org' - name: Install Dependencies @@ -48,14 +48,12 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v5 with: dotnet-version: | - 6.0.x - 7.0.x - 8.0.x + 10.0.x - name: Build run: ./build.sh @@ -64,14 +62,12 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v5 - - uses: actions/setup-dotnet@v1 + - uses: actions/setup-dotnet@v5 with: dotnet-version: | - 6.0.x - 7.0.x - 8.0.x + 10.0.x - name: Build run: | diff --git a/nuke/_build.csproj b/nuke/_build.csproj index 3258f31..a6d62f7 100644 --- a/nuke/_build.csproj +++ b/nuke/_build.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 CS0649;CS0169 .. @@ -11,7 +11,7 @@ - + From 6b9f756f1cd8a71fe5f7203c79d695793f687d98 Mon Sep 17 00:00:00 2001 From: Bogdan Malcev Date: Sat, 4 Jul 2026 01:47:49 +0700 Subject: [PATCH 33/73] Make CreatorCache thread-safe --- src/AngleSharp.Js.Tests/ConstructorTests.cs | 22 +++++++++++++++++++++ src/AngleSharp.Js/Cache/CreatorCache.cs | 14 ++++++------- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/AngleSharp.Js.Tests/ConstructorTests.cs b/src/AngleSharp.Js.Tests/ConstructorTests.cs index 0ec11ca..3e58693 100644 --- a/src/AngleSharp.Js.Tests/ConstructorTests.cs +++ b/src/AngleSharp.Js.Tests/ConstructorTests.cs @@ -1,3 +1,6 @@ +using System.Linq; +using AngleSharp.Dom; + namespace AngleSharp.Js.Tests { using NUnit.Framework; @@ -35,5 +38,24 @@ public async Task CustomEventConstructedWithBubblesTrue() var result = document.QuerySelector("#result").TextContent; Assert.AreEqual("true", result); } + + [Test] + public void CustomEventConstructedConcurrently() + { + var ctx1 = BrowsingContext.New(Configuration.Default.WithJs()); + var ctx2 = BrowsingContext.New(Configuration.Default.WithJs()); + var html = "
"; + Parallel.Invoke(() => Assert(ctx1), () => Assert(ctx2)); + return; + + void Assert(IBrowsingContext context) => Task + .Run(async () => + { + var document = await context.OpenAsync(m => m.Content(html)); + var result = document.QuerySelector("#result").TextContent; + NUnit.Framework.Assert.AreEqual("foo", result); + }) + .Wait(); + } } } diff --git a/src/AngleSharp.Js/Cache/CreatorCache.cs b/src/AngleSharp.Js/Cache/CreatorCache.cs index 5003d93..3c1188e 100644 --- a/src/AngleSharp.Js/Cache/CreatorCache.cs +++ b/src/AngleSharp.Js/Cache/CreatorCache.cs @@ -4,7 +4,7 @@ using Jint.Native.Object; using Jint.Runtime.Descriptors; using System; -using System.Collections.Generic; +using System.Collections.Concurrent; using System.Linq; using System.Reflection; @@ -12,7 +12,7 @@ namespace AngleSharp.Js.Cache { static class CreatorCache { - private static readonly Dictionary> _constructorActions = new Dictionary>(); + private static readonly ConcurrentDictionary> _constructorActions = new(); public static Action GetConstructorAction(this Type type) { @@ -36,13 +36,13 @@ public static Action GetConstructorAction(this T action = (e, o) => { }; } - _constructorActions.Add(type, action); + _constructorActions.TryAdd(type, action); } return action; } - private static readonly Dictionary> _constructorFunctionActions = new Dictionary>(); + private static readonly ConcurrentDictionary> _constructorFunctionActions = new(); public static Action GetConstructorFunctionAction(this Type type) { @@ -69,13 +69,13 @@ public static Action GetConstructorFunctionActio action = (e, o) => { }; } - _constructorFunctionActions.Add(type, action); + _constructorFunctionActions.TryAdd(type, action); } return action; } - private static readonly Dictionary> _instanceActions = new Dictionary>(); + private static readonly ConcurrentDictionary> _instanceActions = new(); public static Action GetInstanceAction(this Type type) { @@ -105,7 +105,7 @@ public static Action GetInstanceAction(this Type action = (e, o) => { }; } - _instanceActions.Add(type, action); + _instanceActions.TryAdd(type, action); } return action; From de4cd7b2bfb59a50b16e95c1fd4a6380b020de88 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Sun, 5 Jul 2026 10:57:39 +0200 Subject: [PATCH 34/73] Updated version --- CHANGELOG.md | 4 +++- CONTRIBUTORS.md | 3 +++ src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj | 2 +- src/AngleSharp.Js.Tests/packages.config | 8 -------- src/AngleSharp.Js.nuspec | 12 ++++++------ src/AngleSharp.Js/AngleSharp.Js.csproj | 2 +- 6 files changed, 14 insertions(+), 17 deletions(-) delete mode 100644 src/AngleSharp.Js.Tests/packages.config diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b1f32..26ffd1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,8 +3,10 @@ Released on ?. - Fixed usage of document ready state (#87) @Sebbs128 +- Fixed `HasChildNodes` is now exposed as a method to DOM (#106) @arekdygas - Updated to use AngleSharp v1 -- Updated for Jint v3 (#89) @tomvanenckevort +- Updated for Jint v4 (#89, #97) @tomvanenckevort @lahma +- Updated CreatorCache to be thread-safe (#110) @badnickname # 0.15.0 diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 19e5b64..4583bc6 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -10,6 +10,9 @@ AngleSharp.Js contains code written by (in order of first pull request / commit) * [doominator42](https://github.com/doominator42) * [Tom van Enckevort](https://github.com/tomvanenckevort) * [Wayne Sebbens](https://github.com/Sebbs128) +* [Marko Lahma](https://github.com/lahma) +* [Bogdan Maltcev](https://github.com/badnickname) +* [arekdygas](https://github.com/arekdygas) Without these awesome people AngleSharp.Js could not exist. Thanks to everyone for your contributions! :beers: diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 9a61a14..52df45e 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -18,7 +18,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/AngleSharp.Js.Tests/packages.config b/src/AngleSharp.Js.Tests/packages.config deleted file mode 100644 index 1f6e70c..0000000 --- a/src/AngleSharp.Js.Tests/packages.config +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/src/AngleSharp.Js.nuspec b/src/AngleSharp.Js.nuspec index b813ebd..1e793ba 100644 --- a/src/AngleSharp.Js.nuspec +++ b/src/AngleSharp.Js.nuspec @@ -17,27 +17,27 @@ html html5 css css3 dom javascript scripting library js scripts runtime jint anglesharp angle - + - + - + - + - + - + diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index e2a7010..c4396a6 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -17,7 +17,7 @@
- + From ae4485ae0bb3f646b462325f6af044e1d094b65d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:51:18 +0300 Subject: [PATCH 35/73] Fix indexed access into DOM collections Indexing a DOM collection from script - `document.getElementsByTagName('p')[0]`, `element.classList[0]`, and everything jQuery builds on top of them - throws a `TargetInvocationException` wrapping `EntryPointNotFoundException`. `IHtmlCollection`, `ITokenList` and `IStringList` declare their numeric indexer as an explicit re-implementation of `IReadOnlyList`'s indexer. A member declared that way on an interface is private and abstract, so the `MethodInfo` obtained from the interface's `PropertyInfo` cannot be invoked: the implementation lives in a different slot and the runtime has no entry point for the one we hand it. Resolve the indexer accessor once, when the prototype is built, against the type the prototype belongs to, and invoke that. Public accessors - the common case, including every string indexer and the numeric indexers of `INodeList`, `INamedNodeMap`, `IStyleSheetList` and friends - are used unchanged. This is what makes the six currently failing tests in `ScriptingTests` and `JqueryTests` fail; they pass again with this change. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/DomTests.cs | 28 +++++++++ .../Proxies/DomPrototypeInstance.cs | 63 +++++++++++++++---- 2 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index 5d1b975..ffabd0b 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -26,5 +26,33 @@ public async Task NodeHasChildNodesWithChildren() var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.hasChildNodes()".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(`
`, 'text/html').body.childNodes[0].nodeName".EvalScriptAsync(); + Assert.AreEqual("DIV", result); + } + + [Test] + public async Task NumericIndexerOfTokenListYieldsTheToken() + { + var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.classList[1]".EvalScriptAsync(); + Assert.AreEqual("b", result); + } } } diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index df226b9..540688a 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -15,9 +15,10 @@ sealed class DomPrototypeInstance : ObjectInstance { private readonly String _name; private readonly EngineInstance _instance; + private readonly Type _type; - private PropertyInfo _numericIndexer; - private PropertyInfo _stringIndexer; + private MethodInfo _numericIndexer; + private MethodInfo _stringIndexer; public DomPrototypeInstance(EngineInstance engine, Type type) : base(engine.Jint) @@ -25,6 +26,7 @@ public DomPrototypeInstance(EngineInstance engine, Type type) var baseType = type.GetTypeInfo().BaseType ?? typeof(Object); _name = type.GetOfficialName(baseType); _instance = engine; + _type = type; Set(GlobalSymbolRegistry.ToStringTag, _name); @@ -45,7 +47,7 @@ public Boolean TryGetFromIndex(Object value, String index, out PropertyDescripto 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; } @@ -70,7 +72,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) { @@ -229,17 +231,52 @@ 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.this[Int32 index]" declared on an + // IHtmlCollection. 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) From 78e1e32019617d957eb8c4133aafd84bc63fa60d Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:53:06 +0300 Subject: [PATCH 36/73] Do not abandon a type's remaining properties after a method-like one `SetNormalProperties` walks the declared properties of a type and registers each of them on the prototype. A property carrying `[DomAccessor(Accessors.Method)]` is registered as a method instead - and then the loop `return`s, so every property declared after it on the same type is silently never registered. The intent was clearly "this property is done, move on"; use `continue`. Whether anything is lost today depends on the order `DeclaredProperties` happens to return members in, which is not specified. With the current AngleSharp DOM the only affected type is `INode` (`hasChildNodes` is followed by one property with no `[DomName]`), so this is a latent defect rather than an observable one - but `INode` is in the type tree of every node prototype, and the next attributed property added after a method-like one would silently disappear from the DOM. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index df226b9..55ef966 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -169,8 +169,8 @@ private void SetNormalProperties(IEnumerable properties) SetMethod(name, property.GetMethod); } - // methods were set, so finish processing - return; + // methods were set, so continue with the next property + continue; } if (accessor == Accessors.Getter || accessor == Accessors.Setter || Array.Exists(names, m => m.Is("item"))) From a64f3e14f7281c9095ad044ff7e6ce9213857b40 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:59:55 +0300 Subject: [PATCH 37/73] Keep event handler properties per node instead of per prototype `onclick` and friends are exposed through a `DomEventInstance` that is created once per prototype - and it stores the assigned function and the subscribed listener in its own fields. Every node of that type therefore shares one handler slot: a.onclick = f; b.onclick = g; a.onclick === g // true a.onclick === b.onclick // true Assigning to `b` also unsubscribes `f` from `a`, so `a` stops reacting to its own handler, and clearing the property on any node clears it for all of them. Move the assigned function and its listener onto the node. The two `ClrFunction` accessors stay on the prototype, shared as before; only the state they read and write is now looked up per node. The dictionary holding it is allocated lazily, so nodes without handlers are unchanged. This also stops the prototype from holding on to the last function assigned anywhere in the document, which kept that closure - and everything it captured - alive for as long as the engine. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/FireEventTests.cs | 70 +++++++++++++++++++ src/AngleSharp.Js/Proxies/DomEventInstance.cs | 50 +++++++++---- src/AngleSharp.Js/Proxies/DomNodeInstance.cs | 41 +++++++++++ 3 files changed, 148 insertions(+), 13 deletions(-) diff --git a/src/AngleSharp.Js.Tests/FireEventTests.cs b/src/AngleSharp.Js.Tests/FireEventTests.cs index cddaf59..0e69f07 100644 --- a/src/AngleSharp.Js.Tests/FireEventTests.cs +++ b/src/AngleSharp.Js.Tests/FireEventTests.cs @@ -156,6 +156,76 @@ public async Task AddAndInvokeClickHandlerWithStringFunctionWontWork() Assert.IsFalse(clicked); } + [Test] + public async Task ClickHandlerIsKeptPerElement() + { + var service = new JsScriptingService(); + var cfg = Configuration.Default.With(service).WithEventLoop(); + var html = @" + + +
+
+ +"; + var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)); + var engine = service.GetOrCreateJint(document); + + Assert.IsTrue(engine.GetValue("aIsF").AsBoolean()); + Assert.IsTrue(engine.GetValue("bIsG").AsBoolean()); + Assert.IsFalse(engine.GetValue("shared").AsBoolean()); + + var log = engine.GetValue("log").AsArray(); + Assert.AreEqual(2.0, log.Get("length").AsNumber()); + Assert.AreEqual("f", log.Get("0").AsString()); + Assert.AreEqual("g", log.Get("1").AsString()); + } + + [Test] + public async Task ClearingClickHandlerOfOneElementKeepsTheOther() + { + var service = new JsScriptingService(); + var cfg = Configuration.Default.With(service).WithEventLoop(); + var html = @" + + +
+
+ +"; + var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)); + var engine = service.GetOrCreateJint(document); + + Assert.IsTrue(engine.GetValue("cleared").AsBoolean()); + + var log = engine.GetValue("log").AsArray(); + Assert.AreEqual(1.0, log.Get("length").AsNumber()); + Assert.AreEqual("g", log.Get("0").AsString()); + } + [Test] public async Task BodyOnloadWorksWhenSetAsAttributeInitially() { diff --git a/src/AngleSharp.Js/Proxies/DomEventInstance.cs b/src/AngleSharp.Js/Proxies/DomEventInstance.cs index 3eb9d23..503caca 100644 --- a/src/AngleSharp.Js/Proxies/DomEventInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomEventInstance.cs @@ -13,8 +13,6 @@ sealed class DomEventInstance private readonly EngineInstance _engine; private readonly MethodInfo _addHandler; private readonly MethodInfo _removeHandler; - private DomEventHandler _handler; - private Function _function; public DomEventInstance(EngineInstance engine, MethodInfo addHandler, MethodInfo removeHandler) { @@ -29,8 +27,12 @@ public DomEventInstance(EngineInstance engine, MethodInfo addHandler, MethodInfo public ClrFunction Setter { get; } - private JsValue GetEventHandler(JsValue thisObject, JsValue[] arguments) => - _function ?? JsValue.Null; + private JsValue GetEventHandler(JsValue thisObject, JsValue[] arguments) + { + var node = thisObject.As(); + var registration = node?.GetEventHandler(this); + return registration?.Function ?? JsValue.Null; + } private JsValue SetEventHandler(JsValue thisObject, JsValue[] arguments) { @@ -38,28 +40,50 @@ private JsValue SetEventHandler(JsValue thisObject, JsValue[] arguments) if (node != null) { - if (_handler != null) + var previous = node.RemoveEventHandler(this); + + if (previous != null) { - _removeHandler?.Invoke(node.Value, new Object[] { _handler }); - _handler = null; - _function = null; + _removeHandler?.Invoke(node.Value, new Object[] { previous.Handler }); } - if (arguments[0] is Function) + if (arguments[0] is Function function) { - _function = arguments[0].As(); - _handler = (s, ev) => + DomEventHandler handler = (s, ev) => { var sender = s.ToJsValue(_engine); var args = ev.ToJsValue(_engine); - _function.Call(sender, new[] { args }); + function.Call(sender, new[] { args }); }; - _addHandler?.Invoke(node.Value, new Object[] { _handler }); + node.SetEventHandler(this, new Registration(function, handler)); + _addHandler?.Invoke(node.Value, new Object[] { handler }); } } return arguments[0]; } + + /// + /// The handler currently assigned to a single node for a single event. + /// + public sealed class Registration + { + public Registration(Function function, DomEventHandler handler) + { + Function = function; + Handler = handler; + } + + /// + /// The function that was assigned, as it has to be handed back on read. + /// + public Function Function { get; } + + /// + /// The listener that was subscribed, as it has to be handed back on removal. + /// + public DomEventHandler Handler { get; } + } } } diff --git a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs index 2d4dec8..dba951b 100644 --- a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs @@ -5,12 +5,15 @@ namespace AngleSharp.Js using Jint.Native.Object; using Jint.Runtime.Descriptors; using System; + using System.Collections.Generic; sealed class DomNodeInstance : ObjectInstance { private readonly EngineInstance _instance; private readonly Object _value; + private Dictionary _eventHandlers; + public DomNodeInstance(EngineInstance engine, Object value) : base(engine.Jint) { @@ -24,6 +27,44 @@ public DomNodeInstance(EngineInstance engine, Object value) public override object ToObject() => _value; + /// + /// Gets the handler assigned to this node for the given event, if any. + /// The handler is per node - the itself is + /// shared by every node using the same prototype. + /// + public DomEventInstance.Registration GetEventHandler(DomEventInstance ev) + { + if (_eventHandlers != null && _eventHandlers.TryGetValue(ev, out var registration)) + { + return registration; + } + + return null; + } + + /// + /// Assigns the handler for the given event to this node. + /// + public void SetEventHandler(DomEventInstance ev, DomEventInstance.Registration registration) + { + _eventHandlers = _eventHandlers ?? new Dictionary(); + _eventHandlers[ev] = registration; + } + + /// + /// Removes and returns the handler assigned to this node for the given event, if any. + /// + public DomEventInstance.Registration RemoveEventHandler(DomEventInstance ev) + { + if (_eventHandlers != null && _eventHandlers.TryGetValue(ev, out var registration)) + { + _eventHandlers.Remove(ev); + return registration; + } + + return null; + } + public override PropertyDescriptor GetOwnProperty(JsValue property) { if (Prototype is DomPrototypeInstance prototype) From e22e05d24a020ac8dc6294896bbf0f454bf04578 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 21:02:03 +0300 Subject: [PATCH 38/73] Parse import maps as JSON instead of interpolating them into script `LoadImportMap` builds the source string `JSON.parse('')` and evaluates it. The content of a `"; + var document = await context.OpenAsync(r => r.Content(html)); + Assert.IsNull(document.GetElementById("test")); + } + + [Test] + public async Task ImportMapContentIsNotEvaluatedAsScript() + { + var config = Configuration.Default.WithJs(); + var context = BrowsingContext.New(config); + var html = "
Test
"; + var document = await context.OpenAsync(r => r.Content(html)); + Assert.IsNotNull(document.GetElementById("test")); + } + [Test] public async Task ModuleScriptWithAbsoluteUrlImportMapShouldRun() { diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index fe3d678..e0f7411 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -5,6 +5,7 @@ namespace AngleSharp.Js using AngleSharp.Text; using Jint; using Jint.Native; + using Jint.Native.Json; using Jint.Native.Object; using System; using System.Collections.Generic; @@ -117,7 +118,10 @@ public JsValue RunScript(String source, String type, String sourceUrl, JsValue c private JsValue LoadImportMap(String source) { - var importMap = _engine.Evaluate($"JSON.parse('{source}')").AsObject(); + // The source is page content, so it must be handed to a JSON parser rather + // than pasted into a script: a single quote already breaks the parse, and + // anything after a closing quote would run as script. + var importMap = new JsonParser(_engine).Parse(source).AsObject(); if (importMap.TryGetValue("scopes", out var scopes)) { From b175ddfefab9d29058e898cda4a16f8dcd7b87ad Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 21:06:22 +0300 Subject: [PATCH 39/73] Cache parsed scripts instead of re-parsing them for every document Every document gets its own engine, and `RunScript` hands the raw source to `Engine.Evaluate(string)`, which parses it again from scratch. A page that loads a library therefore pays the full parse of that library once per document, even though the result is identical every time. Prepare the script once and evaluate the prepared form. Jint documents a prepared script as reusable and thread-safe, so one entry can serve every document that runs the same source. Sources that fail to prepare are handed to the engine as text, so a syntax error is still reported by the engine and not turned into a different exception type. The cache is capped, because nothing bounds how many distinct scripts a process may see; the entries kept are the ones encountered first, which is where shared libraries show up. Also drops `RunScript`'s `context` parameter and the overload that fed it. The parameter was never read, and the overload that converted a node for it had no callers. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/InteractionTests.cs | 24 +++++++++ src/AngleSharp.Js/Cache/ScriptCache.cs | 54 +++++++++++++++++++ src/AngleSharp.Js/EngineInstance.cs | 8 ++- .../Extensions/EngineExtensions.cs | 6 --- 4 files changed, 84 insertions(+), 8 deletions(-) create mode 100644 src/AngleSharp.Js/Cache/ScriptCache.cs diff --git a/src/AngleSharp.Js.Tests/InteractionTests.cs b/src/AngleSharp.Js.Tests/InteractionTests.cs index eff1a33..178524a 100644 --- a/src/AngleSharp.Js.Tests/InteractionTests.cs +++ b/src/AngleSharp.Js.Tests/InteractionTests.cs @@ -113,6 +113,30 @@ public async Task RunScriptSnippetDirectlyGetSimpleValueFromCalculation() Assert.AreEqual(3.0, result); } + [Test] + public async Task RunSameScriptSourceInSeveralDocumentsKeepsStateSeparate() + { + var html = "Test"; + var config = Configuration.Default.WithJs(); + var source = "(function () { window.counter = (window.counter || 0) + 1; return window.counter; })()"; + var first = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + var second = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + + Assert.AreEqual(1.0, first.ExecuteScript(source)); + Assert.AreEqual(1.0, second.ExecuteScript(source)); + Assert.AreEqual(2.0, first.ExecuteScript(source)); + Assert.AreEqual(2.0, second.ExecuteScript(source)); + } + + [Test] + public async Task RunScriptSnippetWithSyntaxErrorThrows() + { + var html = "Test"; + var config = Configuration.Default.WithJs(); + var document = await BrowsingContext.New(config).OpenAsync(m => m.Content(html)); + Assert.Throws(() => document.ExecuteScript("function (")); + } + [Test] public async Task RunScriptAtPressingLink_Issue47() { diff --git a/src/AngleSharp.Js/Cache/ScriptCache.cs b/src/AngleSharp.Js/Cache/ScriptCache.cs new file mode 100644 index 0000000..99faff4 --- /dev/null +++ b/src/AngleSharp.Js/Cache/ScriptCache.cs @@ -0,0 +1,54 @@ +namespace AngleSharp.Js.Cache +{ + using Acornima.Ast; + using Jint; + using Jint.Runtime; + using System; + using System.Collections.Concurrent; + + /// + /// Caches the parsed and analyzed form of scripts. A prepared script is + /// documented by Jint as reusable and thread-safe, so the same one can serve + /// every document that runs the same source - a page loading a library ends up + /// parsing it once instead of once per document. + /// + static class ScriptCache + { + // There is no bound on how many distinct scripts a process may see, so the + // cache does not grow without end. The scripts worth keeping are the ones + // seen first: libraries are referenced early and by many documents. + private const Int32 Capacity = 32; + + private static readonly ConcurrentDictionary> _scripts = + new ConcurrentDictionary>(StringComparer.Ordinal); + + /// + /// Gets the prepared form of the given source. An invalid result means the + /// source could not be prepared and should be handed to the engine as text, + /// so that the engine reports the syntax error itself. + /// + public static Prepared +"; + var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)) + .WhenStable(); + var log = service.GetOrCreateJint(document).GetValue("log").AsArray(); + + Assert.AreEqual(3.0, log.Get("length").AsNumber()); + Assert.AreEqual("a", log.Get("0").AsString()); + Assert.AreEqual("b", log.Get("1").AsString()); + Assert.AreEqual("c", log.Get("2").AsString()); + } + [Test] public async Task SetTimeoutWithStringAsFunction() { diff --git a/src/AngleSharp.Js/Cache/MethodCache.cs b/src/AngleSharp.Js/Cache/MethodCache.cs new file mode 100644 index 0000000..364787e --- /dev/null +++ b/src/AngleSharp.Js/Cache/MethodCache.cs @@ -0,0 +1,70 @@ +namespace AngleSharp.Js.Cache +{ + using AngleSharp.Attributes; + using AngleSharp.Dom; + using System; + using System.Collections.Concurrent; + using System.Reflection; + + /// + /// Describes what building the arguments of a method needs to know about it. + /// All of it is fixed by the method itself, so it is worked out once instead of + /// on every call from script. + /// + sealed class MethodDescription + { + private static readonly ConcurrentDictionary _descriptions = + new ConcurrentDictionary(); + + private MethodDescription(MethodBase method) + { + var parameters = method.GetParameters(); + var descriptions = new ParameterDescription[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + descriptions[i] = new ParameterDescription(parameters[i]); + } + + Parameters = descriptions; + InitDict = method.GetCustomAttribute(); + TakesWindow = parameters.Length > 0 && parameters[0].ParameterType == typeof(IWindow); + TakesParamArray = parameters.Length > 0 && + parameters[parameters.Length - 1].GetCustomAttribute() != null; + } + + public static MethodDescription Of(MethodBase method) => + _descriptions.GetOrAdd(method, m => new MethodDescription(m)); + + public ParameterDescription[] Parameters { get; } + + public DomInitDictAttribute InitDict { get; } + + public Boolean TakesWindow { get; } + + public Boolean TakesParamArray { get; } + } + + /// + /// The parts of a that are read when arguments are + /// built. Reading them from reflection means walking metadata every time. + /// + readonly struct ParameterDescription + { + public ParameterDescription(ParameterInfo parameter) + { + Name = parameter.Name; + ParameterType = parameter.ParameterType; + IsOptional = parameter.IsOptional; + DefaultValue = parameter.IsOptional ? parameter.DefaultValue : null; + } + + public String Name { get; } + + public Type ParameterType { get; } + + public Boolean IsOptional { get; } + + public Object DefaultValue { get; } + } +} diff --git a/src/AngleSharp.Js/Extensions/DomDelegates.cs b/src/AngleSharp.Js/Extensions/DomDelegates.cs index 5c95086..339d05b 100644 --- a/src/AngleSharp.Js/Extensions/DomDelegates.cs +++ b/src/AngleSharp.Js/Extensions/DomDelegates.cs @@ -7,6 +7,7 @@ namespace AngleSharp.Js using Jint.Native.Function; using Jint.Runtime; using System; + using System.Collections.Concurrent; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -16,11 +17,20 @@ static class DomDelegates private static readonly Type[] ToCallbackSignature = new[] { typeof(Function), typeof(EngineInstance) }; private static readonly Type[] ToJsValueSignature = new[] { typeof(Object), typeof(EngineInstance) }; + // Both of these depend on the delegate type alone, so they survive the conversion + // they were built for: turning a function into a delegate is otherwise a reflection + // lookup plus an expression tree compilation, every single time. + private static readonly ConcurrentDictionary _converters = + new ConcurrentDictionary(); + + private static readonly ConcurrentDictionary _factories = + new ConcurrentDictionary(); + public static Delegate ToDelegate(this Type type, Function function, EngineInstance engine) { if (type != typeof(DomEventHandler)) { - var method = typeof(DomDelegates).GetRuntimeMethod("ToCallback", ToCallbackSignature).MakeGenericMethod(type); + var method = _converters.GetOrAdd(type, CreateConverter); return method.Invoke(null, new Object[] { function, engine }) as Delegate; } @@ -44,6 +54,15 @@ public static DomEventHandler ToListener(this Function function, EngineInstance }; public static T ToCallback(this Function function, EngineInstance engine) + { + var factory = (Func)_factories.GetOrAdd(typeof(T), _ => CreateFactory()); + return factory.Invoke(function, engine); + } + + private static MethodInfo CreateConverter(Type type) => + typeof(DomDelegates).GetRuntimeMethod("ToCallback", ToCallbackSignature).MakeGenericMethod(type); + + private static Func CreateFactory() { var methodInfo = typeof(T).GetRuntimeMethods().First(m => m.Name == "Invoke"); var convert = typeof(EngineExtensions).GetRuntimeMethod("ToJsValue", ToJsValueSignature); @@ -55,15 +74,16 @@ public static T ToCallback(this Function function, EngineInstance engine) parameters[i] = Expression.Parameter(mps[i].ParameterType, mps[i].Name); } - var objExpr = Expression.Constant(function); - var engineExpr = Expression.Constant(engine); + var objExpr = Expression.Parameter(typeof(Function), "function"); + var engineExpr = Expression.Parameter(typeof(EngineInstance), "engine"); var call = Expression.Call(objExpr, "Call", new Type[0], new Expression[] { Expression.Call(convert, parameters[0], engineExpr), Expression.NewArrayInit(typeof(JsValue), parameters.Skip(1).Select(m => Expression.Call(convert, m, engineExpr)).ToArray()) }); - return Expression.Lambda(call, parameters).Compile(); + var callback = Expression.Lambda(call, parameters); + return Expression.Lambda>(callback, objExpr, engineExpr).Compile(); } } } diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index e84e3d2..fa535fc 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -10,7 +10,6 @@ namespace AngleSharp.Js using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using System; - using System.Collections.Generic; using System.Reflection; static class EngineExtensions @@ -72,13 +71,14 @@ public static PropertyDescriptor AsProperty(this Engine engine, JsValue getter = public static Object[] BuildArgs(this EngineInstance context, MethodBase method, JsValue[] arguments) { - var parameters = method.GetParameters(); - var initDict = method.GetCustomAttribute(); + var description = MethodDescription.Of(method); + var parameters = description.Parameters; + var initDict = description.InitDict; var max = parameters.Length; var args = new Object[max]; var offset = 0; - if (parameters.Length > 0 && parameters[0].ParameterType == typeof(IWindow)) + if (description.TakesWindow) { if (arguments.Length == 0 || arguments[0].FromJsValue() is IWindow == false) { @@ -86,7 +86,7 @@ public static Object[] BuildArgs(this EngineInstance context, MethodBase method, } } - if (max > 0 && parameters[max - 1].GetCustomAttribute() != null) + if (description.TakesParamArray) { max--; } @@ -137,7 +137,7 @@ public static Object[] BuildArgs(this EngineInstance context, MethodBase method, return args; } - private static JsValue[] ExpandInitDict(JsValue[] arguments, ParameterInfo[] parameters, DomInitDictAttribute initDict, Int32 max, Int32 offset) + private static JsValue[] ExpandInitDict(JsValue[] arguments, ParameterDescription[] parameters, DomInitDictAttribute initDict, Int32 max, Int32 offset) { var newArgs = new JsValue[max - offset]; var end = initDict.Offset - offset; @@ -234,12 +234,10 @@ public static JsValue Call(this EngineInstance instance, MethodInfo method, JsVa { if (method.IsStatic) { - var newArgs = new List - { - nodeInstance, - }; - newArgs.AddRange(arguments); - var parameters = instance.BuildArgs(method, newArgs.ToArray()); + var newArgs = new JsValue[arguments.Length + 1]; + newArgs[0] = nodeInstance; + Array.Copy(arguments, 0, newArgs, 1, arguments.Length); + var parameters = instance.BuildArgs(method, newArgs); return method.Invoke(null, parameters).ToJsValue(instance); } else From a9fdd0a0e8e1845018dc15cc499a9caaa3e4d599 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 21:20:19 +0300 Subject: [PATCH 42/73] Update Jint to 4.14.0 Thirteen minor versions of engine fixes and performance work since 4.1.0, with no source changes needed here. Verified on all target frameworks: net462, net472 and net8.0 build and run the test suite with exactly the same results as 4.1.0. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js/AngleSharp.Js.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AngleSharp.Js/AngleSharp.Js.csproj b/src/AngleSharp.Js/AngleSharp.Js.csproj index c4396a6..a910151 100644 --- a/src/AngleSharp.Js/AngleSharp.Js.csproj +++ b/src/AngleSharp.Js/AngleSharp.Js.csproj @@ -18,7 +18,7 @@ - + From 0e50cc0921281289ce5112f469811a4135ab5ed4 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 21:18:08 +0300 Subject: [PATCH 43/73] Return the same console object for a window `window.console` builds a new `Console` on every access, so the identity a page observes is never stable: window.console === window.console // false window.console.marker = 1; window.console.marker // undefined Every read also allocates the console, a wrapper for it, and re-resolves the logger service. Keep one console per window instead, in a table that does not keep the window alive. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/DomTests.cs | 14 ++++++++++++++ src/AngleSharp.Js/Dom/WindowExtensions.cs | 10 ++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index 88b41b7..e14fe45 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -68,5 +68,19 @@ public async Task NumericIndexerOfTokenListYieldsTheToken() var result = await "new DOMParser().parseFromString(`
`, 'text/html').body.firstChild.classList[1]".EvalScriptAsync(); Assert.AreEqual("b", result); } + + [Test] + public async Task ConsoleIsTheSameObjectOnEveryAccess() + { + var result = await "window.console === window.console".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ConsoleKeepsPropertiesAssignedToIt() + { + var result = await "(function () { window.console.marker = 'kept'; return window.console.marker; })()".EvalScriptAsync(); + Assert.AreEqual("kept", result); + } } } diff --git a/src/AngleSharp.Js/Dom/WindowExtensions.cs b/src/AngleSharp.Js/Dom/WindowExtensions.cs index 27835b9..679ae9d 100644 --- a/src/AngleSharp.Js/Dom/WindowExtensions.cs +++ b/src/AngleSharp.Js/Dom/WindowExtensions.cs @@ -7,6 +7,7 @@ namespace AngleSharp.Js.Dom using AngleSharp.Html.Dom; using AngleSharp.Js.Attributes; using System; + using System.Runtime.CompilerServices; /// /// Defines a set of extensions for the window object. @@ -14,6 +15,9 @@ namespace AngleSharp.Js.Dom [DomExposed("Window")] public static class WindowExtensions { + private static readonly ConditionalWeakTable Consoles = + new ConditionalWeakTable(); + /// /// Posts a message. /// @@ -34,7 +38,9 @@ public static void PostMessage(this IWindow window, String message, String targe public static IWindow Top(this IWindow window) => window.Document.Context?.Creator?.DefaultView; /// - /// Gets the console instance. + /// Gets the console instance. The same instance is returned for the same + /// window, so that `window.console === window.console` holds and anything + /// script puts on the console is still there on the next access. /// /// /// @@ -42,7 +48,7 @@ public static void PostMessage(this IWindow window, String message, String targe [DomAccessor(Accessors.Getter)] public static Console Console(this IWindow window) { - return new Console(window); + return Consoles.GetValue(window, w => new Console(w)); } /// From ef497c268c38abb9e894007be6b9333afba16333 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Sun, 26 Jul 2026 20:57:36 +0300 Subject: [PATCH 44/73] Stop DOM nodes from claiming inherited members as own properties `DomNodeInstance.GetOwnProperty` falls back to returning the prototype's descriptor when the node has no own property of that name. `[[GetOwnProperty]]` is not supposed to look past the object itself, so a node ends up disagreeing with itself: document.body.hasOwnProperty('firstChild') // true Object.getOwnPropertyDescriptor(b, 'firstChild') // an accessor Object.getOwnPropertyNames(document.body) // [] Object.keys(document.body) // [] Library code that feature-detects with `hasOwnProperty` before walking the prototype chain gets the wrong answer, and anything that pairs `getOwnPropertyNames` with `getOwnPropertyDescriptor` sees an object whose keys and descriptors do not agree. Drop the fallback. An indexer is the only thing that can legitimately produce an own property here, so that probe stays; everything else is a member of the DOM interface, lives on the prototype, and is found by the engine's ordinary own -> prototype lookup. Reads, writes, method calls and `in` are unaffected - only the "is this mine?" answer changes. As a side effect the node stops answering the prototype's own-property probe on behalf of the prototype, which is what lets an engine cache a prototype lookup: such caches only engage once the receiver genuinely reports no own property. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_0179sA2T7HuRfRfSc2JirFik --- src/AngleSharp.Js.Tests/DomTests.cs | 28 ++++++++++++++++++++ src/AngleSharp.Js/Proxies/DomNodeInstance.cs | 18 +++++-------- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index e14fe45..bfd1e4c 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -82,5 +82,33 @@ public async Task ConsoleKeepsPropertiesAssignedToIt() var result = await "(function () { window.console.marker = 'kept'; return window.console.marker; })()".EvalScriptAsync(); Assert.AreEqual("kept", result); } + + [Test] + public async Task InheritedMemberIsNotAnOwnPropertyOfTheNode() + { + var result = await "document.createElement('div').hasOwnProperty('firstChild')".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task InheritedMemberIsStillVisibleOnTheNode() + { + var result = await "('firstChild' in document.documentElement) + ',' + (typeof document.documentElement.appendChild)".EvalScriptAsync(); + Assert.AreEqual("true,function", result); + } + + [Test] + public async Task InheritedAccessorStillReadsAndWrites() + { + var result = await "(function () { var d = document.createElement('div'); d.id = 'jint'; return d.id; })()".EvalScriptAsync(); + Assert.AreEqual("jint", result); + } + + [Test] + public async Task AssignedPropertyIsReportedConsistently() + { + var result = await "(function () { var d = document.createElement('div'); d.custom = 1; return d.hasOwnProperty('custom') + ',' + Object.getOwnPropertyNames(d).join(); })()".EvalScriptAsync(); + Assert.AreEqual("true,custom", result); + } } } diff --git a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs index dba951b..d70ba4c 100644 --- a/src/AngleSharp.Js/Proxies/DomNodeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomNodeInstance.cs @@ -67,18 +67,14 @@ public DomEventInstance.Registration RemoveEventHandler(DomEventInstance ev) public override PropertyDescriptor GetOwnProperty(JsValue property) { - if (Prototype is DomPrototypeInstance prototype) + // An indexer is the only thing that can turn into an own property of the node + // itself. The members of the DOM interface live on the prototype, so finding + // them is the engine's job - answering them here would make the node claim + // every inherited member as its own. + if (Prototype is DomPrototypeInstance prototype && + prototype.TryGetFromIndex(_value, property.ToString(), out var descriptor)) { - if (prototype.TryGetFromIndex(_value, property.ToString(), out var descriptor)) - { - return descriptor; - } - - var prototypeProperty = prototype.GetOwnProperty(property); - if (prototypeProperty != PropertyDescriptor.Undefined) - { - return prototypeProperty; - } + return descriptor; } return base.GetOwnProperty(property); From da4c064e307bd4deecb9b2531a8f73696c7c3173 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 00:07:16 +0300 Subject: [PATCH 45/73] Build the constructor of an exposed type when script first names it Every exposed type got a constructor object as the engine was set up: 178 of them per engine, each with its own prototype object, toString function and property descriptors. A document names a handful of them. The property is still registered for every type, with the attributes it had, so nothing about enumerating the window changes - only the object behind it waits for the first read. The prototype keeps the constructor, so reading the name off the window and reading "constructor" off an instance still arrive at the same object; a prototype reached through an instance asks for its constructor itself, since that path never names the type. --- .../DeferredConstructorTests.cs | 141 ++++++++++++++++++ src/AngleSharp.Js/Cache/CreatorCache.cs | 58 +++++-- src/AngleSharp.Js/EngineInstance.cs | 13 ++ .../Extensions/EngineExtensions.cs | 8 +- .../Proxies/DomConstructorDescriptor.cs | 37 +++++ .../Proxies/DomConstructorInstance.cs | 15 +- .../Proxies/DomPrototypeInstance.cs | 20 +++ 7 files changed, 265 insertions(+), 27 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/DeferredConstructorTests.cs create mode 100644 src/AngleSharp.Js/Proxies/DomConstructorDescriptor.cs diff --git a/src/AngleSharp.Js.Tests/DeferredConstructorTests.cs b/src/AngleSharp.Js.Tests/DeferredConstructorTests.cs new file mode 100644 index 0000000..4cb9155 --- /dev/null +++ b/src/AngleSharp.Js.Tests/DeferredConstructorTests.cs @@ -0,0 +1,141 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System.Threading.Tasks; + + /// + /// The constructor of an exposed type is only built once script reads the property it + /// is published under, so these cover what a reader is entitled to see either way. + /// + [TestFixture] + public class DeferredConstructorTests + { + [Test] + public async Task ConstructorIsSameObjectOnWindowAndGlobal() + { + var result = await "String(window.HTMLDivElement === HTMLDivElement)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorIsSameObjectOnEveryRead() + { + var result = await "String((function () { var a = HTMLDivElement; var b = window.HTMLDivElement; return a === b && a === HTMLDivElement; })())".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorIsFunction() + { + var result = await "typeof HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("function", result); + } + + [Test] + public async Task UnreadConstructorIsStillOwnPropertyOfWindow() + { + var result = await "String(window.hasOwnProperty('HTMLTableColElement'))".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task UnreadConstructorIsStillEnumerable() + { + var result = await "String(Object.keys(window).indexOf('HTMLTableColElement') !== -1)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task UnreadConstructorIsStillFoundByForIn() + { + var result = await "String((function () { for (var k in window) { if (k === 'HTMLTableColElement') { return true; } } return false; })())".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorKeepsItsAttributes() + { + var result = await "(function () { var d = Object.getOwnPropertyDescriptor(window, 'HTMLDivElement'); return d.writable + ',' + d.enumerable + ',' + d.configurable; })()".EvalScriptAsync(); + Assert.AreEqual("false,true,false", result); + } + + [Test] + public async Task DescriptorValueIsTheConstructor() + { + var result = await "String(Object.getOwnPropertyDescriptor(window, 'HTMLDivElement').value === HTMLDivElement)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ReadingAConstructorDoesNotChangeTheKeysOfWindow() + { + var result = await "String((function () { var before = Object.keys(window).length; var c = HTMLDivElement; return before === Object.keys(window).length; })())".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructedInstanceIsInstanceOfItsConstructor() + { + var result = await "String(new CustomEvent('foo') instanceof CustomEvent)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorBuildsInstances() + { + var result = await "new CustomEvent('foo').type".EvalScriptAsync(); + Assert.AreEqual("foo", result); + } + + [Test] + public async Task PrototypePointsBackAtItsConstructor() + { + var result = await "String(HTMLDivElement.prototype.constructor === HTMLDivElement)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + // Reaching a prototype through an instance is the one path that never names the + // type, so it is the one that has to pull the constructor in by itself. + [Test] + public async Task InstanceReportsItsConstructorWhenTheNameWasNeverRead() + { + var result = await "screen.constructor.name".EvalScriptAsync(); + Assert.AreEqual("Screen", result); + } + + [Test] + public async Task InstanceReportsTheSameConstructorTheWindowPublishes() + { + var result = await "String(screen.constructor === Screen)".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorIsNotWritable() + { + var result = await "String((function () { var before = HTMLDivElement; window.HTMLDivElement = 5; return window.HTMLDivElement === before; })())".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorIsNotConfigurable() + { + var result = await "String((delete window.HTMLDivElement) === false && typeof HTMLDivElement === 'function')".EvalScriptAsync(); + Assert.AreEqual("true", result); + } + + [Test] + public async Task ConstructorStringifiesAsNativeCode() + { + var result = await "String(HTMLDivElement)".EvalScriptAsync(); + Assert.AreEqual("function HTMLDivElement() { [native code] }", result); + } + + [Test] + public async Task NonConstructableTypeStillRejectsNew() + { + var result = await "(function () { try { new Node(); return 'no throw'; } catch (e) { return 'threw'; } })()".EvalScriptAsync(); + Assert.AreEqual("threw", result); + } + } +} diff --git a/src/AngleSharp.Js/Cache/CreatorCache.cs b/src/AngleSharp.Js/Cache/CreatorCache.cs index 3c1188e..8ecbf23 100644 --- a/src/AngleSharp.Js/Cache/CreatorCache.cs +++ b/src/AngleSharp.Js/Cache/CreatorCache.cs @@ -12,11 +12,16 @@ namespace AngleSharp.Js.Cache { static class CreatorCache { - private static readonly ConcurrentDictionary> _constructorActions = new(); - - public static Action GetConstructorAction(this Type type) + private static readonly ConcurrentDictionary _constructorDefinitions = new(); + + /// + /// Gets what is needed to build the constructor object for a type, or null if the + /// type is not exposed as one. The answer depends on the type alone, so the null + /// is cached as well - most exported types do not get a constructor. + /// + public static ConstructorDefinition GetConstructorDefinition(this Type type) { - if (!_constructorActions.TryGetValue(type, out var action)) + if (!_constructorDefinitions.TryGetValue(type, out var definition)) { var ti = type.GetTypeInfo(); var names = ti.GetCustomAttributes(); @@ -25,21 +30,13 @@ public static Action GetConstructorAction(this T if (name != null && !ti.IsEnum) { var info = ti.DeclaredConstructors.FirstOrDefault(m => m.GetCustomAttributes().Any()); - action = (engine, obj) => - { - var constructor = info != null ? new DomConstructorInstance(engine, info) : new DomConstructorInstance(engine, type); - obj.FastSetProperty(name.OfficialName, new PropertyDescriptor(constructor, false, true, false)); - }; - } - else - { - action = (e, o) => { }; + definition = new ConstructorDefinition(type, name.OfficialName, info); } - _constructorActions.TryAdd(type, action); + _constructorDefinitions.TryAdd(type, definition); } - return action; + return definition; } private static readonly ConcurrentDictionary> _constructorFunctionActions = new(); @@ -111,4 +108,35 @@ public static Action GetInstanceAction(this Type return action; } } + + /// + /// Everything the constructor object of a type is built from. The reflection behind it + /// is the same for every engine, so it is resolved once and kept by + /// - only the object built from it belongs to an engine. + /// + sealed class ConstructorDefinition + { + public ConstructorDefinition(Type type, String name, ConstructorInfo info) + { + Type = type; + Name = name; + Info = info; + } + + /// + /// Gets the type the constructor creates instances of. + /// + public Type Type { get; } + + /// + /// Gets the name the constructor is exposed under. + /// + public String Name { get; } + + /// + /// Gets the constructor to invoke, or null if the type cannot be constructed from + /// script - naming it is still legal, calling it is not. + /// + public ConstructorInfo Info { get; } + } } diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 0253141..55b30a6 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -86,6 +86,19 @@ public EngineInstance(IWindow window, IDictionary assignments, I public ObjectInstance GetDomPrototype(Type type) => _prototypes.GetOrCreate(type, CreatePrototype); + /// + /// Gets the constructor object of the given type, building it on first ask. The + /// prototype keeps it, so that naming the type and reading "constructor" off one of + /// its instances arrive at the same object. + /// + public DomConstructorInstance GetDomConstructor(ConstructorDefinition definition) + { + // Only the prototype of System.Object is not one of ours, and that type is not + // exposed as a constructor, so it never reaches this point. + var prototype = (DomPrototypeInstance)GetDomPrototype(definition.Type); + return prototype.GetConstructor(definition); + } + public JsValue RunScript(String source, String type, String sourceUrl) { if (string.IsNullOrEmpty(type)) diff --git a/src/AngleSharp.Js/Extensions/EngineExtensions.cs b/src/AngleSharp.Js/Extensions/EngineExtensions.cs index ec7ed3f..553cbaa 100644 --- a/src/AngleSharp.Js/Extensions/EngineExtensions.cs +++ b/src/AngleSharp.Js/Extensions/EngineExtensions.cs @@ -193,8 +193,12 @@ public static void AddInstances(this EngineInstance engine, ObjectInstance obj, public static void AddConstructor(this EngineInstance engine, ObjectInstance obj, Type type) { - var apply = type.GetConstructorAction(); - apply.Invoke(engine, obj); + var definition = type.GetConstructorDefinition(); + + if (definition != null) + { + obj.FastSetProperty(definition.Name, new DomConstructorDescriptor(engine, definition)); + } } public static void AddConstructorFunction(this EngineInstance engine, ObjectInstance obj, Type type) diff --git a/src/AngleSharp.Js/Proxies/DomConstructorDescriptor.cs b/src/AngleSharp.Js/Proxies/DomConstructorDescriptor.cs new file mode 100644 index 0000000..00590d7 --- /dev/null +++ b/src/AngleSharp.Js/Proxies/DomConstructorDescriptor.cs @@ -0,0 +1,37 @@ +namespace AngleSharp.Js +{ + using AngleSharp.Js.Cache; + using Jint.Native; + using Jint.Runtime.Descriptors; + + /// + /// The property an exposed type is published under on the window and on the global + /// object. A document names a handful of the types an assembly exposes, but a property + /// is registered for every one of them, so the constructor object behind it is only + /// built once script reads the property. + /// + sealed class DomConstructorDescriptor : PropertyDescriptor + { + private readonly EngineInstance _instance; + private readonly ConstructorDefinition _definition; + private JsValue _resolved; + + // The attributes an eagerly written constructor had: enumerable, but neither + // writable nor configurable. CustomJsValue is what routes a read through + // CustomValue below; Jint reads that flag on every access instead of taking a + // copy of the value, so the descriptor keeps working once one of the engine's + // property caches has taken hold of it. + public DomConstructorDescriptor(EngineInstance instance, ConstructorDefinition definition) + : base(PropertyFlag.OnlyEnumerable | PropertyFlag.CustomJsValue) + { + _instance = instance; + _definition = definition; + } + + protected override JsValue CustomValue + { + get => _resolved ?? (_resolved = _instance.GetDomConstructor(_definition)); + set => _resolved = value; + } + } +} diff --git a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs index 4c36f1f..00783ee 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs @@ -1,11 +1,11 @@ namespace AngleSharp.Js { + using AngleSharp.Js.Cache; using Jint.Native; using Jint.Native.Object; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; - using System; using System.Reflection; sealed class DomConstructorInstance : Constructor @@ -14,12 +14,13 @@ sealed class DomConstructorInstance : Constructor private readonly EngineInstance _instance; private readonly ObjectInstance _objectPrototype; - public DomConstructorInstance(EngineInstance engine, Type type) - : base(engine.Jint, type.GetOfficialName()) + public DomConstructorInstance(EngineInstance engine, ConstructorDefinition definition) + : base(engine.Jint, definition.Name) { var toString = new ClrFunction(Engine, "toString", ToString); - _objectPrototype = engine.GetDomPrototype(type); + _objectPrototype = engine.GetDomPrototype(definition.Type); _instance = engine; + _constructor = definition.Info; FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true)); SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false)); @@ -37,12 +38,6 @@ public DomConstructorInstance(EngineInstance engine, Type type) } } - public DomConstructorInstance(EngineInstance engine, ConstructorInfo constructor) - : this(engine, constructor.DeclaringType) - { - _constructor = constructor; - } - public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { if (_constructor == null) diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index 16c86bc..43ab720 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -1,6 +1,7 @@ namespace AngleSharp.Js { using AngleSharp.Attributes; + using AngleSharp.Js.Cache; using AngleSharp.Text; using Jint.Native.Object; using Jint.Native.Symbol; @@ -20,6 +21,7 @@ sealed class DomPrototypeInstance : ObjectInstance private List> _deferred; private Boolean _membersSet; + private DomConstructorInstance _constructor; private MethodInfo _numericIndexer; private MethodInfo _stringIndexer; @@ -58,8 +60,26 @@ protected override void Initialize() _deferred = null; } + + // It is the constructor object that registers "constructor" here, and it is + // only built once script names the type. A prototype reached through an + // instance instead - the usual way - would otherwise lack the property. + var definition = _type.GetConstructorDefinition(); + + if (definition != null) + { + GetConstructor(definition); + } } + /// + /// Gets the constructor object of the type this prototype belongs to, building it + /// on first ask. Holding it here is what keeps the one script reads off the window + /// and the one an instance reports as its "constructor" the same object. + /// + public DomConstructorInstance GetConstructor(ConstructorDefinition definition) => + _constructor ?? (_constructor = new DomConstructorInstance(_instance, definition)); + // 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() From 186be83d2391b4d794bf9df5f5995ee05d301e8c Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 10:07:10 +0300 Subject: [PATCH 46/73] Make instanceof work for DOM objects `window instanceof Window` and `document.createElement('div') instanceof Element` are both false, and so is every other check against one of the 151 DOM types AngleSharp exposes as an interface (#103). The two sides of the relation never meet. An instance keys its prototype on the internal concrete class - `DomNodeInstance` passes `value.GetType()`, so `HtmlDivElement` - while the constructor keys its own on the exported interface, because `AddConstructors` walks the types carrying `[DomName]` and for the DOM those are interfaces. `PrototypeCache` is keyed by `Type`, so the two end up with a prototype each and the chain walk behind `instanceof` never reaches the constructor's. The 20 types exposed as a class - `Event`, `URL`, `MutationObserver` and the other event types - already worked, because `Construct` hands back a node keyed on that same class. Fold both onto the class that defines the DOM name, i.e. the topmost class carrying it, which is also the class the prototype chain is built from. That makes `HTMLDivElement.prototype` the very object a div already inherits from, so `instanceof` follows from the chain rather than from a special case, and `Object.getPrototypeOf(div) === HTMLDivElement.prototype` holds as well. Two kinds of type cannot be folded and keep answering through an own `Symbol.hasInstance` on the constructor, the way Jint's own `TypeReference` does: a WebIDL mixin such as `ParentNode` has no class to define it, and the closed forms of `IHtmlCollection` are separate types that one prototype cannot stand for. Also give a DOM constructor `Function.prototype` as its prototype - Jint's `Constructor` leaves it at `Object.prototype`, which left them the only functions in the engine without `call`, `apply` or `bind` - and give `DomConstructorFunctionInstance` the `prototype` property it never had, so `new Image() instanceof Image` is an answer rather than a `TypeError`. Since a prototype now stands for a DOM type rather than for a single class, an indexer can no longer be resolved once against the type the prototype was built for: col and colgroup share the `HTMLTableColElement` prototype, and every element class carrying no name of its own shares the one of its nearest named ancestor. Resolve it against whichever object is being indexed instead, cached per target type. Two visible consequences, both matching what a browser does: `b`, `nav`, `noscript` and the other elements AngleSharp has no named class for lose their own untagged prototype level and become `HTMLElement`, so `Object.prototype.toString.call(el)` reports `[object HTMLElement]` rather than `[object Object]`; and `HTMLDivElement.length` is 0 rather than undefined, inherited from `Function.prototype`. Co-Authored-By: Claude Opus 5 (1M context) --- src/AngleSharp.Js.Tests/DomTests.cs | 18 ++ src/AngleSharp.Js.Tests/InstanceOfTests.cs | 196 +++++++++++++++++ src/AngleSharp.Js/Cache/PrototypeCache.cs | 18 +- src/AngleSharp.Js/Cache/PrototypeTypeCache.cs | 207 ++++++++++++++++++ src/AngleSharp.Js/EngineInstance.cs | 4 +- .../Proxies/DomConstructorFunctionInstance.cs | 11 + .../Proxies/DomConstructorInstance.cs | 88 ++++++++ .../Proxies/DomPrototypeInstance.cs | 103 ++++++--- 8 files changed, 605 insertions(+), 40 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/InstanceOfTests.cs create mode 100644 src/AngleSharp.Js/Cache/PrototypeTypeCache.cs diff --git a/src/AngleSharp.Js.Tests/DomTests.cs b/src/AngleSharp.Js.Tests/DomTests.cs index bfd1e4c..489ee32 100644 --- a/src/AngleSharp.Js.Tests/DomTests.cs +++ b/src/AngleSharp.Js.Tests/DomTests.cs @@ -55,6 +55,24 @@ public async Task NumericIndexerOfHtmlCollectionOutOfRangeIsUndefined() Assert.AreEqual("undefined", result); } + // Two closed forms of IHtmlCollection in one document - each resolves its own + // indexer, and the explicit IReadOnlyList re-implementation behind them is the + // one accessor that cannot be invoked as declared. + [Test] + public async Task NumericIndexerWorksForHtmlCollectionsOfDifferentItemTypes() + { + var result = await "(function () { var d = new DOMParser().parseFromString(``, 'text/html'); return d.getElementsByTagName('img')[0].id + ',' + d.images[0].id; })()".EvalScriptAsync(); + Assert.AreEqual("x,x", result); + } + + // col and colgroup are separate classes sharing the HTMLTableColElement prototype. + [Test] + public async Task NumericIndexerWorksForElementsSharingAPrototype() + { + var result = await "(function () { var d = new DOMParser().parseFromString(`
`, 'text/html'); var g = d.getElementsByTagName('colgroup')[0], c = d.getElementsByTagName('col')[0]; return g.classList[1] + ',' + c.classList[1]; })()".EvalScriptAsync(); + Assert.AreEqual("b,d", result); + } + [Test] public async Task NumericIndexerOfNodeListYieldsTheNode() { diff --git a/src/AngleSharp.Js.Tests/InstanceOfTests.cs b/src/AngleSharp.Js.Tests/InstanceOfTests.cs new file mode 100644 index 0000000..633db5b --- /dev/null +++ b/src/AngleSharp.Js.Tests/InstanceOfTests.cs @@ -0,0 +1,196 @@ +namespace AngleSharp.Js.Tests +{ + using NUnit.Framework; + using System.Threading.Tasks; + + [TestFixture] + public class InstanceOfTests + { + // The two cases reported in https://github.com/AngleSharp/AngleSharp.Js/issues/103 + + [Test] + public async Task WindowIsAnInstanceOfWindow() + { + var result = await "window instanceof Window".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfElement() + { + var result = await "document.createElement('div') instanceof Element".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Every level of the chain on its own - the leaf passing says nothing about the rest. + + [Test] + public async Task CreatedElementIsAnInstanceOfItsOwnType() + { + var result = await "document.createElement('div') instanceof HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('div') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfNode() + { + var result = await "document.createElement('div') instanceof Node".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsAnInstanceOfEventTarget() + { + var result = await "document.createElement('div') instanceof EventTarget".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CreatedElementIsNotAnInstanceOfAnUnrelatedType() + { + var result = await "document.createElement('div') instanceof HTMLAnchorElement".EvalScriptAsync(); + Assert.AreEqual("False", result); + } + + [Test] + public async Task DocumentIsAnInstanceOfHtmlDocument() + { + var result = await "document instanceof HTMLDocument".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task DocumentIsAnInstanceOfDocument() + { + var result = await "document instanceof Document".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // An anchor reaches HTMLElement through the URLUtils mixin, which sits in the chain + // because AngleSharp implements it as an abstract class of its own. + + [Test] + public async Task AnchorIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('a') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // An element class carrying no DOM name of its own - a "b" is an HTMLElement in a + // browser, and now here as well. + + [Test] + public async Task UnnamedElementIsAnInstanceOfHtmlElement() + { + var result = await "document.createElement('b') instanceof HTMLElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task UnnamedElementUsesThePrototypeOfHtmlElement() + { + var result = await "Object.getPrototypeOf(document.createElement('b')) === HTMLElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Asserted without "instanceof" on purpose: a Symbol.hasInstance answer would hide a + // broken chain from every test above, but not from these. + + [Test] + public async Task PrototypeOfElementIsThePrototypeOfItsConstructor() + { + var result = await "Object.getPrototypeOf(document.createElement('div')) === HTMLDivElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task PrototypeOfConstructorIsChainedToItsBase() + { + var result = await "Object.getPrototypeOf(HTMLDivElement.prototype) === HTMLElement.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task PrototypeOfWindowIsThePrototypeOfItsConstructor() + { + var result = await "Object.getPrototypeOf(window) === Window.prototype".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ConstructorOfAnElementIsTheExposedConstructor() + { + var result = await "document.createElement('div').constructor === HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // Types reached only through a property, never constructed from script. + + [Test] + public async Task TokenListIsAnInstanceOfDomTokenList() + { + var result = await "document.createElement('div').classList instanceof DOMTokenList".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task CollectionIsAnInstanceOfHtmlCollection() + { + var result = await "document.getElementsByTagName('div') instanceof HTMLCollection".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // A type constructed from script keeps working - these already did. + + [Test] + public async Task ConstructedEventIsAnInstanceOfEvent() + { + var result = await "new Event('foo') instanceof Event".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ConstructedCustomEventIsAnInstanceOfEvent() + { + var result = await "new CustomEvent('foo') instanceof Event".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task ImageIsAnInstanceOfHtmlImageElement() + { + var result = await "new Image() instanceof HTMLImageElement".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + // A DOM constructor is a function, so it inherits from Function.prototype. + + [Test] + public async Task DomConstructorIsAFunction() + { + var result = await "typeof HTMLDivElement".EvalScriptAsync(); + Assert.AreEqual("function", result); + } + + [Test] + public async Task DomConstructorIsAnInstanceOfFunction() + { + var result = await "Window instanceof Function".EvalScriptAsync(); + Assert.AreEqual("True", result); + } + + [Test] + public async Task DomConstructorInheritsCallFromFunctionPrototype() + { + var result = await "typeof HTMLDivElement.call".EvalScriptAsync(); + Assert.AreEqual("function", result); + } + } +} diff --git a/src/AngleSharp.Js/Cache/PrototypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeCache.cs index ffcc2fd..a65eeb7 100644 --- a/src/AngleSharp.Js/Cache/PrototypeCache.cs +++ b/src/AngleSharp.Js/Cache/PrototypeCache.cs @@ -1,25 +1,35 @@ namespace AngleSharp.Js { + using AngleSharp.Js.Cache; using Jint; using Jint.Native.Object; using System; using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Reflection; sealed class PrototypeCache { private readonly ConcurrentDictionary _prototypes; - private readonly Engine _engine; + private readonly ConcurrentDictionary _canonicalTypes; + private readonly IEnumerable _libs; - public PrototypeCache(Engine engine) + public PrototypeCache(Engine engine, IEnumerable libs) { _prototypes = new ConcurrentDictionary { [typeof(Object)] = engine.Intrinsics.Object.PrototypeObject, }; - _engine = engine; + _canonicalTypes = new ConcurrentDictionary(); + _libs = libs; } public ObjectInstance GetOrCreate(Type type, Func creator) => - _prototypes.GetOrAdd(type, creator.Invoke); + _prototypes.GetOrAdd(Canonicalize(type), creator.Invoke); + + // Memoized per engine rather than globally: the set of libraries a document uses is what + // decides the outcome, and that is fixed for an engine but not for the process. + private Type Canonicalize(Type type) => + _canonicalTypes.GetOrAdd(type, m => m.GetDomPrototypeType(_libs)); } } diff --git a/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs new file mode 100644 index 0000000..29e9f16 --- /dev/null +++ b/src/AngleSharp.Js/Cache/PrototypeTypeCache.cs @@ -0,0 +1,207 @@ +namespace AngleSharp.Js.Cache +{ + using AngleSharp.Attributes; + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; + + /// + /// Maps a CLR type onto the single type whose prototype represents it in JS. + /// + /// + /// An instance is wrapped from an internal concrete class (HtmlDivElement), while the + /// constructor exposed to scripts is built from the exported interface (IHtmlDivElement). + /// Left alone the two end up with a prototype each, so neither "instanceof" nor + /// "Object.getPrototypeOf(div) === HTMLDivElement.prototype" can ever hold. Both sides are + /// therefore folded onto the class that defines the DOM name - the topmost class carrying + /// it - which is also the class the prototype chain is built from. + /// + static class PrototypeTypeCache + { + private static readonly ConcurrentDictionary> _definingTypes = new(); + private static readonly ConcurrentDictionary> _exposedTypes = new(); + + /// + /// Gets what the constructor object of the type a prototype belongs to is built from, + /// or null if the type is not exposed as one. + /// + /// + /// A prototype is keyed by the class defining the DOM name, but it is the exported + /// interface next to it that carries the [DomName] - HtmlDivElement has none of its + /// own, IHtmlDivElement is what names HTMLDivElement - so the class has to be traded + /// back for the interface first. + /// + public static ConstructorDefinition GetConstructorDefinition(this Type type, IEnumerable libs) + { + var definition = type.GetConstructorDefinition(); + + if (definition == null) + { + var name = GetCanonicalName(type); + + if (name != null) + { + foreach (var lib in libs) + { + if (GetExposedTypes(lib).TryGetValue(name, out var exposedType)) + { + return exposedType.GetConstructorDefinition(); + } + } + } + } + + return definition; + } + + public static Type GetDomPrototypeType(this Type type, IEnumerable libs) + { + var typeInfo = type.GetTypeInfo(); + + // An enum carries the [DomName] of its owner (NodeType is named "Document"), and the + // closed instantiations of a generic are mutually non-assignable, so a member resolved + // against one of them cannot be invoked on another (IHtmlCollection). Neither may + // be folded onto somebody else's prototype. + if (typeInfo.IsEnum || typeInfo.IsGenericType) + { + return type; + } + + var name = GetCanonicalName(type); + + if (name != null) + { + foreach (var lib in libs) + { + if (GetDefiningTypes(lib).TryGetValue(name, out var definingType)) + { + return definingType; + } + } + } + + return type; + } + + /// + /// Gets the DOM name a type is represented by, which for the many element classes + /// carrying no name of their own (HtmlBoldElement, HtmlSemanticElement, ...) is the name + /// of their nearest named ancestor - just like in a browser, where a "b" element is an + /// HTMLElement. + /// + private static String GetCanonicalName(Type type) + { + var current = type; + + while (current != null) + { + var baseType = current.GetTypeInfo().BaseType; + var name = current.GetOfficialName(baseType); + + if (name != null) + { + return name; + } + + current = baseType; + } + + return null; + } + + private static IReadOnlyDictionary GetDefiningTypes(Assembly assembly) => + _definingTypes.GetOrAdd(assembly, CreateDefiningTypes); + + private static IReadOnlyDictionary CreateDefiningTypes(Assembly assembly) + { + // Ordinal on purpose: XmlHttpRequest is named "XMLHttpRequest" while the + // RequesterState enum next to it is named "XmlHttpRequest". + var result = new Dictionary(StringComparer.Ordinal); + + foreach (var type in GetLoadableTypes(assembly)) + { + var typeInfo = type.GetTypeInfo(); + + // Only a class can define a prototype: the interface is what the DOM exposes, + // but the class is what an instance is built from and what its base type - and + // hence the prototype chain - is taken from. + if (!typeInfo.IsClass || typeInfo.IsGenericType) + { + continue; + } + + var baseType = typeInfo.BaseType; + var name = type.GetOfficialName(baseType); + + if (name == null || String.Equals(name, GetNameOf(baseType), StringComparison.Ordinal)) + { + // 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; + } + + // 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; + } + } + + return result; + } + + private static String GetNameOf(Type type) => + type?.GetOfficialName(type.GetTypeInfo().BaseType); + + private static IReadOnlyDictionary GetExposedTypes(Assembly assembly) => + _exposedTypes.GetOrAdd(assembly, CreateExposedTypes); + + /// + /// Collects the types a DOM name is exposed by, which is what + /// walks - hence exported types only. + /// + private static IReadOnlyDictionary CreateExposedTypes(Assembly assembly) + { + var result = new Dictionary(StringComparer.Ordinal); + + foreach (var type in assembly.ExportedTypes) + { + var typeInfo = type.GetTypeInfo(); + + if (typeInfo.IsEnum) + { + // An enum carries the [DomName] of the type owning it, not one of its own. + continue; + } + + var name = typeInfo.GetCustomAttributes().FirstOrDefault()?.OfficialName; + + // 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))) + { + result[name] = type; + } + } + + return result; + } + + private static IEnumerable GetLoadableTypes(Assembly assembly) + { + try + { + return assembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + return ex.Types.Where(m => m != null); + } + } + } +} diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 55b30a6..5f2033f 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -35,9 +35,9 @@ public EngineInstance(IWindow window, IDictionary assignments, I { options.EnableModules(new JsModuleLoader(this, window.Document, false)); }); - _prototypes = new PrototypeCache(_engine); - _references = new ReferenceCache(); _libs = libs; + _prototypes = new PrototypeCache(_engine, libs); + _references = new ReferenceCache(); foreach (var assignment in assignments) { diff --git a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs index 348ab1a..6a95c26 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorFunctionInstance.cs @@ -3,6 +3,7 @@ namespace AngleSharp.Js.Proxies using Jint.Native; using Jint.Native.Object; using Jint.Runtime; + using Jint.Runtime.Descriptors; using System.Reflection; sealed class DomConstructorFunctionInstance : Constructor @@ -14,6 +15,16 @@ public DomConstructorFunctionInstance(EngineInstance instance, MethodInfo constr { _instance = instance; _constructorFunction = constructorFunction; + + // Jint's Constructor leaves the prototype at Object.prototype, which would make a + // DOM constructor the one function in the engine without call, apply or bind. + Prototype = (ObjectInstance)instance.Jint.Intrinsics.Function.Get("prototype"); + + // Image is a second way of naming HTMLImageElement rather than a type of its own, + // so it publishes the prototype of what it returns. Without one, "instanceof" + // against it is a TypeError rather than an answer. + SetOwnProperty("prototype", new PropertyDescriptor( + instance.GetDomPrototype(constructorFunction.ReturnType), false, false, false)); } public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) diff --git a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs index 00783ee..a85e493 100644 --- a/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomConstructorInstance.cs @@ -3,9 +3,11 @@ namespace AngleSharp.Js using AngleSharp.Js.Cache; using Jint.Native; using Jint.Native.Object; + using Jint.Native.Symbol; using Jint.Runtime; using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; + using System; using System.Reflection; sealed class DomConstructorInstance : Constructor @@ -13,6 +15,7 @@ sealed class DomConstructorInstance : Constructor private readonly ConstructorInfo _constructor; private readonly EngineInstance _instance; private readonly ObjectInstance _objectPrototype; + private readonly Type _type; public DomConstructorInstance(EngineInstance engine, ConstructorDefinition definition) : base(engine.Jint, definition.Name) @@ -21,6 +24,12 @@ public DomConstructorInstance(EngineInstance engine, ConstructorDefinition defin _objectPrototype = engine.GetDomPrototype(definition.Type); _instance = engine; _constructor = definition.Info; + _type = definition.Type; + + // Jint's Constructor leaves the prototype at Object.prototype, which would make a + // DOM constructor the one function in the engine without call, apply or bind. + Prototype = (ObjectInstance)engine.Jint.Intrinsics.Function.Get("prototype"); + FastSetProperty("toString", new PropertyDescriptor(toString, true, false, true)); SetOwnProperty("prototype", new PropertyDescriptor(_objectPrototype, false, false, false)); @@ -38,6 +47,85 @@ public DomConstructorInstance(EngineInstance engine, ConstructorDefinition defin } } + /// + /// Answers "instanceof" itself, because the prototype chain cannot always carry the + /// answer: a mixin such as ParentNode has no class of its own to hang a prototype off, + /// and the closed instantiations of IHtmlCollection<T> are separate types that a + /// single prototype cannot stand for. Built on first ask - most types are never asked. + /// + public override PropertyDescriptor GetOwnProperty(JsValue property) + { + if (property == GlobalSymbolRegistry.HasInstance) + { + var descriptor = base.GetOwnProperty(property); + + if (descriptor == PropertyDescriptor.Undefined) + { + var hasInstance = new ClrFunction(Engine, "[Symbol.hasInstance]", HasInstance, 1, PropertyFlag.Configurable); + descriptor = new PropertyDescriptor(hasInstance, false, false, false); + SetOwnProperty(property, descriptor); + } + + return descriptor; + } + + return base.GetOwnProperty(property); + } + + private JsValue HasInstance(JsValue thisObject, JsValue[] arguments) + { + var value = arguments.Length > 0 ? arguments[0] : JsValue.Undefined; + + if (value is DomNodeInstance node && IsInstance(node.Value)) + { + return JsBoolean.True; + } + + // Not a DOM object of this type, but something may still have been given this + // prototype - Object.create(HTMLDivElement.prototype) is an instance in a browser. + return InheritsFromPrototype(value as ObjectInstance) ? JsBoolean.True : JsBoolean.False; + } + + private Boolean IsInstance(Object value) + { + var type = value.GetType(); + + if (_type.IsAssignableFrom(type)) + { + return true; + } + + // IHtmlCollection is exposed as HTMLCollection, so an instance of any of its + // closed forms answers to it. + if (_type.GetTypeInfo().IsGenericTypeDefinition) + { + foreach (var contract in type.GetTypeInfo().ImplementedInterfaces) + { + if (contract.GetTypeInfo().IsGenericType && contract.GetGenericTypeDefinition() == _type) + { + return true; + } + } + } + + return false; + } + + private Boolean InheritsFromPrototype(ObjectInstance obj) + { + while (obj != null) + { + obj = obj.Prototype; + + if (ReferenceEquals(obj, _objectPrototype)) + { + return true; + } + } + + return false; + } + public override ObjectInstance Construct(JsValue[] arguments, JsValue newTarget) { if (_constructor == null) diff --git a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs index 43ab720..5a0c3d0 100644 --- a/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs +++ b/src/AngleSharp.Js/Proxies/DomPrototypeInstance.cs @@ -8,6 +8,7 @@ namespace AngleSharp.Js using Jint.Runtime.Descriptors; using Jint.Runtime.Interop; using System; + using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -22,8 +23,8 @@ sealed class DomPrototypeInstance : ObjectInstance private List> _deferred; private Boolean _membersSet; private DomConstructorInstance _constructor; - private MethodInfo _numericIndexer; - private MethodInfo _stringIndexer; + private Indexer _numericIndexer; + private Indexer _stringIndexer; public DomPrototypeInstance(EngineInstance engine, Type type) : base(engine.Jint) @@ -48,8 +49,17 @@ protected override void Initialize() SetAllMembers(_type); SetExtensionMembers(); - // DOM objects can have properties added dynamically - Prototype = _instance.GetDomPrototype(_baseType); + // The base type may fold onto this very prototype - a class carrying no DOM name of + // its own shares the one of its nearest named ancestor. Jint's setter answers a + // prototype cycle by quietly doing nothing, which would leave Object.prototype in + // place and silently cut the chain short, so rule it out here. + var parent = _instance.GetDomPrototype(_baseType); + + if (!ReferenceEquals(parent, this)) + { + // DOM objects can have properties added dynamically + Prototype = parent; + } if (_deferred != null) { @@ -64,7 +74,7 @@ protected override void Initialize() // It is the constructor object that registers "constructor" here, and it is // only built once script names the type. A prototype reached through an // instance instead - the usual way - would otherwise lack the property. - var definition = _type.GetConstructorDefinition(); + var definition = _type.GetConstructorDefinition(_instance.Libs); if (definition != null) { @@ -307,7 +317,7 @@ private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) return; } - var getter = ResolveAccessor(property.GetMethod); + var getter = property.GetMethod; if (getter == null) { @@ -316,40 +326,14 @@ private void SetIndexer(PropertyInfo property, ParameterInfo[] indexParameters) if (indexParameters[0].ParameterType == typeof(Int32)) { - _numericIndexer = getter; + _numericIndexer = new Indexer(getter); } else if (indexParameters[0].ParameterType == typeof(String)) { - _stringIndexer = getter; + _stringIndexer = new Indexer(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.this[Int32 index]" declared on an - // IHtmlCollection. 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 @@ -364,6 +348,57 @@ private void SetMethod(String name, MethodInfo method) ), false, false, false)); } } + + /// + /// One of the indexers a prototype offers, invoked against whichever object is being + /// indexed rather than against the type the prototype was created for. + /// + /// + /// A prototype stands for a DOM type, not for a single class - col and colgroup are + /// both an HTMLTableColElement, and every element class carrying no name of its own + /// shares the prototype of its nearest named ancestor. An accessor bound to one of + /// those classes cannot be invoked on the others, so the target decides. + /// + private sealed class Indexer + { + private readonly MethodInfo _declared; + private readonly ConcurrentDictionary _resolved; + + public Indexer(MethodInfo declared) + { + _declared = declared; + + // A public accessor is dispatched virtually and works on any implementation; + // only an explicitly re-implemented one has to be looked up per target. + _resolved = declared.IsPublic ? null : new ConcurrentDictionary(); + } + + public Object Invoke(Object target, Object[] arguments) => + Resolve(target.GetType()).Invoke(target, arguments); + + private MethodInfo Resolve(Type targetType) => + _resolved == null ? _declared : _resolved.GetOrAdd(targetType, ResolveCore); + + private MethodInfo ResolveCore(Type targetType) + { + // An interface may re-implement a member of one of its own base interfaces + // explicitly, e.g. "T IReadOnlyList.this[Int32 index]" declared on an + // IHtmlCollection. Such a member is private and abstract - invoking it + // reflectively throws an EntryPointNotFoundException because the actual + // implementation lives in a different slot. + var name = _declared.Name; + var simpleName = name.Substring(name.LastIndexOf('.') + 1); + var parameters = _declared.GetParameters(); + var parameterTypes = new Type[parameters.Length]; + + for (var i = 0; i < parameters.Length; i++) + { + parameterTypes[i] = parameters[i].ParameterType; + } + + return targetType.GetRuntimeMethod(simpleName, parameterTypes) ?? _declared; + } + } } } From 8af1e07c0cf60fd86727c661d9123daa7905d96a Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 10:36:27 +0300 Subject: [PATCH 47/73] Add AI agent guidance in AGENTS.md Documents the build and test commands, the reflection-based binding architecture, the performance expectations, the AngleSharp code conventions and the test idioms, so an agent does not have to rediscover them from the sources every session. CLAUDE.md only imports it, keeping one file for every agent. Co-Authored-By: Claude Opus 5 (1M context) --- AGENTS.md | 212 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ CLAUDE.md | 8 +++ 2 files changed, 220 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f78f1f2 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,212 @@ +# AGENTS.md + +Guidance for AI coding agents working in this repository. `CLAUDE.md` imports this file, so +keep it the single source of truth and every agent reads the same instructions. + +AngleSharp.Js is an AngleSharp plugin that exposes the AngleSharp DOM to the +[Jint](https://github.com/sebastienros/jint) JavaScript engine. There is no code generation +and no hand-written binding layer: the whole JS surface is derived at runtime by reflecting +over AngleSharp's `[DomName]`-style attributes. + +## Commands + +The orchestrator is NUKE (`nuke/Build.cs`), bootstrapped by `build.ps1` / `build.sh` +(`build.cmd` forwards to either). Default target is `RunUnitTests`. + +```powershell +.\build.ps1 # restore, compile, run the full test suite +.\build.ps1 -Target Compile # other targets: Clean Restore Compile RunUnitTests +.\build.ps1 -Target Package # CopyFiles CreatePackage Package PrePublish Publish +``` + +For the normal edit/test loop use the SDK directly — much faster than the NUKE bootstrap: + +```powershell +dotnet build src/AngleSharp.Js.sln +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 --filter "FullyQualifiedName~InstanceOfTests" +dotnet test src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj -f net8.0 --filter "Name=WindowIsAnInstanceOfWindow" +``` + +- Always pass `-f net8.0` when iterating. On Windows both projects also target `net462` and + `net472`, so omitting it runs everything three times. +- `TreatWarningsAsErrors` is on (`src/Directory.Build.props`) — a warning breaks the build. + There is no separate lint step; the compiler is it. +- The package version is parsed from the top entry of `CHANGELOG.md` (`ReleaseNotesParser`), + not from a csproj property. Release-worthy changes get a `CHANGELOG.md` line. +- `RunUnitTests` runs the suite twice, differing only in a `prefetched` environment variable + that nothing in this repo currently reads — a single run is equivalent locally. +- There is no `global.json`; the bootstrap scripts use the STS channel, CI installs 10.0.x. + +## Architecture + +### Wiring into AngleSharp + +`WithJs()` (`JsConfigurationExtensions`) registers four things: `JsScriptingService`, an +`EventAttributeObserver` (inline `onclick="…"` attributes), a `JsNavigationHandler` +(`javascript:` URLs), and a default `INavigator`. `WithEventLoop()` adds `JsEventLoop`, a +dedicated background thread that serializes script tasks — most DOM-mutating tests need it. + +`JsScriptingService` is AngleSharp's `IScriptingService`. It keeps one `EngineInstance` per +`IWindow` in a `ConditionalWeakTable`, and accepts `text/javascript`, `module` and +`importmap`. **The set of assemblies whose types get exposed is derived from the services +registered in the browsing context** (every `AngleSharp*` assembly behind a registered +service), so adding `WithCss()` or `WithIo()` widens the JS DOM surface. + +`EngineInstance` owns the Jint `Engine` and the per-engine caches. On construction it wraps +the window, walks each library's exported types to publish constructors, constructor +functions and instances, then copies the window's own properties onto the Jint global and +points the global's prototype at the window prototype. `RunScript` locks on the engine. + +### The proxy objects (`src/AngleSharp.Js/Proxies`) + +- **`DomNodeInstance`** — the JS object standing for one CLR DOM object. Identity is + preserved by `ReferenceCache` (a `ConditionalWeakTable`), so the same node always maps to + the same JS object. Only indexers become own properties; interface members live on the + prototype. Writes to the window instance are mirrored onto the Jint global. +- **`DomPrototypeInstance`** — one per DOM type. Members are registered lazily in + `Initialize()`: reflecting the full type tree is the expensive part and most prototypes + are never touched. Also owns the numeric/string indexers and extension members pulled from + `[DomExposed]` types. +- **`DomConstructorInstance`** — the exposed constructor (`HTMLDivElement`, …). The + prototype holds it, so the object script reads off the window and the one an instance + reports as its `constructor` are the same. It answers `Symbol.hasInstance` itself, because + the prototype chain cannot cover mixins (`ParentNode`) or generic collections + (`IHtmlCollection`). +- **`DomConstructorDescriptor`** — the property a constructor is published under. Every + exported type gets one, so the constructor object behind it is built only on first read. + +### Type → prototype canonicalization + +This is the subtlety most changes trip over. Instances are created from internal concrete +classes (`HtmlDivElement`), constructors are built from exported interfaces +(`IHtmlDivElement`), and many element classes carry no `[DomName]` of their own (a `b` +element is just an `HTMLElement`). `PrototypeTypeCache` folds all of these onto the topmost +class that defines a given DOM name, which is what makes `instanceof` and +`Object.getPrototypeOf(div) === HTMLDivElement.prototype` hold. Enums and generic types are +deliberately excluded from folding. + +### Caching rules (`src/AngleSharp.Js/Cache`) + +Split by what the cached value depends on: + +- Process-wide statics — `CreatorCache`, `PrototypeTypeCache`, `MethodCache`, `ScriptCache` + (capped at 32 prepared scripts) — hold values determined by a type, method or source alone. +- Per-engine — `PrototypeCache`, `ReferenceCache` — hold anything that is a `JsValue` or + otherwise bound to one engine, plus anything depending on the engine's library set. + +Anything shared across engines must be thread-safe; the existing caches all use +`Concurrent*` collections. + +### Marshalling + +`EngineExtensions.ToJsValue` and `JsValueExtensions.FromJsValue` / `.As(type, …)` convert +values. `EngineExtensions.BuildArgs` maps JS arguments onto a CLR signature and handles the +DOM-specific cases: an implicit leading `IWindow` parameter, optional parameters, `params` +arrays, and `[DomInitDict]` option objects expanded into positional arguments. + +## Performance + +**Performance is a primary concern in this repository, not an afterthought.** Jint is an +interpreter and every DOM access from script crosses this binding layer, so the binding must +never be the bottleneck. Reflection is the whole mechanism here, and reflection is slow — the +work of the last release cycle was largely making it happen once instead of once per call +(`perf/cache-interop-reflection`, `perf/cache-parsed-scripts`, `perf/lazy-dom-prototypes`, +`perf/lazy-dom-constructors`). Hold new code to that standard. + +The two techniques that carry most of the win: + +- **Cache anything derived from a type, method or source string.** It cannot change for the + lifetime of the process. `MethodDescription.Of` resolves a signature once; + `CreatorCache.GetConstructorDefinition` caches even the *null* answer, because most + exported types are not constructors; `ScriptCache` keeps Jint's `Prepared"; + var document = await BrowsingContext.New(config).OpenAsync(m => m.Content(content)); + + Assert.IsNotNull(document); + } + + [Test] + public async Task RunawayRecursionReportsMaximumCallStackSizeExceeded() + { + // A small limit keeps the failure quick and independent of how much native + // stack the test runner happens to have left. + var config = Configuration.Default + .WithJs(new JsScriptingOptions { MaxCallStackDepth = 200 }) + .WithEventLoop(); + + var document = await BrowsingContext.New(config).OpenNewAsync(); + var error = Assert.Throws(() => document.ExecuteScript(RunawayRecursion)); + + Assert.AreEqual("Maximum call stack size exceeded", error.Message); + } + + [Test] + public async Task DeepButFiniteRecursionStillSucceeds() + { + var config = Configuration.Default + .WithJs() + .WithEventLoop(); + + var document = await BrowsingContext.New(config).OpenNewAsync(); + // Deeper than a 1 MB stack holds, so the engine has to keep going on a fresh + // one instead of reporting the depth as an error. + var result = document.ExecuteScript("function depth(n) { return n === 0 ? 0 : 1 + depth(n - 1); } depth(1500);"); + + Assert.AreEqual(1500.0, result); + } + } +} diff --git a/src/AngleSharp.Js/EngineInstance.cs b/src/AngleSharp.Js/EngineInstance.cs index 5f2033f..7edb1a1 100644 --- a/src/AngleSharp.Js/EngineInstance.cs +++ b/src/AngleSharp.Js/EngineInstance.cs @@ -16,6 +16,9 @@ sealed class EngineInstance { #region Fields + // Jint's StackGuard.Disabled, which is internal. + private const Int32 StackGuardDisabled = -1; + private readonly Engine _engine; private readonly PrototypeCache _prototypes; private readonly ReferenceCache _references; @@ -27,13 +30,18 @@ sealed class EngineInstance #region ctor - public EngineInstance(IWindow window, IDictionary assignments, IEnumerable libs) + public EngineInstance(IWindow window, IDictionary assignments, IEnumerable libs, JsScriptingOptions options) { _importMap = new JsImportMap(); - _engine = new Engine((options) => + _engine = new Engine((o) => { - options.EnableModules(new JsModuleLoader(this, window.Document, false)); + o.EnableModules(new JsModuleLoader(this, window.Document, false)); + // Left alone, the JS call stack is the native one, and a script recursing + // deeper than it holds takes the whole process down - a StackOverflowException + // cannot be caught. Guarded, the engine continues on a fresh stack and finally + // reports an ordinary "Maximum call stack size exceeded" error instead. + o.Constraints.MaxExecutionStackCount = options.MaxCallStackDepth > 0 ? options.MaxCallStackDepth : StackGuardDisabled; }); _libs = libs; _prototypes = new PrototypeCache(_engine, libs); diff --git a/src/AngleSharp.Js/JsConfigurationExtensions.cs b/src/AngleSharp.Js/JsConfigurationExtensions.cs index f6c93e7..5f06651 100644 --- a/src/AngleSharp.Js/JsConfigurationExtensions.cs +++ b/src/AngleSharp.Js/JsConfigurationExtensions.cs @@ -61,9 +61,20 @@ public static IConfiguration WithEventLoop(this IConfiguration configuration, Fu ///
/// The configuration to use. /// The new configuration. - public static IConfiguration WithJs(this IConfiguration configuration) + public static IConfiguration WithJs(this IConfiguration configuration) => + configuration.WithJs(new JsScriptingOptions()); + + /// + /// Sets scripting to true, registers the JavaScript engine with the + /// given options and returns a new configuration with the scripting + /// service and possible auxiliary services, if not yet registered. + /// + /// The configuration to use. + /// The options tuning the engine. + /// The new configuration. + public static IConfiguration WithJs(this IConfiguration configuration, JsScriptingOptions options) { - var service = new JsScriptingService(); + var service = new JsScriptingService(options); var observer = new EventAttributeObserver(service); var handler = new JsNavigationHandler(service); diff --git a/src/AngleSharp.Js/JsEventLoop.cs b/src/AngleSharp.Js/JsEventLoop.cs index 9de88f7..ac94ac0 100644 --- a/src/AngleSharp.Js/JsEventLoop.cs +++ b/src/AngleSharp.Js/JsEventLoop.cs @@ -11,6 +11,14 @@ namespace AngleSharp.Js ///
public sealed class JsEventLoop : IEventLoop, IDisposable { + // Scripts run on this thread, and the JS call stack is the native one. The usual + // 1 MB holds roughly a thousand JavaScript frames, well short of what a browser + // offers, and every frame beyond it costs the engine a hop onto a fresh stack. + // The size is reserved address space rather than memory, but a 32 bit process has + // little of it to spare when it runs many loops, so only a 64 bit one is enlarged; + // zero leaves the thread with the default of the process. + private static readonly Int32 DefaultMaxStackSize = IntPtr.Size == 8 ? 16 * 1024 * 1024 : 0; + private readonly Dictionary> _queues = new Dictionary>(); private readonly Object _lockObj = new Object(); private CancellationTokenSource _cts; @@ -19,8 +27,17 @@ public sealed class JsEventLoop : IEventLoop, IDisposable /// Creates a new event loop thread. ///
public JsEventLoop() + : this(DefaultMaxStackSize) + { + } + + /// + /// Creates a new event loop thread with the given stack size. + /// + /// The stack size of the thread running the scripts. + public JsEventLoop(Int32 maxStackSize) { - var thread = new Thread(Runner) + var thread = new Thread(Runner, maxStackSize) { IsBackground = true, Name = "AngleSharpEventLoop", diff --git a/src/AngleSharp.Js/JsScriptingOptions.cs b/src/AngleSharp.Js/JsScriptingOptions.cs new file mode 100644 index 0000000..fee1509 --- /dev/null +++ b/src/AngleSharp.Js/JsScriptingOptions.cs @@ -0,0 +1,20 @@ +namespace AngleSharp.Js +{ + using System; + + /// + /// Options tuning the JavaScript engine. + /// + public sealed class JsScriptingOptions + { + /// + /// Gets or sets the JavaScript call stack depth that has to be supported + /// before the engine gives up with a "Maximum call stack size exceeded" + /// error. Defaults to 10000, which is roughly what browsers allow. Values + /// of zero or less remove the limit - the engine is then bounded by the + /// native stack alone, so a runaway recursion terminates the process with + /// an uncatchable StackOverflowException. + /// + public Int32 MaxCallStackDepth { get; set; } = 10000; + } +} diff --git a/src/AngleSharp.Js/JsScriptingService.cs b/src/AngleSharp.Js/JsScriptingService.cs index add873e..7ccf070 100644 --- a/src/AngleSharp.Js/JsScriptingService.cs +++ b/src/AngleSharp.Js/JsScriptingService.cs @@ -25,18 +25,29 @@ public class JsScriptingService : IScriptingService private readonly ConditionalWeakTable _contexts; private readonly Dictionary _external; + private readonly JsScriptingOptions _options; #endregion #region ctor /// - /// Creates a new JavaScript engine. + /// Creates a new JavaScript engine using the default options. /// public JsScriptingService() + : this(new JsScriptingOptions()) + { + } + + /// + /// Creates a new JavaScript engine using the given options. + /// + /// The options tuning the engine. + public JsScriptingService(JsScriptingOptions options) { _contexts = new ConditionalWeakTable(); _external = new Dictionary(); + _options = options ?? throw new ArgumentNullException(nameof(options)); } #endregion @@ -113,7 +124,7 @@ internal EngineInstance GetOrCreateInstance(IDocument document) if (!_contexts.TryGetValue(objectContext, out var instance)) { var libs = GetAssemblies(document.Context).ToArray(); - instance = new EngineInstance(objectContext, _external, libs); + instance = new EngineInstance(objectContext, _external, libs, _options); _contexts.Add(objectContext, instance); } From f4dd372a6675ccee27c2036f644f37eac0b80d1a Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 10:53:40 +0300 Subject: [PATCH 49/73] Copy the scripting options the service is given An engine is built per window, long after the options were handed over, so reading them then let a later edit of the caller's object decide how the next document behaves. Take a copy when the service is created instead. A record with init-only properties would say this in the type itself, but init needs an IsExternalInit shim on the netstandard2.0, net462 and net472 targets, and every options type in AngleSharp - LoaderOptions, StyleOptions, HtmlParserOptions - is a plain mutable one. Not worth diverging over a single property that only has to be read once. Co-Authored-By: Claude Opus 5 (1M context) --- src/AngleSharp.Js.Tests/StackGuardTests.cs | 26 +++++++++++++++++++--- src/AngleSharp.Js/JsScriptingOptions.cs | 8 +++++++ src/AngleSharp.Js/JsScriptingService.cs | 5 +++-- 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/src/AngleSharp.Js.Tests/StackGuardTests.cs b/src/AngleSharp.Js.Tests/StackGuardTests.cs index 0a3ea34..f5dfd1a 100644 --- a/src/AngleSharp.Js.Tests/StackGuardTests.cs +++ b/src/AngleSharp.Js.Tests/StackGuardTests.cs @@ -14,6 +14,10 @@ public class StackGuardTests // tests below cannot assert anything weaker than "we got here at all". private const String RunawayRecursion = "function boom() { return boom(); } boom();"; + // Deeper than a 1 MB stack holds, so the engine has to keep going on a fresh one + // instead of reporting the depth as an error. + private const String DeepRecursion = "function depth(n) { return n === 0 ? 0 : 1 + depth(n - 1); } depth(1500);"; + [Test] public async Task RunawayRecursionInPageScriptDoesNotEscapeOpenAsync() { @@ -50,9 +54,25 @@ public async Task DeepButFiniteRecursionStillSucceeds() .WithEventLoop(); var document = await BrowsingContext.New(config).OpenNewAsync(); - // Deeper than a 1 MB stack holds, so the engine has to keep going on a fresh - // one instead of reporting the depth as an error. - var result = document.ExecuteScript("function depth(n) { return n === 0 ? 0 : 1 + depth(n - 1); } depth(1500);"); + var result = document.ExecuteScript(DeepRecursion); + + Assert.AreEqual(1500.0, result); + } + + [Test] + public async Task EditingTheOptionsAfterwardsLeavesTheServiceAlone() + { + var options = new JsScriptingOptions(); + var config = Configuration.Default + .WithJs(options) + .WithEventLoop(); + + // The engine is only built once a document asks for it, so an edit landing + // in between must not be the one deciding how that document behaves. + options.MaxCallStackDepth = 200; + + var document = await BrowsingContext.New(config).OpenNewAsync(); + var result = document.ExecuteScript(DeepRecursion); Assert.AreEqual(1500.0, result); } diff --git a/src/AngleSharp.Js/JsScriptingOptions.cs b/src/AngleSharp.Js/JsScriptingOptions.cs index fee1509..6499a2b 100644 --- a/src/AngleSharp.Js/JsScriptingOptions.cs +++ b/src/AngleSharp.Js/JsScriptingOptions.cs @@ -16,5 +16,13 @@ public sealed class JsScriptingOptions /// an uncatchable StackOverflowException. /// public Int32 MaxCallStackDepth { get; set; } = 10000; + + // An engine is built per window, long after the options were handed over, so + // reading them then would let a later edit of the caller's object decide how + // the next document behaves. The service takes this copy instead. + internal JsScriptingOptions Clone() => new JsScriptingOptions + { + MaxCallStackDepth = MaxCallStackDepth, + }; } } diff --git a/src/AngleSharp.Js/JsScriptingService.cs b/src/AngleSharp.Js/JsScriptingService.cs index 7ccf070..74a95ba 100644 --- a/src/AngleSharp.Js/JsScriptingService.cs +++ b/src/AngleSharp.Js/JsScriptingService.cs @@ -40,14 +40,15 @@ public JsScriptingService() } /// - /// Creates a new JavaScript engine using the given options. + /// Creates a new JavaScript engine using the given options. The options + /// are copied, so that editing them afterwards leaves this engine alone. /// /// The options tuning the engine. public JsScriptingService(JsScriptingOptions options) { _contexts = new ConditionalWeakTable(); _external = new Dictionary(); - _options = options ?? throw new ArgumentNullException(nameof(options)); + _options = (options ?? throw new ArgumentNullException(nameof(options))).Clone(); } #endregion From 4c5a573d188f5bfd40fb7826dbc2138cfdc764ee Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 11:33:30 +0300 Subject: [PATCH 50/73] docs: add scripting integration guide Closes #30 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/README.md | 2 +- docs/general/01-Basics.md | 172 +++++++++++++++++++++++++++++++------- 2 files changed, 143 insertions(+), 31 deletions(-) diff --git a/docs/README.md b/docs/README.md index 20d21e3..b642d95 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,4 +2,4 @@ We have more detailed information regarding the following subjects: -- [API Documentation](tutorials/01-API.md) +- [Scripting with AngleSharp.Js](general/01-Basics.md) diff --git a/docs/general/01-Basics.md b/docs/general/01-Basics.md index e498cb7..fe7ea66 100644 --- a/docs/general/01-Basics.md +++ b/docs/general/01-Basics.md @@ -1,60 +1,172 @@ --- -title: "Getting Started" +title: "Scripting with AngleSharp.Js" section: "AngleSharp.Js" --- -# Getting Started +# Scripting with AngleSharp.Js -## Requirements +AngleSharp.Js runs JavaScript against an AngleSharp document. It integrates the +[Jint](https://github.com/sebastienros/jint) interpreter with AngleSharp, so scripts can +read and change the same DOM that your .NET code uses. This is useful when processing pages +whose behavior depends on script execution, evaluating a snippet in a document context, or +hosting JavaScript APIs alongside your application. -AngleSharp.Js comes currently in two flavors: on Windows for .NET 4.6 and in general targetting .NET Standard 2.0 platforms. +## Install and configure -Most of the features of the library do not require .NET 4.6, which means you could create your own fork and modify it to work with previous versions of the .NET-Framework. - -You need to have AngleSharp installed already. This could be done via NuGet: +Install the package: ```ps1 -Install-Package AngleSharp +Install-Package AngleSharp.Js ``` -## Getting AngleSharp.Js over NuGet +Add `WithJs()` to the AngleSharp configuration. Add `WithEventLoop()` when scripts, events, +or resource callbacks must run in the browser-like task queue. Add a loader when the document +needs to fetch external scripts or other resources. -The simplest way of integrating AngleSharp.Js to your project is by using NuGet. You can install AngleSharp.Js by opening the package manager console (PM) and typing in the following statement: +```cs +var configuration = Configuration.Default + .WithDefaultLoader(new LoaderOptions + { + IsResourceLoadingEnabled = true, + }) + .WithJs() + .WithEventLoop(); -```ps1 -Install-Package AngleSharp.Js +var context = BrowsingContext.New(configuration); +var document = await context.OpenAsync("https://example.com"); + +await document.WaitUntilAvailable(); ``` -You can also use the graphical library package manager ("Manage NuGet Packages for Solution"). Searching for "AngleSharp.Js" in the official NuGet online feed will find this library. +`WithJs()` registers the JavaScript scripting service, support for inline event attributes +such as `onclick`, a navigation handler for `javascript:` URLs, and a default `navigator` +when the configuration does not already provide one. -## Setting up AngleSharp.Js +### Configure the execution stack -To use AngleSharp.Js you need to add it to your `Configuration` coming from AngleSharp itself. +`JsScriptingOptions.MaxCallStackDepth` defaults to 10,000. It makes deep JavaScript recursion +fail with a JavaScript error rather than exhausting the process stack. Leave it positive unless +you explicitly accept the risk of an uncatchable `StackOverflowException`. -If you just want a configuration *that works* you should use the following code: +```cs +var configuration = Configuration.Default + .WithJs(new JsScriptingOptions + { + MaxCallStackDepth = 5000, + }); +``` + +## Execute JavaScript + +HTML `")); +``` + +JavaScript can call delegates and access the public members of objects exposed this way. For +advanced integration, `GetOrCreateJint(document)` returns the document's Jint `Engine`, which +lets host code inspect JavaScript values or invoke JavaScript functions directly. + +### Capture `console.log` + +`console.log` forwards its arguments to an `IConsoleLogger` registered for the browsing +context. Provide one with `WithConsoleLogger`: + +```cs +var configuration = Configuration.Default .WithJs() - .WithConsoleLogger(ctx => new MyConsoleLogger(ctx)); + .WithConsoleLogger(_ => new ApplicationConsoleLogger()); ``` -in the previous example `MyConsoleLogger` refers to a class implementing the `IConsoleLogger` interface. Examples of classes implementing this interface are available in our [samples repository](https://github.com/AngleSharp/AngleSharp.Samples). +Implement `IConsoleLogger.Log(Object[] values)` to send the values to your application's +logging system. Without a logger, calls to `console.log` do not produce output. + +## DOM APIs and integration points + +AngleSharp.Js exposes AngleSharp DOM interfaces to JavaScript dynamically. The available +surface therefore follows the AngleSharp services registered in the browsing context. For +example, adding CSS support also makes its AngleSharp DOM types available to scripts. + +In addition to the DOM provided by AngleSharp, the package supplies: + +- `console.log`, `atob`, `btoa`, `DOMParser`, `Image`, `screen`, and `XMLHttpRequest`. +- `javascript:` URL navigation. +- Inline event-handler attributes and DOM event callbacks. +- ES modules and import maps through Jint's module loader. + +To replace the supplied behavior, register your own compatible AngleSharp service before +calling `WithJs()`. In particular, `WithJs()` preserves an existing `INavigator`, and the +`WithEventLoop` overloads accept either an existing `IEventLoop` or a factory for one. + +## Supported behavior and limitations + +AngleSharp.Js is a DOM and scripting integration, not a browser runtime. Script behavior +depends on the installed Jint and AngleSharp versions, plus the services that your +configuration supplies. Test the browser APIs your application relies on instead of assuming +complete browser parity. + +Notable limitations include: + +- Layout is not calculated unless you add appropriate AngleSharp rendering services. The + package's fallback `scroll*`, `client*`, and `offset*` element properties return `0`. +- The default `navigator` is intentionally minimal. Its platform is empty, registration + methods are no-ops, and its user-agent value is a fixed compatibility string. +- Network-backed features such as external scripts and `XMLHttpRequest` require suitable + AngleSharp requesters and resource loading configuration. +- The JavaScript engine executes application-provided or page-provided code in your process. + Treat untrusted scripts as untrusted code and apply the constraints appropriate to your + application. From 9c58742dac1ee69a9d67a86475ec82c0d7613530 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 11:36:32 +0300 Subject: [PATCH 51/73] docs: clarify browser facade limits Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/general/01-Basics.md | 46 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/docs/general/01-Basics.md b/docs/general/01-Basics.md index fe7ea66..0bb1527 100644 --- a/docs/general/01-Basics.md +++ b/docs/general/01-Basics.md @@ -41,6 +41,20 @@ await document.WaitUntilAvailable(); such as `onclick`, a navigation handler for `javascript:` URLs, and a default `navigator` when the configuration does not already provide one. +### Customize the event loop + +`WithEventLoop()` creates a dedicated `JsEventLoop` for each browsing context. To select the +stack size of its worker thread, use the factory overload: + +```cs +var configuration = Configuration.Default + .WithJs() + .WithEventLoop(_ => new JsEventLoop(32 * 1024 * 1024)); +``` + +You can instead provide an `IEventLoop` implementation when the host application owns +scheduling. Use the factory overload when each browsing context needs an independent loop. + ### Configure the execution stack `JsScriptingOptions.MaxCallStackDepth` defaults to 10,000. It makes deep JavaScript recursion @@ -135,6 +149,18 @@ var configuration = Configuration.Default Implement `IConsoleLogger.Log(Object[] values)` to send the values to your application's logging system. Without a logger, calls to `console.log` do not produce output. +For example, a minimal logger can forward values to `System.Diagnostics`: + +```cs +sealed class ApplicationConsoleLogger : IConsoleLogger +{ + public void Log(Object[] values) + { + Debug.WriteLine(String.Join(" ", values)); + } +} +``` + ## DOM APIs and integration points AngleSharp.Js exposes AngleSharp DOM interfaces to JavaScript dynamically. The available @@ -148,6 +174,20 @@ In addition to the DOM provided by AngleSharp, the package supplies: - Inline event-handler attributes and DOM event callbacks. - ES modules and import maps through Jint's module loader. +### Built-in browser facades + +The additional browser-like APIs are deliberately small. Use this table when deciding whether +they meet a script's needs: + +| API | Available behavior | +| --- | --- | +| `DOMParser` | `parseFromString` creates a document through the configured `IDocumentFactory`. The requested MIME type must be supported by that configuration. | +| `Image` | Creates an AngleSharp `` element; optional width and height become its display dimensions. | +| `window.postMessage` | Queues a `message` event on the current window. It requires an event loop; it does not transfer objects or deliver to another browsing context. | +| `XMLHttpRequest` | Supports `open`, `send`, request headers, status, text responses, and lifecycle events through the configured document loader. | +| `console` | Supports `console.log` only. | +| `screen` | Exposes fixed 1920-by-1080 dimensions and 24-bit color depth for compatibility. | + To replace the supplied behavior, register your own compatible AngleSharp service before calling `WithJs()`. In particular, `WithJs()` preserves an existing `INavigator`, and the `WithEventLoop` overloads accept either an existing `IEventLoop` or a factory for one. @@ -161,12 +201,14 @@ complete browser parity. Notable limitations include: -- Layout is not calculated unless you add appropriate AngleSharp rendering services. The - package's fallback `scroll*`, `client*`, and `offset*` element properties return `0`. +- The package does not calculate layout. Its `scroll*`, `client*`, and `offset*` element + properties return `0`. - The default `navigator` is intentionally minimal. Its platform is empty, registration methods are no-ops, and its user-agent value is a fixed compatibility string. - Network-backed features such as external scripts and `XMLHttpRequest` require suitable AngleSharp requesters and resource loading configuration. +- `XMLHttpRequest` currently provides text responses only. Its `response`, `responseXML`, and + `upload` properties are unavailable, and `responseType` always has its empty value. - The JavaScript engine executes application-provided or page-provided code in your process. Treat untrusted scripts as untrusted code and apply the constraints appropriate to your application. From e0a6830ca1aaababbd66aa7a2da6e4c8bec8e444 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Mon, 27 Jul 2026 10:40:15 +0200 Subject: [PATCH 52/73] Improved XmlHttpRequest integration --- .../Mocks/CaptureRequester.cs | 60 +++++++++++ src/AngleSharp.Js.Tests/ScriptEvalTests.cs | 51 ++++++++++ src/AngleSharp.Js/Dom/XmlHttpRequest.cs | 99 +++++++++++++++++-- 3 files changed, 202 insertions(+), 8 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/Mocks/CaptureRequester.cs diff --git a/src/AngleSharp.Js.Tests/Mocks/CaptureRequester.cs b/src/AngleSharp.Js.Tests/Mocks/CaptureRequester.cs new file mode 100644 index 0000000..3504f38 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Mocks/CaptureRequester.cs @@ -0,0 +1,60 @@ +namespace AngleSharp.Js.Tests.Mocks +{ + using AngleSharp.Dom; + using AngleSharp.Io; + using System; + using System.Collections.Generic; + using System.IO; + using System.Net; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + + sealed class CaptureRequester : EventTarget, IRequester + { + private String _body = String.Empty; + private Dictionary _headers = new Dictionary(); + +#pragma warning disable CS0067 + public event DomEventHandler Requesting; + public event DomEventHandler Requested; +#pragma warning restore CS0067 + + public String Body => _body; + + public IDictionary Headers => _headers; + + public async Task RequestAsync(Request request, CancellationToken cancel) + { + var body = request.Content; + + if (body != null) + { + if (body.CanSeek) + { + body.Seek(0, SeekOrigin.Begin); + } + + using (var reader = new StreamReader(body, Encoding.UTF8, true, 1024, true)) + { + _body = await reader.ReadToEndAsync().ConfigureAwait(false); + } + } + else + { + _body = String.Empty; + } + + _headers = new Dictionary(request.Headers); + + return new DefaultResponse + { + Address = request.Address, + StatusCode = HttpStatusCode.OK, + Content = new MemoryStream(Encoding.UTF8.GetBytes(String.Empty)), + }; + } + + public Boolean SupportsProtocol(String protocol) => true; + } +} diff --git a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs index 82575fe..cf62456 100644 --- a/src/AngleSharp.Js.Tests/ScriptEvalTests.cs +++ b/src/AngleSharp.Js.Tests/ScriptEvalTests.cs @@ -1,12 +1,16 @@ namespace AngleSharp.Js.Tests { using AngleSharp.Dom; + using AngleSharp.Html; using AngleSharp.Html.Dom; using AngleSharp.Io; using AngleSharp.Js.Dom; using AngleSharp.Js.Tests.Mocks; using NUnit.Framework; using System; + using System.IO; + using System.Reflection; + using System.Text; using System.Threading.Tasks; [TestFixture] @@ -135,6 +139,53 @@ public async Task PerformXmlHttpRequestAsynchronousToDelayedResponseShouldWork() Assert.AreEqual(message, result.TextContent); } + [Test] + public async Task PerformXmlHttpRequestWithUrlSearchParamsBodyShouldSerializeCorrectly() + { + var req = new CaptureRequester(); + var cfg = Configuration.Default.WithJs().WithEventLoop().With(req).WithDefaultLoader(); + var script = @" +var body = new URLSearchParams(); +body.append('query', 'dom api'); +body.append('page', '1'); +var xhr = new XMLHttpRequest(); +xhr.open('POST', 'http://example.com/', false); +xhr.send(body); +document.querySelector('#result').textContent = body.toString();"; + var html = "
"; + await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)); + + Assert.AreEqual("query=dom%20api&page=1", req.Body); + Assert.AreEqual("application/x-www-form-urlencoded; charset=UTF-8", req.Headers[HeaderNames.ContentType]); + } + + [Test] + public void SerializeFormDataSetBodyShouldProduceMultipartContent() + { + var formData = new FormDataSet(); + formData.Append("name", "Ada Lovelace", "text/plain"); + formData.Append("role", "Mathematician", "text/plain"); + + var method = typeof(XmlHttpRequest).GetMethod("Serialize", BindingFlags.Static | BindingFlags.NonPublic); + var serialized = method.Invoke(null, new Object[] { formData }); + var serializedType = serialized.GetType(); + var content = (Stream)serializedType.GetProperty("Content").GetValue(serialized); + var contentType = (String)serializedType.GetProperty("ContentType").GetValue(serialized); + + String body; + + using (var reader = new StreamReader(content, Encoding.UTF8, true, 1024, true)) + { + body = reader.ReadToEnd(); + } + + Assert.IsTrue(body.Contains("name=\"name\""), body); + Assert.IsTrue(body.Contains("Ada Lovelace"), body); + Assert.IsTrue(body.Contains("name=\"role\""), body); + Assert.IsTrue(body.Contains("Mathematician"), body); + Assert.IsTrue(contentType.StartsWith("multipart/form-data; boundary="), contentType); + } + [Test] public async Task SetContentOfIFrameElement() { diff --git a/src/AngleSharp.Js/Dom/XmlHttpRequest.cs b/src/AngleSharp.Js/Dom/XmlHttpRequest.cs index c5d95c4..7b59a3e 100644 --- a/src/AngleSharp.Js/Dom/XmlHttpRequest.cs +++ b/src/AngleSharp.Js/Dom/XmlHttpRequest.cs @@ -4,7 +4,10 @@ namespace AngleSharp.Js.Dom using AngleSharp.Browser; using AngleSharp.Dom; using AngleSharp.Dom.Events; + using AngleSharp.Html; using AngleSharp.Io; + using AngleSharp.Io.Dom; + using Jint.Native.Object; using System; using System.Collections.Generic; using System.IO; @@ -220,13 +223,19 @@ public void Send(Object body = null) if (_readyState == RequesterState.Opened) { var requestBody = Serialize(body); + + if (!String.IsNullOrEmpty(requestBody.ContentType) && !ContainsHeader(_headers, HeaderNames.ContentType)) + { + _headers[HeaderNames.ContentType] = requestBody.ContentType; + } + var loader = GetLoader(); if (loader != null) { var request = new DocumentRequest(_url) { - Body = requestBody, + Body = requestBody.Content, Method = _method, MimeType = default, Referer = _window.Document.DocumentUri, @@ -377,17 +386,91 @@ private IEventLoop GetEventLoop() => private IDocumentLoader GetLoader() => _window?.Document?.Context.GetService(); - private static Stream Serialize(Object body) + private static SerializedBody Serialize(Object body) + { + if (body == null) + { + return SerializedBody.Empty; + } + + if (body is ObjectInstance jsObject) + { + var value = jsObject.ToObject(); + + if (!Object.ReferenceEquals(value, body)) + { + return Serialize(value); + } + } + + switch (body) + { + case Stream stream: + if (stream.CanSeek) + { + stream.Seek(0, SeekOrigin.Begin); + } + + return new SerializedBody(stream); + case Byte[] bytes: + return new SerializedBody(new MemoryStream(bytes)); + case ArraySegment segment when segment.Array != null: + return new SerializedBody(new MemoryStream(segment.Array, segment.Offset, segment.Count, false)); + case FormDataSet formDataSet: + var formDataBody = formDataSet.AsMultipart(null, Encoding.UTF8); + var formDataType = String.Concat("multipart/form-data; boundary=", formDataSet.Boundary); + return new SerializedBody(formDataBody, formDataType); + case UrlSearchParams searchParams: + var query = searchParams.ToString(); + var queryBytes = Encoding.UTF8.GetBytes(query); + return new SerializedBody(new MemoryStream(queryBytes), "application/x-www-form-urlencoded; charset=UTF-8"); + case IBlob blob: + var blobBody = blob.Body; + + if (blobBody != null) + { + if (blobBody.CanSeek) + { + blobBody.Seek(0, SeekOrigin.Begin); + } + + return new SerializedBody(blobBody, blob.Type); + } + + break; + } + + var content = body.ToString(); + var contentBytes = Encoding.UTF8.GetBytes(content); + return new SerializedBody(new MemoryStream(contentBytes)); + } + + private static Boolean ContainsHeader(Dictionary headers, String name) { - if (body != null) + foreach (var pair in headers) { - //TODO Different Types? - var content = body.ToString(); - var bytes = Encoding.UTF8.GetBytes(content); - return new MemoryStream(bytes); + if (String.Equals(pair.Key, name, StringComparison.OrdinalIgnoreCase)) + { + return true; + } } - return Stream.Null; + return false; + } + + private readonly struct SerializedBody + { + public static readonly SerializedBody Empty = new SerializedBody(Stream.Null); + + public SerializedBody(Stream content, String contentType = null) + { + Content = content ?? Stream.Null; + ContentType = contentType; + } + + public Stream Content { get; } + + public String ContentType { get; } } private void Fire(String eventName) => From 6714582fcff72103cf3dab027c3e7dbdac3f8004 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 11:41:47 +0300 Subject: [PATCH 53/73] docs: clarify integration behavior Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/general/01-Basics.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/general/01-Basics.md b/docs/general/01-Basics.md index 0bb1527..1fbd388 100644 --- a/docs/general/01-Basics.md +++ b/docs/general/01-Basics.md @@ -101,6 +101,9 @@ With an event loop configured, use the document extensions to control when work | `WhenStable()` | Wait until the work already in the event loop has completed. | | `WaitUntilAvailable()` | Wait for document completion and then for the event loop to stabilize. | +These methods do not wait for asynchronous I/O that a script starts after the marker has been +queued. For example, await an event dispatched by the script after an `XMLHttpRequest` finishes. + For example, wait for scripts that change the document before reading the result: ```cs @@ -135,6 +138,9 @@ JavaScript can call delegates and access the public members of objects exposed t advanced integration, `GetOrCreateJint(document)` returns the document's Jint `Engine`, which lets host code inspect JavaScript values or invoke JavaScript functions directly. +Registering a `JsScriptingService` directly does not add the auxiliary integration that +`WithJs()` supplies: inline event-handler attributes and `javascript:` URL navigation. + ### Capture `console.log` `console.log` forwards its arguments to an `IConsoleLogger` registered for the browsing @@ -183,7 +189,7 @@ they meet a script's needs: | --- | --- | | `DOMParser` | `parseFromString` creates a document through the configured `IDocumentFactory`. The requested MIME type must be supported by that configuration. | | `Image` | Creates an AngleSharp `` element; optional width and height become its display dimensions. | -| `window.postMessage` | Queues a `message` event on the current window. It requires an event loop; it does not transfer objects or deliver to another browsing context. | +| `window.postMessage` | Queues a `message` event on the current window. It requires an event loop; it does not transfer objects, deliver to another browsing context, or enforce `targetOrigin`. | | `XMLHttpRequest` | Supports `open`, `send`, request headers, status, text responses, and lifecycle events through the configured document loader. | | `console` | Supports `console.log` only. | | `screen` | Exposes fixed 1920-by-1080 dimensions and 24-bit color depth for compatibility. | @@ -208,7 +214,8 @@ Notable limitations include: - Network-backed features such as external scripts and `XMLHttpRequest` require suitable AngleSharp requesters and resource loading configuration. - `XMLHttpRequest` currently provides text responses only. Its `response`, `responseXML`, and - `upload` properties are unavailable, and `responseType` always has its empty value. + `upload` properties are unavailable, `responseType` always has its empty value, and its + `timeout` and `withCredentials` settings do not affect requests. - The JavaScript engine executes application-provided or page-provided code in your process. Treat untrusted scripts as untrusted code and apply the constraints appropriate to your application. From 4139242b70630adb6270e776dc074f43c982a848 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 11:43:56 +0300 Subject: [PATCH 54/73] docs: retain getting started title Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- docs/README.md | 2 +- docs/general/01-Basics.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/README.md b/docs/README.md index b642d95..29d9695 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,4 +2,4 @@ We have more detailed information regarding the following subjects: -- [Scripting with AngleSharp.Js](general/01-Basics.md) +- [Getting Started](general/01-Basics.md) diff --git a/docs/general/01-Basics.md b/docs/general/01-Basics.md index 1fbd388..106a181 100644 --- a/docs/general/01-Basics.md +++ b/docs/general/01-Basics.md @@ -1,8 +1,8 @@ --- -title: "Scripting with AngleSharp.Js" +title: "Getting Started" section: "AngleSharp.Js" --- -# Scripting with AngleSharp.Js +# Getting Started AngleSharp.Js runs JavaScript against an AngleSharp document. It integrates the [Jint](https://github.com/sebastienros/jint) interpreter with AngleSharp, so scripts can From d0e9dadfc70f2229652fcee95382eb46d4465c44 Mon Sep 17 00:00:00 2001 From: Florian Rappl Date: Mon, 27 Jul 2026 10:48:53 +0200 Subject: [PATCH 55/73] Verify dispatch of DOM content loaded event --- src/AngleSharp.Js.Tests/FireEventTests.cs | 26 ++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/AngleSharp.Js.Tests/FireEventTests.cs b/src/AngleSharp.Js.Tests/FireEventTests.cs index bd6a4c9..16d2567 100644 --- a/src/AngleSharp.Js.Tests/FireEventTests.cs +++ b/src/AngleSharp.Js.Tests/FireEventTests.cs @@ -284,10 +284,8 @@ public async Task SetTimeoutWithNormalFunction() } [Test] - public async Task DomContentLoadedEventIsFired_Issue50() + public async Task DomContentLoadedEventIsFiredOnDocument_Issue50() { - //TODO Check this as well on the window level - currently works - //only against document (se AngleSharp#789) var cfg = Configuration.Default.WithJs().WithEventLoop(); var html = @" @@ -307,6 +305,28 @@ public async Task DomContentLoadedEventIsFired_Issue50() Assert.AreEqual("Success!", div?.TextContent); } + [Test] + public async Task DomContentLoadedEventIsFiredOnWindow_Issue50() + { + var cfg = Configuration.Default.WithJs().WithEventLoop(); + var html = @" + + + +"; + var document = await BrowsingContext.New(cfg).OpenAsync(m => m.Content(html)) + .WhenStable(); + + var div = document.QuerySelector("div"); + Assert.AreEqual("Success!", div?.TextContent); + } + [Test] public async Task DocumentLoadEventIsFired_Issue42() { From ec054cbfccb09d8afb1805153a1525c19aee4926 Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Mon, 27 Jul 2026 11:45:34 +0300 Subject: [PATCH 56/73] Fix html5test execution Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 + .../AngleSharp.Js.Tests.csproj | 4 + .../Fixtures/Html5Test/README.md | 20 + .../Fixtures/Html5Test/assets/csp.html | 19 + .../Fixtures/Html5Test/assets/detect.html | 1 + .../Fixtures/Html5Test/index.html | 357 ++ .../Fixtures/Html5Test/scripts/8/data.js | 2654 ++++++++++ .../Fixtures/Html5Test/scripts/8/engine.js | 4563 +++++++++++++++++ .../Fixtures/Html5Test/scripts/base.js | 1053 ++++ .../Fixtures/Html5Test/whichbrowser-stub.js | 22 + src/AngleSharp.Js.Tests/Helpers.cs | 34 + src/AngleSharp.Js.Tests/Html5TestFixture.cs | 38 + src/AngleSharp.Js.Tests/Html5TestTests.cs | 47 + .../JavascriptErrorTests.cs | 19 + .../Mocks/FaultyHttpClientRequester.cs | 14 + .../Mocks/MockHttpClientRequester.cs | 9 +- src/AngleSharp.Js.Tests/PageTests.cs | 34 +- src/AngleSharp.Js.Tests/ScriptEvalTests.cs | 53 + src/AngleSharp.Js/Dom/XmlHttpRequest.cs | 7 +- .../JsConfigurationExtensions.cs | 2 +- src/AngleSharp.Js/JsEventLoop.cs | 29 +- 21 files changed, 8967 insertions(+), 14 deletions(-) create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/README.md create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/csp.html create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/detect.html create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/index.html create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/data.js create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/engine.js create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/base.js create mode 100644 src/AngleSharp.Js.Tests/Fixtures/Html5Test/whichbrowser-stub.js create mode 100644 src/AngleSharp.Js.Tests/Html5TestFixture.cs create mode 100644 src/AngleSharp.Js.Tests/Html5TestTests.cs create mode 100644 src/AngleSharp.Js.Tests/Mocks/FaultyHttpClientRequester.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d82e72..745dcf7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ Released on ?. - Updated to use AngleSharp v1 - Updated for Jint v4 (#89, #97) @tomvanenckevort @lahma - Updated CreatorCache to be thread-safe (#110) @badnickname +- Report uncaught event-loop exceptions through the browsing context +- Fixed XMLHttpRequest handling for relative URLs and failed requests # 0.15.0 diff --git a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj index 52df45e..d0fa541 100644 --- a/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj +++ b/src/AngleSharp.Js.Tests/AngleSharp.Js.Tests.csproj @@ -12,6 +12,10 @@
+ + + + diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/README.md b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/README.md new file mode 100644 index 0000000..9f55fb5 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/README.md @@ -0,0 +1,20 @@ +# HTML5test fixture + +This fixture was retrieved from `https://html5test.com/` on 2026-07-27: + +- `https://html5test.com/` +- `https://html5test.com/scripts/base.js` +- `https://html5test.com/scripts/8/engine.js` +- `https://html5test.com/scripts/8/data.js` +- `https://html5test.com/assets/detect.html` +- `https://html5test.com/assets/csp.html` + +HTML5test is Copyright (c) 2010-2016 Niels Leenheer and is distributed under +the MIT license, reproduced in `index.html`. + +The Cloudflare email-decoder script was removed from `index.html`. The remote +WhichBrowser detector was replaced by `whichbrowser-stub.js`, which reports a +clean browser and keeps the fixture deterministic. A wrapper around the +destructuring feature probe converts Jint's unsupported `eval` path into the +feature-test's expected `SyntaxError` (https://github.com/sebastienros/jint/issues/2825). +No test engine code was modified. diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/csp.html b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/csp.html new file mode 100644 index 0000000..423e959 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/csp.html @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/detect.html b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/detect.html new file mode 100644 index 0000000..ed6ea21 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/assets/detect.html @@ -0,0 +1 @@ +&&< diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/index.html b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/index.html new file mode 100644 index 0000000..d6b6072 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/index.html @@ -0,0 +1,357 @@ + + + + HTML5test - How well does your browser support HTML5? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+

HTML5test how well does your browser support HTML5?

+ + +
+ +
+ + +
+
+

HTML5test is dead

+ +
+

+ HTML5test is dead. It's been dead for a while. In fact it hasn't been updated since 2016. +

+

+ And that is fine. This website has served it's purpose and helped popularise HTML5 with a + general audience and developers. It pushed companies to invest in their browsers and it kept them honest. + And from talking to people working for those companies over the years it worked. It helped + convince people higher up to invest more resources, because nobody wants their browser to look + bad. +

+

+ The goal of this website was always to push browsers to adopt HTML5. + To make HTML5 available for users and developers in all browsers. + And if just one feature is now available to developers in all browsers thanks to HTML5test, + this website has served it's purpose. And I know for sure that it has served it's purpose. + HTML5 is now generally supported and there aren't any truly bad browsers anymore. +

+

+ I'll try to keep this page online as a snapshot of the original test, and there is an unofficial + updated version available at html5test.co. +

+

+ It was fun to work on this project while it lasted. I have some awesome memories of talking to + the people at the W3C, Apple, Mozilla, Google and Microsoft. Thanks for all the support over the years! +

+

+ Niels Leenheer +

+
+
+
+ +
+ +
+ + +
+
+
+ + + + diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/data.js b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/data.js new file mode 100644 index 0000000..d8e2b02 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/data.js @@ -0,0 +1,2654 @@ + + +var tests = [ + + { + id: 'semantics', + name: 'Semantics', + column: 'left', + items: [ + { + id: 'parsing', + name: 'Parsing rules', + status: 'stable', + items: [ + { + id: 'doctype', + name: '<!DOCTYPE html> triggers standards mode', + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/syntax.html#the-doctype' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/syntax.html#the-doctype' ] + ] + }, { + id: 'tokenizer', + name: 'HTML5 tokenizer', + value: 3, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/syntax.html#parsing' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/syntax.html#parsing' ], + [ 'mdn', '/Web/Guide/HTML/HTML5/HTML5_Parser' ] + ] + }, { + id: 'tree', + name: 'HTML5 tree building', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/syntax.html#parsing' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/syntax.html#parsing' ], + [ 'mdn', '/Web/Guide/HTML/HTML5/HTML5_Parser' ] + ] + }, + + 'HTML5 defines rules for embedding SVG and MathML inside a regular HTML document. The following tests only check if the browser is following the HTML5 parsing rules for inline SVG and MathML, not if the browser can actually understand and render it.', + + { + id: 'svg', + name: 'Parsing inline SVG', + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#svg' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/embedded-content.html#svg-0' ], + [ 'mdn', '/Web/SVG' ] + ] + + }, { + id: 'mathml', + name: 'Parsing inline MathML', + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#mathml' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/embedded-content.html#mathml' ], + [ 'mdn', '/Web/MathML' ] + ] + } + ] + }, { + id: 'elements', + name: 'Elements', + status: 'stable', + items: [ + { + id: 'dataset', + name: 'Embedding custom non-visible data', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/dom.html#embedding-custom-non-visible-data-with-the-data-*-attributes' ], + [ 'mdn', '/Web/API/HTMLElement/dataset' ] + ] + }, + + 'New or modified elements', + + { + id: 'section', + name: 'Section elements', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Sections_and_Outlines_of_an_HTML5_document' ] + ], + items: [ + { + id: 'section', + name: 'section element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-section-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-section-element' ] + ] + }, { + id: 'nav', + name: 'nav element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-nav-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-nav-element' ] + ] + }, { + id: 'article', + name: 'article element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-article-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-article-element' ] + ] + }, { + id: 'aside', + name: 'aside element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-aside-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-aside-element' ] + ] + }, { + id: 'header', + name: 'header element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-header-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-header-element' ] + ] + }, { + id: 'footer', + name: 'footer element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/sections.html#the-footer-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-footer-element' ] + ] + } + ] + + }, { + id: 'grouping', + name: 'Grouping content elements', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Sections_and_Outlines_of_an_HTML5_document' ] + ], + items: [ + { + id: 'main', + name: 'main element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/html/wg/drafts/html/master/single-page.html#the-main-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-main-element' ] + ] + }, { + id: 'figure', + name: 'figure element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/grouping-content.html#the-figure-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-figure-element' ] + ] + }, { + id: 'figcaption', + name: 'figcaption element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/grouping-content.html#the-figcaption-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-figcaption-element' ] + ] + }, { + id: 'ol', + name: 'reversed attribute on the ol element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/grouping-content.html#the-ol-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-ol-element' ] + ] + } + ] + }, { + id: 'semantic', + name: 'Text-level semantic elements', + items: [ + { + id: 'download', + name: 'download attribute on the a element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-a-element' ], + [ 'whatwg', 'http://developers.whatwg.org/links.html#attr-hyperlink-download' ] + ] + }, { + id: 'ping', + name: 'ping attribute on the a element', + status: 'proposal', + value: 1, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#ping' ] + ] + }, { + id: 'mark', + name: 'mark element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-mark-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-mark-element' ] + ] + }, { + id: 'ruby', + name: 'ruby, rt and rp elements', + value: 3, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-ruby-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-ruby-element' ] + ] + }, { + id: 'time', + name: 'time element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-time-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-time-element' ] + ] + }, { + id: 'data', + name: 'data element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-data-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-data-element' ] + ] + }, { + id: 'wbr', + name: 'wbr element', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/text-level-semantics.html#the-wbr-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/semantics.html#the-wbr-element' ] + ] + } + ] + }, { + id: 'interactive', + name: 'Interactive elements', + items: [ + { + id: 'details', + name: 'details element', + value: 1, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/forms.html#the-details-element' ] + ] + }, { + id: 'summary', + name: 'summary element', + value: 1, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/forms.html#the-summary-element' ] + ] + }, { + id: 'menutoolbar', + name: 'menu element of type toolbar', + status: 'proposal', + value: { maximum: 1, award: { OLD: 0 } }, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/forms.html#the-menu-element' ] + ] + }, { + id: 'menucontext', + name: 'menu element of type context', + status: 'proposal', + value: { maximum: 2, award: { OLD: 1 } }, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/forms.html#the-menu-element' ] + ] + }, { + id: 'dialog', + name: 'dialog element', + status: 'proposal', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/html/wg/drafts/html/master/single-page.html#the-dialog-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/forms.html#the-dialog-element' ] + ] + } + ] + }, + + 'Global attributes or methods', + + { + id: 'hidden', + name: 'hidden attribute', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/editing.html#the-hidden-attribute' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/interaction.html#the-hidden-attribute' ] + ] + }, { + id: 'dynamic', + name: 'Dynamic markup insertion', + items: [ + { + id: 'outerHTML', + name: 'outerHTML property', + value: 1, + urls: [ + [ 'w3c', 'https://dvcs.w3.org/hg/innerhtml/raw-file/tip/index.html#widl-Element-outerHTML' ] + ] + }, { + id: 'insertAdjacentHTML', + name: 'insertAdjacentHTML function', + value: 1, + urls: [ + [ 'w3c', 'https://dvcs.w3.org/hg/innerhtml/raw-file/tip/index.html#widl-Element-insertAdjacentHTML-void-DOMString-position-DOMString-text' ] + ] + } + ] + } + ] + }, { + id: 'form', + name: 'Forms', + status: 'stable', + items: [ + 'Field types', + + { + id: 'text', + name: 'input type=text', + items: [ + { + id: 'element', + name: 'Minimal element support' + }, { + id: 'selection', + name: 'Selection Direction', + value: 2 + } + ] + }, { + id: 'search', + name: 'input type=search', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#text-(type=text)-state-and-search-state-(type=search)' + } + ] + }, { + id: 'tel', + name: 'input type=tel', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#telephone-state-(type=tel)' + } + ] + }, { + id: 'url', + name: 'input type=url', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#url-state-(type=url)' + }, { + id: 'validation', + name: 'Field validation', + url: 'http://www.w3.org/TR/html5/forms.html#the-constraint-validation-api' + } + ] + }, { + id: 'email', + name: 'input type=email', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#e-mail-state-(type=email)' + }, { + id: 'validation', + name: 'Field validation', + url: 'http://www.w3.org/TR/html5/forms.html#the-constraint-validation-api' + } + ] + }, { + id: 'date', + name: 'input type=date', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#date-state-(type=date)' + }, { + id: 'ui', + value: 2, + name: 'Custom user-interface' + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsDate', + name: 'valueAsDate() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasdate' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'month', + name: 'input type=month', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#month-state-(type=month)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsDate', + name: 'valueAsDate() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasdate' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'week', + name: 'input type=week', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#week-state-(type=week)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsDate', + name: 'valueAsDate() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasdate' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'time', + name: 'input type=time', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#time-state-(type=time)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsDate', + name: 'valueAsDate() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasdate' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'datetime-local', + name: 'input type=datetime-local', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#local-date-and-time-state-(type=datetime-local)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'number', + name: 'input type=number', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#number-state-(type=number)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'validation', + name: 'Field validation', + url: 'http://www.w3.org/TR/html5/forms.html#the-constraint-validation-api' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'range', + name: 'input type=range', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#range-state-(type=range)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + }, { + id: 'min', + name: 'min attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-min' + }, { + id: 'max', + name: 'max attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-max' + }, { + id: 'step', + name: 'step attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-step' + }, { + id: 'stepDown', + name: 'stepDown() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepdown' + }, { + id: 'stepUp', + name: 'stepUp() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-stepup' + }, { + id: 'valueAsNumber', + name: 'valueAsNumber() method', + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-valueasnumber' + } + ] + }, { + id: 'color', + name: 'input type=color', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#color-state-(type=color)' + }, { + id: 'ui', + name: 'Custom user-interface', + value: 2 + }, { + id: 'sanitization', + name: 'Value sanitization', + url: 'http://www.w3.org/TR/html5/forms.html#value-sanitization-algorithm' + } + ] + }, { + id: 'checkbox', + name: 'input type=checkbox', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#checkbox-state-(type=checkbox)' + }, { + id: 'indeterminate', + name: 'indeterminate property', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-indeterminate' + } + ] + }, { + id: 'image', + name: 'input type=image', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#image-button-state-(type=image)' + }, { + id: 'width', + name: 'width property', + value: 0, + url: 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-dim-width' + }, { + id: 'height', + name: 'height property', + value: 0, + url: 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-dim-height' + } + ] + }, { + id: 'file', + name: 'input type=file', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#file-upload-state-(type=file)' + }, { + id: 'files', + name: 'files property', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#dom-input-files' + }, { + id: 'directory', + status: 'experimental', + name: 'Directory upload support', + value: 1, + url: 'https://wicg.github.io/directory-upload/proposal.html' + } + ] + }, { + id: 'textarea', + name: 'textarea', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#the-textarea-element' + }, { + id: 'maxlength', + name: 'maxlength attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#attr-textarea-maxlength' + }, { + id: 'wrap', + name: 'wrap attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#attr-textarea-wrap' + } + ] + }, { + id: 'select', + name: 'select', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#the-select-element' + }, { + id: 'required', + name: 'required attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#attr-select-required' + } + ] + }, { + id: 'fieldset', + name: 'fieldset', + items: [ + { + id: 'element', + name: 'Minimal element support', + url: 'http://www.w3.org/TR/html5/forms.html#the-fieldset-element' + }, { + id: 'elements', + name: 'elements attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#dom-fieldset-elements' + }, { + id: 'disabled', + name: 'disabled attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#dom-fieldset-disabled' + } + ] + }, { + id: 'datalist', + name: 'datalist', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#the-datalist-element' + }, { + id: 'list', + name: 'list attribute for fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-list' + } + ] + }, { + id: 'output', + name: 'output', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#the-output-element' + } + ] + }, { + id: 'progress', + name: 'progress', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#the-progress-element' + } + ] + }, { + id: 'meter', + name: 'meter', + items: [ + { + id: 'element', + name: 'Minimal element support', + value: 2, + url: 'http://www.w3.org/TR/html5/forms.html#the-meter-element' + } + ] + }, + + 'Fields', + + { + id: 'validation', + name: 'Field validation', + items: [ + { + id: 'pattern', + name: 'pattern attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-pattern' + }, { + id: 'required', + name: 'required attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-required' + } + ] + }, { + id: 'association', + name: 'Association of controls and forms', + value: 2, + items: [ + { + id: 'control', + name: 'control property on labels', + url: 'http://www.w3.org/TR/html5/forms.html#dom-label-control' + }, { + id: 'form', + name: 'form property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fae-form' + }, { + id: 'formAction', + name: 'formAction property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fs-formaction' + }, { + id: 'formEnctype', + name: 'formEnctype property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fs-formenctype' + }, { + id: 'formMethod', + name: 'formMethod property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fs-formmethod' + }, { + id: 'formNoValidate', + name: 'formNoValidate property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fs-formnovalidate' + }, { + id: 'formTarget', + name: 'formTarget property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fs-formtarget' + }, { + id: 'labels', + name: 'labels property on fields', + url: 'http://www.w3.org/TR/html5/forms.html#dom-lfe-labels' + } + ] + }, { + id: 'other', + name: 'Other attributes', + value: 2, + items: [ + { + id: 'autofocus', + name: 'autofocus attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fe-autofocus' + }, { + id: 'autocomplete', + name: 'autocomplete attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-fe-autocomplete' + }, { + id: 'placeholder', + name: 'placeholder attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-placeholder' + }, { + id: 'multiple', + name: 'multiple attribute', + url: 'http://www.w3.org/TR/html5/forms.html#attr-input-multiple' + }, { + id: 'dirname', + name: 'dirname attribute', + url: 'https://www.w3.org/TR/html5/forms.html#attr-fe-dirname' + } + ] + }, { + id: 'selectors', + name: 'CSS selectors', + value: 2, + items: [ + { + id: 'valid', + name: ':valid selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-valid' + }, { + id: 'invalid', + name: ':invalid selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-invalid' + }, { + id: 'optional', + name: ':optional selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-optional' + }, { + id: 'required', + name: ':required selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-required' + }, { + id: 'in-range', + name: ':in-range selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-in-range' + }, { + id: 'out-of-range', + name: ':out-of-range selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-out-of-range' + }, { + id: 'read-write', + name: ':read-write selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-read-write' + }, { + id: 'read-only', + name: ':read-only selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-read-only' + } + ] + }, { + id: 'events', + name: 'Events', + value: 2, + items: [ + { + id: 'oninput', + name: 'oninput event', + url: 'http://www.w3.org/TR/html5/forms.html#event-input-input' + }, { + id: 'onchange', + name: 'onchange event', + url: 'http://www.w3.org/TR/html5/forms.html#event-input-change' + }, { + id: 'oninvalid', + name: 'oninvalid event', + url: 'http://www.w3.org/TR/html5/webappapis.html#events' + } + ] + }, + + 'Forms', + + { + id: 'formvalidation', + name: 'Form validation', + items: [ + { + id: 'checkValidity', + name: 'checkValidity method', + value: 3, + url: 'http://www.w3.org/TR/html5/forms.html#dom-form-checkvalidity' + }, { + id: 'noValidate', + name: 'noValidate attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/forms.html#dom-fs-novalidate' + } + ] + } + ] + }, { + id: 'components', + status: 'stable', + name: 'Web Components', + items: [ + { + id: 'custom', + name: 'Custom elements', + value: 4, + urls: [ + [ 'w3c', 'http://w3c.github.io/webcomponents/spec/custom/' ] + ] + }, { + id: 'shadowdom', + name: 'Shadow DOM', + status: 'experimental', + value: { maximum: 4, award: { OLD: 2 } }, + urls: [ + [ 'w3c', 'http://w3c.github.io/webcomponents/spec/shadow/' ] + ] + }, { + id: 'template', + name: 'HTML templates', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html-templates/' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#the-template-element' ], + [ 'wp', '/tutorials/webcomponents/htmlimports' ] + ] + }, { + id: 'imports', + name: 'HTML imports', + status: 'rejected', + urls: [ + [ 'w3c', 'http://w3c.github.io/webcomponents/spec/imports/' ] + ] + } + ] + } + ] + }, + + + { + id: 'deviceaccess', + name: 'Device Access', + column: 'left', + items: [ + { + id: 'location', + name: 'Location and Orientation', + status: 'stable', + items: [ + { + id: 'geolocation', + name: 'Geolocation', + value: 15, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/geolocation-API/' ], + [ 'wp', '/apis/geolocation' ], + [ 'mdn', '/Web/API/Geolocation/Using_geolocation' ] + ] + }, { + id: 'orientation', + name: 'Device Orientation', + value: 3, + urls: [ + [ 'w3c', 'http://dev.w3.org/geo/api/spec-source-orientation.html' ], + [ 'mdn', '/Web/API/DeviceOrientationEvent' ] + ] + }, { + id: 'motion', + name: 'Device Motion', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/geo/api/spec-source-orientation.html' ], + [ 'mdn', '/Web/API/DeviceMotionEvent' ] + ] + } + ] + }, { + id: 'output', + name: 'Output', + status: 'proposal', + items: [ + { + id: 'requestFullScreen', + name: 'Full screen support', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html#api' ], + [ 'wp', '/dom/Element/requestFullscreen' ], + [ 'mdn', '/Web/Guide/API/DOM/Using_full_screen_mode' ] + ] + }, { + id: 'notifications', + name: 'Web Notifications', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/notifications/' ], + [ 'whatwg', 'https://notifications.spec.whatwg.org' ] + ] + } + ] + }, { + id: 'input', + name: 'Input', + status: 'proposal', + items: [ + { + id: 'getGamepads', + name: 'Gamepad control', + value: { maximum: 2, award: { PREFIX: 1 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/gamepad/' ], + [ 'wp', '/apis/gamepad' ] + ] + }, { + id: 'pointerevents', + name: 'Pointer Events', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/pointerevents/' ], + [ 'wp', '/concepts/Pointer_Events' ] + ] + }, { + id: 'pointerLock', + name: 'Pointer Lock support', + value: { maximum: 3, award: { PREFIX: 2 } }, + urls: [ + [ 'w3c', 'http://dvcs.w3.org/hg/pointerlock/raw-file/default/index.html' ], + [ 'wp', '/dom/Element/requestPointerLock' ], + [ 'mdn', '/Web/API/Pointer_Lock_API' ] + ] + } + ] + } + ] + }, + + { + id: 'multimedia', + name: 'Multimedia', + column: 'right', + items: [ + { + id: 'video', + name: 'Video', + status: 'stable', + items: [ + { + id: 'element', + name: 'video element', + value: 16, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#the-video-element' ], + [ 'wp', '/html/elements/video' ], + [ 'mdn', '/Web/Guide/HTML/Using_HTML5_audio_and_video' ] + ] + }, { + id: 'subtitle', + name: 'Subtitles', + value: 8, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#the-track-element' ], + [ 'wp', '/html/elements/track' ] + ] + }, { + id: 'audiotracks', + name: 'Audio track selection', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#dom-media-audiotracks' ] + ] + }, { + id: 'videotracks', + name: 'Video track selection', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#dom-media-videotracks' ] + ] + }, { + id: 'poster', + name: 'Poster images', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-video-poster' ], + [ 'wp', '/dom/HTMLVideoElement/poster' ] + ] + }, { + id: 'canplaytype', + name: 'Codec detection', + value: 4, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#dom-navigator-canplaytype' ], + [ 'wp', '/dom/HTMLMediaElement/canPlayType' ] + ] + }, + + 'Video codecs', + + { + id: 'codecs.mp4.mpeg4', + name: 'MPEG-4 ASP support', + status: 'optional' + }, { + id: 'codecs.mp4.h264', + name: 'H.264 support', + status: 'optional', + urls: [ + [ 'other', 'http://ip.hhi.de/imagecom_G1/assets/pdfs/csvt_overview_0305.pdf' ] + ] + }, { + id: 'codecs.mp4.h265', + name: 'H.265 support', + status: 'optional' + }, { + id: 'codecs.ogg.theora', + name: 'Ogg Theora support', + status: 'optional', + urls: [ + [ 'xiph', 'http://theora.org/doc/Theora.pdf' ] + ] + }, { + id: 'codecs.webm.vp8', + name: 'WebM with VP8 support', + status: 'optional', + urls: [ + [ 'webm', 'http://www.webmproject.org/' ], + [ 'ietf', 'http://www.rfc-editor.org/rfc/rfc6386.txt' ] + ] + }, { + id: 'codecs.webm.vp9', + name: 'WebM with VP9 support', + status: 'optional', + urls: [ + [ 'webm', 'http://www.webmproject.org/' ], + [ 'ietf', 'http://tools.ietf.org/id/draft-grange-vp9-bitstream-00.txt' ] + ] + } + ] + }, { + id: 'audio', + name: 'Audio', + status: 'stable', + items: [ + { + id: 'element', + name: 'audio element', + value: 18, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#the-audio-element' ], + [ 'wp', '/html/elements/audio' ], + [ 'mdn', '/Web/Guide/HTML/Using_HTML5_audio_and_video' ] + ] + }, { + id: 'loop', + name: 'Loop audio', + value: 1, + url: 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-media-loop' + }, { + id: 'preload', + name: 'Preload in the background', + value: 1, + url: 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-media-preload' + }, + + + 'Advanced', + + { + id: 'webaudio', + name: 'Web Audio API', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/webaudio/' ], + [ 'wp', '/apis/webaudio' ] + ] + }, + + { + id: 'speechrecognition', + name: 'Speech Recognition', + status: 'experimental', + value: { maximum: 3, award: { PREFIX: 2 } }, + url: 'https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html' + }, + + { + id: 'speechsynthesis', + name: 'Speech Synthesis', + status: 'experimental', + value: { maximum: 2, award: { PREFIX: 1 } }, + url: 'https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html' + }, + + 'Audio codecs', + + { + id: 'codecs.pcm', + name: 'PCM audio support', + status: 'optional' + }, { + id: 'codecs.mp3', + name: 'MP3 support', + status: 'optional' + }, { + id: 'codecs.mp4.aac', + name: 'AAC support', + status: 'optional' + }, { + id: 'codecs.mp4.ac3', + name: 'Dolby Digital support', + status: 'optional' + }, { + id: 'codecs.mp4.ec3', + name: 'Dolby Digital Plus support', + status: 'optional' + }, { + id: 'codecs.ogg.vorbis', + name: 'Ogg Vorbis support', + status: 'optional' + }, { + id: 'codecs.ogg.opus', + name: 'Ogg Opus support', + status: 'optional' + }, { + id: 'codecs.webm.vorbis', + name: 'WebM with Vorbis support', + status: 'optional' + }, { + id: 'codecs.webm.opus', + name: 'WebM with Opus support', + status: 'optional' + } + ] + }, { + id: 'streaming', + name: 'Streaming', + status: 'stable', + items: [ + { + id: 'mediasource', + name: 'Media Source extensions', + value: { maximum: 5, award: { PREFIX: 2 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/media-source/' ], + [ 'wp', '/apis/media_source_extensions' ] + ] + }, { + id: 'drm', + name: 'DRM support', + status: 'controversial', + url: 'http://www.w3.org/TR/encrypted-media/' + }, + + 'Adaptive bit rate', + + { + id: 'type.dash', + name: 'Dynamic Adaptive Streaming / MPEG-DASH' + }, { + id: 'type.hls', + name: 'HTTP Live Streaming / HLS' + }, + + 'Codecs', + + { + id: 'video.codecs', + name: 'Video codecs', + items: [ + { + id: 'mp4.h264', + name: 'MP4 with H.264 support', + status: 'optional', + urls: [ + [ 'other', 'http://ip.hhi.de/imagecom_G1/assets/pdfs/csvt_overview_0305.pdf' ] + ] + }, { + id: 'mp4.h265', + name: 'MP4 with H.265 support', + status: 'optional' + }, { + id: 'ts.h264', + name: 'TS with H.264 support', + status: 'optional', + urls: [ + [ 'other', 'http://ip.hhi.de/imagecom_G1/assets/pdfs/csvt_overview_0305.pdf' ] + ] + }, { + id: 'ts.h265', + name: 'TS with H.265 support', + status: 'optional' + }, { + id: 'webm.vp8', + name: 'WebM with VP8 support', + status: 'optional', + urls: [ + [ 'webm', 'http://www.webmproject.org/' ], + [ 'ietf', 'http://www.rfc-editor.org/rfc/rfc6386.txt' ] + ] + }, { + id: 'webm.vp9', + name: 'WebM with VP9 support', + status: 'optional', + urls: [ + [ 'webm', 'http://www.webmproject.org/' ], + [ 'ietf', 'http://tools.ietf.org/id/draft-grange-vp9-bitstream-00.txt' ] + ] + } + ] + }, { + id: 'audio.codecs', + name: 'Audio codecs', + items: [ + { + id: 'mp4.aac', + name: 'MP4 with AAC support', + status: 'optional' + }, { + id: 'mp4.ac3', + name: 'MP4 with Dolby Digital support', + status: 'optional' + }, { + id: 'mp4.ec3', + name: 'MP4 with Dolby Digital Plus support', + status: 'optional' + }, { + id: 'ts.aac', + name: 'TS with AAC support', + status: 'optional' + }, { + id: 'ts.ac3', + name: 'TS with Dolby Digital support', + status: 'optional' + }, { + id: 'ts.ec3', + name: 'TS with Dolby Digital Plus support', + status: 'optional' + }, { + id: 'webm.vorbis', + name: 'WebM with Vorbis support', + status: 'optional' + }, { + id: 'webm.opus', + name: 'WebM with Opus support', + status: 'optional' + } + ] + } + ] + } + ] + }, + + { + id: 'graphicseffects', + name: '3D, Graphics & Effects', + column: 'right', + items: [ + { + id: 'responsive', + status: 'stable', + name: 'Responsive images', + items: [ + { + id: 'picture', + name: 'picture element', + value: 5, + urls: [ + [ 'ricg', 'http://responsiveimages.org/' ], + [ 'w3c', 'http://www.w3.org/html/wg/drafts/html/master/single-page.html#the-picture-element' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/embedded-content.html#the-picture-element' ] + ] + }, { + id: 'srcset', + name: 'srcset attribute', + value: 5, + urls: [ + [ 'ricg', 'http://responsiveimages.org/' ], + [ 'w3c', 'http://www.w3.org/html/wg/drafts/srcset/w3c-srcset/' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/embedded-content.html#attr-img-srcset' ] + ] + }, { + id: 'sizes', + name: 'sizes attribute', + value: 5, + urls: [ + [ 'ricg', 'http://responsiveimages.org/' ], + [ 'w3c', 'http://www.w3.org/html/wg/drafts/html/master/single-page.html#valid-source-size-list' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/embedded-content.html#dom-img-sizes' ], + ] + } + ] + }, { + id: 'canvas', + name: '2D Graphics', + status: 'stable', + items: [ + { + id: 'context', + name: 'Canvas 2D graphics', + value: 10, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/' ], + [ 'wp', '/apis/canvas' ], + [ 'mdn', '/Web/API/Canvas_API' ] + ] + }, + + 'Drawing primitives', + + { + id: 'text', + name: 'Text support', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/#drawing-text-to-the-canvas' ], + [ 'wp', '/apis/canvas/CanvasRenderingContext2D/fillText' ] + ] + }, { + id: 'path', + name: 'Path support', + value: { maximum: 2, award: { OLD: 1 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/#path-objects' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#path2d-objects' ] + ] + }, { + id: 'ellipse', + name: 'Ellipse support', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/#dom-context-2d-ellipse' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#dom-context-2d-ellipse' ] + ] + }, { + id: 'dashed', + name: 'Dashed line support', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/#dom-context-2d-setlinedash' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#dom-context-2d-setlinedash' ] + ] + }, { + id: 'focusring', + name: 'System focus ring support', + value: 1, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#dom-context-2d-drawfocusifneeded' ] + ] + }, + + 'Features', + + { + id: 'hittest', + name: 'Hit testing support', + status: 'proposal', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/2dcontext/#dom-context-2d-addhitregion' ], + [ 'whatwg', 'http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html#dom-context-2d-addhitregion' ] + ] + }, { + id: 'blending', + name: 'Blending modes', + status: 'proposal', + value: 5, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/compositing-1/#canvascompositingandblending' ] + ] + }, + + 'Image export formats', + + { + id: 'png', + name: 'PNG support', + status: 'optional' + }, { + id: 'jpeg', + name: 'JPEG support', + status: 'optional' + }, { + id: 'jpegxr', + name: 'JPEG-XR support', + status: 'optional' + }, { + id: 'webp', + name: 'WebP support', + status: 'optional' + } + ] + }, { + id: '3d', + status: 'stable', + name: '3D and VR', + items: [ + '3D Graphics', + + { + id: 'webgl', + name: 'WebGL', + value: { maximum: 15, award: { PREFIX: 10 } }, + urls: [ + [ 'khronos', 'https://www.khronos.org/registry/webgl/specs/latest/1.0/' ], + [ 'wp', '/webgl' ], + [ 'mdn', '/Web/API/WebGL_API' ] + ] + + }, { + id: 'webgl2', + name: 'WebGL 2', + status: 'experimental', + value: 5, + urls: [ + [ 'khronos', 'https://www.khronos.org/registry/webgl/specs/latest/2.0/' ], + [ 'wp', '/webgl' ], + [ 'mdn', '/Web/API/WebGL_API' ] + ] + + }, + + 'VR Headset', + + { + id: 'webvr', + name: 'WebVR', + status: 'experimental', + value: 3, + url: 'https://w3c.github.io/webvr/' + + } + ] + }, { + id: 'animation', + status: 'stable', + name: 'Animation', + items: [ + { + id: 'webanimation', + name: 'Web Animations API', + status: 'experimental', + value: 3, + urls: [ + [ 'w3c', 'https://w3c.github.io/web-animations/' ] + ] + }, { + id: 'requestAnimationFrame', + name: 'window.requestAnimationFrame', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/animation-timing/#requestAnimationFrame' ], + [ 'wp', '/dom/Window/requestAnimationFrame' ], + [ 'mdn', '/Web/API/window/requestAnimationFrame' ] + ] + } + ] + } + ] + }, + + { + id: 'connectivity', + name: 'Connectivity', + column: 'left', + items: [ + { + id: 'communication', + status: 'stable', + name: 'Communication', + items: [ + { + id: 'eventSource', + name: 'Server-Sent Events', + value: 5, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/eventsource/' ], + [ 'mdn', '/Web/API/Server-sent_events/Using_server-sent_events' ] + ] + }, + + { + id: 'beacon', + name: 'Beacon', + status: 'proposal', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/beacon/' ], + [ 'mdn', '/Web/API/Navigator/sendBeacon' ] + ] + }, + + { + id: 'fetch', + name: 'Fetch', + status: 'proposal', + value: 6, + urls: [ + [ 'whatwg', 'https://fetch.spec.whatwg.org/' ], + [ 'mdn', '/Web/API/Fetch_API' ] + ] + }, + + + 'XMLHttpRequest Level 2', + + { + id: 'xmlhttprequest2.upload', + name: 'Upload files', + value: 5, + url: 'http://www.w3.org/TR/XMLHttpRequest2/#the-upload-attribute' + }, { + id: 'xmlhttprequest2.response', + name: 'Response type support', + urls: [ + [ 'mdn', '/Web/API/XMLHttpRequest' ] + ], + items: [ + { + id: 'text', + name: 'Text response type', + value: 1, + url: 'http://www.w3.org/TR/XMLHttpRequest2/#dom-xmlhttprequest-responsetype' + }, { + id: 'document', + name: 'Document response type', + value: 2, + url: 'http://www.w3.org/TR/XMLHttpRequest2/#dom-xmlhttprequest-responsetype' + }, { + id: 'array', + name: 'ArrayBuffer response type', + value: 2, + url: 'http://www.w3.org/TR/XMLHttpRequest2/#dom-xmlhttprequest-responsetype' + }, { + id: 'blob', + name: 'Blob response type', + value: 2, + url: 'http://www.w3.org/TR/XMLHttpRequest2/#dom-xmlhttprequest-responsetype' + } + ] + }, + + 'WebSocket', + + { + id: 'websocket.basic', + name: 'Basic socket communication', + value: { maximum: 10, award: { PREFIX: 7, OLD: 5 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/websockets/' ], + [ 'mdn', '/Web/API/WebSockets_API' ] + ] + }, { + id: 'websocket.binary', + name: 'ArrayBuffer and Blob support', + value: 5, + urls: [ + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/comms.html#dom-websocket-binarytype' ], + [ 'mdn', '/Web/API/WebSockets_API' ] + ] + } + ] + }, { + id: 'streams', + status: 'experimental', + name: 'Streams', + items: [ + { + id: 'readable', + name: 'Readable streams', + value: 4, + urls: [ + [ 'whatwg', 'https://streams.spec.whatwg.org/' ] + ] + }, { + id: 'writeable', + name: 'Writable streams', + value: 2, + urls: [ + [ 'whatwg', 'https://streams.spec.whatwg.org/' ] + ] + } + ] + }, { + id: 'rtc', + name: 'Peer To Peer', + status: 'stable', + items: [ + 'Connectivity', + + { + id: 'webrtc', + name: 'WebRTC 1.0', + value: { maximum: 15, award: { PREFIX: 10 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/webrtc/' ], + [ 'wp', '/apis/webrtc/RTCPeerConnection' ], + [ 'mdn', '/Web/Guide/API/WebRTC' ] + ] + }, { + id: 'objectrtc', + name: 'ObjectRTC API for WebRTC', + status: 'proposal', + value: { maximum: 15, award: { PREFIX: 10 }, conditional: '!rtc.webrtc' }, + urls: [ + [ 'w3c', 'http://ortc.org/wp-content/uploads/2014/10/ortc.html' ] + ] + }, { + id: 'datachannel', + name: 'Data channel', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/webrtc/#peer-to-peer-data-api' ], + [ 'wp', '/apis/webrtc/RTCDataChannel' ], + [ 'mdn', '/Web/Guide/API/WebRTC' ] + ] + }, + + 'Input', + + { + key: 'media.getUserMedia', + name: 'Access the webcam', + value: { maximum: 15, award: { PREFIX: 10, OLD: 10 } }, + urls: [ + [ 'w3c', 'http://dev.w3.org/2011/webrtc/editor/getusermedia.html' ], + [ 'wp', '/dom/Navigator/getUserMedia' ], + [ 'mdn', '/Web/Guide/API/WebRTC' ] + ] + }, { + key: 'media.getDisplayMedia', + name: 'Screen Capture', + status: 'experimental', + value: 5, + urls: [ + [ 'w3c', 'https://w3c.github.io/mediacapture-screen-share/' ] + ] + }, { + key: 'media.enumerateDevices', + name: 'Enumerate devices', + status: 'proposal', + value: 3, + urls: [ + [ 'w3c', 'https://w3c.github.io/mediacapture-main/#mediadevices' ] + ] + }, + + 'Recording', + + { + id: 'recorder', + name: 'Media Stream recorder', + status: 'proposal', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/mediastream-recording/' ] + ] + } + ] + } + ] + }, + + { + id: 'performanceintegration', + name: 'Performance & Integration', + column: 'left', + items: [ + { + id: 'interaction', + status: 'stable', + name: 'User interaction', + items: [ + 'Drag and drop', + + { + id: 'dragdrop.attributes', + name: 'Attributes', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Drag_and_drop' ] + ], + items: [ + { + id: 'draggable', + name: 'draggable attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/editing.html#the-draggable-attribute' + }, { + id: 'dropzone', + name: 'dropzone attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/editing.html#the-dropzone-attribute ' + } + ] + }, { + id: 'dragdrop.events', + name: 'Events', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Drag_and_drop' ] + ], + items: [ + { + id: 'ondrag', + name: 'ondrag event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondragstart', + name: 'ondragstart event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondragenter', + name: 'ondragenter event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondragover', + name: 'ondragover event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondragleave', + name: 'ondragleave event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondragend', + name: 'ondragend event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + }, { + id: 'ondrop', + name: 'ondrop event', + url: 'http://www.w3.org/TR/html5/editing.html#dndevents' + } + ] + }, + + 'HTML editing', + + { + id: 'editing.elements', + name: 'Editing elements', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Content_Editable' ] + ], + items: [ + { + id: 'contentEditable', + name: 'contentEditable attribute', + value: 5, + url: 'http://www.w3.org/TR/html5/editing.html#contenteditable' + }, { + id: 'isContentEditable', + name: 'isContentEditable property', + value: 1, + url: 'http://www.w3.org/TR/html5/editing.html#contenteditable' + } + ] + }, { + id: 'editing.documents', + name: 'Editing documents', + urls: [ + [ 'mdn', '/Web/Guide/HTML/Content_Editable' ] + ], + items: [ + { + id: 'designMode', + name: 'designMode attribute', + value: 1, + url: 'http://www.w3.org/TR/html5/editing.html#designMode' + } + ] + }, { + id: 'editing.selectors', + name: 'CSS selectors', + value: { maximum: 2, award: { PREFIX: 1 } }, + urls: [ + [ 'mdn', '/Web/Guide/HTML/Content_Editable' ] + ], + items: [ + { + id: 'read-write', + name: ':read-write selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-read-write' + }, { + id: 'read-only', + name: ':read-only selector', + url: 'http://www.w3.org/TR/html5/links.html#selector-read-only' + } + ] + }, { + id: 'editing.apis', + name: 'APIs', + value: 2, + urls: [ + [ 'mdn', '/Web/Guide/HTML/Content_Editable' ] + ], + items: [ + { + id: 'execCommand', + name: 'execCommand method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + }, { + id: 'queryCommandEnabled', + name: 'queryCommandEnabled method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + }, { + id: 'queryCommandIndeterm', + name: 'queryCommandIndeterm method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + }, { + id: 'queryCommandState', + name: 'queryCommandState method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + }, { + id: 'queryCommandSupported', + name: 'queryCommandSupported method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + }, { + id: 'queryCommandValue', + name: 'queryCommandValue method', + url: 'https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html' + } + ] + }, + + 'Clipboard', + + { + id: 'clipboard', + name: 'Clipboard API and events', + value: 5, + url: 'https://w3c.github.io/clipboard-apis/' + }, + + 'Spellcheck', + + { + id: 'spellcheck', + name: 'spellcheck attribute', + value: 2, + url: 'http://www.w3.org/TR/html5/editing.html#attr-spellcheck' + } + ] + }, { + id: 'performance', + status: 'stable', + name: 'Performance', + items: [ + 'Workers', + + { + id: 'worker', + name: 'Web Workers', + value: 10, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/workers/#dedicated-workers-and-the-worker-interface' ], + [ 'mdn', '/Web/API/Web_Workers_API/Using_web_workers' ] + ] + }, { + id: 'sharedWorker', + name: 'Shared Workers', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/workers/#shared-workers-and-the-sharedworker-interface' ], + [ 'mdn', '/Web/API/Web_Workers_API/Using_web_workers' ] + ] + }, + + 'Other', + + { + id: 'requestIdleCallback', + name: 'window.requestIdleCallback', + status: 'experimental', + value: 1, + url: 'https://w3c.github.io/requestidlecallback/#the-requestidlecallback-method' + } + ] + }, { + id: 'security', + status: 'stable', + name: 'Security', + items: [ + { + id: 'crypto', + name: 'Web Cryptography API', + status: 'proposal', + value: { maximum: 5, award: { PREFIX: 3 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/WebCryptoAPI/' ] + ] + }, { + id: 'csp10', + name: 'Content Security Policy 1', + value: 3, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/CSP1/' ], + [ 'mdn', '/Web/Security/CSP' ] + ] + }, { + id: 'csp11', + name: 'Content Security Policy 2', + status: 'proposal', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/CSP2/' ], + [ 'mdn', '/Web/Security/CSP' ] + ] + }, { + id: 'cors', + name: 'Cross-Origin Resource Sharing', + value: 4, + urls: [ + [ 'mdn', '/Web/HTTP/Access_control_CORS' ] + ] + }, { + id: 'integrity', + name: 'Subresource Integrity', + status: 'proposal', + value: 2, + url: 'http://www.w3.org/TR/SRI/' + }, { + id: 'postMessage', + name: 'Cross-document messaging', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/html5/postmsg/' ], + [ 'wp', '/apis/web-messaging' ], + [ 'mdn', '/Web/API/Window/postMessage' ] + ] + }, + + 'Authentication', + + { + id: 'authentication', + name: 'Web Authentication / FIDO 2', + status: 'experimental', + value: 3, + url: 'https://w3c.github.io/webauthn/' + }, + + { + id: 'credential', + name: 'Credential Management', + status: 'experimental', + value: 3, + url: 'http://w3c.github.io/webappsec-credential-management/' + }, + + 'Iframes', + + { + id: 'sandbox', + name: 'Sandboxed iframe', + value: 4, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-iframe-sandbox' ], + [ 'mdn', '/Web/HTML/Element/iframe#attr-sandbox' ] + ] + }, { + id: 'srcdoc', + name: 'iframe with inline contents', + value: 4, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/embedded-content-0.html#attr-iframe-srcdoc' ], + [ 'mdn', '/Web/HTML/Element/iframe#attr-srcdoc' ] + ] + } + ] + }, { + id: 'payments', + status: 'experimental', + name: 'Payments', + items: [ + { + id: 'payments', + name: 'Web Payments', + value: 5, + url: 'https://w3c.github.io/browser-payment-api/specs/paymentrequest.html' + } + ] + } + ] + }, + + { + id: 'offlinestorage', + name: 'Offline & Storage', + column: 'right', + items: [ + { + id: 'offline', + name: 'Web applications', + status: 'stable', + items: [ + 'Offline resources', + + { + id: 'applicationCache', + name: 'Application Cache', + value: 3, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/browsers.html#offline' ], + [ 'wp', '/apis/appcache/ApplicationCache' ], + [ 'mdn', '/Web/HTML/Using_the_application_cache' ] + ] + }, { + id: 'serviceWorkers', + name: 'Service Workers', + status: 'proposal', + value: 10, + urls: [ + [ 'w3c', 'https://www.w3.org/TR/service-workers/' ], + [ 'mdn', '/Web/API/Service_Worker_API' ] + ] + }, { + id: 'pushMessages', + name: 'Push Messages', + status: 'proposal', + value: 2, + urls: [ + [ 'w3c', 'https://w3c.github.io/push-api/' ], + [ 'mdn', '/Web/API/Push_API' ] + ] + }, + + 'Content and Scheme handlers', + + { + id: 'registerProtocolHandler', + name: 'Custom scheme handlers', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/webappapis.html#custom-handlers' ], + [ 'mdn', '/Web-based_protocol_handlers' ] + ] + }, { + id: 'registerContentHandler', + name: 'Custom content handlers', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/webappapis.html#custom-handlers' ], + [ 'mdn', '/Web/API/Navigator/registerContentHandler' ] + ] + } + ] + }, { + id: 'storage', + name: 'Storage', + status: 'stable', + items: [ + 'Key-value storage', + + { + id: 'sessionStorage', + name: 'Session Storage', + value: 5, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/webstorage/#the-sessionstorage-attribute' ], + [ 'wp', '/apis/web-storage' ], + [ 'mdn', '/Web/API/Web_Storage_API' ] + ] + }, { + id: 'localStorage', + name: 'Local Storage', + value: 5, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/webstorage/#the-localstorage-attribute' ], + [ 'wp', '/apis/web-storage' ], + [ 'mdn', '/Web/API/Web_Storage_API' ] + ] + }, + + 'Database storage', + + { + id: 'indexedDB.basic', + name: 'IndexedDB', + value: { maximum: 21, award: { PREFIX: 16 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/IndexedDB/' ], + [ 'wp', '/apis/indexeddb' ], + [ 'mdn', '/Web/API/IndexedDB_API' ] + ] + }, { + id: 'indexedDB.blob', + name: 'Objectstore Blob support', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/IndexedDB/' ], + [ 'wp', '/apis/indexeddb' ], + [ 'mdn', '/Web/API/IndexedDB_API' ] + ] + }, { + id: 'indexedDB.arraybuffer', + name: 'Objectstore ArrayBuffer support', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/IndexedDB/' ], + [ 'wp', '/apis/indexeddb' ], + [ 'mdn', '/Web/API/IndexedDB_API' ] + ] + }, + + 'The Web SQL Database specification is no longer being updated and has been replaced by IndexedDB. Because at least 3 vendors have shipped implementations of this specification we still include it in this test.', + + { + id: 'sqlDatabase', + name: 'Web SQL Database', + status: 'rejected', + value: { maximum: 5, conditional: '!storage.indexedDB.basic' }, + + url: 'http://www.w3.org/TR/webdatabase/' + } + ] + }, { + id: 'files', + name: 'Files', + status: 'stable', + items: [ + 'Reading files', + + { + id: 'fileReader', + name: 'Basic support for reading files', + value: 7, + urls: [ + [ 'w3c', 'http://dev.w3.org/2006/webapi/FileAPI/#filereader-interface' ], + [ 'wp', '/apis/file' ], + [ 'mdn', '/Using_files_from_web_applications' ] + ] + }, { + id: 'fileReader.blob', + name: 'Create a Blob from a file', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/2006/webapi/FileAPI/#dfn-Blob' ], + ] + }, { + id: 'fileReader.dataURL', + name: 'Create a Data URL from a Blob', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/2006/webapi/FileAPI/#dfn-readAsDataURL' ], + ] + }, { + id: 'fileReader.arraybuffer', + name: 'Create an ArrayBuffer from a Blob', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/2006/webapi/FileAPI/#dfn-readAsArrayBuffer' ], + ] + }, { + id: 'fileReader.objectURL', + name: 'Create a Blob URL from a Blob', + value: 2, + urls: [ + [ 'w3c', 'http://dev.w3.org/2006/webapi/FileAPI/#dfn-createObjectURL' ], + ] + }, + + 'Accessing the file system', + + { + id: 'getFileSystem', + name: 'FileSystem API', + status: 'experimental', + urls: [ + [ 'w3c', 'http://w3c.github.io/filesystem-api/' ], + ] + }, + + 'The Directories and System API proposal has failed to gain traction among browser vendors and is only supported in some Webkit based browsers. No additional points are awarded for supporting this API.', + + { + id: 'fileSystem', + name: 'File API: Directories and System', + status: 'rejected', + urls: [ + [ 'w3c', 'http://www.w3.org/TR/file-system-api/' ], + [ 'wp', '/apis/filesystem' ] + ] + } + ] + } + ] + }, + + { + id: 'other', + name: 'Other', + column: 'right', + items: [ + { + id: 'scripting', + name: 'Scripting', + status: 'stable', + items: [ + 'Script execution', + + { + id: 'async', + name: 'Asynchronous script execution', + value: 3, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/scripting-1.html#attr-script-async' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#attr-script-async' ], + [ 'mdn', '/Web/HTML/Element/script' ], + [ 'wp', '/html/elements/script' ] + ] + }, { + id: 'defer', + name: 'Defered script execution', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/scripting-1.html#attr-script-defer' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#attr-script-defer' ], + [ 'mdn', '/Web/HTML/Element/script' ], + [ 'wp', '/html/elements/script' ] + ] + }, { + id: 'executionevents', + name: 'Script execution events', + status: 'rejected', + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/scripting-1.html#the-script-element' ], + [ 'whatwg', 'http://www.whatwg.org/specs/web-apps/current-work/multipage/scripting-1.html#the-script-element' ], + [ 'mdn', '/Web/Events/beforescriptexecute' ] + ] + }, { + id: 'onerror', + name: 'Runtime script error reporting', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/webappapis.html#runtime-script-errors' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/webappapis.html#runtime-script-errors' ], + [ 'mdn', '/Web/API/GlobalEventHandlers/onerror' ] + ] + }, + + 'ECMAScript 5', + + { + id: 'es5.json', + name: 'JSON encoding and decoding', + value: 2, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-json-object' ], + [ 'mdn', '/JSON' ], + [ 'wp', '/apis/json' ] + ] + }, + + 'ECMAScript 6', + + { + id: 'es6.modules', + name: 'Modules', + value: 3, + urls: [ + [ 'ecma', 'https://tc39.github.io/ecma262/#prod-Module' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/scripting.html#attr-script-type' ], + ] + }, { + id: 'es6.class', + name: 'Classes', + value: 1, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-class-definitions' ], + ] + }, { + id: 'es6.arrow', + name: 'Arrow functions', + value: 1, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-arrow-function-definitions' ], + ] + }, { + id: 'es6.promises', + name: 'Promises', + value: 3, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects' ], + [ 'mdn', '/Web/JavaScript/Reference/Global_Objects/Promise' ] + ] + }, { + id: 'es6.template', + name: 'Template strings', + value: 1, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-template-literals' ], + ] + }, { + id: 'es6.datatypes', + name: 'Typed arrays', + value: 2, + status: 'stable', + urls: [ + [ 'khronos', 'http://www.khronos.org/registry/typedarray/specs/latest/' ], + [ 'ecma', 'http://www.ecma-international.org/ecma-262/6.0/#sec-structured-data' ] + ] + }, { + id: 'es6.i18n', + name: 'Internationalization', + value: 2, + urls: [ + [ 'ecma', 'http://www.ecma-international.org/ecma-402/1.0/' ], + [ 'mdn', '/Web/JavaScript/Reference/Global_Objects/Intl' ] + ] + }, + + 'ECMAScript 7', + + { + id: 'es7.async', + name: 'Async and Await', + value: 3, + urls: [ + [ 'ecma', 'https://tc39.github.io/ecmascript-asyncawait/' ] + ] + }, + + 'Other API\'s', + + { + id: 'base64', + name: 'Base64 encoding and decoding', + value: 1, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/webappapis.html#atob' ], + [ 'whatwg', 'https://html.spec.whatwg.org/multipage/webappapis.html#atob' ], + [ 'mdn', '/Web/API/WindowBase64/atob' ] + ] + }, { + id: 'mutationObserver', + name: 'Mutation Observer', + value: { maximum: 2, award: { PREFIX: 1 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/dom/#mutation-observers' ], + [ 'mdn', '/Web/API/MutationObserver' ] + ] + }, { + id: 'url', + name: 'URL API', + value: { maximum: 2, award: { PREFIX: 1 } }, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/url/' ] + ] + }, { + id: 'encoding', + name: 'Encoding API', + value: 2, + urls: [ + [ 'whatwg', 'https://encoding.spec.whatwg.org' ], + [ 'mdn', '/Web/API/TextDecoder' ] + ] + } + ] + }, { + id: 'other', + name: 'Other', + status: 'stable', + items: [ + { + id: 'history', + name: 'Session history', + value: 4, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/html5/browsers.html#the-history-interface' ], + [ 'wp', '/dom/History' ], + [ 'mdn', '/Web/Guide/API/DOM/Manipulating_the_browser_history' ] + ] + }, { + id: 'pagevisiblity', + name: 'Page Visibility', + value: 2, + urls: [ + [ 'w3c', 'http://www.w3.org/TR/page-visibility/' ], + [ 'mdn', '/Web/Guide/User_experience/Using_the_Page_Visibility_API' ] + ] + }, { + id: 'getSelection', + name: 'Text selection', + value: 2, + url: 'http://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#selections' + }, { + id: 'scrollIntoView', + name: 'Scroll into view', + value: 1, + url: 'http://dev.w3.org/csswg/cssom-view/#dom-element-scrollintoview' + } + ] + } + ] + } +] diff --git a/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/engine.js b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/engine.js new file mode 100644 index 0000000..5041149 --- /dev/null +++ b/src/AngleSharp.Js.Tests/Fixtures/Html5Test/scripts/8/engine.js @@ -0,0 +1,4563 @@ + +Test = +Test8 = (function () { + + var release = 8; + + var NO = 0, + YES = 1, + OLD = 2, + BUGGY = 4, + PREFIX = 8, + BLOCKED = 16, + DISABLED = 32, + UNCONFIRMED = 64, + UNKNOWN = 128, + EXPERIMENTAL = 256; + + var blacklists = []; + + + var testsuite = [ + + /* doctype */ + + function (results) { + results.addItem({ + key: 'parsing.doctype', + passed: document.compatMode == 'CSS1Compat' + }); + }, + + + /* tokenizer */ + + function (results) { + var result = true; + var e = document.createElement('div'); + + try { + e.innerHTML = ""; + result &= e.firstChild && e.firstChild.nodeName == "DIV"; + result &= e.firstChild && (e.firstChild.attributes[0].nodeName == "\"foo" || e.firstChild.attributes[0].name == "\"foo"); + + e.innerHTML = ""; + result &= e.firstChild && e.firstChild.getAttribute("href") == "\nbar"; + + e.innerHTML = ""; + result &= e.firstChild == null; + + e.innerHTML = "\u000D"; + result &= e.firstChild && e.firstChild.nodeValue == "\u000A"; + + e.innerHTML = "⟨⟩"; + result &= e.firstChild.nodeValue == "\u27E8\u27E9"; + + e.innerHTML = "'"; + result &= e.firstChild.nodeValue == "'"; + + e.innerHTML = "ⅈ"; + result &= e.firstChild.nodeValue == "\u2148"; + + e.innerHTML = "𝕂"; + result &= e.firstChild.nodeValue == "\uD835\uDD42"; + + e.innerHTML = "∉"; + result &= e.firstChild.nodeValue == "\u2209"; + + e.innerHTML = ''; + result &= e.firstChild && e.firstChild.nodeType == 8 && e.firstChild.nodeValue == '?import namespace="foo" implementation="#bar"'; + + e.innerHTML = ''; + result &= e.firstChild && e.firstChild.nodeType == 8 && e.firstChild.nodeValue == 'foo--bar'; + + e.innerHTML = ''; + result &= e.firstChild && e.firstChild.nodeType == 8 && e.firstChild.nodeValue == '[CDATA[x]]'; + + e.innerHTML = "-->"; + result &= e.firstChild && e.firstChild.firstChild && e.firstChild.firstChild.nodeValue == ""; + result &= e.firstChild && e.firstChild.firstChild && e.firstChild.firstChild.nodeValue == ""; + result &= e.firstChild && e.firstChild.firstChild && e.firstChild.firstChild.nodeValue == ""; + result &= e.firstChild && e.firstChild.firstChild && e.firstChild.firstChild.nodeValue == "