From 452eeb22d67dab28072e2793c860d0a70829666a Mon Sep 17 00:00:00 2001
From: flashwave <me@flash.moe>
Date: Thu, 20 Mar 2025 21:49:41 +0000
Subject: [PATCH] Updated JS stuffs.

---
 assets/assproc.js                  | 108 ----------------------
 assets/makai.js/csrf.js            |  24 +++++
 assets/makai.js/elem.js            | 111 -----------------------
 assets/makai.js/elems/head.js      |  17 ++--
 assets/makai.js/elems/sidelist.jsx |   2 +-
 assets/makai.js/html.js            | 139 +++++++++++++++++++++++++++++
 assets/makai.js/main.js            |   7 +-
 assets/makai.js/np/client.js       |  13 ++-
 assets/makai.js/np/init.js         |   5 +-
 assets/makai.js/tools/ascii.js     |   6 +-
 assets/makai.js/tools/whois.js     |  31 +++----
 assets/makai.js/utility.js         |  65 --------------
 assets/makai.js/xhr.js             |  67 ++++++++------
 assets/utils.js                    |  35 --------
 build.js                           |   4 +-
 src/DeveloperRoutes.php            |   7 +-
 src/SSHKeys/SSHKeysRoutes.php      |  11 +--
 src/Tools/Ascii/AsciiRoutes.php    |   5 +-
 src/Tools/ToolsRoutes.php          |   7 +-
 src/Tools/Whois/WhoisRoutes.php    |  13 ++-
 templates/html.twig                |   2 +-
 21 files changed, 267 insertions(+), 412 deletions(-)
 delete mode 100644 assets/assproc.js
 create mode 100644 assets/makai.js/csrf.js
 delete mode 100644 assets/makai.js/elem.js
 create mode 100644 assets/makai.js/html.js
 delete mode 100644 assets/makai.js/utility.js
 delete mode 100644 assets/utils.js

diff --git a/assets/assproc.js b/assets/assproc.js
deleted file mode 100644
index bf457ca..0000000
--- a/assets/assproc.js
+++ /dev/null
@@ -1,108 +0,0 @@
-const fs = require('fs');
-const path = require('path');
-const readline = require('readline');
-const utils = require('./utils.js');
-
-exports.process = async function(root, options) {
-    const macroPrefix = options.prefix || '#';
-    const entryPoint = options.entry || '';
-
-    root = fs.realpathSync(root);
-
-    const included = [];
-
-    const processFile = async function(fileName) {
-        const fullPath = path.join(root, fileName);
-        if(included.includes(fullPath))
-            return '';
-        included.push(fullPath);
-
-        if(!fullPath.startsWith(root))
-            return '/* *** INVALID PATH: ' + fullPath + ' */';
-        if(!fs.existsSync(fullPath))
-            return '/* *** FILE NOT FOUND: ' + fullPath + ' */';
-
-        const lines = readline.createInterface({
-            input: fs.createReadStream(fullPath),
-            crlfDelay: Infinity,
-        });
-
-        let output = '';
-        let lastWasEmpty = false;
-
-        if(options.showPath)
-            output += "/* *** PATH: " + fullPath + " */\n";
-
-        for await(const line of lines) {
-            const lineTrimmed = utils.trim(line);
-            if(lineTrimmed === '')
-                continue;
-
-            if(line.startsWith(macroPrefix)) {
-                const args = lineTrimmed.split(' ');
-                const macro = utils.trim(utils.trimStart(args.shift(), macroPrefix));
-
-                switch(macro) {
-                    case 'comment':
-                        break;
-
-                    case 'include': {
-                        const includePath = utils.trimEnd(args.join(' '), ';');
-                        output += utils.trim(await processFile(includePath));
-                        output += "\n";
-                        break;
-                    }
-
-                    case 'buildvars':
-                        if(typeof options.buildVars === 'object') {
-                            const bvTarget = options.buildVarsTarget || 'window';
-                            const bvProps = [];
-
-                            for(const bvName in options.buildVars)
-                                bvProps.push(`${bvName}: { value: ${JSON.stringify(options.buildVars[bvName])} }`);
-
-                            if(Object.keys(bvProps).length > 0)
-                                output += `Object.defineProperties(${bvTarget}, { ${bvProps.join(', ')} });\n`;
-                        }
-                        break;
-
-                    default:
-                        output += line;
-                        output += "\n";
-                        break;
-                }
-            } else {
-                output += line;
-                output += "\n";
-            }
-        }
-
-        return output;
-    };
-
-    return await processFile(entryPoint);
-};
-
-exports.housekeep = function(assetsPath) {
-    const files = fs.readdirSync(assetsPath).map(fileName => {
-        const stats = fs.statSync(path.join(assetsPath, fileName));
-        return {
-            name: fileName,
-            lastMod: stats.mtimeMs,
-        };
-    }).sort((a, b) => b.lastMod - a.lastMod).map(info => info.name);
-
-    const regex = /^(.+)[\-\.]([a-f0-9]+)\.(.+)$/i;
-    const counts = {};
-
-    for(const fileName of files) {
-        const match = fileName.match(regex);
-        if(match) {
-            const name = match[1] + '-' + match[3];
-            counts[name] = (counts[name] || 0) + 1;
-
-            if(counts[name] > 5)
-                fs.unlinkSync(path.join(assetsPath, fileName));
-        } else console.log(`Encountered file name in assets folder with unexpected format: ${fileName}`);
-    }
-};
diff --git a/assets/makai.js/csrf.js b/assets/makai.js/csrf.js
new file mode 100644
index 0000000..7f637da
--- /dev/null
+++ b/assets/makai.js/csrf.js
@@ -0,0 +1,24 @@
+#include html.js
+
+const $csrf = (() => {
+    let elem;
+    const getElement = () => {
+        if(elem === undefined)
+            elem = $query('meta[name="csrf-token"]');
+        return elem;
+    };
+
+    return {
+        get token() {
+            return getElement()?.content ?? '';
+        },
+        set token(token) {
+            if(typeof token !== 'string')
+                throw 'token must be a string';
+
+            const elem = getElement();
+            if(elem instanceof HTMLMetaElement)
+                elem.content = token;
+        },
+    };
+})();
diff --git a/assets/makai.js/elem.js b/assets/makai.js/elem.js
deleted file mode 100644
index 9abac54..0000000
--- a/assets/makai.js/elem.js
+++ /dev/null
@@ -1,111 +0,0 @@
-const $t = document.createTextNode.bind(document);
-const $er = (type, props, ...children) => $e({ tag: type, attrs: props, child: children });
-
-const $e = function(info, attrs, child, created) {
-    info = info || {};
-
-    if(typeof info === 'string') {
-        info = {tag: info};
-        if(attrs)
-            info.attrs = attrs;
-        if(child)
-            info.child = child;
-        if(created)
-            info.created = created;
-    }
-
-    const elem = document.createElement(info.tag || 'div');
-
-    if(info.attrs) {
-        const attrs = info.attrs;
-
-        for(let key in attrs) {
-            const attr = attrs[key];
-            if(attr === undefined || attr === null)
-                continue;
-
-            switch(typeof attr) {
-                case 'function':
-                    if(key.substring(0, 2) === 'on')
-                        key = key.substring(2).toLowerCase();
-                    elem.addEventListener(key, attr);
-                    break;
-
-                case 'object':
-                    if(attr instanceof Array) {
-                        if(key === 'class')
-                            key = 'classList';
-
-                        const prop = elem[key];
-                        let addFunc = null;
-
-                        if(prop instanceof Array)
-                            addFunc = prop.push.bind(prop);
-                        else if(prop instanceof DOMTokenList)
-                            addFunc = prop.add.bind(prop);
-
-                        if(addFunc !== null) {
-                            for(let j = 0; j < attr.length; ++j)
-                                addFunc(attr[j]);
-                        } else {
-                            if(key === 'classList')
-                                key = 'class';
-                            elem.setAttribute(key, attr.toString());
-                        }
-                    } else {
-                        for(const attrKey in attr)
-                            elem[key][attrKey] = attr[attrKey];
-                    }
-                    break;
-
-                case 'boolean':
-                    if(attr)
-                        elem.setAttribute(key, '');
-                    break;
-
-                default:
-                    if(key === 'className')
-                        key = 'class';
-                    elem.setAttribute(key, attr.toString());
-                    break;
-            }
-        }
-    }
-
-    if(info.child) {
-        let children = info.child;
-
-        if(!Array.isArray(children))
-            children = [children];
-
-        for(const child of children) {
-            switch(typeof child) {
-                case 'string':
-                    elem.appendChild($t(child));
-                    break;
-
-                case 'object':
-                    if(child instanceof Element)
-                        elem.appendChild(child);
-                    else if(child.getElement) {
-                        const childElem = child.getElement();
-                        if(childElem instanceof Element)
-                            elem.appendChild(childElem);
-                        else
-                            elem.appendChild($e(child));
-                    } else
-                        elem.appendChild($e(child));
-                    break;
-
-                default:
-                    elem.appendChild($t(child.toString()));
-                    break;
-            }
-        }
-    }
-
-    if(info.created)
-        info.created(elem);
-
-    return elem;
-};
diff --git a/assets/makai.js/elems/head.js b/assets/makai.js/elems/head.js
index 16c7655..dda8b32 100644
--- a/assets/makai.js/elems/head.js
+++ b/assets/makai.js/elems/head.js
@@ -1,13 +1,10 @@
-#include elem.js
-#include xhr.js
-
 const MakaiSiteHeaderImages = function() {
     const url = '/header-bgs.json';
 
     let headers;
     const all = async () => {
         if(!Array.isArray(headers))
-            headers = (await $x.get('/header-bgs.json')).json();
+            headers = (await $xhr.get('/header-bgs.json', { type: 'json' })).body;
 
         return headers;
     };
@@ -59,18 +56,19 @@ const MakaiSiteHeader = function(element) {
                 if(t < 1)
                     requestAnimationFrame(updateTransition);
                 else {
-                    $r(prevImage);
+                    prevImage.remove();
                     resolve();
                 }
             };
 
             prevImage.style.zIndex = '2';
 
-            const nextImage = $e({
-                tag: 'img',
-                attrs: {
+            const nextImage = $element(
+                'img',
+                {
                     alt: url,
                     src: url,
+                    style: { zIndex: '1' },
                     onerror: () => {
                         prevImage.style.opacity = null;
                         prevImage.style.zIndex = null;
@@ -81,8 +79,7 @@ const MakaiSiteHeader = function(element) {
                         requestAnimationFrame(updateTransition);
                     },
                 },
-                style: { zIndex: '1' },
-            });
+            );
             bgElem.appendChild(nextImage);
         });
     };
diff --git a/assets/makai.js/elems/sidelist.jsx b/assets/makai.js/elems/sidelist.jsx
index 0673846..0b856a2 100644
--- a/assets/makai.js/elems/sidelist.jsx
+++ b/assets/makai.js/elems/sidelist.jsx
@@ -105,7 +105,7 @@ const MakaiSideListElement = function(element) {
     };
 
     const clearBody = insertEmpty => {
-        $rc(bodyElem);
+        $removeChildren(bodyElem);
         insertEmptyElement();
     };
 
diff --git a/assets/makai.js/html.js b/assets/makai.js/html.js
new file mode 100644
index 0000000..35fe14d
--- /dev/null
+++ b/assets/makai.js/html.js
@@ -0,0 +1,139 @@
+const $id = document.getElementById.bind(document);
+const $query = document.querySelector.bind(document);
+const $queryAll = document.querySelectorAll.bind(document);
+const $text = document.createTextNode.bind(document);
+
+const $insertBefore = function(target, element) {
+    target.parentNode.insertBefore(element, target);
+};
+
+const $appendChild = function(element, child) {
+    switch(typeof child) {
+        case 'undefined':
+            break;
+
+        case 'string':
+            element.appendChild($text(child));
+            break;
+
+        case 'function':
+            $appendChild(element, child());
+            break;
+
+        case 'object':
+            if(child === null)
+                break;
+
+            if(child instanceof Node)
+                element.appendChild(child);
+            else if(child?.element instanceof Node)
+                element.appendChild(child.element);
+            else if(typeof child?.toString === 'function')
+                element.appendChild($text(child.toString()));
+            break;
+
+        default:
+            element.appendChild($text(child.toString()));
+            break;
+    }
+};
+
+const $appendChildren = function(element, ...children) {
+    for(const child of children)
+        $appendChild(element, child);
+};
+
+const $removeChildren = function(element) {
+    while(element.lastChild)
+        element.removeChild(element.lastChild);
+};
+
+const $fragment = function(props, ...children) {
+    const fragment = document.createDocumentFragment();
+    $appendChildren(fragment, ...children);
+    return fragment;
+};
+
+const $element = function(type, props, ...children) {
+    if(typeof type === 'function')
+        return new type(props ?? {}, ...children);
+
+    const element = document.createElement(type ?? 'div');
+
+    if(props)
+        for(let key in props) {
+            const prop = props[key];
+            if(prop === undefined || prop === null)
+                continue;
+
+            switch(typeof prop) {
+                case 'function':
+                    if(key.substring(0, 2) === 'on')
+                        key = key.substring(2).toLowerCase();
+                    element.addEventListener(key, prop);
+                    break;
+
+                case 'object':
+                    if(prop instanceof Array) {
+                        if(key === 'class')
+                            key = 'classList';
+
+                        const attr = element[key];
+                        let addFunc = null;
+
+                        if(attr instanceof Array)
+                            addFunc = attr.push.bind(attr);
+                        else if(attr instanceof DOMTokenList)
+                            addFunc = attr.add.bind(attr);
+
+                        if(addFunc !== null) {
+                            for(let j = 0; j < prop.length; ++j)
+                                addFunc(prop[j]);
+                        } else {
+                            if(key === 'classList')
+                                key = 'class';
+                            element.setAttribute(key, prop.toString());
+                        }
+                    } else {
+                        if(key === 'class' || key === 'className')
+                            key = 'classList';
+
+                        let setFunc = null;
+                        if(element[key] instanceof DOMTokenList)
+                            setFunc = (ak, av) => { if(av) element[key].add(ak); };
+                        else if(element[key] instanceof CSSStyleDeclaration)
+                            setFunc = (ak, av) => {
+                                if(ak.includes('-'))
+                                    element[key].setProperty(ak, av);
+                                else
+                                    element[key][ak] = av;
+                            };
+                        else
+                            setFunc = (ak, av) => { element[key][ak] = av; };
+
+                        for(const attrKey in prop) {
+                            const attrValue = prop[attrKey];
+                            if(attrValue)
+                                setFunc(attrKey, attrValue);
+                        }
+                    }
+                    break;
+
+                case 'boolean':
+                    if(prop)
+                        element.setAttribute(key, '');
+                    break;
+
+                default:
+                    if(key === 'className')
+                        key = 'class';
+
+                    element.setAttribute(key, prop.toString());
+                    break;
+            }
+        }
+
+    $appendChildren(element, ...children);
+
+    return element;
+};
diff --git a/assets/makai.js/main.js b/assets/makai.js/main.js
index a6a1cad..9494452 100644
--- a/assets/makai.js/main.js
+++ b/assets/makai.js/main.js
@@ -1,11 +1,14 @@
-#include elem.js
+#include csrf.js
+#include html.js
+#include xhr.js
+
 #include elems/head.js
 #include np/init.js
 #include tools/ascii.js
 #include tools/whois.js
 
 const makai = (() => {
-    const header = new MakaiSiteHeader($q('.js-header'));
+    const header = new MakaiSiteHeader($query('.js-header'));
 
     const runIfPathStartsWith = (prefix, func, ...args) => {
         if(location.pathname === prefix || location.pathname.startsWith(`${prefix}/`))
diff --git a/assets/makai.js/np/client.js b/assets/makai.js/np/client.js
index 9b4f7a2..ba33c3d 100644
--- a/assets/makai.js/np/client.js
+++ b/assets/makai.js/np/client.js
@@ -1,5 +1,3 @@
-#include xhr.js
-
 const MakaiNowPlaying = function(userName) {
     const noCoverUrl = 'https://lastfm.freetls.fastly.net/i/u/174s/2a96cbd8b46e442fc41c2b86b821562f.png';
     const fetchTarget = `https://now.flash.moe/get.php?u=${userName}`;
@@ -31,15 +29,14 @@ const MakaiNowPlaying = function(userName) {
 
     return {
         fetch: async () => {
-            const result = await $x.get(fetchTarget);
-            if(result.status !== 200)
-                throw `http ${result.status}`;
+            const { status, body } = await $xhr.get(fetchTarget, { type: 'json' });
+            if(status !== 200)
+                throw `http ${status}`;
 
-            let info = result.json();
-            if(info.length < 1)
+            if(body.length < 1)
                 throw 'no data';
 
-            return format(info[0]);
+            return format(body[0]);
         },
     };
 };
diff --git a/assets/makai.js/np/init.js b/assets/makai.js/np/init.js
index b394043..b51b9fd 100644
--- a/assets/makai.js/np/init.js
+++ b/assets/makai.js/np/init.js
@@ -1,9 +1,8 @@
-#include utility.js
 #include np/client.js
 #include np/element.jsx
 
 const MakaiNowPlayingInit = siteHeader => {
-    const target = $q('.js-np-target');
+    const target = $query('.js-np-target');
     if(!(target instanceof Element))
         return;
 
@@ -13,7 +12,7 @@ const MakaiNowPlayingInit = siteHeader => {
 
     const client = new MakaiNowPlaying(userName);
     const element = new MakaiNowPlayingElement;
-    $rp(target, element.element);
+    target.replaceWith(element.element);
 
     const update = () => {
         client.fetch().then(result => {
diff --git a/assets/makai.js/tools/ascii.js b/assets/makai.js/tools/ascii.js
index 633c550..f52c572 100644
--- a/assets/makai.js/tools/ascii.js
+++ b/assets/makai.js/tools/ascii.js
@@ -1,8 +1,6 @@
-#include utility.js
-
 const MakaiASCII = () => {
-    const chars = $qa('.js-ascii-char');
-    const search = $q('.js-ascii-search');
+    const chars = $queryAll('.js-ascii-char');
+    const search = $query('.js-ascii-search');
 
     const charsFilter = (filter) => {
         if(!filter) {
diff --git a/assets/makai.js/tools/whois.js b/assets/makai.js/tools/whois.js
index 73963e7..c3b41ad 100644
--- a/assets/makai.js/tools/whois.js
+++ b/assets/makai.js/tools/whois.js
@@ -1,17 +1,15 @@
-#include utility.js
-#include xhr.js
 #include elems/sidelist.jsx
 
 const MakaiWHOIS = () => {
     let locked = false;
-    const input = $q('.js-whois-input');
-    const submit = $q('.js-whois-submit');
-    const result = $q('.js-whois-body');
-    const tabs = $q('.js-whois-tabs');
+    const input = $query('.js-whois-input');
+    const submit = $query('.js-whois-submit');
+    const result = $query('.js-whois-body');
+    const tabs = $query('.js-whois-tabs');
 
     const historic = [];
     const history = (() => {
-        const element = $q('.js-whois-sidelist');
+        const element = $query('.js-whois-sidelist');
         if(element instanceof Element)
             return new MakaiSideListElement(element);
     })();
@@ -40,18 +38,17 @@ const MakaiWHOIS = () => {
         if(!lock())
             return;
 
-        $x.post('/tools/whois/lookup', {}, { _csrfp: $csrfp.get(), target: target }).then(output => {
-            let headers = output.headers();
-            if(headers.has('x-csrfp'))
-                $csrfp.set(headers.get('x-csrfp'));
+        $xhr.post('/tools/whois/lookup', { type: 'json' }, { _csrfp: $csrf.token, target: target }).then(output => {
+            if(output.headers.has('x-csrfp'))
+                $csrf.token = output.headers.get('x-csrfp');
 
-            let resp = output.json();
+            let resp = output.body;
             if(resp.error)
                 alert(resp.text);
 
             let count = 0;
 
-            $rc(tabs);
+            $removeChildren(tabs);
 
             if(resp.result && Array.isArray(resp.result.responses) && resp.result.responses.length > 0) {
                 if(!historic.includes(resp.result.target)) {
@@ -60,16 +57,16 @@ const MakaiWHOIS = () => {
                 }
 
                 for(const response of resp.result.responses) {
-                    const tab = $e({tag: 'a'});
-                    const tabHeader = $e();
-                    const tabServer = $e();
+                    const tab = $element('a');
+                    const tabHeader = $element();
+                    const tabServer = $element();
 
                     tab.href = 'javascript:;';
                     tab.className = 'whois-result-tab';
                     if(count === 0) tab.className += ' whois-result-tab-active';
 
                     tab.onclick = () => {
-                        const active = $q('.whois-result-tab-active');
+                        const active = $query('.whois-result-tab-active');
                         if(active) active.classList.remove('whois-result-tab-active');
                         tab.classList.add('whois-result-tab-active');
                         result.textContent = response.lines.join("\r\n").trim();
diff --git a/assets/makai.js/utility.js b/assets/makai.js/utility.js
deleted file mode 100644
index b4e9c8d..0000000
--- a/assets/makai.js/utility.js
+++ /dev/null
@@ -1,65 +0,0 @@
-const $i = document.getElementById.bind(document);
-const $c = document.getElementsByClassName.bind(document);
-const $q = document.querySelector.bind(document);
-const $qa = document.querySelectorAll.bind(document);
-
-const $r = function(element) {
-    if(element && element.parentNode)
-        element.parentNode.removeChild(element);
-};
-
-const $ri = function(name) {
-    $r($i(name));
-};
-
-const $ib = function(ref, elem) {
-    ref.parentNode.insertBefore(elem, ref);
-};
-
-const $rc = function(element) {
-    while(element.lastChild)
-        element.removeChild(element.lastChild);
-};
-
-const $rp = function(target, replace) {
-    $ib(target, replace);
-    $r(target);
-};
-
-const $ar = function(array, index) {
-    array.splice(index, 1);
-};
-const $ari = function(array, item) {
-    let index;
-    while(array.length > 0 && (index = array.indexOf(item)) >= 0)
-        $ar(array, index);
-};
-const $arf = function(array, predicate) {
-    let index;
-    while(array.length > 0 && (index = array.findIndex(predicate)) >= 0)
-        $ar(array, index);
-};
-
-const $as = function(array) {
-    if(array.length < 2)
-        return;
-
-    for(let i = array.length - 1; i > 0; --i) {
-        let j = Math.floor(Math.random() * (i + 1)),
-            tmp = array[i];
-        array[i] = array[j];
-        array[j] = tmp;
-    }
-};
-
-const $csrfp = (function() {
-    const elem = $q('meta[name="csrfp-token"]');
-
-    return {
-        get: () => elem?.content ?? '',
-        set: token => {
-            if(elem != null)
-                elem.content = token?.toString() ?? '';
-        },
-    };
-})();
diff --git a/assets/makai.js/xhr.js b/assets/makai.js/xhr.js
index c3bb292..1fd5566 100644
--- a/assets/makai.js/xhr.js
+++ b/assets/makai.js/xhr.js
@@ -1,15 +1,24 @@
-const $x = (function() {
+#include csrf.js
+
+const $xhr = (function() {
     const send = function(method, url, options, body) {
-        options ??= {};
+        if(options === undefined)
+            options = {};
+        else if(typeof options !== 'object')
+            throw 'options must be undefined or an object';
+
+        Object.freeze(options);
 
         const xhr = new XMLHttpRequest;
-        const requestHeadersRaw = options?.headers ?? {};
         const requestHeaders = new Map;
 
-        if(typeof requestHeadersRaw === 'object')
-            for(const name in requestHeadersRaw)
-                if(requestHeadersRaw.hasOwnProperty(name))
-                    requestHeaders.set(name.toLowerCase(), requestHeadersRaw[name]);
+        if('headers' in options && typeof options.headers === 'object')
+            for(const name in options.headers)
+                if(options.headers.hasOwnProperty(name))
+                    requestHeaders.set(name.toLowerCase(), options.headers[name]);
+
+        if(options.csrf)
+            requestHeaders.set('x-csrf-token', $csrf.token);
 
         if(typeof options.download === 'function') {
             xhr.onloadstart = ev => options.download(ev);
@@ -29,6 +38,9 @@ const $x = (function() {
         if(typeof options.timeout === 'number')
             xhr.timeout = options.timeout;
 
+        if(typeof options.type === 'string')
+            xhr.responseType = options.type;
+
         if(typeof options.abort === 'function')
             options.abort(() => xhr.abort());
 
@@ -39,7 +51,7 @@ const $x = (function() {
             if(body instanceof URLSearchParams) {
                 requestHeaders.set('content-type', 'application/x-www-form-urlencoded');
             } else if(body instanceof FormData) {
-                requestHeaders.set('content-type', 'multipart/form-data');
+                // content-type is implicitly set
             } else if(body instanceof Blob || body instanceof ArrayBuffer || body instanceof DataView) {
                 if(!requestHeaders.has('content-type'))
                     requestHeaders.set('content-type', 'application/octet-stream');
@@ -54,30 +66,33 @@ const $x = (function() {
         }
 
         return new Promise((resolve, reject) => {
-            let responseHeaders = undefined;
+            xhr.onload = ev => {
+                const headers = (headersString => {
+                    const headers = new Map;
 
-            xhr.onload = ev => resolve({
-                status: xhr.status,
-                body: () => xhr.responseText,
-                json: () => JSON.parse(xhr.responseText),
-                headers: () => {
-                    if(responseHeaders !== undefined)
-                        return responseHeaders;
-
-                    responseHeaders = new Map;
-
-                    const raw = xhr.getAllResponseHeaders().trim().split(/[\r\n]+/);
+                    const raw = headersString.trim().split(/[\r\n]+/);
                     for(const name in raw)
                         if(raw.hasOwnProperty(name)) {
                             const parts = raw[name].split(': ');
-                            responseHeaders.set(parts.shift(), parts.join(': '));
+                            headers.set(parts.shift(), parts.join(': '));
                         }
 
-                    return responseHeaders;
-                },
-                xhr: xhr,
-                ev: ev,
-            });
+                    return headers;
+                })(xhr.getAllResponseHeaders());
+
+                if(headers.has('x-csrf-token'))
+                    $csrf.token = headers.get('x-csrf-token');
+
+                resolve({
+                    get ev() { return ev; },
+                    get xhr() { return xhr; },
+
+                    get status() { return xhr.status; },
+                    get headers() { return headers; },
+                    get body() { return xhr.response; },
+                    get text() { return xhr.responseText; },
+                });
+            };
 
             xhr.onerror = ev => reject({
                 xhr: xhr,
diff --git a/assets/utils.js b/assets/utils.js
deleted file mode 100644
index d825d83..0000000
--- a/assets/utils.js
+++ /dev/null
@@ -1,35 +0,0 @@
-const crypto = require('crypto');
-
-exports.strtr = (str, replacements) => str.toString().replace(
-    /{([^}]+)}/g, (match, key) => replacements[key] || match
-);
-
-const trim = function(str, chars, flags) {
-    if(chars === undefined)
-        chars = " \n\r\t\v\0";
-
-    let start = 0,
-        end = str.length;
-
-    if(flags & 0x01)
-        while(start < end && chars.indexOf(str[start]) >= 0)
-            ++start;
-
-    if(flags & 0x02)
-        while(end > start && chars.indexOf(str[end - 1]) >= 0)
-            --end;
-
-    return (start > 0 || end < str.length)
-        ? str.substring(start, end)
-        : str;
-};
-
-exports.trimStart = (str, chars) => trim(str, chars, 0x01);
-exports.trimEnd = (str, chars) => trim(str, chars, 0x02);
-exports.trim = (str, chars) => trim(str, chars, 0x03);
-
-exports.shortHash = function(text) {
-    const hash = crypto.createHash('sha256');
-    hash.update(text);
-    return hash.digest('hex').substring(0, 8);
-};
diff --git a/build.js b/build.js
index 06a9b6c..afc83ae 100644
--- a/build.js
+++ b/build.js
@@ -11,7 +11,9 @@ const fs = require('fs');
         public: pathJoin(__dirname, 'public'),
         debug: isDebug,
         swc: {
-            es: 'es2020',
+            es: 'es2021',
+            jsx: '$element',
+            jsxf: '$fragment',
         },
     };
 
diff --git a/src/DeveloperRoutes.php b/src/DeveloperRoutes.php
index a8f8435..2be55b5 100644
--- a/src/DeveloperRoutes.php
+++ b/src/DeveloperRoutes.php
@@ -1,6 +1,7 @@
 <?php
 namespace Makai;
 
+use Index\Http\HttpResponseBuilder;
 use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
 use Index\Http\Routing\Routes\ExactRoute;
 use Index\Templating\TplEnvironment;
@@ -17,7 +18,7 @@ class DeveloperRoutes implements RouteHandler {
     ) {}
 
     #[ExactRoute('GET', '/')]
-    public function getIndex($response, $request) {
+    public function getIndex() {
         $projectInfos = $this->projects->getProjects(
             featuredOnly: true,
             deleted: false,
@@ -54,12 +55,12 @@ class DeveloperRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/tools')]
-    public function temporaryRedirect($response): void {
+    public function temporaryRedirect(HttpResponseBuilder $response): void {
         $response->redirect('/');
     }
 
     #[ExactRoute('GET', '/contact')]
-    public function permanentRedirect($response): void {
+    public function permanentRedirect(HttpResponseBuilder $response): void {
         $response->redirect('/', true);
     }
 }
diff --git a/src/SSHKeys/SSHKeysRoutes.php b/src/SSHKeys/SSHKeysRoutes.php
index 1e576cb..6e47afe 100644
--- a/src/SSHKeys/SSHKeysRoutes.php
+++ b/src/SSHKeys/SSHKeysRoutes.php
@@ -2,6 +2,7 @@
 namespace Makai\SSHKeys;
 
 use DateTimeInterface;
+use Index\Http\{HttpRequest,HttpResponseBuilder};
 use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
 use Index\Http\Routing\Routes\ExactRoute;
 
@@ -13,7 +14,7 @@ class SSHKeysRoutes implements RouteHandler {
     ) {}
 
     #[ExactRoute('GET', '/ssh_keys')]
-    public function getSshKeys($response, $request): array|string {
+    public function getSshKeys(HttpResponseBuilder $response, HttpRequest $request): array|string {
         $minLevel = (int)$request->getParam('l', FILTER_SANITIZE_NUMBER_INT);
         $includeComment = $request->hasParam('c');
         $json = $request->hasParam('j');
@@ -47,7 +48,7 @@ class SSHKeysRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/authorized_keys')]
-    public function getAuthorisedKeys($response, $request): string {
+    public function getAuthorisedKeys(HttpResponseBuilder $response): string {
         $response->setTypePlain();
         $body = '';
 
@@ -59,7 +60,7 @@ class SSHKeysRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/git_keys_ro')]
-    public function getGitKeysReadOnly($response, $request): string {
+    public function getGitKeysReadOnly(HttpResponseBuilder $response): string {
         $response->setTypePlain();
         $body = '';
 
@@ -71,7 +72,7 @@ class SSHKeysRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/git_keys_rw')]
-    public function getGitKeysReadWrite($response, $request): string {
+    public function getGitKeysReadWrite(HttpResponseBuilder $response): string {
         $response->setTypePlain();
         $body = '';
 
@@ -83,7 +84,7 @@ class SSHKeysRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/ssh.php')]
-    public function getSshPhp($response, $request): void {
+    public function getSshPhp(HttpResponseBuilder $response, HttpRequest $request): void {
         $query = [];
 
         $minLevel = (int)$request->getParam('l', FILTER_SANITIZE_NUMBER_INT);
diff --git a/src/Tools/Ascii/AsciiRoutes.php b/src/Tools/Ascii/AsciiRoutes.php
index 5ef9d56..25df852 100644
--- a/src/Tools/Ascii/AsciiRoutes.php
+++ b/src/Tools/Ascii/AsciiRoutes.php
@@ -1,6 +1,7 @@
 <?php
 namespace Makai\Tools\Ascii;
 
+use Index\Http\HttpResponseBuilder;
 use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
 use Index\Http\Routing\Routes\ExactRoute;
 use Index\Templating\TplEnvironment;
@@ -13,7 +14,7 @@ class AsciiRoutes implements RouteHandler {
     ) {}
 
     #[ExactRoute('GET', '/tools/ascii')]
-    public function getAsciiTable($response, $request): string {
+    public function getAsciiTable(HttpResponseBuilder $response): string {
         return $this->templating->render('tools/ascii/index', [
             'chars' => AsciiCharacter::all(),
         ]);
@@ -21,7 +22,7 @@ class AsciiRoutes implements RouteHandler {
 
     #[ExactRoute('GET', '/ascii')]
     #[ExactRoute('GET', '/ascii.php')]
-    public function getAsciiPHP($response, $request): void {
+    public function getAsciiPHP(HttpResponseBuilder $response): void {
         $response->redirect('/tools/ascii', true);
     }
 }
diff --git a/src/Tools/ToolsRoutes.php b/src/Tools/ToolsRoutes.php
index ac80da7..186599d 100644
--- a/src/Tools/ToolsRoutes.php
+++ b/src/Tools/ToolsRoutes.php
@@ -2,6 +2,7 @@
 namespace Makai\Tools;
 
 use Index\XString;
+use Index\Http\{HttpRequest,HttpResponseBuilder};
 use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
 use Index\Http\Routing\Routes\ExactRoute;
 
@@ -9,7 +10,7 @@ class ToolsRoutes implements RouteHandler {
     use RouteHandlerCommon;
 
     #[ExactRoute('GET', '/tools/random')]
-    public function getRandomString($response, $request): string {
+    public function getRandomString(HttpResponseBuilder $response, HttpRequest $request): string {
         $response->setTypePlain();
 
         $length = (int)$request->getParam('length', FILTER_SANITIZE_NUMBER_INT);
@@ -27,7 +28,7 @@ class ToolsRoutes implements RouteHandler {
 
     #[ExactRoute('GET', '/tools/hajime-hash')]
     #[ExactRoute('GET', '/tools/hidoi-hash')]
-    public function getHajimeHash($response, $request): string {
+    public function getHajimeHash(HttpResponseBuilder $response, HttpRequest $request): string {
         $response->setTypePlain();
         if(!$request->hasParam('text'))
             return 'text param is empty';
@@ -37,7 +38,7 @@ class ToolsRoutes implements RouteHandler {
 
     #[ExactRoute('GET', '/key.php')]
     #[ExactRoute('GET', '/rngstr')]
-    public function getRedirect($response, $request): void {
+    public function getRedirect(HttpResponseBuilder $response, HttpRequest $request): void {
         $url = '/tools/random';
 
         $length = (int)$request->getParam('length', FILTER_SANITIZE_NUMBER_INT);
diff --git a/src/Tools/Whois/WhoisRoutes.php b/src/Tools/Whois/WhoisRoutes.php
index e421749..f4768c1 100644
--- a/src/Tools/Whois/WhoisRoutes.php
+++ b/src/Tools/Whois/WhoisRoutes.php
@@ -4,7 +4,10 @@ namespace Makai\Tools\Whois;
 use Exception;
 use Memcached;
 use Index\Cache\CacheBackends;
+use Index\Http\HttpResponseBuilder;
+use Index\Http\Content\FormContent;
 use Index\Http\Routing\{RouteHandler,RouteHandlerCommon};
+use Index\Http\Routing\Processors\Before;
 use Index\Http\Routing\Routes\ExactRoute;
 use Index\Templating\TplEnvironment;
 use Makai\CSRFPContainer;
@@ -23,17 +26,13 @@ class WhoisRoutes implements RouteHandler {
     }
 
     #[ExactRoute('GET', '/whois')]
-    public function getWhoisPHP($response, $request): void {
+    public function getWhoisPHP(HttpResponseBuilder $response): void {
         $response->redirect('/tools/whois', true);
     }
 
     #[ExactRoute('POST', '/tools/whois/lookup')]
-    public function postLookup($response, $request) {
-        if(!$request->isFormContent())
-            return 400;
-
-        $content = $request->getContent();
-
+    #[Before('input:urlencoded')]
+    public function postLookup(HttpResponseBuilder $response, FormContent $content) {
         if(!$this->csrfp->verifyToken((string)$content->getParam('_csrfp')))
             return [
                 'error' => true,
diff --git a/templates/html.twig b/templates/html.twig
index fa47c41..2279c2b 100644
--- a/templates/html.twig
+++ b/templates/html.twig
@@ -4,7 +4,7 @@
         <meta charset="{{ master_charset|default('utf-8') }}">
         {% if master_title is defined and master_title is not empty %}<title>{{ master_title }}</title>{% endif %}
         {% block master_head %}{% endblock %}
-        {% if csrfp_available() %}<meta name="csrfp-token" content="{{ csrfp_token() }}">{% endif %}
+        {% if csrfp_available() %}<meta name="csrf-token" content="{{ csrfp_token() }}">{% endif %}
     </head>
     <body>
         {% block master_body %}{% endblock %}