diff options
Diffstat (limited to 'src')
54 files changed, 1737 insertions, 278 deletions
diff --git a/src/_data/env.js b/src/_data/env.js deleted file mode 100644 index a497313..0000000 --- a/src/_data/env.js +++ /dev/null @@ -1,3 +0,0 @@ -const process = require("process"); - -module.exports = () => process.env;
\ No newline at end of file diff --git a/src/_data/metadata.js b/src/_data/metadata.js deleted file mode 100644 index 7f07bb6..0000000 --- a/src/_data/metadata.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - title: "Sudomsg", - url: "https://sudomsg.xyz/", - language: "en-GB", - theme: "#8b0000", - description: "Messages from root", - feed: { - atom: "/feed.xml", - json: "/feed.json" - }, - author: { - name: "Marc Pervaz Boocha", - email: "mboocha@sudomsg.xyz", - github: "https://github.com/marcthe12", - linkedin: "https://www.linkedin.com/in/marc-pervaz-boocha-200706236/", - image: "/favicon/512.png", - url: "/about/#marc-pervaz-boocha" - } -}; diff --git a/src/_includes/base.njk b/src/_includes/base.njk deleted file mode 100644 index 19cf0d8..0000000 --- a/src/_includes/base.njk +++ /dev/null @@ -1,52 +0,0 @@ ---- -nav: - Home: "/" - Blog: "/blog" - About: "/about" - Git: "/cgit" ---- -<!DOCTYPE html> -<html lang="{{ metadata.language }}"> - <head> - <meta charset="utf-8"> - <meta property="og:locale content=en_GB"> - <title property="og:title">{{ title or metadata.title }}</title> - <meta property="og:site_name" content="{{ metadata.title }}"> - <meta name="author" content="{{ metadata.author.name }}"> - <meta name="description" property="og:description content="{{ description or data.metadata }}"> - <meta property="og:type" content="website"> - {% if keywords %} - <meta name="keywords" contents="{{ keywords | join }}"> - {% endif %} - <meta name="viewport" content="width=device-width, initial-scale=1"> - <meta name="twitter:card" content="summary"> - <meta property=og:url content="{{ page.url | absoluteUrl(metadata.url)}}"> - <link rel="canonical" href="{{ page.url | absoluteUrl(metadata.url)}}"> - <link rel="alternate" href="{{ metadata.feed.atom }}" type="application/atom+xml" title="{{ metadata.title }}"> - <link rel="alternate" href="{{ metadata.feed.json }}" type="application/feed+json" title="{{ metadata.title }}"> - <meta property="og:image" content="/favicon/1024.png"> - <link rel="icon" href="/favicon.ico" sizes="any"> - <link rel="icon" href="/favicon/icon.svg" type="image/svg+xml"> - <link rel="apple-touch-icon" href="/favicon/192.png"> - <link rel="manifest" href="/app.webmanifest"> - <link rel="stylesheet" href="/assets/index.css"> - <link rel="stylesheet" href="/vendor/prism.css"> - <script type="module" src="/assets/index.js" defer></script> - </head> - <body> - <header> - <nav> - <a href="#" id="nav-toogle">Sudomsg</a> - {% for name, location in nav %} - <a class="navlinks" href="{{location}}" >{{name}}</a> - {% endfor %} - </nav> - </header> - <main> - {{ content | safe }} - </main> - <footer> - Subscribe: <a href="{{metadata.feed.atom}}">RSS</a> <a href="{{ metadata.feed.json }}">JSON</a> - </footer> - </body> -</html> diff --git a/src/_includes/page.njk b/src/_includes/page.njk deleted file mode 100644 index 263e09c..0000000 --- a/src/_includes/page.njk +++ /dev/null @@ -1,7 +0,0 @@ ---- -layout: base ---- -<article> - <h1 id="{{title | slug }}">{{title}}</h1> - {{content|safe}} -</article> diff --git a/src/_includes/post.njk b/src/_includes/post.njk deleted file mode 100644 index f613809..0000000 --- a/src/_includes/post.njk +++ /dev/null @@ -1,9 +0,0 @@ ---- -layout: page -tags: - - posts ---- -<small> - <time datetime="{{page.date | htmlDateString }}">{{ page.date | readableDate}}</time> - <a rel=author href="{{ metadata.author.url }}">{{ metadata.author.name }}</a> -</small> -{{content|safe}} diff --git a/src/about.njk b/src/about.njk deleted file mode 100644 index ee867fd..0000000 --- a/src/about.njk +++ /dev/null @@ -1,14 +0,0 @@ ---- -layout: page -title: About Me ---- - -<h2>{{ metadata.author.name }}</h2> -<img alt="A Photo of me" src="{{ metadata.author.image}}" class="side"> -<p>I am an analytical and passionate second year CHRIST University student pursuing B. Tech in Computer Engineering in Bengaluru. I am eager to further my knowledge, develop my skills and gain experience to convert my interest in computers into a fulfilling career. -<p>Contact Details -<ul> -<li><a href="mailto:{{ metadata.author.email }}">Email</a> -<li><a href="{{ metadata.author.github}}">Github</a> -<li><a href="{{ metadata.author.linkedin}}">LinkedIn</a> -</ol> diff --git a/src/blog.njk b/src/blog.njk deleted file mode 100644 index 9998e70..0000000 --- a/src/blog.njk +++ /dev/null @@ -1,17 +0,0 @@ ---- -layout: base -title: Blog ---- -<div role="feed" aria-busy="false"> -{% for post in collections.posts %} - <article aria-posinset="{{loop.index}}" aria-setsize="{{loop.len}}"> - <h1> - <a href="{{post.url}}">{{post.data.title}}</a> - </h1> - <small> - <time datetime="{{page.date.toISOString()}}">{{page.date.toDateString()}}</time> - <a rel=author href="{{metadata.author.url}}">{{ metadata.author.name }}</a> - </small> - <p>{{post.data.description}}</p> - </article> -{% endfor %} -</div> diff --git a/src/client/index.ts b/src/client/index.ts new file mode 100644 index 0000000..cee201b --- /dev/null +++ b/src/client/index.ts @@ -0,0 +1,20 @@ +window.addEventListener("load", async function () { + try { + const navlinks : HTMLAnchorElement[] = Array.from(document.getElementsByClassName("navlinks")) as HTMLAnchorElement[] + + document.getElementById("nav-toogle")?.addEventListener("click", async () => { + navlinks.forEach(navlink => { navlink.style.display === "hidden" ? "block" : "hidden" }) + return false + }) + + Array.from(document.getElementsByTagName("time")).forEach(time => { + time.textContent = new Date(time.dateTime).toLocaleDateString(undefined,{dateStyle:"full"}) + }) + + if ("serviceWorker" in navigator) { + await navigator.serviceWorker.register("/sw.js", { type: "module" }) + } + } catch (error) { + console.error(error) + } +}) diff --git a/src/gen/atom.njk b/src/gen/atom.njk deleted file mode 100644 index a603195..0000000 --- a/src/gen/atom.njk +++ /dev/null @@ -1,27 +0,0 @@ ---- -permalink: "/feed.xml" ---- -<?xml version="1.0" encoding="utf-8"?> -<feed xmlns="http://www.w3.org/2005/Atom"> - <title>{{ metadata.title }}</title> - <subtitle>${data.metadata.description}</subtitle> - {% set absoluteUrl %}{{ metadata.feed.path | url | absoluteUrl(metadata.url) }}{% endset %} - <link href="{{ metadata.feed.path | absoluteUrl(metadata.url) }}" rel="self"/> - <link href="{{ metadata.url }}"/> - <updated>{{ collections.posts | getNewestCollectionItemDate | dateToRfc3339 }}</updated> - <id>{{ metadata.feed.id }}</id> - <author> - <name>{{ metadata.author.name }}</name> - <email>{{ metadata.author.email }}</email> - </author> - {% for post in collections.posts | reverse %} - {% set absolutePostUrl %}{{ post.url | absoluteUrl(metadata.url) }}{% endset %} - <entry> - <title>{{ post.data.title }}</title> - <link href="{{ absolutePostUrl }}"/> - <updated>{{ post.date | dateToRfc3339 }}</updated> - <id>{{ absolutePostUrl }}</id> - <content type="html">{{ post.templateContent | htmlToAbsoluteUrls(absolutePostUrl) }}</content> - </entry> - {% endfor %} -</feed> diff --git a/src/gen/error.njk b/src/gen/error.njk deleted file mode 100644 index dc5599c..0000000 --- a/src/gen/error.njk +++ /dev/null @@ -1,13 +0,0 @@ ---- -layout: base -pagination: - data: err - size: 1 -err: - offline: The Page is offline - 404: Not Found -permalink: "/{{pagination.items}}.html" ---- - -<h1 style="color: rgb(139 0 0)">ERROR: {{pagination.items}}</h1> -<p>{{err[pagination.items]}}</p> diff --git a/src/gen/feedjson.11ty.js b/src/gen/feedjson.11ty.js deleted file mode 100644 index 7ddbf51..0000000 --- a/src/gen/feedjson.11ty.js +++ /dev/null @@ -1,35 +0,0 @@ -module.exports = class { - data() { - return { - permalink: "/feed.json" - }; - } - - async render(data) { - const out = { - version: "https://jsonfeed.org/version/1.1", - title: data.metadata.title, - language: data.metadata.language, - home_page_url: data.metadata.url, - feed_url: data.page.url, - description: data.metadata.description, - author: { - name: data.metadata.author.name, - url: data.metadata.author.url - }, - items: (data.collections.posts || []).map( - async function (e) { - const absolutePostUrl = this.absoluteUrl(this.url(e.url), data.metadata.url) - return { - id: absolutePostUrl, - url: absolutePostUrl, - title: e.data.title, - date_published: this.dateToRfc3339(e.date), - content_html: htmlToAbsoluteUrls(e.templateContent, absolutePostUrl), - } - } - ) - } - return JSON.stringify(out) - } -} diff --git a/src/gen/gen.11tydata.js b/src/gen/gen.11tydata.js deleted file mode 100644 index 5694649..0000000 --- a/src/gen/gen.11tydata.js +++ /dev/null @@ -1,9 +0,0 @@ -const path = require("path"); - -module.exports = { - eleventyExcludeFromCollections: true, - permalink: false, - eleventyComputed: { - //permalink: data => data.permalink || `/${path.relative("/gen", data.page.filePathStem)}.${data.page.outputFileExtension}` - } -} diff --git a/src/gen/manifest.11ty.js b/src/gen/manifest.11ty.js deleted file mode 100644 index e41df83..0000000 --- a/src/gen/manifest.11ty.js +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = class { - data() { - return { - permalink: "/app.webmanifest" - }; - } - - render(data) { - return JSON.stringify({ - $schema: "https://json.schemastore.org/web-manifest-combined.json", - name: data.metadata.title, - lang: data.metadata.language, - start_url: "/", - id: "/", - scope: "/", - display: "minimal-ui", - background_color: data.metadata.theme, - theme_color: data.metadata.theme, - description: data.metadata.description, - icons: [192, 512, 1024].map(size => ({ - src: `/favicon/${size}.png`, - type: "image/png", - sizes: `${size}x${size}`, - purpose: "maskable" - })) - }); - } -};
\ No newline at end of file diff --git a/src/gen/metadata.11ty.js b/src/gen/metadata.11ty.js deleted file mode 100644 index 56bf6af..0000000 --- a/src/gen/metadata.11ty.js +++ /dev/null @@ -1,13 +0,0 @@ -module.exports = class { - data() { - return { - //permalink: "/assets/metadata.js" - permalink: false - }; - } - - render() { - const date = new Date() - return `export default = ${date.toISOString()}` - } -}; diff --git a/src/gen/robot.njk b/src/gen/robot.njk deleted file mode 100644 index faf9e2f..0000000 --- a/src/gen/robot.njk +++ /dev/null @@ -1,6 +0,0 @@ ---- -permalink: /robot.txt ---- -User-agent: * -Disallow: -Sitemap: {{ "/sitemap.xml" | absoluteUrl(data.metadata.url)}} diff --git a/src/gen/sitemap.njk b/src/gen/sitemap.njk deleted file mode 100644 index c2030de..0000000 --- a/src/gen/sitemap.njk +++ /dev/null @@ -1,12 +0,0 @@ ---- -permalink: "/sitemap.xml" ---- -<?xml version="1.0" encoding="utf-8"?> -<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"> -{% for page in collections.all %} - <url> - <loc>{{ page.url | absoluteUrl(metadata.url) }}</loc> - <lastmod>{{ page.date | htmlDateString }}</lastmod> - </url> -{% endfor %} -</urlset> diff --git a/src/index.njk b/src/index.njk deleted file mode 100644 index a56bc07..0000000 --- a/src/index.njk +++ /dev/null @@ -1,6 +0,0 @@ ---- -layout: page -title: Welcome ---- - -<p> Hello diff --git a/src/post/post.11tydata.js b/src/post/post.11tydata.js deleted file mode 100644 index 6ae489a..0000000 --- a/src/post/post.11tydata.js +++ /dev/null @@ -1,8 +0,0 @@ -const slugify = require("@sindresorhus/slugify"); - -module.exports = () => ({ - layout: "post", - eleventyComputed: { - permalink: data => `/posts/${slugify(data.title)}/` - } -}); diff --git a/src/server/app.ts b/src/server/app.ts new file mode 100644 index 0000000..40183e9 --- /dev/null +++ b/src/server/app.ts @@ -0,0 +1,31 @@ +import express from "express" +import relDir from "./utils/relDir.js" +import { errHandler, notFound, offline } from "./errHanadler.js" + +const dir = relDir(import.meta.url) + +await import("./build.js") +export const app = express() + +app.set("views", false) +app.set("etag", "strong") +app.set("x-powered-by", false) +app.set("trust proxy", true) + +app.use((await import("morgan")).default(":remote-addr :method :url :http-version :status :response-time ms")) +app.use((await import("./router.js")).default) +app.use("/offline", offline) +app.use((await import("./img.js")).default(dir("../../assets"))) +app.use(express.static(dir("../../assets"), { index: false })) +app.use(express.static(dir("../client"), { index: false })) +app.use(express.static(dir("../worker"), { index: false })) +app.use((await import("./img.js")).default(dir("/static"))) +app.use(express.static(dir("/static"), { index: false })) +app.get("/favicon.ico", (_req, res) => { + res.status(204).send() +}) + +app.use(notFound) +app.use(errHandler) + +export default app diff --git a/src/server/build.ts b/src/server/build.ts new file mode 100644 index 0000000..d6ed1ab --- /dev/null +++ b/src/server/build.ts @@ -0,0 +1,31 @@ +import { posts } from "./template/Post.js" +import { pages } from "./template/Base.js" + +const postMod = [ +] + +const pageMod = [ + ...postMod, + "./content/about.js", + "./content/index.js" +] + +const routes = [ + ...pageMod, + "./content/webmanifest.js", + "./content/robots.js", + "./content/blog.js", + "./content/sitemap.js", + "./content/feed.js", +] + +function datecmp(a: { date_mod?: Date| undefined }, b: { date_mod?: Date | undefined }): number { + return (a.date_mod ?? new Date()).valueOf() - (b.date_mod ?? new Date()).valueOf() +} + +await Promise.all(postMod.map(mod => import(mod))) +posts.sort(datecmp).reverse() +await Promise.all(pageMod.map(mod => import(mod))) +pages.sort(datecmp).reverse() +await Promise.all(routes.map(mod => import(mod))) + diff --git a/src/server/content/about.ts b/src/server/content/about.ts new file mode 100644 index 0000000..4a988e3 --- /dev/null +++ b/src/server/content/about.ts @@ -0,0 +1,32 @@ +import Page from "../template/Page.js" +import metadata from "../metadata.js" +import { c } from "../template/vdom.js" +import h from "../template/header.js" +import schema from "../utils/schema.js" +import { img, picture, source, div, p, span, ul, li, a } from "../template/html.js" + +Page({ + title: "About Me", + url: "/about", + content() { + return c(div, { itemscope: true, itemtype: schema("Person") }, + c(h, { level: 2, itemprop: "name" }, metadata.author.name), + c(picture, { class: "side" }, + c(source, { srcset: [128, 256, 512].map(width => `${metadata.author.image}?format=png&width=${width} ${width}w`).join(", ") }), + c(img, { itemprop: "image", alt: "A Photo of me", src: metadata.author.image }) + ), + c(p, { itemprop: "description" }, "I am an analytical and passionate third year ", + c(span, { itemprop: "affiliation", itemtype: schema("CollegeOrUniversity"), itemscope: true }, c(span, { itemprop: "name" }, "CHRIST University")), + " ", c(span, {}, "student"), ` pursuing B. Tech in Computer Engineering in Bengaluru. I am eager to further my knowledge, develop my skills ", + "and gain experience to convert my interest in computers into a fulfilling career.`), + c(p, {}, "Contact Details"), + c(ul, {}, + c(li, {}, c(a, { href: `mailto:${metadata.author.email}`, itemprop: "email" }, "Email")), + c(li, {}, c(a, { href: "https://github.com/marcthe12", itemprop: "sameas" }, "Github")), + c(li, {}, c(a, { href: "https://www.linkedin.com/in/marc-pervaz-boocha-200706236/", itemprop: "sameas" }, "LinkedIn")), + ), + ) + } +}).setupRoute() + + diff --git a/src/server/content/blog.ts b/src/server/content/blog.ts new file mode 100644 index 0000000..373b1ca --- /dev/null +++ b/src/server/content/blog.ts @@ -0,0 +1,35 @@ +import { posts } from "../template/Post.js" +import Base from "../template/Base.js" +import { c } from "../template/vdom.js" +import h from "../template/header.js" +import relUrl from "../utils/relUrl.js" +import schema from "../utils/schema.js" +import metadata from "../metadata.js" +import { div, article, a, small, time, span, p } from "../template/html.js" + +Base({ + title: "Blog Posts", + url: "/blog", + content() { + return c(div, { itemscope: true, itemtype: schema("Blog"), role: "feed", "aria-busy": "false" }, + ...posts.map((post, index, { length }) => { + const { date_mod, date_pub, title, description, url } = post + return c(article, { itemprop: "blogPost", itemscope: true, itemtype: schema("BlogPosting"), "aria-posinset": index, "aria-setsize": length }, + c(h, { level: 1, itemprop: "headline" }, c(a, { href: relUrl(url), itemprop: "sameAs" }, title)), + c(small, {}, + date_pub != date_mod ? [ + c(time, { datetime: date_mod.toISOString(), itemprop: "dateModified" }, date_mod.toDateString()), + "- Modified " + ] : [], + c(time, { datetime: date_pub.toISOString(), itemprop: "datePublished" }, date_pub.toDateString()) + , " - ", + c(span, { itemprop: "author", itemscope: true, itemtype: schema("Person") }, + c(a, { href: metadata.author.url, rel: "author", itemprop: "url" }, + c(span, { itemprop: "name" }, metadata.author.name)) + ), + ), + c(p, { itemprop: "abstract" }, description) + ) + })) + } +}).setupRoute() diff --git a/src/server/content/feed.ts b/src/server/content/feed.ts new file mode 100644 index 0000000..b0d6c0f --- /dev/null +++ b/src/server/content/feed.ts @@ -0,0 +1,32 @@ +import curl from "../utils/curl.js" +import setStingRoute from "../utils/setStingRoute.js" +import { posts } from "../template/Post.js" +import metadata from "../metadata.js" +import { doctype } from "../template/xml.js" +import { c } from "../template/vdom.js" +import { feed, title, link, summary, id, author, subtitle, updated, name, email, entry } from "../template/atom.js" + +setStingRoute("/feed", ["application/atom+xml", "xml"], async () => doctype({}, + c(feed, { xmlns: new URL("http://www.w3.org/2005/Atom"), "xml:lang": metadata.language }, + c(title, {}, metadata.title), + c(subtitle, {}, metadata.description), + c(link, { href: curl(metadata.feed.atom), rel: "self" }), + c(link, { href: metadata.url }), + ...(posts[0] ? [c(updated, { date: posts[0].date_mod })] : []), + c(id, { id: metadata.url }), + c(author, {}, + c(name, {}, metadata.author.name), + c(email, {}, metadata.author.email), + ), + ...posts.map(({ url, date_mod, title: Title, description }) => + c(entry, {}, + c(title, {}, Title), + c(link, { href: url }), + c(updated, { date: date_mod }), + c(id, { id: url }), + c(summary, {}, description) + ) + ) + ) +)) + diff --git a/src/server/content/index.ts b/src/server/content/index.ts new file mode 100644 index 0000000..6eaa3de --- /dev/null +++ b/src/server/content/index.ts @@ -0,0 +1,20 @@ +import { c } from "../template/vdom.js" +import code from "../template/syntax.js" +import Page from "../template/Page.js" +import { pre } from "../template/html.js" + +Page({ + title: "Welcome", + url: "/", + isHighlight: true, + content() { + return c(pre, { class: "pad", }, + "$ ", c(code, { lang: "bash" }, "sudo cat ~root/msg"), + ` +Hello World! +Welcome to my blog. +I occassion leave my writings here. Hope you will enjoy them :). +$` + ) + } +}).setupRoute() diff --git a/src/server/content/robots.ts b/src/server/content/robots.ts new file mode 100644 index 0000000..ad8e7ab --- /dev/null +++ b/src/server/content/robots.ts @@ -0,0 +1,9 @@ +import curl from "../utils/curl.js" +import setStingRoute from "../utils/setStingRoute.js" + +setStingRoute("/robots.txt", "robots.txt", async () => Object.entries({ + "User-agent": "*", + Disallow: "", + Sitemap: curl("sitemap.xml") +}).map(([key, val]) => `${key}: ${val}`).join("\n") +) diff --git a/src/server/content/sitemap.ts b/src/server/content/sitemap.ts new file mode 100644 index 0000000..c9e3ef5 --- /dev/null +++ b/src/server/content/sitemap.ts @@ -0,0 +1,16 @@ +import { pages } from "../template/Base.js" +import setStingRoute from "../utils/setStingRoute.js" +import { doctype } from "../template/xml.js" +import { c } from "../template/vdom.js" +import { lastmod, loc, url, urlset } from "../template/sitemap.js" + +setStingRoute("/sitemap.xml", "sitemap.xml", async () => doctype({}, + c(urlset, { xmlns: new URL("http://www.sitemaps.org/schemas/sitemap/0.9") }, + ...pages.map(page => + c(url, {}, + c(loc, {}, (page.url?.href) ?? ""), + ...(page.date_mod ? [c(lastmod, {}, page.date_mod.toISOString())] : []) + ) + ) + ) +)) diff --git a/src/server/content/webmanifest.ts b/src/server/content/webmanifest.ts new file mode 100644 index 0000000..6e4031e --- /dev/null +++ b/src/server/content/webmanifest.ts @@ -0,0 +1,26 @@ +import metadata from "../metadata.js" +import { contentType } from "mime-types" +import setStingRoute from "../utils/setStingRoute.js" + +setStingRoute("/app.webmanifest", "app.webmanifest", async () => ({ + $schema: "https://json.schemastore.org/web-manifest-combined.json", + name: metadata.title, + lang: metadata.language, + start_url: "/", + id: "/", scope: "/", + display: "minimal-ui", + background_color: metadata.theme, + theme_color: metadata.theme, + description: metadata.description, + icons: ["png", "svg"].flatMap(format => format == "svg" ? { + src: "/favicon.svg", + type: contentType(format), + sizes: "any", + purpose: "any maskable" + } : [192, 512, 1024].map(size => ({ + src: `/favicon.svg?format=${format}&width=${size}`, + type: contentType(format), + sizes: `${size}x${size}`, + purpose: "maskable" + }))) +})) diff --git a/src/server/errHanadler.ts b/src/server/errHanadler.ts new file mode 100644 index 0000000..84f9b26 --- /dev/null +++ b/src/server/errHanadler.ts @@ -0,0 +1,85 @@ +import Base from "./template/Base.js" +import createError from "http-errors" +import t, { c } from "./template/vdom.js" +import h from "./template/header.js" +import { STATUS_CODES } from "node:http" +import app from "./app.js" +import isDevel from "./utils/isDevel.js" +import isDefined from "./utils/isDefined.js" +import type express from "express" +import type { HttpError } from "http-errors" + +interface ErrObj { + code: number | string, + msg: string, + debug?: string | undefined +} + +async function errorTemplate(data: ErrObj): Promise<{ html: string; text: string; obj: ErrObj }> { + const { code, msg, debug } = data + const arg = { + ...data, + title: msg, + content() { + return [ + c(h, { level: 1, style: "color: darkred" }, `ERROR: ${code}`), + t("p", {}, msg), + isDefined(debug) && isDevel(app) ? t("p", {}, t("pre", {}, debug)) : "" + ] + } + } + return { + html: await Base(arg).render(), + text: `${code} - ${msg} +${debug || ""}`, + obj: data + } + +} + +async function errRender(res: express.Response, opt: ErrObj) { + const msg = await errorTemplate(opt) + res.format({ + html: () => { + res.send(msg.html) + }, + json: () => { + res.send(msg.obj) + }, + default: () => { + res.type("txt").send(msg.text) + } + }) +} + +export const errHandler = async function ( + err: HttpError, + _req: express.Request, + res: express.Response, + _next: express.NextFunction +) { + res.status(err.status || 500) + + await errRender(res, { + code: res.statusCode, + msg: res.statusMessage || STATUS_CODES[res.statusCode] || "Unknown", + debug: err.stack + }) + + console.error(err + (err.stack ?? "")) +} + +export function notFound(_req: express.Request, + _res: express.Response, + next: express.NextFunction +) { + next(createError(404)) +} + +export async function offline(_req: express.Request, + res: express.Response) { + await errRender(res, { + code: "Offline", + msg: "Can not reach the website. Check your Network", + }) +}
\ No newline at end of file diff --git a/src/server/img.ts b/src/server/img.ts new file mode 100644 index 0000000..7006c3d --- /dev/null +++ b/src/server/img.ts @@ -0,0 +1,83 @@ +import sharp from "sharp" +import { createReadStream } from "node:fs" +import { extname, join } from "node:path" +import createError from "http-errors" +import type express from "express" + +function transform(format: keyof sharp.FormatEnum, width?: number, height?: number): sharp.Sharp { + var transform = sharp().withMetadata() + + if (format) { + transform = transform.toFormat(format) + } + + if (width || height) { + transform = transform.resize({ width, height }) + } + + return transform +} + +function errorHand(next: express.NextFunction) { + return function (err: NodeJS.ErrnoException) { + switch (err.code) { + case "ENOENT": + next() + break + case "EISDIR": + case "EPERM": + next(createError(401)) + break + default: + next(err) + } + } +} + +export default function (path: string) { + return async function (req: express.Request, res: express.Response, next: express.NextFunction) { + const err_handler = errorHand(next) + + const filepath = join(path, req.path) + const ext = extname(filepath).substring(1) + if (![ + "svg", "heic", "heif", "avif", "jpeg", "jpg", "jpe", "tile", "dz", + "png", "raw", "tiff", "tif", "webp", "gif", "jp2", "jpx", "j2k", + "j2c", "jxl" + ].includes(ext)) { + return next() + } + const width = parseInt(String(req.query['width'])) + const height = parseInt(String(req.query['height'])) + var { format = ext } = req.query as { format?: string } + if (format == "svg") { + if (ext == "svg") { + const stream = createReadStream(filepath) + stream.on("error", err_handler) + res.format({ + [format]: function () { + return stream.pipe(res) + } + }) + } else { + return next(createError(400)) + } + return + } + + const stream = createReadStream(filepath) + stream.on("error", err_handler) + res.format({ + [format]: function () { + return stream.pipe( + transform( + format as keyof sharp.FormatEnum, + !isNaN(width) ? width : undefined, + !isNaN(height) ? height : undefined + ) + ).pipe(res) + } + }) + } +} + diff --git a/src/server/metadata.ts b/src/server/metadata.ts new file mode 100644 index 0000000..607c7eb --- /dev/null +++ b/src/server/metadata.ts @@ -0,0 +1,18 @@ +import { URL } from "node:url" + +export default { + title: "Sudomsg", + url: new URL("https://sudomsg.xyz/"), + language: "en-GB", + theme: "#8b0000", + description: "Messages from root", + feed: { + atom: "/feed", + }, + author: { + name: "Marc Pervaz Boocha", + email: "mboocha@sudomsg.xyz", + image: "/favicon.svg", + url: "/about/#marc-pervaz-boocha" + } +} diff --git a/src/server/router.ts b/src/server/router.ts new file mode 100644 index 0000000..8d2626c --- /dev/null +++ b/src/server/router.ts @@ -0,0 +1,5 @@ +import { Router } from "express" + +export default Router() + + diff --git a/src/server/server.ts b/src/server/server.ts new file mode 100644 index 0000000..add22e2 --- /dev/null +++ b/src/server/server.ts @@ -0,0 +1,23 @@ +#!/usr/bin/env node +import { createServer } from "node:http" + +const server = createServer() +server.on("request", (await import("./app.js")).default) +server.on("error", console.error) +server.on("listening", function (this: typeof server) { + const addr = this.address() + console.log(`Listening on ${addr ? typeof addr === "string" ? addr : `${addr.port} on ${addr.address}` : "Unknown Socket"}`) +}) +server.on("close", function () { + console.log("HTTP server closed") +}) + +server.listen({ + host: "::", + port: 8080 +}) + +process.on("SIGTERM", function () { + console.warn("SIGTERM signal received: closing HTTP server") + server.close() +}) diff --git a/src/server/template/Base.ts b/src/server/template/Base.ts new file mode 100644 index 0000000..e5128ee --- /dev/null +++ b/src/server/template/Base.ts @@ -0,0 +1,117 @@ +import metadata from "../metadata.js" +import curl from "../utils/curl.js" +import setStingRoute from "../utils/setStingRoute.js" +import isDefined from "../utils/isDefined.js" +import { Attribute, doctype, html as html_1, head, meta, title as title_1, link, script, body, header, nav, a, main, footer } from "./html.js" +import { c, node } from "./vdom.js" +import type { URL } from "url" + +export var pages: (ReturnType<typeof Base>)[] = [] + +type content<T extends node = node> = T | (() => T) + +interface BasePageI<T extends node = node> { + url?: string | URL | undefined, + content: content<T>, + date_mod?: Date | undefined, + date_pub?: Date +} + +export interface BaseI<T extends node = node> extends BasePageI<T> { + title?: string, + description?: string, + keywords?: string[], + isHighlight?: boolean +} + +export function content<Type extends BasePageI>({ data }: Attribute & { data: Type }): node { + return data.content instanceof Function ? data.content() : data.content +} + +function BasePage<Type extends BasePageI>(data: Type): Type & { + setupRoute(url?: URL): void; + render(): Promise<string>; + date_mod: Type["date_pub"]; + url: URL | undefined +} { + return { + ...data, + setupRoute(url?: URL): void { + url ??= this.url + if (isDefined(url)) { + setStingRoute(url.pathname, "html", this.render.bind(this)) + } else { + throw new Error(); + + } + }, + async render(): Promise<string> { + console.profile() + const a = doctype(c(content, { data: this })) + console.profileEnd() + return a + }, + date_mod: data.date_mod ?? data.date_pub, + url: isDefined(data.url) ? curl(data.url) : undefined, + } +} + +export default function Base<Type extends BaseI>(data: Type): ReturnType<typeof BasePage<Type>> { + const arg = { + ...data, + content() { + var { title, description, keywords, url, isHighlight = true } = this + if (!url) { + isHighlight = false + } + return c(html_1, { lang: metadata.language }, + c(head, {}, + ...(url ? [c(meta, { property: "og:locale", content: metadata.language })] : []), + c(title_1, { property: "og:title" }, title || metadata.title), + c(meta, { name: "theme-color", content: metadata.theme }), + ...(url ? [ + c(meta, { name: "description", property: "og:description", content: description || metadata.description }), + ...(Array.isArray(keywords) ? [c(meta, { name: "keywords", contents: keywords })] : []), + c(meta, { property: "og:url", content: url }), + c(link, { rel: "canonical", href: url }), + c(meta, { property: "og:site_name", content: metadata.title }), + c(meta, { name: "author", content: metadata.author.name }), + c(meta, { property: "og:type", content: "website" }), + c(meta, { name: "viewport", content: "width=device-width, initial-scale=1" }), + c(meta, { name: "twitter:card", content: "summary" }), + c(link, { rel: "alternate", href: metadata.feed.atom, type: "application/atom+xml", title: metadata.title }), + c(meta, { property: "og:image", content: "/favicon.svg?format=png&width=1024" }) + ] : []), + c(link, { rel: "icon", href: "/favicon.ico", sizes: "any" }), + c(link, { rel: "icon", href: "/favicon.svg", type: "image/svg+xml" }), + c(link, { rel: "apple-touch-icon", href: "/favicon.svg?format:png&width=192" }), + c(link, { rel: "manifest", href: "/app.webmanifest" }), + c(link, { rel: "stylesheet", href: "/index.css" }), + c(script, { type: "module", src: "/index.js" }), + ...(isHighlight ? [c(link, { rel: "stylesheet", href: "/syntax.css" })] : []) + ), + c(body, {}, + c(header, {}, c(nav, {}, + c(a, { href: "#", id: "nav-toogle" }, "Sudomsg"), + ...Object.entries({ + Home: "/", + Blog: "/blog", + About: "/about", + Git: "/cgit", + }).map(([name, href]) => c(a, { href, class: "navlinks" }, name)) + )), + c(main, {}, c(content, { data })), + c(footer, {}, + "Subscribe: ", + c(a, { href: metadata.feed.atom, type: "application/atom+xml" }, "Atom") + ) + ) + ) + } + } + const base = BasePage(arg) + if (isDefined(base.url)) { + pages.push(base) + } + return base +} diff --git a/src/server/template/Page.ts b/src/server/template/Page.ts new file mode 100644 index 0000000..ec76e97 --- /dev/null +++ b/src/server/template/Page.ts @@ -0,0 +1,23 @@ +import Base, { BaseI, content } from "./Base.js" +import h from "./header.js" +import { article } from "./html.js" +import { c, node } from "./vdom.js" + +export interface PageI<T extends node = node> extends BaseI<T> { + title: string +} + +export default function Page<Type extends PageI>(data: Type): ReturnType<typeof Base<Type>> { + const arg = { + ...data, + content() { + var { title } = this + return c(article, {}, + c(h, { level: 1 }, title), + c(content, { data }) + ) + } + + } + return Base(arg) +}
\ No newline at end of file diff --git a/src/server/template/Post.ts b/src/server/template/Post.ts new file mode 100644 index 0000000..c348f8b --- /dev/null +++ b/src/server/template/Post.ts @@ -0,0 +1,48 @@ +import schema from "../utils/schema.js" +import metadata from "../metadata.js" +import Base, { content } from "./Base.js" +import h from "./header.js" +import slugify from "@sindresorhus/slugify" +import { c } from "./vdom.js" +import type { PageI } from "./Page.js" +import { article, small, time, span, a, p, div } from "./html.js" + +export var posts: ReturnType<typeof Post>[] = [] + +interface PostI extends Omit<PageI, "url"> { + title: string, + description: string, + date_pub: Date, + date_mod?: Date +} + +export default function Post<Type extends PostI>(data: Type): ReturnType<typeof Base<Type & {url: string;}>> { + var args = { + ...data, + content() { + var { date_mod, title, date_pub, description } = this + return c(article, { itemscope: true, itemtype: schema("BlogPosting") }, + c(h, { level: 1, itemprop: "headline" }, title), + c(small, {}, + ...(date_mod ? [ + c(time, { datetime: date_mod.toISOString(), itemprop: "dateModified" }, date_mod.toDateString()), + "- Modified " + ] : []), + c(time, { datetime: date_pub.toISOString(), itemprop: "datePublished" }, date_pub.toDateString()) + , " - ", + c(span, { itemprop: "author", itemscope: true, itemtype: schema("Person") }, + c(a, { href: metadata.author.url, rel: "author", itemprop: "url" }, + c(span, { itemprop: "name" }, metadata.author.name)) + ), + c(p, { itemprop: "abstract" }, description) + ), + c(div, { itemprop: "articleBody" }, c(content, { data })) + ) + }, + url: `/posts/${slugify(data.title)}` + } + const post = Base(args) + posts.push(post) + return post +} + diff --git a/src/server/template/atom.ts b/src/server/template/atom.ts new file mode 100644 index 0000000..82c9fe4 --- /dev/null +++ b/src/server/template/atom.ts @@ -0,0 +1,114 @@ +import type { element, node } from "./vdom.js"; +import t, { Base, Lang, Attribute as Attr } from "./xml.js"; + +export interface Attribute extends Attr, Base, Lang { } + +export function feed(attr: Attribute, + ...content: element<"id" | "title" | "updated" | "author" | + "link" | "author" | "category" | "contributor" | "generator" + | "icon" | "logo" | "rights" | "subtitle" | "entry">[] +) { + return t("feed", attr, ...content); +} + +export function entry( + attr: Attribute, + ...content: element< + "id" | "title" | "updated" | "author" | "content" | "link" | + "summary" | "category" | "contributor" | "rights" | "published" | "source">[] +) { + return t("entry", attr, ...content); +} + +export function author(attr: Attribute, ...content: [element<"name" | "email">, element<"name" | "email">]) { + return t("author", attr, ...content); +} + +export function contributor(attr: Attribute, ...content: [element<"name" | "email">, element<"name" | "email">]) { + return t("contributor", attr, ...content); +} + +export function id({ id, ...prop }: Attribute & { id: URL }) { + return t("id", prop, id.href); +} + +export function updated({ date, ...prop }: Attribute & { date: Date }) { + return t("updated", prop, date.toISOString()); +} + +export function update({ date, ...prop }: Attribute & { date: Date } ) { + return t("update", prop, date.toISOString()); +} + +export function icon(attr: Attribute, ...content: string[]) { + return t("icon", attr, ...content); +} + +export function logo(attr: Attribute, ...content: string[]) { + return t("logo", attr, ...content); +} + +export function name(attr: Attribute, content: string) { + return t("name", attr, content); +} + +export function email(attr: Attribute, content: string) { + return t("email", attr, content); +} + +interface TextAttribute extends Attribute { + type?: string; +} + +export function title(attr: TextAttribute, ...content: node[]) { + return t("title", attr, ...content); +} + +export function subtitle(attr: TextAttribute, ...content: string[]) { + return t("subtitle", attr, ...content); +} + +export function summary(attr: TextAttribute, ...content: string[]) { + return t("summary", attr, ...content); +} + +export function rights(attr: TextAttribute, ...content: string[]) { + return t("rights", attr, ...content); +} + +export function content(attr: TextAttribute, ...child: node[]) { + return t("content", attr, ...child); +} + + +interface generatorAttribute extends Attribute { + uri?: string; + version?: string +} + +export function generator(attr: generatorAttribute, ...content: string[]) { + return t("generator", attr, ...content); +} + +interface linkAttribute extends Attribute { + href: URL; + rel?: "alternate" | "enclosure" | "self" | "via"; + type?: string; + hreflang?: string; + title?: string; + length?: number; +} + +export function link(attr: linkAttribute) { + return t("link", { ...attr, href: attr.href.href, length: "" + (attr.length ?? "") }); +} + +interface catAttribute extends Attribute { + term: string; + scheme?: URL; + label?: string; +} + +export function category(attr: catAttribute) { + return t("category", { ...attr, href: attr.scheme?.href }); +} diff --git a/src/server/template/header.ts b/src/server/template/header.ts new file mode 100644 index 0000000..04da87d --- /dev/null +++ b/src/server/template/header.ts @@ -0,0 +1,10 @@ +import slugify from "@sindresorhus/slugify" +import t, { Attribute, getText, node } from "./vdom.js" + +interface head extends Attribute { + level: 1 | 2 | 3 | 4 | 5 | 6 +} + +export default function ({ level, ...attr }: head, ...content: node[]) { + return t(`h${level}`, { id: slugify(getText(content)), ...attr }, ...content) +} diff --git a/src/server/template/html.ts b/src/server/template/html.ts new file mode 100644 index 0000000..ff0a0cc --- /dev/null +++ b/src/server/template/html.ts @@ -0,0 +1,602 @@ +import isDefined from "../utils/isDefined.js" +import tag, { render as rend, prenderdata, node, Attribute as Attr, element } from "./vdom.js" + +export function doctype(...content: node[]) { + return `<!DOCTYPE html>${render(content).content}` +} + +export const render = rend("html", function (node, content) { + const close = [ + "img", + "input", + "br", + "hr", + "frame", + "area", + "base", + "basefont", + "col", + "isindex", + "link", + "meta", + "param", + "html", + "body", + "p", + "li", + "dt", + "dd", + "option", + "thead", + "th", + "tbody", + "tr", + "td", + "tfoot", + "colgroup", + ].includes(node.name.toLowerCase()) ? undefined : `</${node.name}>` + var list = Object.entries(node.attr).map(([prop, val]) => { + if (!isDefined(val)) { + return "" + } + + if (typeof val == "boolean") { + return val ? prop : "" + } + var v: string = Array.isArray(val) ? val.join(" ") : "" + val + + if (/[\s?"'<=>`]/.test(v)) { + v = `"${v}"` + } + return `${prop}=${v}` + }).join(" ") + if (list) { + list = ` ${list} ` + } + const open = `<${node.name}${list}>` + return `${open}${content ?? ""}${close ?? ""}` +}) + +export const data = prenderdata("html") + +export interface Attribute extends Attr { + class?: string | string[], + id?: string, + accesskey?: string + dir?: "ltr" | "rtr" | "auto" + draggable?: "true" | "false" | "auto" + lang?: string + role?: string + is?: string + itemid?: URL + itemprop?: string + itemref?: string[] + itemtype?: URL + itemscope?: boolean + property?: string +} + +export default function t<T extends string = string>(name: T, attr?: Attribute, ...content: node[]) { + return tag(name, { + ...attr, + itemtype: attr?.itemtype?.href, + itemid: attr?.itemtype?.href + }, ...content) +} + +export function body(attr: Attribute, ...content: node[]) { + return t("body", attr, ...content); +} + +export function head(attr: Attribute, ...content: element[]) { + return t("head", attr, ...content); +} + +export function html(attr: Attribute, ...content: node[]) { + return t("html", attr, ...content); +} + +export function a(attr: Attribute, ...content: node[]) { + return t("a", attr, ...content); +} + + +export function abbr(attr: Attribute, ...content: node[]) { + return t("abbr", attr, ...content); +} + + +export function address(attr: Attribute, ...content: node[]) { + return t("address", attr, ...content); +} + + +export function area(attr: Attribute) { + return t("area", attr); +} + + +export function article(attr: Attribute, ...content: node[]) { + return t("article", attr, ...content); +} + + +export function aside(attr: Attribute, ...content: node[]) { + return t("aside", attr, ...content); +} + + +export function audio(attr: Attribute, ...content: node[]) { + return t("audio", attr, ...content); +} + + +export function b(attr: Attribute, ...content: node[]) { + return t("b", attr, ...content); +} + + +export function base(attr: Attribute, ...content: node[]) { + return t("base", attr, ...content); +} + + +export function bdi(attr: Attribute, ...content: node[]) { + return t("bdi", attr, ...content); +} + + +export function bdo(attr: Attribute, ...content: node[]) { + return t("bdo", attr, ...content); +} + + +export function blockquote(attr: Attribute, ...content: node[]) { + return t("blockquote", attr, ...content); +} + +export function br(attr: Attribute) { + return t("br", attr); +} + + +export function button(attr: Attribute, ...content: node[]) { + return t("button", attr, ...content); +} + + +export function canvas(attr: Attribute, ...content: node[]) { + return t("canvas", attr, ...content); +} + + +export function caption(attr: Attribute, ...content: node[]) { + return t("caption", attr, ...content); +} + + +export function cite(attr: Attribute, ...content: node[]) { + return t("cite", attr, ...content); +} + + +export function code(attr: Attribute, ...content: node[]) { + return t("code", attr, ...content); +} + + +export function col(attr: Attribute) { + return t("col", attr); +} + + +export function colgroup(attr: Attribute, ...content: node[]) { + return t("colgroup", attr, ...content); +} + + +export function hdata(attr: Attribute, ...content: node[]) { + return t("data", attr, ...content); +} + + +export function datalist(attr: Attribute, ...content: node[]) { + return t("datalist", attr, ...content); +} + + +export function dd(attr: Attribute, ...content: node[]) { + return t("dd", attr, ...content); +} + + +export function del(attr: Attribute, ...content: node[]) { + return t("del", attr, ...content); +} + + +export function details(attr: Attribute, ...content: node[]) { + return t("details", attr, ...content); +} + + +export function dfn(attr: Attribute, ...content: node[]) { + return t("dfn", attr, ...content); +} + + +export function dialog(attr: Attribute, ...content: node[]) { + return t("dialog", attr, ...content); +} + + +export function div(attr: Attribute, ...content: node[]) { + return t("div", attr, ...content); +} + + +export function dl(attr: Attribute, ...content: node[]) { + return t("dl", attr, ...content); +} + + +export function dt(attr: Attribute, ...content: node[]) { + return t("dt", attr, ...content); +} + + +export function em(attr: Attribute, ...content: node[]) { + return t("em", attr, ...content); +} + + +export function embed(attr: Attribute) { + return t("embed", attr); +} + + +export function fieldset(attr: Attribute, ...content: node[]) { + return t("fieldset", attr, ...content); +} + + +export function figcaption(attr: Attribute, ...content: node[]) { + return t("figcaption", attr, ...content); +} + + +export function figure(attr: Attribute, ...content: node[]) { + return t("figure", attr, ...content); +} + + +export function footer(attr: Attribute, ...content: node[]) { + return t("footer", attr, ...content); +} + + +export function form(attr: Attribute, ...content: node[]) { + return t("form", attr, ...content); +} + +export function header(attr: Attribute, ...content: node[]) { + return t("header", attr, ...content); +} + + +export function hgroup(attr: Attribute, ...content: node[]) { + return t("hgroup", attr, ...content); +} + + +export function hr(attr: Attribute) { + return t("hr", attr); +} + +export function i(attr: Attribute, ...content: node[]) { + return t("i", attr, ...content); +} + + +export function iframe(attr: Attribute, ...content: node[]) { + return t("iframe", attr, ...content); +} + + +export function img(attr: Attribute) { + return t("img", attr); +} + + +export function input(attr: Attribute) { + return t("input", attr); +} + + +export function ins(attr: Attribute, ...content: node[]) { + return t("ins", attr, ...content); +} + + +export function kbd(attr: Attribute, ...content: node[]) { + return t("kbd", attr, ...content); +} + + +export function label(attr: Attribute, ...content: node[]) { + return t("label", attr, ...content); +} + + +export function legend(attr: Attribute, ...content: node[]) { + return t("legend", attr, ...content); +} + + +export function li(attr: Attribute, ...content: node[]) { + return t("li", attr, ...content); +} + + +export function link(attr: Attribute) { + return t("link", attr); +} + + +export function main(attr: Attribute, ...content: node[]) { + return t("main", attr, ...content); +} + + +export function map(attr: Attribute, ...content: node[]) { + return t("map", attr, ...content); +} + + +export function mark(attr: Attribute, ...content: node[]) { + return t("mark", attr, ...content); +} + + +export function menu(attr: Attribute, ...content: node[]) { + return t("menu", attr, ...content); +} + + +export function meta(attr: Attribute) { + return t("meta", attr); +} + + +export function meter(attr: Attribute, ...content: node[]) { + return t("meter", attr, ...content); +} + + +export function nav(attr: Attribute, ...content: node[]) { + return t("nav", attr, ...content); +} + + +export function noscript(attr: Attribute, ...content: node[]) { + return t("noscript", attr, ...content); +} + + +export function object(attr: Attribute, ...content: node[]) { + return t("object", attr, ...content); +} + + +export function ol(attr: Attribute, ...content: node[]) { + return t("ol", attr, ...content); +} + + +export function optgroup(attr: Attribute, ...content: node[]) { + return t("optgroup", attr, ...content); +} + + +export function option(attr: Attribute, ...content: node[]) { + return t("option", attr, ...content); +} + + +export function output(attr: Attribute, ...content: node[]) { + return t("output", attr, ...content); +} + + +export function p(attr: Attribute, ...content: node[]) { + return t("p", attr, ...content); +} + + +export function picture(attr: Attribute, ...content: node[]) { + return t("picture", attr, ...content); +} + + +export function pre(attr: Attribute, ...content: node[]) { + return t("pre", attr, ...content); +} + + +export function progress(attr: Attribute, ...content: node[]) { + return t("progress", attr, ...content); +} + + +export function q(attr: Attribute, ...content: node[]) { + return t("q", attr, ...content); +} + + +export function rp(attr: Attribute, ...content: node[]) { + return t("rp", attr, ...content); +} + + +export function rt(attr: Attribute, ...content: node[]) { + return t("rt", attr, ...content); +} + + +export function ruby(attr: Attribute, ...content: node[]) { + return t("ruby", attr, ...content); +} + + +export function s(attr: Attribute, ...content: node[]) { + return t("s", attr, ...content); +} + + +export function samp(attr: Attribute, ...content: node[]) { + return t("samp", attr, ...content); +} + + +export function script(attr: Attribute, ...content: node[]) { + return t("script", attr, ...content); +} + + +export function section(attr: Attribute, ...content: node[]) { + return t("section", attr, ...content); +} + + +export function select(attr: Attribute, ...content: node[]) { + return t("select", attr, ...content); +} + + +export function slot(attr: Attribute, ...content: node[]) { + return t("slot", attr, ...content); +} + + +export function small(attr: Attribute, ...content: node[]) { + return t("small", attr, ...content); +} + + +export function source(attr: Attribute) { + return t("source", attr); +} + + +export function span(attr: Attribute, ...content: node[]) { + return t("span", attr, ...content); +} + + +export function strong(attr: Attribute, ...content: node[]) { + return t("strong", attr, ...content); +} + + +export function style(attr: Attribute, ...content: node[]) { + return t("style", attr, ...content); +} + + +export function sub(attr: Attribute, ...content: node[]) { + return t("sub", attr, ...content); +} + + +export function summary(attr: Attribute, ...content: node[]) { + return t("summary", attr, ...content); +} + + +export function sup(attr: Attribute, ...content: node[]) { + return t("sup", attr, ...content); +} + + +export function table(attr: Attribute, ...content: node[]) { + return t("table", attr, ...content); +} + + +export function tbody(attr: Attribute, ...content: node[]) { + return t("tbody", attr, ...content); +} + + +export function td(attr: Attribute, ...content: node[]) { + return t("td", attr, ...content); +} + + +export function template(attr: Attribute, ...content: node[]) { + return t("template", attr, ...content); +} + + +export function textarea(attr: Attribute, ...content: node[]) { + return t("textarea", attr, ...content); +} + + +export function tfoot(attr: Attribute, ...content: node[]) { + return t("tfoot", attr, ...content); +} + + +export function th(attr: Attribute, ...content: node[]) { + return t("th", attr, ...content); +} + + +export function thead(attr: Attribute, ...content: node[]) { + return t("thead", attr, ...content); +} + + +export function time(attr: Attribute, ...content: node[]) { + return t("time", attr, ...content); +} + + +export function title(attr: Attribute, ...content: node[]) { + return t("title", attr, ...content); +} + + +export function tr(attr: Attribute, ...content: node[]) { + return t("tr", attr, ...content); +} + + +export function track(attr: Attribute) { + return t("track", attr); +} + + +export function u(attr: Attribute, ...content: node[]) { + return t("u", attr, ...content); +} + + +export function ul(attr: Attribute, ...content: node[]) { + return t("ul", attr, ...content); +} + + +export function hvar(attr: Attribute, ...content: node[]) { + return t("var", attr, ...content); +} + +export function video(attr: Attribute, ...content: node[]) { + return t("video", attr, ...content); +} + + +export function wbr(attr: Attribute) { + return t("wbr", attr); +}
\ No newline at end of file diff --git a/src/server/template/sitemap.ts b/src/server/template/sitemap.ts new file mode 100644 index 0000000..0a59ec5 --- /dev/null +++ b/src/server/template/sitemap.ts @@ -0,0 +1,31 @@ +import type { element } from "./vdom.js"; +import t, { Attribute } from "./xml.js"; + +export function urlset(attr: Attribute, ...content: element<'url'>[]) { + return t("urlset", attr, ...content); +} + +export function url( + attr: Attribute, + ...content: element<"loc" | "lastmod" | "changefreq" | "priority">[] +) { + return t("url", attr, ...content); +} + +export function loc(attr: Attribute, ...content: string[]) { + return t("loc", attr, ...content); +} + +export function lastmod(attr: Attribute, content: string) { + return t("lastmod", attr, content); +} + +export function changefreq(attr: Attribute, + content: "always" | "hourly" | "daily" | "weekly" | "monthly" | "yearly" | "never" +) { + return t("changefreq", attr, content); +} + +export function priority(attr: Attribute, content: string) { + return t("priority", attr, content); +}
\ No newline at end of file diff --git a/src/server/template/syntax.ts b/src/server/template/syntax.ts new file mode 100644 index 0000000..82b37e8 --- /dev/null +++ b/src/server/template/syntax.ts @@ -0,0 +1,18 @@ +import hljs from "highlight.js" +import { data, Attribute, code as Code } from "./html.js" +import { c } from "./vdom.js" + +interface Syntax extends Attribute { + lang: string +} + +export default function code({ lang, ...attr }: Syntax, content: string) { + return c( + Code, + { + class: attr.class ?? "hljs", + ...attr + }, + data(hljs.highlight(content,{ language: lang }).value) + ) +}
\ No newline at end of file diff --git a/src/server/template/table.ts b/src/server/template/table.ts new file mode 100644 index 0000000..34f2771 --- /dev/null +++ b/src/server/template/table.ts @@ -0,0 +1,32 @@ +import { c, node } from "./vdom.js"; +import { Attribute, caption, table as Table, td, th, thead, tr } from "./html.js"; + +interface TableAttr extends Attribute { + header: boolean; + data: node[][]; + caption?: string; +} + +export default function table({ header, data, caption: captio, ...attr }: TableAttr) { + const capt = captio ? c(caption, {}, captio) : undefined; + if (header) { + const [head, ...tbldata] = data; + return c(Table, attr, + ...tbldata.map( + row => c(tr, {}, ...row.map( + key => c(td, {}, key ?? "") + )) + ), + c(thead, {}, c(tr, {}, ...(head ?? []).map(e => c(th, {}, e ?? "")))), + ...(capt ? [capt] : []) + ); + } else { + return c(Table, attr, ...data.map( + row => c(tr, {}, ...row.map( + key => c(td, {}, key ?? "") + )) + ), + ...(capt ? [capt] : []) + ); + } +} diff --git a/src/server/template/vdom.ts b/src/server/template/vdom.ts new file mode 100644 index 0000000..899565b --- /dev/null +++ b/src/server/template/vdom.ts @@ -0,0 +1,105 @@ +export type component< + T extends Attribute = Attribute, + U extends node[] = node[], + R extends node = node +> = ((attr: T, ...content: U) => R) + +export interface Attribute { + [key: string]: unknown, +} + +export interface element<T extends string = string> { + name: T, + attr: Attribute, + content: node[], +} + +export function iselement<T extends string = string>(e: unknown): e is element<T> { + return typeof e === "object" && e !== null && "name" in e; +} + +export interface typed_data<T extends string = string> { + content: string, + type: T +} + +export function istyped<T extends string = string>(e: unknown): e is typed_data<T> { + return typeof e === "object" && e !== null && "type" in e; +} + +export type node = element | typed_data | node[] | string + +export function c< + T extends Attribute = Attribute, + U extends node[] = node[], + R extends node = node +>(name: component<T, U, R>, attr: T, ...content: U): R { + return name(attr, ...content) +} + +export default function t<T extends string = string>(name: T, attr?: Attribute, ...content: node[]): element<T> { + return { + name, + attr: attr ?? ({} as Attribute), + content + } +} + +export function frag<T extends node[] = node[]>(_attr: Attribute, ...content: T) { + return content +} + +export function join({sep = ""}: Attribute & {sep?: string}, ...content: string[]) { + return content.join(sep) +} + +export function prenderdata<T extends string = string>(type: T) { + return function (content: string): typed_data<T> { + return { + content, + type + } + } +} + +export function render<T extends string = string>(type: T, rendFunc: (node: element, content: string) => string) { + const data = prenderdata(type) + return function rend(Node: node): typed_data<T> { + if (Array.isArray(Node)) { + return data(Node.map(element => rend(element).content ?? "").join("")) + } + + if (typeof Node == "string") { + return data(Node) + } + + if (iselement(Node)) { + return data(rendFunc(Node, rend(Node.content).content)) + } + + if (istyped<T>(Node)) { + if (Node.type == type) { + return Node + } else { + throw new Error(Node.type + "is not valid. The type must be:" + type) + } + } + throw new TypeError("" + Node) + } +} + +export function getText(node: node): string { + if (Array.isArray(node)) { + return node.map(element => getText(element) ?? "").join("") + } + + if (typeof node == "string") { + return node + } + + if (iselement(node)) { + return getText(node.content) + } + + throw new TypeError("" + node) +} diff --git a/src/server/template/xml.ts b/src/server/template/xml.ts new file mode 100644 index 0000000..fa6dbb4 --- /dev/null +++ b/src/server/template/xml.ts @@ -0,0 +1,56 @@ +import isDefined from "../utils/isDefined.js" +import tag, { render, prenderdata, node, element, Attribute as Attr } from "./vdom.js" + +type encoding = "UTF-8" | "UTF-16" | "ISO-10646-UCS-2" | "ISO-10646-UCS-4" | "ISO-8859-1" | "ISO-8859-2" | "ISO-8859-3" | "ISO-8859-4" | + "ISO-8859-5" | "ISO-8859-6" | "ISO-8859-7" | "ISO-8859-8" | "ISO-8859-9" | "ISO-2022-JP" | "Shift_JIS" | "EUC-JP" + +interface xmlDeclaration { + version?: "1.0", + encoding?: encoding + standalone?: boolean +} + +export function doctype(options: xmlDeclaration, content: node): string { + const version = `version=${options.version ?? `"1.0"`} ` + const encoding = isDefined(options.encoding) ? `encoding="${options.encoding}" ` : "" + const standalone = isDefined(options.standalone) ? `standalone="${options.standalone ? "yes" : "no"}" ` : "" + return `<?xml ${version}${encoding}${standalone}?>${xmlrender(content).content}` +} + +export const xmlrender = render("xml", function (node: element, content: string) { + var list = Object.entries(node.attr).map(([prop, val]): string => { + if (!isDefined(val)) { + return "" + } + var v: string = Array.isArray(val) ? val.join(" ") : "" + val + return `${prop}="${v}"` + }).join(" ") + if (list) { + list = ` ${list} ` + } + const open = `<${node.name}${list}>` + const close = `</${node.name}>` + if (content || !close) { + return `${open}${content || ""}${close}` + } else { + return `<${node.name}${list}/>` + } +}) + +export const xmldata = prenderdata("xml") + +export interface Attribute extends Attr { + xmlns?: URL +} + +export interface Base extends Attribute { + ["xml:base"]?: string +} + +export interface Lang extends Attribute { + ["xml:lang"]?: string +} + +export default function t<T extends string = string>(name: T, attr?: Attribute, ...content: node[]) { + return tag(name, { ...attr, xmlns: attr?.xmlns?.href }, ...content) +} diff --git a/src/server/utils/createUrl.ts b/src/server/utils/createUrl.ts new file mode 100644 index 0000000..2a05665 --- /dev/null +++ b/src/server/utils/createUrl.ts @@ -0,0 +1,5 @@ +import { URL } from "node:url"; + +export default function createUrl(url: string | URL, base?: string | URL | undefined): URL { + return url instanceof URL ? url : new URL(url, base); +} diff --git a/src/server/utils/curl.ts b/src/server/utils/curl.ts new file mode 100644 index 0000000..e422493 --- /dev/null +++ b/src/server/utils/curl.ts @@ -0,0 +1,7 @@ +import type { URL } from "node:url"; +import metadata from "../metadata.js"; +import createUrl from "./createUrl.js"; + +export default function curl(path: string | URL) { + return createUrl(path, metadata.url); +} diff --git a/src/server/utils/isDefined.ts b/src/server/utils/isDefined.ts new file mode 100644 index 0000000..fe42bdf --- /dev/null +++ b/src/server/utils/isDefined.ts @@ -0,0 +1,3 @@ +export default function isDefined<T>(val: T | undefined | null): val is T { + return val !== undefined && val !== null; +} diff --git a/src/server/utils/isDevel.ts b/src/server/utils/isDevel.ts new file mode 100644 index 0000000..6d03f4d --- /dev/null +++ b/src/server/utils/isDevel.ts @@ -0,0 +1,5 @@ +import type express from "express"; + +export default function isDevel(app: express.Express) { + return app.get("env") === "development"; +} diff --git a/src/server/utils/relDir.ts b/src/server/utils/relDir.ts new file mode 100644 index 0000000..1e3cb3c --- /dev/null +++ b/src/server/utils/relDir.ts @@ -0,0 +1,9 @@ +import { dirname, join } from "node:path"; +import { URL, fileURLToPath } from "node:url"; + +export default function relDir(url: URL | string) { + const base = dirname(fileURLToPath(url)); + return function (dirname: string) { + return join(base, dirname); + }; +} diff --git a/src/server/utils/relUrl.ts b/src/server/utils/relUrl.ts new file mode 100644 index 0000000..5345ebf --- /dev/null +++ b/src/server/utils/relUrl.ts @@ -0,0 +1,5 @@ +import type { URL } from "node:url"; + +export default function relUrl(url: URL) { + return url.pathname + url.search + url.hash; +} diff --git a/src/server/utils/schema.ts b/src/server/utils/schema.ts new file mode 100644 index 0000000..989d163 --- /dev/null +++ b/src/server/utils/schema.ts @@ -0,0 +1,6 @@ +import type { URL } from "node:url"; +import createUrl from "./createUrl.js"; + +export default function schema(type: string | URL): URL { + return createUrl(type, "http://schema.org/"); +} diff --git a/src/server/utils/setStingRoute.ts b/src/server/utils/setStingRoute.ts new file mode 100644 index 0000000..e0fbd94 --- /dev/null +++ b/src/server/utils/setStingRoute.ts @@ -0,0 +1,6 @@ +import router from "../router.js"; +import strHandler from "./strHandler.js"; + +export default function setStingRoute<T>(url: string, type: string | string[], content: () => Promise<T>): void { + router.get(url, strHandler(type, content)); +} diff --git a/src/server/utils/strHandler.ts b/src/server/utils/strHandler.ts new file mode 100644 index 0000000..5be21e0 --- /dev/null +++ b/src/server/utils/strHandler.ts @@ -0,0 +1,8 @@ +import type express from "express"; + +export default function strHandler<T>(type: string | string[], content: () => Promise<T>) { + return async (_req: express.Request, res: express.Response) => { + const data = await content(); + res.format(Object.fromEntries((Array.isArray(type) ? type : [type]).map(t => [t, () => res.send(data)]))); + }; +} diff --git a/src/worker/sw.ts b/src/worker/sw.ts new file mode 100644 index 0000000..e125879 --- /dev/null +++ b/src/worker/sw.ts @@ -0,0 +1,51 @@ +declare var self: ServiceWorkerGlobalScope; +export {}; + + +const sw_cache = { + offline: "/offline", + default: [ + "/index.js", + "/index.css", + "/app.webmanifest", + "/favicon.svg" + ], + store: "app", +} + +async function install() { + const cache = await self.caches.open(sw_cache.store) + return cache.addAll([sw_cache.offline, ...sw_cache.default]) +} + +async function activate() { + const keys = await self.caches.keys() + return Promise.all(keys.map(key => { + return key !== sw_cache.store ? self.caches.delete(key) : undefined + })) +} + +async function req(event: FetchEvent) { + const cache = await caches.open(sw_cache.store) + const cachedResponse = await cache.match(event.request) + var networkResponse + try { + networkResponse = await fetch(event.request) + if (networkResponse.ok) { + cache.put(event.request, networkResponse.clone()) + } + } catch(error) { + console.error(error) + if (event.request.mode === "navigate") { + networkResponse = await caches.match("/offline") + } + } + return (cachedResponse || networkResponse) as Response + +} + +self.addEventListener("install", event => event.waitUntil(install())) + +self.addEventListener("activate", event => event.waitUntil(activate())) + +self.addEventListener("fetch", event => event.respondWith(req(event))) diff --git a/src/worker/tsconfig.json b/src/worker/tsconfig.json new file mode 100644 index 0000000..f485c2e --- /dev/null +++ b/src/worker/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "importHelpers": true, + "lib": ["es2022", "webworker"], + "composite": true, + "sourceMap": true + }, +} |