diff options
Diffstat (limited to 'app')
| -rw-r--r-- | app/constants.mjs | 9 | ||||
| -rw-r--r-- | app/directory.mjs | 116 | ||||
| -rw-r--r-- | app/directory.ts | 210 | ||||
| -rw-r--r-- | app/index.ts (renamed from app/index.mjs) | 54 | ||||
| -rw-r--r-- | app/static/css/globalstyles.css | 100 | ||||
| -rw-r--r-- | app/static/scripts/purge.js | 15 | ||||
| -rw-r--r-- | app/static/scripts/rebuild.js | 14 | ||||
| -rw-r--r-- | app/views/error.ejs | 19 | ||||
| -rw-r--r-- | app/views/index.ejs | 18 | ||||
| -rw-r--r-- | app/views/page.ejs | 20 | ||||
| -rw-r--r-- | app/views/partials/header.ejs | 12 | ||||
| -rw-r--r-- | app/views/partials/navbar.ejs | 3 | ||||
| -rw-r--r-- | app/views/purge.ejs | 24 | ||||
| -rw-r--r-- | app/views/rebuild.ejs | 23 | ||||
| -rw-r--r-- | app/wikiparser.mjs | 7 |
15 files changed, 243 insertions, 401 deletions
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 ? '' : `<h1>${title}</h1>`}${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 + `<div class="navbar-element"><a href="/${page}"${current == page ? ' class="highlight"' : ''}>${pages[page].displayTitle}</a></div>`; - } - 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<string, Page>; + 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<string, Page> { + 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 ? '' : `<h1>${title}</h1>`}${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.ts index 0f14ccf..9b16ec5 100644 --- a/app/index.mjs +++ b/app/index.ts @@ -1,38 +1,44 @@ 'use strict'; -import { SERVER_PORT } from './constants.mjs'; -import * as directory from './directory.mjs'; +import { PageDirectory, Page, PageMetadata } from './directory.js'; import express from 'express'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; +import dotenv from 'dotenv'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); +dotenv.config() const app = express(); +const directory = new PageDirectory(process.env.PAGES_DIR); directory.rebuild(); -app.use(express.static(__dirname + '/static')); +function navbar(current: string = ''): string { + let navbar = ''; + directory.primaryPages.forEach(page => { + navbar += `<div class="navbar-element"><a href="/${page.standardName}"${current == page.standardName ? ' class="highlight"' : ''}>${page.metadata.displayTitle}</a></div>`; + }) + return navbar +} + +app.use(express.static('static')); app.set('view engine', 'ejs'); -app.set('views', __dirname + '/views'); +app.set('views', 'views'); app.get('/:page.wiki', (req, res) => { let path = req.params.page; - let page = directory.pageFor(path); + let raw = directory.getRaw(path); - if (!page) { + if (!raw) { error(res, 404); return; } res.type('text/plain'); - res.send(page.raw).end(); + res.send(raw).end(); }); app.get('/:page?', (req, res) => { let path = req.params.page ?? 'index'; - let page = directory.pageFor(path); + let page = directory.get(path); if (!page) { error(res, 404); @@ -40,17 +46,17 @@ app.get('/:page?', (req, res) => { } res.render('page.ejs', { - navbar: directory.getNavbar(path), + navbar: navbar(), path: path, content: page.html, - title: page.displayTitle, - buildTime: page.buildTime.toString() + 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.rawDataFor(path); + let page = directory.get(path); if (!page) { error(res, 404); @@ -58,16 +64,16 @@ app.get('/special/purge/:page?', (req, res) => { } res.render('purge.ejs', { - navbar: directory.getNavbar(), + navbar: navbar(), page: path, - buildTime: page.buildTime?.toString() ?? 'never', - buildTimeRelative: Math.round((Date.now() - page.buildTime?.getTime()) / 1000 / 60) + 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.rawDataFor(path); + let page = directory.get(path); if (!page) { error(res, 404); @@ -83,7 +89,7 @@ app.get('/special/purge/:page/confirm', (req, res) => { app.get('/special/rebuild', (req, res) => { res.render('rebuild.ejs', { - navbar: directory.getNavbar() + navbar: navbar() }); }); @@ -95,13 +101,13 @@ app.get('/special/rebuild/confirm', (req, res) => { } }); -app.listen(SERVER_PORT, () => { - console.log(`App listening on ${SERVER_PORT}`); +app.listen(process.env.PORT, () => { + console.log(`App listening on ${process.env.PORT}`); }); function error(res, code) { res.render('error.ejs', { code: code, - navbar: directory.getNavbar() + 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('<div class=\'box\'>Successfully purged page.</div>'); - }, - error: () => { - $('#response').html('<div class=\'box\'>Could not purge page. Try again later.</div>'); - } - }); - }); -}); 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('<div class=\'box\'>Successfully rebuilt page directory.</div>'); - }, - error: () => { - $('#response').html('<div class=\'box\'>Could not rebuild page directory. Try again later.</div>'); - } - }); - }); -}); 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 @@ -<!DOCTYPE html> -<html> -<head> - <title>Error: <%= code %></title> - <link rel="stylesheet" href="/css/globalstyles.css"> -</head> -<body> - <div id="main-container"> - <%- include('partials/header') %> - <div id="content-container"> - <%- include('partials/navbar') %> - <div id="content"> - <h1>An error occurred (<%= code %>)</h1> - <p>Go <a href="/">home</a>?</p> - </div> - </div> - </div> -</body> -</html> 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 @@ -<!DOCTYPE html> -<html> -<head> - <title><%= title %></title> - <link rel="stylesheet" href="/css/globalstyles.css"> -</head> -<body> - <div id="main-container"> - <%- include('partials/header') %> - <div id="content-container"> - <%- include('partials/navbar') %> - <div id="content"> - <%- page %> - </div> - </div> - </div> -</body> -</html> 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 @@ -<!DOCTYPE html> -<html> -<head> - <title><%= title %></title> - <link rel="stylesheet" href="/css/globalstyles.css"> -</head> -<body> - <div id="main-container"> - <%- include('partials/header') %> - <div id="content-container"> - <%- include('partials/navbar') %> - <div id="content"> - <%- content %> - <hr> - <span class=footer><a href="https://github.com/LMBishop/website">GitHub</a> | <a href="/<%= path %>.wiki">View raw</a> | Page built: <%= buildTime %> | <a href="/special/purge/<%= path %>">Purge this page</a></span> - </div> - </div> - </div> -</body> -</html> 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 @@ -<pre class="website-name"> - -<!-- -██╗ ███╗ ███╗██████╗ ██╗███████╗██╗ ██╗ ██████╗ ██████╗ -██║ ████╗ ████║██╔══██╗██║██╔════╝██║ ██║██╔═══██╗██╔══██╗ -██║ ██╔████╔██║██████╔╝██║███████╗███████║██║ ██║██████╔╝ -██║ ██║╚██╔╝██║██╔══██╗██║╚════██║██╔══██║██║ ██║██╔═══╝ -███████╗██║ ╚═╝ ██║██████╔╝██║███████║██║ ██║╚██████╔╝██║ -╚══════╝╚═╝ ╚═╝╚═════╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚═╝ - - --> -</pre> 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 @@ -<div id="navbar"> - <%- navbar %> -</div> 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 @@ -<!DOCTYPE html> -<html> -<head> - <title>Purge page</title> - <link rel="stylesheet" href="/css/globalstyles.css"> - <script src="https://code.jquery.com/jquery-3.6.0.min.js" ntegrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> - <script src="/scripts/purge.js"></script> -</head> -<body> - <div id="main-container"> - <%- include('partials/header') %> - <div id="content-container"> - <%- include('partials/navbar') %> - <div id="content"> - <h1>Purge page</h1> - <span id="response"></span> - <p>Are you sure you wish to purge the page <span class="highlight"><%= page %></span>?</p> - <p>The last build time for this page was <span class="highlight"><%= buildTime %></span> (<span class="highlight"><%= buildTimeRelative %></span> minutes ago).</p> - <button id="confirm" data-page="<%= page %>">Confirm</button> - </div> - </div> - </div> -</body> -</html> 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 @@ -<!DOCTYPE html> -<html> -<head> - <title>Rebuild</title> - <link rel="stylesheet" href="/css/globalstyles.css"> - <script src="https://code.jquery.com/jquery-3.6.0.min.js" ntegrity="sha256-/xUj+3OJU5yExlq6GSYGSHk7tPXikynS7ogEvDej/m4=" crossorigin="anonymous"></script> - <script src="/scripts/rebuild.js"></script> -</head> -<body> - <div id="main-container"> - <%- include('partials/header') %> - <div id="content-container"> - <%- include('partials/navbar') %> - <div id="content"> - <h1>Rebuild</h1> - <span id="response"></span> - <p>Are you sure you wish to rebuild the page directory?</p> - <button id="confirm">Confirm</button> - </div> - </div> - </div> -</body> -</html> 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, '"')); |
