Initial Release

This commit is contained in:
2026-05-24 21:58:23 -07:00
commit 0a782e0096
209 changed files with 11657 additions and 0 deletions
@@ -0,0 +1,17 @@
import { inspect } from 'util'
export default function Log(
severity: 'FATAL' | 'ERROR' | 'WARN' | 'INFO' | 'DEBUG' | 'DATA',
service: string,
message: string,
data?: any
) {
const d = new Date()
const t = `${d.toDateString().slice(4)} ${d.toTimeString().slice(0, 8)}`
process.stdout.write(`${t} [${severity}] ${service}: ${message}\x1b[0m\n`)
if (data) {
process.stdout.write(inspect(data, false, Infinity, true))
process.stdout.write('\n---\n\n')
}
if (severity === 'FATAL') process.exit(1)
}
@@ -0,0 +1,132 @@
import { RawAttributes } from 'node-html-parser/dist/nodes/html'
import { Element, TableElement } from '../types/articles'
import { parse } from 'node-html-parser'
import hljs from "highlight.js"
import { inspect } from 'util'
/** Parses and extracts elements from an Article */
export default function parseArticle(someDocument: string): [RawAttributes, Array<Element>] {
const clean = (s: string) => s.split('\n').map(s => s.trim()).join(' ').trim()
const root = parse(someDocument, {}).querySelector('article')
const data = new Array<Element>()
if (!root || root.tagName !== 'ARTICLE') {
throw 'Expected Article Tag'
}
for (const tag of root.childNodes) {
const tagName = tag.rawTagName.trim().toLowerCase()
// @ts-expect-error
const element = tag as HTMLElement
switch (tagName) {
case 'quote':
case 'header':
case 'subheader':
data.push({
'type': tagName,
'value': tag.textContent
})
break
case 'text':
data.push({
'type': 'text',
'items': (
tag.childNodes.map(child => {
const data: Record<string, string> = {}
if (child.rawTagName === 'a') {
// @ts-expect-error
const c = child as HTMLElement
data.href = c.getAttribute('href')!
data.target = c.getAttribute('target')!
}
return Object.assign(data, {
'tag': child.rawTagName,
'content': clean(child.textContent),
})
})
.filter(c => c.content.length !== 0)
)
})
break
case 'code':
const code = tag.textContent.trim()
const syntax = element.getAttribute('syntax')!
data.push({
'type': 'code',
'content': (
syntax !== 'skip'
? hljs.highlight(code, { 'language': syntax }).value
: code
)
})
break
case 'list':
data.push({
'type': 'list',
'items': (
tag.childNodes
.map(c => clean(c.textContent))
.filter(e => e.length !== 0)
)
})
break
case 'table':
const table: TableElement = {
type: 'table',
items: []
}
for (const row of tag.childNodes) {
const items = new Array<string>()
if (row.rawTagName !== 'row') continue
for (const column of row.childNodes) {
if (column.rawTagName !== 'column') continue
items.push(clean(column.textContent))
}
table.items.push(items)
}
data.push(table)
break
case 'image':
case 'banner':
case 'beanie':
data.push({
'type': tagName,
'resource': element.getAttribute('resource')!,
'caption': element.getAttribute('caption')!,
'alt': element.getAttribute('alt')!,
})
break
case 'video':
data.push({
'type': tagName,
'resource': element.getAttribute('resource')!,
'caption': element.getAttribute('caption')!,
'alt': element.getAttribute('alt')!,
'cover': element.getAttribute('cover')!,
})
break
case 'audio':
data.push({
'type': tagName,
'resource': element.getAttribute('resource')!,
'name': element.getAttribute('name')!,
})
break
case '': break
default:
throw `Unsupported Tag Name '${tagName}'`
}
}
return [
root.rawAttributes,
data
]
}
@@ -0,0 +1,145 @@
import { CONTENT_TYPE_MAP, PROCESSOR_ASSETS, PROCESSOR_DIRECT, PROCESSOR_INDEX, PROCESSOR_PREFIX, PROCESSOR_PRESETS, PRODUCTION } from '../constants'
import { existsSync, readFileSync } from 'fs'
import { extname, join } from 'path'
import { createHash } from 'crypto'
import cleanCss from 'clean-css'
import * as js from 'uglify-js'
import * as ts from 'typescript'
import sharp from 'sharp'
import Log from './logger'
import { byteSize } from './pugHelpers'
const PROCCESED = new Map()
// Background processes a file making it available at the returned path
export function processFile(uri: string, dry = false) {
return new Promise<string>(async (ok, cancel) => {
if (!uri.startsWith(PROCESSOR_PREFIX)) {
cancel(`URI '${uri}' must begin with ${PROCESSOR_PREFIX}`)
return
}
if (dry) {
ok(`/asset-processor/${uri.slice(PROCESSOR_PREFIX.length)}`)
return
}
if (PRODUCTION && PROCCESED.has(uri)) {
return ok(PROCCESED.get(uri))
}
// Read Contents From Disk
const t = Date.now()
const [pathname, search] = uri.slice(PROCESSOR_PREFIX.length).split('?', 2)
const params = new URLSearchParams(search)
const path = join(process.cwd(), 'assets', pathname)
if (!existsSync(path)) {
cancel(`Path '${path}' does not exist`)
return
}
const input = readFileSync(path)
const extension = extname(pathname).slice(1)
function complete(ext: string, output: string | Buffer) {
const outHash = createHash('md5').update(output).digest('hex').slice(0, 8)
const uriPath =
(params.get('direct') ? PROCESSOR_DIRECT : '') +
`/assets/${pathname.slice(0, pathname.length - extension.length)}${ext}`
const uriFull = `${uriPath}?v=${outHash}`
const index = PROCESSOR_ASSETS.push({
'type': CONTENT_TYPE_MAP[ext] || 'application/octet-stream',
'size': Buffer.byteLength(output),
'file': output,
}) - 1
PROCESSOR_INDEX[uriPath] = index
PROCESSOR_INDEX[uriFull] = index
PROCCESED.set(uri, uriFull)
Log('DEBUG', 'processFile', `'${uriFull}' (${byteSize(output.length)}) (${Date.now() - t}ms)`)
ok(uriFull)
}
switch (extension) {
// Resize Image
case 'webp':
case 'jpeg':
case 'jpg':
case 'png':
case 'gif': {
const preset = PROCESSOR_PRESETS[params.get('preset') || '']
const animated = params.get('animated') !== null
complete(
preset ? preset.format : extension,
preset ? await sharp(input, { animated })
.resize(preset.width, preset.height, preset.resize)
.toFormat(preset.format, { 'quality': preset.quality })
.toBuffer()
: input
)
break
}
// Minify Script
case 'js': {
const minified = js.minify(input.toString(), { mangle: true })
if (minified.error) {
cancel(`JavaScript Minify Error: ${minified.error}`)
return
}
complete('js', minified.code)
break
}
// Transpile and Minify Script
case 'ts': {
const script = ts.transpile(input.toString(), {
'target': ts.ScriptTarget.ES2022,
'module': ts.ModuleKind.CommonJS,
'lib': ['DOM', 'ES2022'],
'allowSyntheticDefaultImports': true,
'skipLibCheck': true,
'strict': true,
})
const minified = js.minify(script, { mangle: true })
if (minified.error) {
cancel(`JavaScript Minify Error: ${minified.error}`)
return
}
complete('js', minified.code)
break
}
// Minify Styles
case 'css': {
const css = input.toString()
const search = /('file:\/\/[^']+')/g
const promises = new Array<ReturnType<typeof processFile>>()
// Search for URIs inside CSS File
css.replaceAll(search, uri => {
promises.push(processFile(uri.slice(1, -1), !PRODUCTION))
return uri
})
Promise.all(promises).then(results => {
// Replace all URIs and Minify
let index = 0
const minifed = new cleanCss().minify(
css.replaceAll(search, () => results[index++])
)
if (minifed.errors.length) {
cancel(`Minify CSS Error: ${minifed.errors.join(', ')}`)
return
}
complete('css', minifed.styles)
}).catch(e => {
cancel(`CSS Processing Errors: ${e}`)
})
break
}
// Unsupported, Skip Processing!
default:
complete(extension, input)
break
}
})
}
@@ -0,0 +1,60 @@
import { byteSize, calcReadTime, formatDate, formatHref } from "./pugHelpers"
import { PROCESSOR_PREFIX, PRODUCTION } from "../constants"
import { processFile } from "./processFile"
import parse from "node-html-parser"
import pug from 'pug'
const replaceAttr = new Array<string>('src', 'href', 'content', 'poster')
const replaceQuery = new Array<string>(
'img[src]', // Image Elements
'source[src]', // Video Sources
'audio[src]', // Audio Sources
'script[src]', // Script Elements
'link[href]', // CSS Links
'meta[content]', // Meta OG Tags
'a[href]', // Download Anchors
'video[poster]', // Video Posters
).join(',')
// Render a View and Process all Files
export default async function processView(path: string, locals: any): Promise<string> {
const document = parse(
pug.renderFile(path, Object.assign(locals, {
'pretty': !PRODUCTION,
'formatBytes': byteSize,
'formatDate': formatDate,
'formatString': formatHref,
'calcReadTime': calcReadTime,
})),
{ comment: true }
)
// Process Inline Styles
for await (const elem of document.querySelectorAll('*[style]')) {
let css = elem.getAttribute('style')
if (!css) continue
const search = /('file:\/\/[^']+')/g
const promises = new Array<ReturnType<typeof processFile>>()
css.replaceAll(search, uri => {
promises.push(processFile(uri.slice(1, -1), !PRODUCTION))
return uri
})
await Promise.all(promises).then(results => {
let index = 0
css = css!.replaceAll(search, () => results[index++])
})
elem.setAttribute('style', css)
}
// Process Element Sources
for (const elem of document.querySelectorAll(replaceQuery)) {
const attrKey = replaceAttr.find(e => elem.getAttribute(e))
if (!attrKey) continue
const attrVal = elem.getAttribute(attrKey)
if (attrVal && attrVal.startsWith(PROCESSOR_PREFIX)) {
elem.setAttribute(attrKey, await processFile(attrVal, !PRODUCTION))
}
}
return document.toString()
}
@@ -0,0 +1,66 @@
import { Element } from "../types/articles"
// Format ISO Timestamp into Generic Date (ex: Jun 26th 2003)
export function formatDate(isoTimestamp: string): string {
const t = new Date(isoTimestamp)
const m = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dece'][t.getMonth()]
const d = t.getDate()
const y = t.getFullYear()
return `${m} ${d}${d === 1 ? 'st' : d === 2 ? 'nd' : d === 3 ? 'rd' : 'th'} ${y}`
}
// Format String for Usage in HREF Tags
export function formatHref(text: string): string {
return text
.trim()
.toLowerCase()
.replace(/[^A-Za-z0-9 ]/g, '')
.replace(/\s+/g, '-')
}
// Determine byte size by length
export function byteSize(length: number) {
if (length > 1e+9) return `${(length / 1e+9).toFixed(2)}gb`
if (length > 1e+6) return `${(length / 1e+6).toFixed(2)}mb`
if (length > 1024) return `${(length / 1024).toFixed(2)}kb`
return `${length}b`
}
// Estimate Required Time to Read this Article
export function calcReadTime(articleElements: Array<Element>): number {
return Math.ceil(
articleElements
.map((e): number => {
const AVERAGE_READING_SPEED = 200 // wpm
switch (e.type) {
// Calculate seconds based on average reading speed divided by amount of characters
case 'list': return e.items.map(e => e.length).reduce((a, b) => a + b, 0) / AVERAGE_READING_SPEED
case 'text': return e.items.map(e => e.content.split(' ').length).reduce((a, b) => a + b, 0) / AVERAGE_READING_SPEED
case 'code': return e.content.split(' ').length / AVERAGE_READING_SPEED
case 'subheader':
case 'header':
case 'quote':
return e.value.split(' ').length / AVERAGE_READING_SPEED
case 'table':
return e.items
.map(r => r.map(c => c.split(' ').length).reduce((a, b) => a + b, 0))
.reduce((a, b) => a + b, 0)
/ AVERAGE_READING_SPEED
// Static increases for each image element
case 'image':
case 'banner':
case 'beanie':
return 0.25 // 15 seconds
// Static increases for each video element
case 'video':
case 'audio':
return 0.50 // 30 seconds
}
})
.reduce((a, b) => a + b, 0)
)
}
@@ -0,0 +1,51 @@
import { Request, RequestHandler, Response } from 'express'
import { PRODUCTION } from '../constants'
import { createHash } from 'crypto'
import processView from './processView'
import Log from './logger'
// Renders a View from Disk and Serves it with Caching Enabled
export async function Static(path: string): Promise<RequestHandler> {
if (PRODUCTION) {
Log('INFO', 'static', `Pre-Render: '${path}'`)
const content = await processView(path, {})
const length = Buffer.byteLength(content)
const hash = createHash('md5').update(content).digest('hex')
return function (req: Request, res: Response) {
if (req.headers['if-none-match'] === hash) {
res.status(304).end()
return
}
res.setHeader('Content-Type', 'text/html')
res.setHeader('Content-Length', length)
res.setHeader('ETag', hash)
res.end(content)
}
} else {
return async function (req: Request, res: Response) {
Log('INFO', 'static', `Render: '${path}'`)
processView(path, {})
.then(html => res.send(html))
.catch(err => res
.status(500)
.end('Caught Rendering Error! Check Console for more details.\n' + String(err))
)
}
}
}
// Redirect to Another Location
export function Redirect(location: string) {
return function (req: Request, res: Response) {
res.redirect(location)
}
}
// Return a Response Code with an Empty Body
export function Empty(status: number) {
return function (req: Request, res: Response) {
res.status(status).end()
}
}