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 --- 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 +- 16 files changed, 326 insertions(+), 484 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 (limited to 'app') 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, '"')); -- cgit v1.2.3-70-g09d2