@filteringdev/namulink 19.0.0-build.0 → 19.0.0-build.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,7 +8,7 @@
8
8
  // @downloadURL https://cdn.jsdelivr.net/npm/@filteringdev/namulink@latest/dist/NamuLink.user.js
9
9
  // @license MPL-2.0
10
10
  //
11
- // @version 19.0.0-build.0
11
+ // @version 19.0.0-build.2
12
12
  // @author PiQuark6046 and contributors
13
13
  //
14
14
  // @grant unsafeWindow
@@ -23,15 +23,7 @@
23
23
  // ==/UserScript==
24
24
 
25
25
 
26
- (()=>{function w(r={}){let e=R(r),p=e.Events.Navigate,f=e.Events.Rendered,m=a=>{window.dispatchEvent(new CustomEvent(p,{detail:a}))},l=a=>{window.dispatchEvent(new CustomEvent(f,{detail:a}))},u=window.location.href,i=0,t=!1,n=a=>{if(t)return;let s=window.location.href;if(s===u)return;let c=u;u=s;let d=++i;m({Seq:d,From:c,To:s,Cause:a}),(async()=>{let y=e.Root();if(!y){if(d!==i||t)return;l({Seq:d,Url:s,Ok:!0});return}let S=await M({Root:y,StableForMs:e.StableForMs,SampleWindowMs:e.SampleWindowMs,Threshold:e.Threshold,TimeoutMs:e.TimeoutMs,Ignore:e.IgnoreMutation});d!==i||t||l({Seq:d,Url:s,Ok:S})})()},o=a=>{let s=history[a].bind(history);Object.defineProperty(history,a,{value:(...d)=>{let y=s(...d);return queueMicrotask(()=>n(a)),y},configurable:!0,writable:!0})};o("pushState"),o("replaceState");let g=()=>n("popstate");window.addEventListener("popstate",g);let h=()=>n("hashchange");return e.WatchHashChange&&window.addEventListener("hashchange",h),queueMicrotask(()=>{if(t)return;let a=++i;m({Seq:a,From:null,To:window.location.href,Cause:"init"}),(async()=>{let s=e.Root();if(!s){if(a!==i||t)return;l({Seq:a,Url:window.location.href,Ok:!0});return}let c=await M({Root:s,StableForMs:e.StableForMs,SampleWindowMs:e.SampleWindowMs,Threshold:e.Threshold,TimeoutMs:e.TimeoutMs,Ignore:e.IgnoreMutation});a!==i||t||l({Seq:a,Url:window.location.href,Ok:c})})()}),()=>{t=!0,window.removeEventListener("popstate",g),e.WatchHashChange&&window.removeEventListener("hashchange",h)}}function b(r){if(r.type==="attributes"){let e=r.attributeName??"";if(e==="class"||e==="style"||e.startsWith("aria-")||e.startsWith("data-"))return!0}return!1}function R(r){return{Root:r.Root??(()=>document.querySelector("#app")??document.body),StableForMs:r.StableForMs??900,SampleWindowMs:r.SampleWindowMs??900,Threshold:r.Threshold??3,TimeoutMs:r.TimeoutMs??12e3,IgnoreMutation:r.IgnoreMutation??b,WatchHashChange:r.WatchHashChange??!0,Events:{Navigate:r.Events?.Navigate??"SpaNavigate",Rendered:r.Events?.Rendered??"SpaRendered"}}}async function M(r){let e=[],p=performance.now(),f=!1,m=new MutationObserver(u=>{let i=performance.now();for(let o of u){if(r.Ignore?.(o))continue;let g=1;o.type==="childList"&&(g=2),e.push({T:i,Score:g})}let t=i-r.SampleWindowMs;for(;e.length&&e[0].T<t;)e.shift();e.reduce((o,g)=>o+g.Score,0)>r.Threshold&&(p=i)});m.observe(r.Root,{subtree:!0,childList:!0,attributes:!0,characterData:!0});let l=performance.now();return await new Promise(u=>{let i=()=>{if(f)return;let t=performance.now();if(t-p>=r.StableForMs){f=!0,m.disconnect(),u(!0);return}if(t-l>=r.TimeoutMs){f=!0,m.disconnect(),u(!1);return}setTimeout(i,100)};setTimeout(i,0)})}function E(){let r=`
27
- self.onmessage = (e) => {
28
- const { entries, jobId } = e.data;
29
- // entries: Array<[string, number]>
30
- // \uB0B4\uB9BC\uCC28\uC21C \uC815\uB82C
31
- entries.sort((a, b) => b[1] - a[1]);
32
- self.postMessage({ jobId, sorted: entries });
33
- };
34
- `;return URL.createObjectURL(new Blob([r],{type:"text/javascript"}))}function P(r){let e=E(),p=Array.from({length:r},()=>new Worker(e));return{Workers:p,dispose(){p.forEach(f=>f.terminate()),URL.revokeObjectURL(e)}}}function v(r=document){let e=Object.create(null),p=r.createTreeWalker(r.documentElement||r,NodeFilter.SHOW_ELEMENT),f=p.currentNode;for(;f;){if(f instanceof Element)for(let m of f.attributes){let l=m.name;l.startsWith("data-v-")&&(e[l]=(e[l]||0)+1)}f=p.nextNode()}return e}function C(r){let e=[];function p(l){e.push(l);let u=e.length-1;for(;u>0;){let i=u-1>>1;if(e[i].count>=e[u].count)break;[e[i],e[u]]=[e[u],e[i]],u=i}}function f(){let l=e[0],u=e.pop();if(e.length){e[0]=u;let i=0;for(;;){let t=i*2+1,n=t+1,o=i;if(t<e.length&&e[t].count>e[o].count&&(o=t),n<e.length&&e[n].count>e[o].count&&(o=n),o===i)break;[e[i],e[o]]=[e[o],e[i]],i=o}}return l}for(let l=0;l<r.length;l++){let u=r[l];if(u&&u.length){let[i,t]=u[0];p({Count:t,Attr:i,ChunkIndex:l,IndexInChunk:0})}}let m=[];for(;e.length;){let{Attr:l,Count:u,ChunkIndex:i,IndexInChunk:t}=f();m.push([l,u]);let n=t+1,o=r[i];if(n<o.length){let[g,h]=o[n];p({Count:h,Attr:g,ChunkIndex:i,IndexInChunk:n})}}return m}async function x(r){let e=Object.entries(r),p=Math.max(1,navigator.hardwareConcurrency||1),f=Math.min(8,Math.max(1,p-1)),l=e.length>=5e3&&f>1?f:1,u=P(l);try{let i=Array.from({length:l},()=>[]);for(let o=0;o<e.length;o++)i[o%l].push(e[o]);let t=await Promise.all(i.map((o,g)=>new Promise((h,a)=>{let s=u.Workers[g],c=g+":"+Date.now(),d=S=>{S.data?.jobId===c&&(s.removeEventListener("message",d),s.removeEventListener("error",y),h(S.data.sorted))},y=S=>{s.removeEventListener("message",d),s.removeEventListener("error",y),a(S)};s.addEventListener("message",d),s.addEventListener("error",y),s.postMessage({entries:o,jobId:c})}))),n=t.length===1?t[0]:C(t);return{Result:n,TotalKeys:n.length,WorkerCount:l,HardwareConcurrency:p}}finally{u.dispose()}}var W=typeof unsafeWindow<"u"?unsafeWindow:window;function k(r,e="NamuLink"){let p=r.Function.prototype.toString,f=[[/for *\( *; *; *\) *switch *\( *_[a-z0-9]+\[_[a-z0-9]+\([a-z0-9]+\)\] *=_[a-z0-9]+/,/_[a-z0-9]+\[('|")[A-Z]+('|")\]\)\(\[ *\]\)/,/0x[a-z0-9]+ *\) *; *case/],[/; *return *this\[_0x[a-z0-9]+\( *0x[0-9a-z]+ *\)/,/; *if *\( *_0x[a-z0-9]+ *&& *\( *_0x[a-z0-9]+ *= *_0x[a-z0-9]+/,/\) *, *void *\( *this *\[ *_0x[a-z0-9]+\( *0x[0-9a-z]+ *\) *\] *= *_0x[a-z0-9]+ *\[/]];r.Function.prototype.bind=new Proxy(r.Function.prototype.bind,{apply(t,n,o){let g=Reflect.apply(p,n,o);return f.filter(h=>h.filter(a=>a.test(g)).length>=3).length===1?(console.debug(`[${e}]: Function.prototype.bind:`,n),Reflect.apply(t,()=>{},[])):Reflect.apply(t,n,o)}});let m=[[/\( *\) *=> *{ *var *_0x[0-9a-z]+ *= *a0_0x[0-9a-f]+ *; *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\]\(\); *}/,/\( *\) *=> *{ *var *_0x[0-9a-z]+ *= *a0_0x[0-9a-f]+ *; *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\]\(\); *}/],[/\( *\) *=> *{ *var _0x[a-z0-9]+ *= *_0x[a-z0-9]+ *; *if *\( *this\[ *_0x[a-z0-9]+ *\( *0x[0-9a-f]+ *\) *\] *\) *return *clearTimeout/,/\( *0x[0-9a-f]+ *\) *\] *\) *, *void *\( *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\] *= *void *\([x0-9a-f*+-]+ *\) *\) *; *this\[_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\] *\(\) *;/]];function l(t){let n=[];for(;t.parentElement;)n.push(t.parentElement),t=t.parentElement;return n}let u=()=>{let t=[...document.querySelectorAll('div[class*=" "] div[class]')].filter(n=>n instanceof HTMLElement);t=t.filter(n=>{let o=Number(getComputedStyle(n).getPropertyValue("padding-left").replaceAll("px","")),g=Number(getComputedStyle(n).getPropertyValue("padding-right").replaceAll("px","")),h=Number(getComputedStyle(n).getPropertyValue("padding-top").replaceAll("px","")),a=Number(getComputedStyle(n).getPropertyValue("padding-bottom").replaceAll("px",""));return o>5&&g>5&&h>5&&a>5}),t=t.filter(n=>[...n.querySelectorAll("*")].filter(o=>o instanceof HTMLElement&&getComputedStyle(o).getPropertyValue("animation-timing-function")==="ease-in-out").length>=3),t=t.filter(n=>l(n).some(o=>Number(getComputedStyle(o).getPropertyValue("margin-top").replaceAll("px",""))>10)),t=t.filter(n=>n.innerText.length<1e3),t=t.filter(n=>[...n.querySelectorAll('*[href="/RecentChanges"]')].filter(o=>o instanceof HTMLElement&&getComputedStyle(o).getPropertyValue("display")!=="none").length===0),t=t.filter(n=>!n.innerText.includes((new URL(location.href).searchParams.get("from")||"")+"\uC5D0\uC11C \uB118\uC5B4\uC634")),t=t.filter(n=>!/\[[0-9]+\] .+/.test(n.innerText)),t.forEach(n=>n.setAttribute("style","display: none !important; visibility: hidden !important;"))};r.setTimeout=new Proxy(r.setTimeout,{apply(t,n,o){let g=Reflect.apply(p,o[0],o);return m.filter(h=>h.filter(a=>a.test(g)).length>=1).length===1?(console.debug(`[${e}]: setTimeout:`,o[0]),Reflect.apply(t,n,[u,1500])):Reflect.apply(t,n,o)}}),document.readyState==="loading"?window.addEventListener("DOMContentLoaded",()=>{w({Root:()=>document.getElementById("#app"),StableForMs:900,SampleWindowMs:900,Threshold:3,TimeoutMs:12e3,IgnoreMutation:b,WatchHashChange:!0})}):window.addEventListener("DOMContentLoaded",()=>{w({Root:()=>document.getElementById("#app"),StableForMs:900,SampleWindowMs:900,Threshold:3,TimeoutMs:12e3,IgnoreMutation:b,WatchHashChange:!0})});let i=async()=>{let t=v(document),{Result:n,TotalKeys:o,WorkerCount:g,HardwareConcurrency:h}=await x(t),a=[];n.filter(([,s])=>s<=30).forEach(([s,c])=>{a.push(...[...document.querySelectorAll(`[${s}]`)].filter(d=>d instanceof HTMLElement))}),a=a.filter(s=>getComputedStyle(s).getPropertyValue("display")==="flex"),a=a.filter(s=>[...s.querySelectorAll("*")].some(c=>c instanceof HTMLElement&&typeof c.click=="function")),a=a.filter(s=>[...s.querySelectorAll("*")].filter(c=>c instanceof HTMLElement&&c.getBoundingClientRect().bottom-c.getBoundingClientRect().top>100&&c.getBoundingClientRect().right-c.getBoundingClientRect().left>100).length<=50),a=a.filter(s=>{let c=[...s.querySelectorAll("*")].filter(d=>d.getBoundingClientRect().bottom-d.getBoundingClientRect().top>25&&d.getBoundingClientRect().right-d.getBoundingClientRect().left>25&&d instanceof SVGPathElement&&d.getAttribute("d")!==null||d instanceof HTMLImageElement&&d.src.includes("//i.namu.wiki/i/")).length;return 1<=c&&c<=6}),a=a.filter(s=>[...s.querySelectorAll("*")].some(c=>c instanceof HTMLElement&&getComputedStyle(c,"::after").getPropertyValue("content").includes(":")&&c.getBoundingClientRect().right-c.getBoundingClientRect().left>20)===!1),a=a.filter(s=>[...s.querySelectorAll("*")].every(c=>c instanceof HTMLElement&&!new Date(c.getHTML()).getTime())),console.debug(`[${e}]`,a),a.forEach(s=>{setInterval(()=>{s.setAttribute("style","display: none !important; visibility: hidden !important;")},250)})};window.addEventListener("SpaRendered",()=>setTimeout(i,2500)),window.addEventListener("SpaRendered",i)}k(W);})();
26
+ (()=>{var s=typeof unsafeWindow<"u"?unsafeWindow:window;function m(n,r="NamuLink"){let u=n.Function.prototype.call,p=n.Reflect.apply,c=new CustomEvent("PL2PlaceHolder"),d=[[/function *\( *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *\) *{ *var *_0x[a-f0-9]+/,/('|")td('|") *, *{ *('|")class('|") *: *\( *-? *0x[a-f0-9]+ *\+ *-? *0x[a-f0-9]+ *\+ *0x[a-f0-9]+ *, *_0x[a-f0-9]+ *\[ *_0x[a-f0-9]+ */,/\( *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\) *, *('|")onClick('|") *: *_0x[a-f0-9]+ *\[ *-? *0x[a-f0-9]+ *\* *-? *0x[a-f0-9]+/,/_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\) *, *('|")colspan('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *=== *_0x[a-f0-9]+ *\[ *_0x[a-f0-9]+ *\(/]],g=[[/new *Map *\( *Object *\[ *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\] *\( *{ *('|")pretendard('|") *: *{ *('|")fontFamily('|") *: */,/('|")fontFamily('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")styleUrl('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")isGoogleFonts/,/('|")popper--wide('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")popper__title('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *} *} *,/]],o=!1;n.Function.prototype.call=new Proxy(u,{apply(t,e,l){if(o)return p(t,e,l);o=!0;let i=String(e);return i.length<5e4&&!g.some(a=>a.every(f=>f.test(i)))&&d.filter(a=>a.filter(f=>f.test(i)).length===a.length).length===1?(console.debug(`[${r}]: Function.prototype.call called for PowerLink Skeleton:`,e),n.document.dispatchEvent(c),o=!1,p(t,()=>{},[])):(o=!1,p(t,e,l))}}),n.document.addEventListener("PL2PlaceHolder",()=>{setTimeout(()=>{let t=new Set([...n.document.querySelectorAll('div[class] div[class*=" "] div[class*=" "] ~ div[class*=" "]')]);t=new Set([...t].filter(e=>e instanceof HTMLElement)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("padding-top").replaceAll(/px$/g,""))>20)),t=new Set([...t,...[...t].flatMap(e=>[...e.querySelectorAll("*")])]),t=new Set([...t].filter(e=>e instanceof HTMLElement&&e.innerText.trim().length===0)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("border-bottom-width").replaceAll(/px/g,""))>=1)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("border-left-width").replaceAll(/px/g,""))>=1)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("border-right-width").replaceAll(/px/g,""))>=1)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("border-top-width").replaceAll(/px/g,""))>=1)),console.debug(`[${r}]: Removing PowerLink Skeleton Containers:`,t),t.forEach(e=>{e.setAttribute("style","display: none !important;")})},2500),setTimeout(()=>{let t=new Set([...n.document.querySelectorAll('div[class*=" "] div[class*=" "] ~ div[class*=" "]')]);t=new Set([...t].filter(e=>e instanceof HTMLElement)),t=new Set([...t].filter(e=>Number(getComputedStyle(e).getPropertyValue("margin-bottom").replaceAll(/px$/g,""))>15||Number(getComputedStyle(e).getPropertyValue("padding-top").replaceAll(/px$/g,""))>20)),t=new Set([...t].filter(e=>e instanceof HTMLElement&&e.innerText.trim().length===0)),t=new Set([...t].filter(e=>[...e.querySelectorAll("*")].some(l=>l instanceof HTMLElement&&Number(getComputedStyle(l).getPropertyValue("padding-top").replaceAll(/px/g,""))>=5&&Number(getComputedStyle(l).getPropertyValue("padding-bottom").replaceAll(/px/g,""))>=5&&Number(getComputedStyle(l).getPropertyValue("padding-left").replaceAll(/px/g,""))>=5&&Number(getComputedStyle(l).getPropertyValue("padding-right").replaceAll(/px/g,""))>=5))),console.debug(`[${r}]: Removing PowerLink Skeleton Containers:`,t),t.forEach(e=>{e.setAttribute("style","display: none !important;")})},2500)})}m(s);})();
35
27
  /*!
36
28
  * @license MPL-2.0
37
29
  * This Source Code Form is subject to the terms of the Mozilla Public
package/dist/index.js CHANGED
@@ -7,127 +7,75 @@
7
7
  * Contributors:
8
8
  * - See Git history at https://github.com/FilteringDev/NamuLink for detailed authorship information.
9
9
  */
10
- import * as SPA from './spa.js';
11
- import * as Sort from './sort.js';
12
10
  const Win = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;
13
11
  export function RunNamuLinkUserscript(BrowserWindow, UserscriptName = 'NamuLink') {
14
- const ProtectedFunctionPrototypeToString = BrowserWindow.Function.prototype.toString;
15
- let PowerLinkGenerationPositiveRegExps = [[
16
- /for *\( *; *; *\) *switch *\( *_[a-z0-9]+\[_[a-z0-9]+\([a-z0-9]+\)\] *=_[a-z0-9]+/,
17
- /_[a-z0-9]+\[('|")[A-Z]+('|")\]\)\(\[ *\]\)/,
18
- /0x[a-z0-9]+ *\) *; *case/
19
- ], [
20
- /; *return *this\[_0x[a-z0-9]+\( *0x[0-9a-z]+ *\)/,
21
- /; *if *\( *_0x[a-z0-9]+ *&& *\( *_0x[a-z0-9]+ *= *_0x[a-z0-9]+/,
22
- /\) *, *void *\( *this *\[ *_0x[a-z0-9]+\( *0x[0-9a-z]+ *\) *\] *= *_0x[a-z0-9]+ *\[/
12
+ const OriginalFunctionPrototypeCall = BrowserWindow.Function.prototype.call;
13
+ const OriginalReflectApply = BrowserWindow.Reflect.apply;
14
+ let PL2Event = new CustomEvent('PL2PlaceHolder');
15
+ const PL2MajorFuncCallPatterns = [[
16
+ /function *\( *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *, *_0x[a-f0-9]+ *\) *{ *var *_0x[a-f0-9]+/,
17
+ /('|")td('|") *, *{ *('|")class('|") *: *\( *-? *0x[a-f0-9]+ *\+ *-? *0x[a-f0-9]+ *\+ *0x[a-f0-9]+ *, *_0x[a-f0-9]+ *\[ *_0x[a-f0-9]+ */,
18
+ /\( *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\) *, *('|")onClick('|") *: *_0x[a-f0-9]+ *\[ *-? *0x[a-f0-9]+ *\* *-? *0x[a-f0-9]+/,
19
+ /_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\) *, *('|")colspan('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *=== *_0x[a-f0-9]+ *\[ *_0x[a-f0-9]+ *\(/
23
20
  ]];
24
- BrowserWindow.Function.prototype.bind = new Proxy(BrowserWindow.Function.prototype.bind, {
25
- // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
26
- apply(Target, ThisArg, Args) {
27
- let StringifiedFunc = Reflect.apply(ProtectedFunctionPrototypeToString, ThisArg, Args);
28
- if (PowerLinkGenerationPositiveRegExps.filter(PowerLinkGenerationPositiveRegExp => PowerLinkGenerationPositiveRegExp.filter(Index => Index.test(StringifiedFunc)).length >= 3).length === 1) {
29
- console.debug(`[${UserscriptName}]: Function.prototype.bind:`, ThisArg);
30
- return Reflect.apply(Target, () => { }, []);
31
- }
32
- return Reflect.apply(Target, ThisArg, Args);
33
- }
34
- });
35
- let PowerLinkGenerationSkeletionPositiveRegExps = [[
36
- /\( *\) *=> *{ *var *_0x[0-9a-z]+ *= *a0_0x[0-9a-f]+ *; *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\]\(\); *}/,
37
- /\( *\) *=> *{ *var *_0x[0-9a-z]+ *= *a0_0x[0-9a-f]+ *; *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\]\(\); *}/
38
- ], [
39
- /\( *\) *=> *{ *var _0x[a-z0-9]+ *= *_0x[a-z0-9]+ *; *if *\( *this\[ *_0x[a-z0-9]+ *\( *0x[0-9a-f]+ *\) *\] *\) *return *clearTimeout/,
40
- /\( *0x[0-9a-f]+ *\) *\] *\) *, *void *\( *this\[ *_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\] *= *void *\([x0-9a-f*+-]+ *\) *\) *; *this\[_0x[a-z0-9]+\( *0x[0-9a-f]+ *\) *\] *\(\) *;/
21
+ const FalsePositiveSignPatterns = [[
22
+ /new *Map *\( *Object *\[ *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *\] *\( *{ *('|")pretendard('|") *: *{ *('|")fontFamily('|") *: */,
23
+ /('|")fontFamily('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")styleUrl('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")isGoogleFonts/,
24
+ /('|")popper--wide('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *, *('|")popper__title('|") *: *_0x[a-f0-9]+ *\( *0x[a-f0-9]+ *\) *} *} *,/
41
25
  ]];
42
- function GetParents(Ele) {
43
- let Parents = [];
44
- while (Ele.parentElement) {
45
- Parents.push(Ele.parentElement);
46
- Ele = Ele.parentElement;
47
- }
48
- return Parents;
49
- }
50
- const HidePowerLinkPlaceholderWithAccount = () => {
51
- let AdContainers = [...document.querySelectorAll('div[class*=" "] div[class]')].filter(AdContainer => AdContainer instanceof HTMLElement);
52
- AdContainers = AdContainers.filter((AdContainer) => {
53
- let AdContainerPaddingLeft = Number(getComputedStyle(AdContainer).getPropertyValue('padding-left').replaceAll('px', ''));
54
- let AdContainerPaddingRight = Number(getComputedStyle(AdContainer).getPropertyValue('padding-right').replaceAll('px', ''));
55
- let AdContainerPaddingTop = Number(getComputedStyle(AdContainer).getPropertyValue('padding-top').replaceAll('px', ''));
56
- let AdContainerPaddingBottom = Number(getComputedStyle(AdContainer).getPropertyValue('padding-bottom').replaceAll('px', ''));
57
- return AdContainerPaddingLeft > 5 && AdContainerPaddingRight > 5 && AdContainerPaddingTop > 5 && AdContainerPaddingBottom > 5;
58
- });
59
- AdContainers = AdContainers.filter(AdContainer => {
60
- return [...AdContainer.querySelectorAll('*')].filter(Ele => Ele instanceof HTMLElement &&
61
- getComputedStyle(Ele).getPropertyValue('animation-timing-function') === 'ease-in-out').length >= 3;
62
- });
63
- AdContainers = AdContainers.filter(AdContainer => GetParents(AdContainer).some(Parent => Number(getComputedStyle(Parent).getPropertyValue('margin-top').replaceAll('px', '')) > 10));
64
- AdContainers = AdContainers.filter(AdContainer => AdContainer.innerText.length < 1000);
65
- AdContainers = AdContainers.filter(AdContainer => [...AdContainer.querySelectorAll('*[href="/RecentChanges"]')].filter(Ele => Ele instanceof HTMLElement && getComputedStyle(Ele).getPropertyValue('display') !== 'none').length === 0);
66
- AdContainers = AdContainers.filter(AdContainer => !AdContainer.innerText.includes((new URL(location.href).searchParams.get('from') || '') + '에서 넘어옴'));
67
- AdContainers = AdContainers.filter(AdContainer => !/\[[0-9]+\] .+/.test(AdContainer.innerText));
68
- AdContainers.forEach(Ele => Ele.setAttribute('style', 'display: none !important; visibility: hidden !important;'));
69
- };
70
- BrowserWindow.setTimeout = new Proxy(BrowserWindow.setTimeout, {
26
+ let InHook = false;
27
+ BrowserWindow.Function.prototype.call = new Proxy(OriginalFunctionPrototypeCall, {
71
28
  apply(Target, ThisArg, Args) {
72
- let StringifiedFunc = Reflect.apply(ProtectedFunctionPrototypeToString, Args[0], Args);
73
- if (PowerLinkGenerationSkeletionPositiveRegExps.filter(PowerLinkGenerationSkeletionPositiveRegExp => PowerLinkGenerationSkeletionPositiveRegExp.filter(Index => Index.test(StringifiedFunc)).length >= 1).length === 1) {
74
- console.debug(`[${UserscriptName}]: setTimeout:`, Args[0]);
75
- return Reflect.apply(Target, ThisArg, [HidePowerLinkPlaceholderWithAccount, 1500]);
29
+ // Prevent infinite recursion when the hook itself calls Function.prototype.call
30
+ if (InHook) {
31
+ return OriginalReflectApply(Target, ThisArg, Args);
32
+ }
33
+ InHook = true;
34
+ const Stringified = String(ThisArg);
35
+ if (Stringified.length < 50000 &&
36
+ !FalsePositiveSignPatterns.some(Patterns => Patterns.every(Pattern => Pattern.test(Stringified))) &&
37
+ PL2MajorFuncCallPatterns.filter(Patterns => Patterns.filter(Pattern => Pattern.test(Stringified)).length === Patterns.length).length === 1) {
38
+ console.debug(`[${UserscriptName}]: Function.prototype.call called for PowerLink Skeleton:`, ThisArg);
39
+ BrowserWindow.document.dispatchEvent(PL2Event);
40
+ InHook = false;
41
+ return OriginalReflectApply(Target, () => { }, []);
76
42
  }
77
- return Reflect.apply(Target, ThisArg, Args);
43
+ InHook = false;
44
+ return OriginalReflectApply(Target, ThisArg, Args);
78
45
  }
79
46
  });
80
- if (document.readyState === 'loading') {
81
- window.addEventListener('DOMContentLoaded', () => {
82
- SPA.InstallSpaNavigationBridge({
83
- Root: () => document.getElementById('#app'),
84
- StableForMs: 900,
85
- SampleWindowMs: 900,
86
- Threshold: 3,
87
- TimeoutMs: 12000,
88
- IgnoreMutation: SPA.DefaultIgnoreMutation,
89
- WatchHashChange: true
47
+ BrowserWindow.document.addEventListener('PL2PlaceHolder', () => {
48
+ setTimeout(() => {
49
+ let ContainerElements = new Set([...BrowserWindow.document.querySelectorAll('div[class] div[class*=" "] div[class*=" "] ~ div[class*=" "]')]);
50
+ ContainerElements = new Set([...ContainerElements].filter(Container => Container instanceof HTMLElement));
51
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('padding-top').replaceAll(/px$/g, '')) > 20));
52
+ ContainerElements = new Set([...ContainerElements, ...[...ContainerElements].flatMap(Container => [...Container.querySelectorAll('*')])]);
53
+ ContainerElements = new Set([...ContainerElements].filter(Container => Container instanceof HTMLElement && Container.innerText.trim().length === 0));
54
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('border-bottom-width').replaceAll(/px/g, '')) >= 1));
55
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('border-left-width').replaceAll(/px/g, '')) >= 1));
56
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('border-right-width').replaceAll(/px/g, '')) >= 1));
57
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('border-top-width').replaceAll(/px/g, '')) >= 1));
58
+ console.debug(`[${UserscriptName}]: Removing PowerLink Skeleton Containers:`, ContainerElements);
59
+ ContainerElements.forEach(Container => {
60
+ Container.setAttribute('style', 'display: none !important;');
90
61
  });
91
- });
92
- }
93
- else {
94
- // 이미 DOMContentLoaded 이후
95
- window.addEventListener('DOMContentLoaded', () => {
96
- SPA.InstallSpaNavigationBridge({
97
- Root: () => document.getElementById('#app'),
98
- StableForMs: 900,
99
- SampleWindowMs: 900,
100
- Threshold: 3,
101
- TimeoutMs: 12000,
102
- IgnoreMutation: SPA.DefaultIgnoreMutation,
103
- WatchHashChange: true
62
+ }, 2500);
63
+ setTimeout(() => {
64
+ let ContainerElements = new Set([...BrowserWindow.document.querySelectorAll('div[class*=" "] div[class*=" "] ~ div[class*=" "]')]);
65
+ ContainerElements = new Set([...ContainerElements].filter(Container => Container instanceof HTMLElement));
66
+ ContainerElements = new Set([...ContainerElements].filter(Container => Number(getComputedStyle(Container).getPropertyValue('margin-bottom').replaceAll(/px$/g, '')) > 15 ||
67
+ Number(getComputedStyle(Container).getPropertyValue('padding-top').replaceAll(/px$/g, '')) > 20));
68
+ ContainerElements = new Set([...ContainerElements].filter(Container => Container instanceof HTMLElement && Container.innerText.trim().length === 0));
69
+ ContainerElements = new Set([...ContainerElements].filter(Container => [...Container.querySelectorAll('*')].some(Child => Child instanceof HTMLElement &&
70
+ (Number(getComputedStyle(Child).getPropertyValue('padding-top').replaceAll(/px/g, '')) >= 5 &&
71
+ Number(getComputedStyle(Child).getPropertyValue('padding-bottom').replaceAll(/px/g, '')) >= 5 &&
72
+ Number(getComputedStyle(Child).getPropertyValue('padding-left').replaceAll(/px/g, '')) >= 5 &&
73
+ Number(getComputedStyle(Child).getPropertyValue('padding-right').replaceAll(/px/g, '')) >= 5))));
74
+ console.debug(`[${UserscriptName}]: Removing PowerLink Skeleton Containers:`, ContainerElements);
75
+ ContainerElements.forEach(Container => {
76
+ Container.setAttribute('style', 'display: none !important;');
104
77
  });
105
- });
106
- }
107
- const Handler = async () => {
108
- let HTMLEle = Sort.CollectDataVAttributes(document);
109
- const { Result, TotalKeys, WorkerCount, HardwareConcurrency } = await Sort.RankCountsWithWorkersParallel(HTMLEle);
110
- let TargetedAttrsDOMs = [];
111
- Result.filter(([, Count]) => Count <= 30).forEach(([Attr, Count]) => {
112
- TargetedAttrsDOMs.push(...[...document.querySelectorAll(`[${Attr}]`)].filter((El) => El instanceof HTMLElement));
113
- });
114
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => getComputedStyle(El).getPropertyValue('display') === 'flex');
115
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => [...El.querySelectorAll('*')].some(Child => Child instanceof HTMLElement && typeof Child.click === 'function'));
116
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => [...El.querySelectorAll('*')].filter(Child => Child instanceof HTMLElement && Child.getBoundingClientRect().bottom - Child.getBoundingClientRect().top > 100 && Child.getBoundingClientRect().right - Child.getBoundingClientRect().left > 100).length <= 50);
117
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => {
118
- let Count = [...El.querySelectorAll('*')].filter(Child => (Child.getBoundingClientRect().bottom - Child.getBoundingClientRect().top > 25 && Child.getBoundingClientRect().right - Child.getBoundingClientRect().left > 25 && (Child instanceof SVGPathElement && Child.getAttribute('d') !== null) || (Child instanceof HTMLImageElement && Child.src.includes('//i.namu.wiki/i/')))).length;
119
- return 1 <= Count && Count <= 6;
120
- });
121
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => [...El.querySelectorAll('*')].some(Child => Child instanceof HTMLElement && getComputedStyle(Child, '::after').getPropertyValue('content').includes(':') && Child.getBoundingClientRect().right - Child.getBoundingClientRect().left > 20) === false);
122
- TargetedAttrsDOMs = TargetedAttrsDOMs.filter(El => [...El.querySelectorAll('*')].every(Child => Child instanceof HTMLElement && !new Date(Child.getHTML()).getTime()));
123
- console.debug(`[${UserscriptName}]`, TargetedAttrsDOMs);
124
- TargetedAttrsDOMs.forEach(El => {
125
- setInterval(() => {
126
- El.setAttribute('style', 'display: none !important; visibility: hidden !important;');
127
- }, 250);
128
- });
129
- };
130
- window.addEventListener('SpaRendered', () => setTimeout(Handler, 2500));
131
- window.addEventListener('SpaRendered', Handler);
78
+ }, 2500);
79
+ });
132
80
  }
133
81
  RunNamuLinkUserscript(Win);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@filteringdev/namulink",
3
- "version": "19.0.0-build.0",
3
+ "version": "19.0.0-build.2",
4
4
  "description": "",
5
5
  "type": "module",
6
6
  "scripts": {
package/dist/sort.d.ts DELETED
@@ -1,13 +0,0 @@
1
- export declare function CreateSortWorkerURL(): string;
2
- export declare function MakeWorkerPool(Size: number): {
3
- Workers: Worker[];
4
- dispose(): void;
5
- };
6
- export declare function CollectDataVAttributes(Root?: Document): Record<string, number>;
7
- export declare function MergeSortedChunks(Chunks: Array<Array<[string, number]>>): Array<[string, number]>;
8
- export declare function RankCountsWithWorkersParallel(Counts: Record<string, number>): Promise<{
9
- Result: [string, number][];
10
- TotalKeys: number;
11
- WorkerCount: number;
12
- HardwareConcurrency: number;
13
- }>;
package/dist/sort.js DELETED
@@ -1,147 +0,0 @@
1
- export function CreateSortWorkerURL() {
2
- const WorkerCode = `
3
- self.onmessage = (e) => {
4
- const { entries, jobId } = e.data;
5
- // entries: Array<[string, number]>
6
- // 내림차순 정렬
7
- entries.sort((a, b) => b[1] - a[1]);
8
- self.postMessage({ jobId, sorted: entries });
9
- };
10
- `;
11
- return URL.createObjectURL(new Blob([WorkerCode], { type: 'text/javascript' }));
12
- }
13
- export function MakeWorkerPool(Size) {
14
- const Url = CreateSortWorkerURL();
15
- const Workers = Array.from({ length: Size }, () => new Worker(Url));
16
- return {
17
- Workers,
18
- dispose() {
19
- Workers.forEach(W => W.terminate());
20
- URL.revokeObjectURL(Url);
21
- }
22
- };
23
- }
24
- export function CollectDataVAttributes(Root = document) {
25
- const Counts = Object.create(null);
26
- const Walker = Root.createTreeWalker(Root.documentElement || Root, NodeFilter.SHOW_ELEMENT);
27
- let Node = Walker.currentNode;
28
- while (Node) {
29
- if (Node instanceof Element) {
30
- for (const Attr of Node.attributes) {
31
- const Name = Attr.name;
32
- if (Name.startsWith('data-v-')) {
33
- Counts[Name] = (Counts[Name] || 0) + 1;
34
- }
35
- }
36
- }
37
- Node = Walker.nextNode();
38
- }
39
- return Counts;
40
- }
41
- export function MergeSortedChunks(Chunks) {
42
- // chunks: Array<Array<[string, number]>> 각각 내림차순
43
- // 결과: 전체 내림차순
44
- // 간단한 max-heap (count 기준)
45
- const Heap = [];
46
- function Push(Item) {
47
- Heap.push(Item);
48
- let I = Heap.length - 1;
49
- while (I > 0) {
50
- const P = (I - 1) >> 1;
51
- if (Heap[P].count >= Heap[I].count)
52
- break;
53
- [Heap[P], Heap[I]] = [Heap[I], Heap[P]];
54
- I = P;
55
- }
56
- }
57
- ;
58
- function Pop() {
59
- const Top = Heap[0];
60
- const Last = Heap.pop();
61
- if (Heap.length) {
62
- Heap[0] = Last;
63
- let I = 0;
64
- while (true) {
65
- const L = I * 2 + 1;
66
- const R = L + 1;
67
- let M = I;
68
- if (L < Heap.length && Heap[L].count > Heap[M].count)
69
- M = L;
70
- if (R < Heap.length && Heap[R].count > Heap[M].count)
71
- M = R;
72
- if (M === I)
73
- break;
74
- [Heap[I], Heap[M]] = [Heap[M], Heap[I]];
75
- I = M;
76
- }
77
- }
78
- return Top;
79
- }
80
- for (let C = 0; C < Chunks.length; C++) {
81
- const Arr = Chunks[C];
82
- if (Arr && Arr.length) {
83
- const [Attr, Count] = Arr[0];
84
- Push({ Count, Attr, ChunkIndex: C, IndexInChunk: 0 });
85
- }
86
- }
87
- const Out = [];
88
- while (Heap.length) {
89
- const { Attr, Count, ChunkIndex, IndexInChunk } = Pop();
90
- Out.push([Attr, Count]);
91
- const NextIndex = IndexInChunk + 1;
92
- const Arr = Chunks[ChunkIndex];
93
- if (NextIndex < Arr.length) {
94
- const [NAttr, NCount] = Arr[NextIndex];
95
- Push({ Count: NCount, Attr: NAttr, ChunkIndex, IndexInChunk: NextIndex });
96
- }
97
- }
98
- return Out;
99
- }
100
- export async function RankCountsWithWorkersParallel(Counts) {
101
- const Entries = Object.entries(Counts); // [attr, count][]
102
- // 논리 코어 수 기반 워커 개수 결정
103
- // - 너무 많이 만들면 오히려 역효과라 상한을 둠
104
- const Hc = Math.max(1, navigator.hardwareConcurrency || 1);
105
- const WorkerCount = Math.min(8, Math.max(1, Hc - 1));
106
- // entries가 적으면 병렬화 이득이 거의 없음 → 1개로
107
- const ShouldParallel = Entries.length >= 5000 && WorkerCount > 1;
108
- const ActualWorkers = ShouldParallel ? WorkerCount : 1;
109
- const Pool = MakeWorkerPool(ActualWorkers);
110
- try {
111
- // chunks 분할
112
- const Chunks = Array.from({ length: ActualWorkers }, () => []);
113
- for (let I = 0; I < Entries.length; I++) {
114
- Chunks[I % ActualWorkers].push(Entries[I]);
115
- }
116
- // 각 워커에 정렬 요청
117
- const SortedChunks = await Promise.all(Chunks.map((ChunkEntries, Idx) => {
118
- return new Promise((Resolve, Reject) => {
119
- const Worker = Pool.Workers[Idx];
120
- const JobId = Idx + ':' + Date.now();
121
- const OnMsg = (E) => {
122
- if (E.data?.jobId !== JobId)
123
- return;
124
- Worker.removeEventListener('message', OnMsg);
125
- Worker.removeEventListener('error', OnErr);
126
- Resolve(E.data.sorted);
127
- };
128
- const OnErr = (Err) => {
129
- Worker.removeEventListener('message', OnMsg);
130
- Worker.removeEventListener('error', OnErr);
131
- Reject(Err);
132
- };
133
- Worker.addEventListener('message', OnMsg);
134
- Worker.addEventListener('error', OnErr);
135
- Worker.postMessage({ entries: ChunkEntries, jobId: JobId });
136
- });
137
- }));
138
- // 병합해서 전체 순위 생성
139
- const Merged = (SortedChunks.length === 1)
140
- ? SortedChunks[0]
141
- : MergeSortedChunks(SortedChunks);
142
- return { Result: Merged, TotalKeys: Merged.length, WorkerCount: ActualWorkers, HardwareConcurrency: Hc };
143
- }
144
- finally {
145
- Pool.dispose();
146
- }
147
- }
package/dist/spa.d.ts DELETED
@@ -1,46 +0,0 @@
1
- /*!
2
- * @license MPL-2.0
3
- * This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
- *
7
- * Contributors:
8
- * - See Git history at https://github.com/FilteringDev/NamuLink for detailed authorship information.
9
- */
10
- type SpaNavigateCause = 'init' | 'pushState' | 'replaceState' | 'popstate' | 'hashchange';
11
- export type SpaNavigateDetail = {
12
- Seq: number;
13
- From: string | null;
14
- To: string;
15
- Cause: SpaNavigateCause;
16
- };
17
- export type SpaRenderedDetail = {
18
- Seq: number;
19
- Url: string;
20
- Ok: boolean;
21
- };
22
- export type IgnoreMutation = (Mutation: MutationRecord) => boolean;
23
- export type SpaBridgeOptions = {
24
- Root?: () => Node | null;
25
- StableForMs?: number;
26
- SampleWindowMs?: number;
27
- Threshold?: number;
28
- TimeoutMs?: number;
29
- IgnoreMutation?: IgnoreMutation;
30
- WatchHashChange?: boolean;
31
- Events?: {
32
- Navigate?: 'SpaNavigate';
33
- Rendered?: 'SpaRendered';
34
- };
35
- };
36
- /**
37
- * SPA 라우팅(URL 변경)을 감지해 커스텀 이벤트를 발행하고,
38
- * DOM이 충분히 안정화되면 'Rendered' 이벤트까지 발행하는 브릿지
39
- *
40
- * Events (PascalCase):
41
- * - 'SpaNavigate' => CustomEvent<SpaNavigateDetail>
42
- * - 'SpaRendered' => CustomEvent<SpaRenderedDetail>
43
- */
44
- export declare function InstallSpaNavigationBridge(Options?: SpaBridgeOptions): () => void;
45
- export declare function DefaultIgnoreMutation(Mutation: MutationRecord): boolean;
46
- export {};
package/dist/spa.js DELETED
@@ -1,189 +0,0 @@
1
- /*!
2
- * @license MPL-2.0
3
- * This Source Code Form is subject to the terms of the Mozilla Public
4
- * License, v. 2.0. If a copy of the MPL was not distributed with this
5
- * file, You can obtain one at https://mozilla.org/MPL/2.0/.
6
- *
7
- * Contributors:
8
- * - See Git history at https://github.com/FilteringDev/NamuLink for detailed authorship information.
9
- */
10
- /**
11
- * SPA 라우팅(URL 변경)을 감지해 커스텀 이벤트를 발행하고,
12
- * DOM이 충분히 안정화되면 'Rendered' 이벤트까지 발행하는 브릿지
13
- *
14
- * Events (PascalCase):
15
- * - 'SpaNavigate' => CustomEvent<SpaNavigateDetail>
16
- * - 'SpaRendered' => CustomEvent<SpaRenderedDetail>
17
- */
18
- export function InstallSpaNavigationBridge(Options = {}) {
19
- const Opts = NormalizeOptions(Options);
20
- const EventNavigate = Opts.Events.Navigate;
21
- const EventRendered = Opts.Events.Rendered;
22
- const FireNavigate = (Detail) => {
23
- window.dispatchEvent(new CustomEvent(EventNavigate, { detail: Detail }));
24
- };
25
- const FireRendered = (Detail) => {
26
- window.dispatchEvent(new CustomEvent(EventRendered, { detail: Detail }));
27
- };
28
- let LastUrl = window.location.href;
29
- let NavSeq = 0;
30
- let Disposed = false;
31
- const OnUrlMaybeChanged = (Cause) => {
32
- if (Disposed)
33
- return;
34
- const Url = window.location.href;
35
- if (Url === LastUrl)
36
- return;
37
- const From = LastUrl;
38
- LastUrl = Url;
39
- const Seq = ++NavSeq;
40
- FireNavigate({ Seq, From, To: Url, Cause });
41
- void (async () => {
42
- const Root = Opts.Root();
43
- if (!Root) {
44
- if (Seq !== NavSeq || Disposed)
45
- return;
46
- FireRendered({ Seq, Url, Ok: true });
47
- return;
48
- }
49
- const Ok = await WaitForDomMostlyStable({
50
- Root: Root,
51
- StableForMs: Opts.StableForMs,
52
- SampleWindowMs: Opts.SampleWindowMs,
53
- Threshold: Opts.Threshold,
54
- TimeoutMs: Opts.TimeoutMs,
55
- Ignore: Opts.IgnoreMutation,
56
- });
57
- if (Seq !== NavSeq || Disposed)
58
- return;
59
- FireRendered({ Seq, Url, Ok });
60
- })();
61
- };
62
- const PatchHistory = (MethodName) => {
63
- const Original = history[MethodName].bind(history);
64
- const Patched = (...Args) => {
65
- const Ret = Original(...Args);
66
- queueMicrotask(() => OnUrlMaybeChanged(MethodName));
67
- return Ret;
68
- };
69
- Object.defineProperty(history, MethodName, {
70
- value: Patched,
71
- configurable: true,
72
- writable: true,
73
- });
74
- };
75
- PatchHistory('pushState');
76
- PatchHistory('replaceState');
77
- const OnPopState = () => OnUrlMaybeChanged('popstate');
78
- window.addEventListener('popstate', OnPopState);
79
- const OnHashChange = () => OnUrlMaybeChanged('hashchange');
80
- if (Opts.WatchHashChange)
81
- window.addEventListener('hashchange', OnHashChange);
82
- queueMicrotask(() => {
83
- if (Disposed)
84
- return;
85
- const Seq = ++NavSeq;
86
- FireNavigate({ Seq, From: null, To: window.location.href, Cause: 'init' });
87
- void (async () => {
88
- const Root = Opts.Root();
89
- if (!Root) {
90
- if (Seq !== NavSeq || Disposed)
91
- return;
92
- FireRendered({ Seq, Url: window.location.href, Ok: true });
93
- return;
94
- }
95
- const Ok = await WaitForDomMostlyStable({
96
- Root: Root,
97
- StableForMs: Opts.StableForMs,
98
- SampleWindowMs: Opts.SampleWindowMs,
99
- Threshold: Opts.Threshold,
100
- TimeoutMs: Opts.TimeoutMs,
101
- Ignore: Opts.IgnoreMutation,
102
- });
103
- if (Seq !== NavSeq || Disposed)
104
- return;
105
- FireRendered({ Seq, Url: window.location.href, Ok: Ok });
106
- })();
107
- });
108
- return () => {
109
- Disposed = true;
110
- window.removeEventListener('popstate', OnPopState);
111
- if (Opts.WatchHashChange)
112
- window.removeEventListener('hashchange', OnHashChange);
113
- };
114
- }
115
- export function DefaultIgnoreMutation(Mutation) {
116
- if (Mutation.type === 'attributes') {
117
- const Name = Mutation.attributeName ?? '';
118
- if (Name === 'class' || Name === 'style')
119
- return true;
120
- if (Name.startsWith('aria-') || Name.startsWith('data-'))
121
- return true;
122
- }
123
- return false;
124
- }
125
- function NormalizeOptions(Options) {
126
- return {
127
- Root: Options.Root ?? (() => document.querySelector('#app') ?? document.body),
128
- StableForMs: Options.StableForMs ?? 900,
129
- SampleWindowMs: Options.SampleWindowMs ?? 900,
130
- Threshold: Options.Threshold ?? 3,
131
- TimeoutMs: Options.TimeoutMs ?? 12000,
132
- IgnoreMutation: Options.IgnoreMutation ?? DefaultIgnoreMutation,
133
- WatchHashChange: Options.WatchHashChange ?? true,
134
- Events: {
135
- Navigate: Options.Events?.Navigate ?? 'SpaNavigate',
136
- Rendered: Options.Events?.Rendered ?? 'SpaRendered',
137
- },
138
- };
139
- }
140
- async function WaitForDomMostlyStable(Opts) {
141
- const Events = [];
142
- let LastAboveAt = performance.now();
143
- let Done = false;
144
- const Observer = new MutationObserver((List) => {
145
- const Now = performance.now();
146
- for (const M of List) {
147
- if (Opts.Ignore?.(M))
148
- continue;
149
- let Score = 1;
150
- if (M.type === 'childList')
151
- Score = 2;
152
- Events.push({ T: Now, Score });
153
- }
154
- const Cutoff = Now - Opts.SampleWindowMs;
155
- while (Events.length && Events[0].T < Cutoff)
156
- Events.shift();
157
- const WindowScore = Events.reduce((Sum, E) => Sum + E.Score, 0);
158
- if (WindowScore > Opts.Threshold)
159
- LastAboveAt = Now;
160
- });
161
- Observer.observe(Opts.Root, {
162
- subtree: true,
163
- childList: true,
164
- attributes: true,
165
- characterData: true,
166
- });
167
- const Start = performance.now();
168
- return await new Promise((Resolve) => {
169
- const Tick = () => {
170
- if (Done)
171
- return;
172
- const Now = performance.now();
173
- if (Now - LastAboveAt >= Opts.StableForMs) {
174
- Done = true;
175
- Observer.disconnect();
176
- Resolve(true);
177
- return;
178
- }
179
- if (Now - Start >= Opts.TimeoutMs) {
180
- Done = true;
181
- Observer.disconnect();
182
- Resolve(false);
183
- return;
184
- }
185
- setTimeout(Tick, 100);
186
- };
187
- setTimeout(Tick, 0);
188
- });
189
- }