From 3f91a121b33151cd466de930d0e68bdf87f4d19e Mon Sep 17 00:00:00 2001 From: LMBishop <13875753+LMBishop@users.noreply.github.com> Date: Mon, 20 Dec 2021 14:48:04 +0000 Subject: Convert to typescript --- .eslintignore | 2 +- .gitignore | 1 + app/constants.mjs | 9 -- app/directory.mjs | 116 ---------------------- app/directory.ts | 210 ++++++++++++++++++++++++++++++++++++++++ app/index.mjs | 107 -------------------- app/index.ts | 113 +++++++++++++++++++++ app/static/css/globalstyles.css | 100 ------------------- app/static/scripts/purge.js | 15 --- app/static/scripts/rebuild.js | 14 --- app/views/error.ejs | 19 ---- app/views/index.ejs | 18 ---- app/views/page.ejs | 20 ---- app/views/partials/header.ejs | 12 --- app/views/partials/navbar.ejs | 3 - app/views/purge.ejs | 24 ----- app/views/rebuild.ejs | 23 ----- app/wikiparser.mjs | 7 +- package-lock.json | 193 ++++++++++++++++++++++++++++++++---- package.json | 5 +- pages/template/blog.wiki | 2 + pages/templates/blog.wiki | 2 - static/css/globalstyles.css | 100 +++++++++++++++++++ static/scripts/purge.js | 15 +++ static/scripts/rebuild.js | 14 +++ tsconfig.json | 11 +++ views/error.ejs | 19 ++++ views/index.ejs | 18 ++++ views/page.ejs | 20 ++++ views/partials/header.ejs | 12 +++ views/partials/navbar.ejs | 3 + views/purge.ejs | 24 +++++ views/rebuild.ejs | 23 +++++ 33 files changed, 768 insertions(+), 506 deletions(-) delete mode 100644 app/constants.mjs delete mode 100644 app/directory.mjs create mode 100644 app/directory.ts delete mode 100644 app/index.mjs create mode 100644 app/index.ts delete mode 100644 app/static/css/globalstyles.css delete mode 100644 app/static/scripts/purge.js delete mode 100644 app/static/scripts/rebuild.js delete mode 100644 app/views/error.ejs delete mode 100644 app/views/index.ejs delete mode 100644 app/views/page.ejs delete mode 100644 app/views/partials/header.ejs delete mode 100644 app/views/partials/navbar.ejs delete mode 100644 app/views/purge.ejs delete mode 100644 app/views/rebuild.ejs create mode 100644 pages/template/blog.wiki delete mode 100644 pages/templates/blog.wiki create mode 100644 static/css/globalstyles.css create mode 100644 static/scripts/purge.js create mode 100644 static/scripts/rebuild.js create mode 100644 tsconfig.json create mode 100644 views/error.ejs create mode 100644 views/index.ejs create mode 100644 views/page.ejs create mode 100644 views/partials/header.ejs create mode 100644 views/partials/navbar.ejs create mode 100644 views/purge.ejs create mode 100644 views/rebuild.ejs diff --git a/.eslintignore b/.eslintignore index 8ec4cb0..3eaeff0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,2 +1,2 @@ -app/static/* +static/* node_modules/* diff --git a/.gitignore b/.gitignore index 713d500..17c578e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ +build/ .env diff --git a/app/constants.mjs b/app/constants.mjs deleted file mode 100644 index ace7e2f..0000000 --- a/app/constants.mjs +++ /dev/null @@ -1,9 +0,0 @@ -'use strict'; - -export const SERVER_PORT = 3000; -export const PARSER_MAX_RECURSION = 20; -export const PURGE_COOLDOWN_MIN = -1; -export const REBUILD_COOLDOWN_MIN = -1; -export const PAGES_DIR = 'pages'; -export const TEMPLATE_DIR = 'pages/tempates'; -export const IMAGES_DIR = 'pages/images'; diff --git a/app/directory.mjs b/app/directory.mjs deleted file mode 100644 index b470bc7..0000000 --- a/app/directory.mjs +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -import { PAGES_DIR, PURGE_COOLDOWN_MIN, REBUILD_COOLDOWN_MIN } from './constants.mjs'; -import { parse } from './wikiparser.mjs'; -import { readFileSync, readdirSync } from 'fs'; - -const pages = {}; -const metadata = {}; - -export function pageFor(path) { - path = path.replace(/[^a-z0-9]/gi, '_').toLowerCase(); - let page = pages[path]; - if (!page) { - return undefined; - } - - if (!page.html) { - buildPage(path); - return pages[path]; - } - - return page; -} - -function buildPage(path) { - let data; - try { - data = readFileSync(`${PAGES_DIR}/${path}.wiki`, 'utf-8'); - } catch { - return false; - } - let result = parse(data); - let title = result.metadata.displayTitle ?? 'Unnamed page'; - let content = `${result.metadata.notitle ? '' : `

${title}

`}${result.html}`; - - let page = { - html: content, - raw: data, - buildTime: result.metadata.buildTime, - primary: result.metadata.primary ?? false, - sortOrder: result.metadata.sortOrder ?? -1, - notitle: result.metadata.notitle ?? false, - displayTitle: title - }; - pages[path] = page; - return true; -} - -export function rebuild() { - if (metadata.fileTreeBuildTime + REBUILD_COOLDOWN_MIN * 60 * 1000 > Date.now()) { - return false; - } - for (var page in pages) { - delete pages[page]; - } - - readdirSync(PAGES_DIR).forEach(file => { - if (!file.endsWith('.wiki')) { - return; - } - file = file.replace('.wiki', ''); - buildPage(file); - }); - - let primaryPages = []; - for (const page of Object.keys(pages)) { - if (pages[page].primary) { - primaryPages.push(page); - } - } - primaryPages.sort((a, b) => { - return pages[a].sortOrder - pages[b].sortOrder; - }); - metadata.navbar = primaryPages; - metadata.fileTreeBuildTime = new Date(); - return true; -} - -export function exists(path) { - return !!pages[path]; -} - -export function rawDataFor(path) { - return pages[path]; -} - -export function purge(path) { - let page = pages[path]; - if (page) { - if (page.buildTime.getTime() + PURGE_COOLDOWN_MIN * 60 * 1000 > Date.now()) { - return false; - } else { - pages[path] = {}; - if (buildPage(path)) { - return true; - } - delete pages[path]; - } - } - return false; -} - -export function getPages() { - return pages; -} - -export function getNavbar(current = '') { - if (!metadata.navbar) { - return ''; - } - let navbar = ''; - for (const page of metadata.navbar) { - navbar = navbar + ``; - } - return navbar; -} diff --git a/app/directory.ts b/app/directory.ts new file mode 100644 index 0000000..6449e8e --- /dev/null +++ b/app/directory.ts @@ -0,0 +1,210 @@ +'use strict'; + +import { parse } from './wikiparser.mjs'; +import { readFileSync, readdirSync, statSync } from 'fs'; +import glob from 'glob'; + +export class PageDirectory { + + pages: Record; + primaryPages: Page[]; + pagePath: string; + lastBuild: number; + + constructor(root: string) { + this.lastBuild = 0; + this.pages = {}; + this.pagePath = root; + + this.rebuild(); + } + + /** + * Build this page directory. + * + * @returns whether the directory was built + */ + rebuild(): boolean { + if (this.lastBuild + parseInt(process.env.REBUILD_COOLDOWN_MIN, 10) * 60 * 1000 > Date.now()) { + return false; + } + for (var page in this.pages) { + delete this.pages[page]; + } + + let pages = glob.sync(`**/*.wiki`, { cwd: this.pagePath }) + + pages.forEach(page => { + page = page.replace('.wiki', '').replace('/', ':').replace(/[^a-z0-9:]/gi, '_').toLowerCase(); + this.pages[page] = this.buildPage(page); + }); + + let primaryPages = []; + Object.entries(this.pages).forEach(([name, page]) => { + if (page.metadata.includeInNavbar) { + primaryPages.push(page); + } + }); + + primaryPages.sort((a, b) => { + return a.metadata.sortOrder - b.metadata.sortOrder; + }); + this.primaryPages = primaryPages; + this.lastBuild = Date.now(); + return true; + } + + /** + * Get whether a page exists with this name. + * + * @param name standard name for page + * @returns whether the page exists + */ + exists(name: string): boolean { + return !!this.pages[this.convertNameToStandard(name)]; + } + + /** + * Get a page. + * + * @param name standard name for page + * @returns page + */ + get(name: string): Page { + name = this.convertNameToStandard(name); + let page = this.pages[name]; + if (!page) { + return undefined; + } + + if (!page.html) { + return this.buildPage(name) + } + + return page; + } + + /** + * Get the raw wikitext for a page. + * + * @param name standard name for page + * @returns raw wikitext + */ + getRaw(name: string): string { + name = this.convertNameToStandard(name); + return this.pages[name]?.raw; + } + + /** + * Purge (rebuild) a page. + * + * @param name standard name for page + * @returns whether the page was rebuilt + */ + purge(name: string): boolean { + name = this.convertNameToStandard(name); + let page = this.pages[name]; + if (page) { + if (page.buildTime + parseInt(process.env.PURGE_COOLDOWN_MIN, 10) * 60 * 1000 > Date.now()) { + return false; + } else { + delete this.pages[name]; + if (this.buildPage(name)) { + return true; + } + } + } + return false; + } + + /** + * Get all pages. + * + * @returns all pages + */ + getPages(): Record { + return this.pages; + } + + /** + * Get primary pages. + * + * @param current + * @returns + */ + getPrimaryPages(): Page[] { + return this.primaryPages; + } + + /** + * Build a page. + * + * @param path standard name for page + * @returns newly built page, or undefined + */ + private buildPage(name: string): Page { + name = this.convertNameToStandard(name); + let data: string; + try { + data = readFileSync(`${this.pagePath}/${this.convertStandardToFilePath(name)}`, 'utf-8'); + } catch { + return undefined; + } + let result = parse(data); + let title = result.metadata.displayTitle ?? name; + let content = `${result.metadata.notitle ? '' : `

${title}

`}${result.html}`; + + let page: Page = { + html: content, + raw: data, + standardName: name, + buildTime: Date.now(), + metadata: { + includeInNavbar: result.metadata.primary ?? false, + sortOrder: result.metadata.sortOrder ?? -1, + showTitle: !result.metadata.notitle ?? true, + displayTitle: title + } + }; + this.pages[name] = page; + return page; + } + + /** + * Convert a page name to a standard name. + * A standard name is the key used by the page directory. + * + * @param name non-standard name for a page + */ + private convertNameToStandard(name: string): string { + return name.replace(/[^a-z0-9:]/gi, '_').toLowerCase(); + } + + /** + * Convert a standard name to a file path. + * + * @param name standard name for a page + */ + private convertStandardToFilePath(name: string): string { + let [first, second] = name.split(':'); + let [title, subpage] = ((second) ? second : first).split('.') + let namespace = (second) ? first : undefined + + return `${namespace ? `${namespace}/` : ''}${title}${subpage ? `.${subpage}` : ''}.wiki` + } +}; + +export type Page = { + html: string; + raw: string; + standardName: string, + buildTime: number; + metadata: PageMetadata; +}; + +export type PageMetadata = { + displayTitle?: string; + sortOrder?: number; + showTitle?: boolean; + includeInNavbar?: boolean; +}; diff --git a/app/index.mjs b/app/index.mjs deleted file mode 100644 index 0f14ccf..0000000 --- a/app/index.mjs +++ /dev/null @@ -1,107 +0,0 @@ -'use strict'; - -import { SERVER_PORT } from './constants.mjs'; -import * as directory from './directory.mjs'; -import express from 'express'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const app = express(); - -directory.rebuild(); - -app.use(express.static(__dirname + '/static')); -app.set('view engine', 'ejs'); -app.set('views', __dirname + '/views'); - -app.get('/:page.wiki', (req, res) => { - let path = req.params.page; - let page = directory.pageFor(path); - - if (!page) { - error(res, 404); - return; - } - - res.type('text/plain'); - res.send(page.raw).end(); -}); - -app.get('/:page?', (req, res) => { - let path = req.params.page ?? 'index'; - let page = directory.pageFor(path); - - if (!page) { - error(res, 404); - return; - } - - res.render('page.ejs', { - navbar: directory.getNavbar(path), - path: path, - content: page.html, - title: page.displayTitle, - buildTime: page.buildTime.toString() - }); -}); - -app.get('/special/purge/:page?', (req, res) => { - let path = req.params.page ?? 'index'; - let page = directory.rawDataFor(path); - - if (!page) { - error(res, 404); - return; - } - - res.render('purge.ejs', { - navbar: directory.getNavbar(), - page: path, - buildTime: page.buildTime?.toString() ?? 'never', - buildTimeRelative: Math.round((Date.now() - page.buildTime?.getTime()) / 1000 / 60) - }); -}); - -app.get('/special/purge/:page/confirm', (req, res) => { - let path = req.params.page; - let page = directory.rawDataFor(path); - - if (!page) { - error(res, 404); - return; - } - - if (directory.purge(path)) { - res.status(200).send(); - } else { - res.status(429).send(); - } -}); - -app.get('/special/rebuild', (req, res) => { - res.render('rebuild.ejs', { - navbar: directory.getNavbar() - }); -}); - -app.get('/special/rebuild/confirm', (req, res) => { - if (directory.rebuild()) { - res.status(200).send(); - } else { - res.status(429).send(); - } -}); - -app.listen(SERVER_PORT, () => { - console.log(`App listening on ${SERVER_PORT}`); -}); - -function error(res, code) { - res.render('error.ejs', { - code: code, - navbar: directory.getNavbar() - }); -} diff --git a/app/index.ts b/app/index.ts new file mode 100644 index 0000000..9b16ec5 --- /dev/null +++ b/app/index.ts @@ -0,0 +1,113 @@ +'use strict'; + +import { PageDirectory, Page, PageMetadata } from './directory.js'; +import express from 'express'; +import dotenv from 'dotenv'; + +dotenv.config() + +const app = express(); +const directory = new PageDirectory(process.env.PAGES_DIR); + +directory.rebuild(); + +function navbar(current: string = ''): string { + let navbar = ''; + directory.primaryPages.forEach(page => { + navbar += ``; + }) + return navbar +} + +app.use(express.static('static')); +app.set('view engine', 'ejs'); +app.set('views', 'views'); + +app.get('/:page.wiki', (req, res) => { + let path = req.params.page; + let raw = directory.getRaw(path); + + if (!raw) { + error(res, 404); + return; + } + + res.type('text/plain'); + res.send(raw).end(); +}); + +app.get('/:page?', (req, res) => { + let path = req.params.page ?? 'index'; + let page = directory.get(path); + + if (!page) { + error(res, 404); + return; + } + + res.render('page.ejs', { + navbar: navbar(), + path: path, + content: page.html, + title: page.metadata.displayTitle, + buildTime: new Date(page.buildTime) + }); +}); + +app.get('/special/purge/:page?', (req, res) => { + let path = req.params.page ?? 'index'; + let page = directory.get(path); + + if (!page) { + error(res, 404); + return; + } + + res.render('purge.ejs', { + navbar: navbar(), + page: path, + buildTime: new Date(page.buildTime) ?? 'never', + buildTimeRelative: Math.round((Date.now() - page.buildTime) / 1000 / 60) + }); +}); + +app.get('/special/purge/:page/confirm', (req, res) => { + let path = req.params.page; + let page = directory.get(path); + + if (!page) { + error(res, 404); + return; + } + + if (directory.purge(path)) { + res.status(200).send(); + } else { + res.status(429).send(); + } +}); + +app.get('/special/rebuild', (req, res) => { + res.render('rebuild.ejs', { + navbar: navbar() + }); +}); + +app.get('/special/rebuild/confirm', (req, res) => { + if (directory.rebuild()) { + res.status(200).send(); + } else { + res.status(429).send(); + } +}); + +app.listen(process.env.PORT, () => { + console.log(`App listening on ${process.env.PORT}`); +}); + +function error(res, code) { + res.render('error.ejs', { + code: code, + navbar: navbar() + }); +} diff --git a/app/static/css/globalstyles.css b/app/static/css/globalstyles.css deleted file mode 100644 index 774c6ca..0000000 --- a/app/static/css/globalstyles.css +++ /dev/null @@ -1,100 +0,0 @@ -@import url('https://fonts.googleapis.com/css2?family=Cousine:ital,wght@0,400;0,700;1,400;1,700&display=swap'); - -.website-name { - font-size: 10px; - font-weight: 700; - line-height: 1.2; - color: #ddd; - text-shadow: 0px 1px 10px #9876aa; -} - -html, body { - border: 0; - margin: 0; - background-color: #111; - color: #ddd; - font-family: 'Cousine', monospace, sans-serif; - line-height: 1.3; -} - -h1, h2, h3, h4, h5, h6 { - color: #cc7832 -} - -.code-block { - background-color: #222; - border: solid 1px #333; - padding: 10px; -} - -#navbar { - background-color: #222; - width: 100%; - display: flex; - flex-direction: row; - justify-content: flex-start; - align-items: stretch; - gap: 10px; -} - -.navbar-element { - background-color: #222; - display: flex; - height: 30px; - text-align: center; - line-height: 30px; - padding: 10px; -} - -.navbar-element:hover { - background-color: #888; - transition: 0.2s; - cursor: pointer; -} - -.navbar-element > a { - color: #fff; - text-decoration: none; -} - -.navbar-element > .highlight { - color: #ffc66d; -} - -#main-container { - max-width: 1200px; - margin: 0 auto; -} - -#content-container { - box-shadow: 0px 0px 15px 10px rgba(152,118,170,0.05); -} - -#content { - padding: 20px; - max-width: 1200px; - background-color: #2b2b2b; - margin: 0 auto; -} - -a { - color: #9876aa; - text-decoration: underline; -} - -.highlight { - color: #ffc66d; -} - -.footer { - font-size: 10px; -} - -.redlink { - color: #ff4136; -} - -.box { - border: solid 1px #fff; - padding: 10px; -} diff --git a/app/static/scripts/purge.js b/app/static/scripts/purge.js deleted file mode 100644 index 5ee34f0..0000000 --- a/app/static/scripts/purge.js +++ /dev/null @@ -1,15 +0,0 @@ -$(() => { - $('#confirm').click(() => { - let page = $('#confirm').data('page'); - $.ajax({ - type: 'GET', - url: `/special/purge/${page}/confirm`, - success: () => { - $('#response').html('
Successfully purged page.
'); - }, - error: () => { - $('#response').html('
Could not purge page. Try again later.
'); - } - }); - }); -}); diff --git a/app/static/scripts/rebuild.js b/app/static/scripts/rebuild.js deleted file mode 100644 index 8fd0e2e..0000000 --- a/app/static/scripts/rebuild.js +++ /dev/null @@ -1,14 +0,0 @@ -$(() => { - $('#confirm').click(() => { - $.ajax({ - type: 'GET', - url: `/special/rebuild/confirm`, - success: () => { - $('#response').html('
Successfully rebuilt page directory.
'); - }, - error: () => { - $('#response').html('
Could not rebuild page directory. Try again later.
'); - } - }); - }); -}); diff --git a/app/views/error.ejs b/app/views/error.ejs deleted file mode 100644 index 88e1a27..0000000 --- a/app/views/error.ejs +++ /dev/null @@ -1,19 +0,0 @@ - - - - Error: <%= code %> - - - -
- <%- include('partials/header') %> -
- <%- include('partials/navbar') %> -
-

An error occurred (<%= code %>)

-

Go home?

-
-
-
- - diff --git a/app/views/index.ejs b/app/views/index.ejs deleted file mode 100644 index f47b830..0000000 --- a/app/views/index.ejs +++ /dev/null @@ -1,18 +0,0 @@ - - - - <%= title %> - - - -
- <%- include('partials/header') %> -
- <%- include('partials/navbar') %> -
- <%- page %> -
-
-
- - diff --git a/app/views/page.ejs b/app/views/page.ejs deleted file mode 100644 index 41ee1e2..0000000 --- a/app/views/page.ejs +++ /dev/null @@ -1,20 +0,0 @@ - - - - <%= title %> - - - -
- <%- include('partials/header') %> -
- <%- include('partials/navbar') %> -
- <%- content %> -
- GitHub | View raw | Page built: <%= buildTime %> | Purge this page -
-
-
- - diff --git a/app/views/partials/header.ejs b/app/views/partials/header.ejs deleted file mode 100644 index bd11ce0..0000000 --- a/app/views/partials/header.ejs +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
-
diff --git a/app/views/partials/navbar.ejs b/app/views/partials/navbar.ejs deleted file mode 100644 index ff0c84d..0000000 --- a/app/views/partials/navbar.ejs +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/app/views/purge.ejs b/app/views/purge.ejs deleted file mode 100644 index 04bae61..0000000 --- a/app/views/purge.ejs +++ /dev/null @@ -1,24 +0,0 @@ - - - - Purge page - - - - - -
- <%- include('partials/header') %> -
- <%- include('partials/navbar') %> -
-

Purge page

- -

Are you sure you wish to purge the page <%= page %>?

-

The last build time for this page was <%= buildTime %> (<%= buildTimeRelative %> minutes ago).

- -
-
-
- - diff --git a/app/views/rebuild.ejs b/app/views/rebuild.ejs deleted file mode 100644 index 9f9cfaa..0000000 --- a/app/views/rebuild.ejs +++ /dev/null @@ -1,23 +0,0 @@ - - - - Rebuild - - - - - -
- <%- include('partials/header') %> -
- <%- include('partials/navbar') %> -
-

Rebuild

- -

Are you sure you wish to rebuild the page directory?

- -
-
-
- - diff --git a/app/wikiparser.mjs b/app/wikiparser.mjs index 512d985..e49a09c 100644 --- a/app/wikiparser.mjs +++ b/app/wikiparser.mjs @@ -18,7 +18,6 @@ // in an action of contract, negligence or other tortious action, arising out of or in // connection with the use or performance of this software. -import { PARSER_MAX_RECURSION, TEMPLATE_DIR, IMAGES_DIR } from './constants.mjs'; import dateFormat from 'dateformat'; import htmlEscape from 'escape-html'; import * as fs from 'fs'; @@ -40,7 +39,7 @@ export function parse(data) { let outText = data; - for (let l = 0, last = ''; l < PARSER_MAX_RECURSION; l++) { + for (let l = 0, last = ''; l < parseInt(process.env.PARSER_MAX_RECURSION, 10); l++) { if (last === outText) break; last = outText; outText = outText @@ -117,7 +116,7 @@ export function parse(data) { // Templates: {{template}} .replace(re(r`{{ \s* ([^#}|]+?) (\|[^}]+)? }} (?!})`), (_, title, params = '') => { if (/{{/.test(params)) return _; - const page = TEMPLATE_DIR + '/' + title.trim().replace(/ /g, '_'); + const page = 'Template:' + title.trim().replace(/ /g, '_'); // Retrieve template content let content = ''; @@ -151,7 +150,7 @@ export function parse(data) { // Images: [[File:Image.png|options|caption]] .replace(re(r`\[\[ (?:File|Image): (.+?) (\|.+?)? \]\]`), (_, file, params) => { if (/{{/.test(params)) return _; - const path = IMAGES_DIR + '/' + file.trim().replace(/ /g, '_'); + const path = 'File:' + file.trim().replace(/ /g, '_'); let caption = ''; let imageData = {}; let imageArgs = params.split('|').map((arg) => arg.replace(/"/g, '"')); diff --git a/package-lock.json b/package-lock.json index 78ced12..526fb0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,13 +9,16 @@ "version": "unpublished", "license": "UNLICENSED", "dependencies": { + "@types/express": "^4.17.13", "dateformat": "^5.0.2", "dotenv": "^10.0.0", "ejs": "^3.1.6", "escape-html": "^1.0.3", - "express": "^4.17.1" + "express": "^4.17.1", + "glob": "^7.2.0" }, "devDependencies": { + "@types/glob": "^7.2.0", "eslint": "^8.2.0" } }, @@ -105,6 +108,89 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.26", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.26.tgz", + "integrity": "sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "node_modules/@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "dependencies": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "node_modules/@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.1.tgz", + "integrity": "sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ==" + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "node_modules/@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "node_modules/accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -840,8 +926,7 @@ "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "node_modules/functional-red-black-tree": { "version": "1.0.1", @@ -853,7 +938,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -968,7 +1052,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -1182,7 +1265,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "dependencies": { "wrappy": "1" } @@ -1228,7 +1310,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -1604,8 +1685,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "node_modules/yallist": { "version": "4.0.0", @@ -1683,6 +1763,89 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "requires": { + "@types/node": "*" + } + }, + "@types/express": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", + "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.18", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "@types/express-serve-static-core": { + "version": "4.17.26", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.26.tgz", + "integrity": "sha512-zeu3tpouA043RHxW0gzRxwCHchMgftE8GArRsvYT0ByDMbn19olQHx5jLue0LxWY6iYtXb7rXmuVtSkhy9YZvQ==", + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*" + } + }, + "@types/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", + "dev": true, + "requires": { + "@types/minimatch": "*", + "@types/node": "*" + } + }, + "@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" + }, + "@types/minimatch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz", + "integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==", + "dev": true + }, + "@types/node": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.1.tgz", + "integrity": "sha512-NXKvBVUzIbs6ylBwmOwHFkZS2EXCcjnqr8ZCRNaXBkHAf+3mn/rPcJxwrzuc6movh8fxQAsUUfYklJ/EG+hZqQ==" + }, + "@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" + }, + "@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" + }, + "@types/serve-static": { + "version": "1.13.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", + "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -2249,8 +2412,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "functional-red-black-tree": { "version": "1.0.1", @@ -2262,7 +2424,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -2341,7 +2502,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -2504,7 +2664,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -2540,8 +2699,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -2811,8 +2969,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "yallist": { "version": "4.0.0", diff --git a/package.json b/package.json index 8443acd..9e79fda 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,17 @@ "author": "Leonardo Bishop", "license": "UNLICENSED", "dependencies": { + "@types/express": "^4.17.13", "dateformat": "^5.0.2", "dotenv": "^10.0.0", "ejs": "^3.1.6", "escape-html": "^1.0.3", - "express": "^4.17.1" + "express": "^4.17.1", + "glob": "^7.2.0" }, "type": "module", "devDependencies": { + "@types/glob": "^7.2.0", "eslint": "^8.2.0" } } diff --git a/pages/template/blog.wiki b/pages/template/blog.wiki new file mode 100644 index 0000000..92b60bd --- /dev/null +++ b/pages/template/blog.wiki @@ -0,0 +1,2 @@ +Blog post • {{{date}}}
+
diff --git a/pages/templates/blog.wiki b/pages/templates/blog.wiki deleted file mode 100644 index 92b60bd..0000000 --- a/pages/templates/blog.wiki +++ /dev/null @@ -1,2 +0,0 @@ -Blog post • {{{date}}}
-
diff --git a/static/css/globalstyles.css b/static/css/globalstyles.css new file mode 100644 index 0000000..774c6ca --- /dev/null +++ b/static/css/globalstyles.css @@ -0,0 +1,100 @@ +@import url('https://fonts.googleapis.com/css2?family=Cousine:ital,wght@0,400;0,700;1,400;1,700&display=swap'); + +.website-name { + font-size: 10px; + font-weight: 700; + line-height: 1.2; + color: #ddd; + text-shadow: 0px 1px 10px #9876aa; +} + +html, body { + border: 0; + margin: 0; + background-color: #111; + color: #ddd; + font-family: 'Cousine', monospace, sans-serif; + line-height: 1.3; +} + +h1, h2, h3, h4, h5, h6 { + color: #cc7832 +} + +.code-block { + background-color: #222; + border: solid 1px #333; + padding: 10px; +} + +#navbar { + background-color: #222; + width: 100%; + display: flex; + flex-direction: row; + justify-content: flex-start; + align-items: stretch; + gap: 10px; +} + +.navbar-element { + background-color: #222; + display: flex; + height: 30px; + text-align: center; + line-height: 30px; + padding: 10px; +} + +.navbar-element:hover { + background-color: #888; + transition: 0.2s; + cursor: pointer; +} + +.navbar-element > a { + color: #fff; + text-decoration: none; +} + +.navbar-element > .highlight { + color: #ffc66d; +} + +#main-container { + max-width: 1200px; + margin: 0 auto; +} + +#content-container { + box-shadow: 0px 0px 15px 10px rgba(152,118,170,0.05); +} + +#content { + padding: 20px; + max-width: 1200px; + background-color: #2b2b2b; + margin: 0 auto; +} + +a { + color: #9876aa; + text-decoration: underline; +} + +.highlight { + color: #ffc66d; +} + +.footer { + font-size: 10px; +} + +.redlink { + color: #ff4136; +} + +.box { + border: solid 1px #fff; + padding: 10px; +} diff --git a/static/scripts/purge.js b/static/scripts/purge.js new file mode 100644 index 0000000..5ee34f0 --- /dev/null +++ b/static/scripts/purge.js @@ -0,0 +1,15 @@ +$(() => { + $('#confirm').click(() => { + let page = $('#confirm').data('page'); + $.ajax({ + type: 'GET', + url: `/special/purge/${page}/confirm`, + success: () => { + $('#response').html('
Successfully purged page.
'); + }, + error: () => { + $('#response').html('
Could not purge page. Try again later.
'); + } + }); + }); +}); diff --git a/static/scripts/rebuild.js b/static/scripts/rebuild.js new file mode 100644 index 0000000..8fd0e2e --- /dev/null +++ b/static/scripts/rebuild.js @@ -0,0 +1,14 @@ +$(() => { + $('#confirm').click(() => { + $.ajax({ + type: 'GET', + url: `/special/rebuild/confirm`, + success: () => { + $('#response').html('
Successfully rebuilt page directory.
'); + }, + error: () => { + $('#response').html('
Could not rebuild page directory. Try again later.
'); + } + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..76ab177 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "outDir": "./build", + "allowJs": true, + "target": "es5", + "module": "es2020", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true + }, + "include": ["./app/**/*.ts"], +} diff --git a/views/error.ejs b/views/error.ejs new file mode 100644 index 0000000..88e1a27 --- /dev/null +++ b/views/error.ejs @@ -0,0 +1,19 @@ + + + + Error: <%= code %> + + + +
+ <%- include('partials/header') %> +
+ <%- include('partials/navbar') %> +
+

An error occurred (<%= code %>)

+

Go home?

+
+
+
+ + diff --git a/views/index.ejs b/views/index.ejs new file mode 100644 index 0000000..f47b830 --- /dev/null +++ b/views/index.ejs @@ -0,0 +1,18 @@ + + + + <%= title %> + + + +
+ <%- include('partials/header') %> +
+ <%- include('partials/navbar') %> +
+ <%- page %> +
+
+
+ + diff --git a/views/page.ejs b/views/page.ejs new file mode 100644 index 0000000..41ee1e2 --- /dev/null +++ b/views/page.ejs @@ -0,0 +1,20 @@ + + + + <%= title %> + + + +
+ <%- include('partials/header') %> +
+ <%- include('partials/navbar') %> +
+ <%- content %> +
+ GitHub | View raw | Page built: <%= buildTime %> | Purge this page +
+
+
+ + diff --git a/views/partials/header.ejs b/views/partials/header.ejs new file mode 100644 index 0000000..bd11ce0 --- /dev/null +++ b/views/partials/header.ejs @@ -0,0 +1,12 @@ +
+
+
+
diff --git a/views/partials/navbar.ejs b/views/partials/navbar.ejs new file mode 100644 index 0000000..ff0c84d --- /dev/null +++ b/views/partials/navbar.ejs @@ -0,0 +1,3 @@ + diff --git a/views/purge.ejs b/views/purge.ejs new file mode 100644 index 0000000..04bae61 --- /dev/null +++ b/views/purge.ejs @@ -0,0 +1,24 @@ + + + + Purge page + + + + + +
+ <%- include('partials/header') %> +
+ <%- include('partials/navbar') %> +
+

Purge page

+ +

Are you sure you wish to purge the page <%= page %>?

+

The last build time for this page was <%= buildTime %> (<%= buildTimeRelative %> minutes ago).

+ +
+
+
+ + diff --git a/views/rebuild.ejs b/views/rebuild.ejs new file mode 100644 index 0000000..9f9cfaa --- /dev/null +++ b/views/rebuild.ejs @@ -0,0 +1,23 @@ + + + + Rebuild + + + + + +
+ <%- include('partials/header') %> +
+ <%- include('partials/navbar') %> +
+

Rebuild

+ +

Are you sure you wish to rebuild the page directory?

+ +
+
+
+ + -- cgit v1.2.3-70-g09d2