@anywhichway/lazui
Version:
Single page apps and lazy loading sites with minimal JavaScript or client build processes.
225 lines (218 loc) • 11.9 kB
JavaScript
import {examplify} from "./examplify.js";
async function userouter({attribute,lazui,options}) {
lazui.useRouter = useRouter;
const {prefix,JSON,url} = lazui,
el = attribute.ownerElement,
{importName="default",isClass,allowRemote,markdownProcessor} = options;
await import(attribute.value).then(async (module) => {
const Router = module[importName],
router = isClass ? new Router(options.options) : Router(options.options),
parts = prefix.split("-"),
lazuiProtocol = (parts[0]==="data" ? parts[1] : parts[0]) + ":",
host = url.href;
let markdown = markdownProcessor ? (await import(markdownProcessor.src))[markdownProcessor.importName||"default"] : undefined;
if(markdown && markdownProcessor.isClass) {
const instance = new markdown(markdownProcessor.options);
markdown = instance[markdownProcessor.call].bind(instance);
} else if(markdown && markdownProcessor.call) {
markdown = markdown[markdownProcessor.call].bind(markdown);
}
lazui.useRouter(router, {allowRemote,prefix,JSON,markdown,lazuiProtocol,host});
})
}
function useRouter(router,{prefix,lazuiProtocol,host,markdown,JSON = globalThis.JSON,root = document.documentElement,allowRemote,all=(req) => {
if(req.mode==="document") {
return new Response("Not Found",{status:404})
}
if(req.URL.pathname.endsWith(".md")) {
req.headers.set("Accept-Include","true");
}
//return globalThis.fetch(req);
}}={}) {
// (req,resp)
// (req,resp,next)
// (ctx,next)
// must support router.get("*",handler) and router.all("*",...) and return of a Response object (both Hono and Itty do)
router.all("*", async (arg1,arg2,arg3) => {
let c = arg1.req ? arg1 : {req:arg1},
next = typeof arg2 === "function" ? arg2 : typeof arg3 === "function" ? arg3 : null;
const url = c.req.URL = new URL(c.req.url, document.baseURI),
method = c.req.method.toLowerCase(),
isLocal = c.req.url.startsWith(document.location.origin);
let mode = c.req.mode || c.req.raw.mode;
if(url.href.replace(url.hash,"")===document.location.href.replace(document.location.hash,"")) {
const el = document.getElementById(url.hash.slice(1));
if(el) return new Response(el.innerHTML,{headers:{"content-type":"text/html"}});
}
if(isLocal || allowRemote) {
let node;
if (isLocal) {
const nodes = root.querySelectorAll(`template[${prefix}\\:url\\:${method}$="${url.pathname.split("/").pop()}"]`);
for(const candidate of nodes) {
const candidateURL = new URL(candidate.getAttribute(`${prefix}:url:${method}`),window.location);
if(candidateURL.href===url.href) {
node = candidate;
break;
}
}
} else if (allowRemote && /^(http|https):/i.test(c.req.url)) {
node = root.querySelector(`template[${prefix}\\:url\\:${method}="${c.req.url}"]`);
}
if(node) {
if(node.hasAttribute(`${prefix}:options`)) {
const options = JSON.parse(node.getAttribute(`${prefix}:options`));
if(options?.url?.handlers) {
const handlers = globalThis[options?.url?.handlers] || (await import(new URL(options.url.handlers,window.location).href)).default;
["get","post","put","delete","head","patch","options"].forEach((key) => {
if(typeof handlers[key] === "function") node[key] = handlers[key];
})
}
}
if(typeof node[method] === "function") {
const resp = await node[method](c.req.raw);
if(resp) return resp;
}
if(node.hasAttribute(`${prefix}:mode`)) mode = node.getAttribute(`${prefix}:mode`); // overwrites
const status = node.getAttribute(`${prefix}:status`) || 200,
headers = {"content-type": node.getAttribute(`${prefix}:content-type`) || "text/plain"};
for (const attr of [...node.attributes]) {
if (attr.name.startsWith(`${prefix}:header-`)) headers[attr.name.substring(12)] = attr.value;
else if (attr.name === `${prefix}:headers`) Object.assign(headers, JSON.parse(attr.value))
}
if (method === "post" || method === "put") {
let response;
if(mode!=="document") {
try {
response = await fetch(c.req.raw);
} catch {
}
}
let target;
const targets = root.querySelectorAll(`[${prefix}\\:url\\:get="${url.pathname.split("/").pop()}"]`);
for(const candidate of targets) {
const candidateURL = new URL(candidate.getAttribute(`${prefix}:url:get`),window.location);
if(candidateURL.href===url.href) {
target = candidate;
break;
}
}
if(!target) {
target = document.createElement("template");
target.setAttribute(`${prefix}:url:get`,c.req.url);
node.after(target);
}
target.setAttribute(`${prefix}:content-type`,response?.status===200 ? response.headers.get("content-type") : c.req.headers.get("content-type")||"text/plain");
target.setAttribute(`${prefix}:status`,response?.status===200 ? response.status : 200);
target.innerHTML = response?.status===200 ? await response.text() : await c.req.text();
if(node.innerHTML.trim()) return new Response(node.innerHTML,{status:status||200,headers})
return new Response(target.innerHTML,{status:200, headers:response?.status===200 ? response.status.headers : headers}); // should return contents of the POST or put element
} else if (method === "get") {
if(url.pathname.endsWith(".md") && !node.hasAttribute(`${prefix}:content-type`)) {
node.setAttribute(`${prefix}:content-type`,"text/markdown");
}
let response;
if(mode!=="document") {
try {
response = await fetch(c.req.raw);
} catch {
}
}
let value = response?.status===200 ? await response.text() : node.innerHTML;
if(value.length!==0) {
const status = response?.status===200 ? response.status : 200;
node.setAttribute(`${prefix}:status`,status);
if(response) {
node.setAttribute(`${prefix}:content-type`,response.headers.get("content-type"));
for(const [key,value] of response.headers.entries()) {
node.setAttribute(`${prefix}:header-${key}`,value);
}
}
if(node.getAttribute(`${prefix}:content-type`)==="text/markdown") {
value = markdown(examplify(value.trim()));
}
return new Response(value,{status, headers:response?.status===200 ? response.status.headers : headers})
}
} else if(method === "delete") {
let response;
if(mode!=="document") {
try {
response = await fetch(c.req.raw);
} catch {
}
}
node.innerHTML = "";
node.setAttribute(`${prefix}:status`,"404");
return new Response("ok", {status, headers});
}
} else if(url.protocol===lazuiProtocol) {
const pathname = url.pathname.slice(1),
parts = pathname.split("/");
parts.shift();
if(parts[0]==="localStorage" || parts[0]==="sessionStorage" || parts[0]==="indexedDB") {
const storage = globalThis[parts[0]];
if(parts[1]==="!clear") {
storage.clear();
return new Response("ok",{status:200});
} else if(method==="get") {
const value = storage.getItem(parts[1]);
if(value) return new Response(value,{status:200});
return new Response("Not Found",{status:404});
} else if(method==="post" || method==="put") {
const text = await c.req.text();
storage.setItem(parts[1],text);
return new Response(text,{status:200});
} else if(method==="delete") {
storage.removeItem(parts[1]);
return new Response("ok",{status:200});
} else if(method==="patch") {
const toPatch = storage.getItem(parts[1]);
if(!toPatch) return new Response("Not Found",{status:404});
if(c.req.headers.get("content-type")==="application/json") {
const text = await c.req.text();
let json;
try {
json = JSON.parse(text);
} catch(e) {
return new Response(`Bad Request: malformed JSON ${e}`,{status:400});
}
Object.assign(toPatch,JSON.parse(text));
return new Response(JSON.stringify(toPatch),{status:200});
} else {
const text = await c.req.text();
storage.setItem(parts[1],text);
return new Response(text,{status:200});
}
}
} else if(parts[0]==="indexedDB") {
} else {
const src = host + pathname;
return window.fetch(src);
}
}
}
if(mode==="document") {
return new Response("Not Found",{status: 404});
}
if(next) await next();
});
if (all) router.all("*", all);
const fetch = router.fetch.bind(router);
router.fetch = async (request) => {
if(typeof request === "string") {
if(request.startsWith("{")) {
const json = JSON.parse(request);
const mode = json.mode;
if(mode==="document") delete json.mode;
request = new Request(json.url,json);
if(mode==="document") {
Object.defineProperty(request,"mode",{value:mode});
}
} else {
request = new Request(request);
}
}
return fetch(request)
}
return this.router = router;
}
export {userouter,useRouter};