diff --git a/src/client/navigation.ts b/src/client/navigation.ts index c3abcff..fb8985a 100644 --- a/src/client/navigation.ts +++ b/src/client/navigation.ts @@ -1,4 +1,4 @@ -import { MetaGame, Menu } from '../common/types'; +import { MetaGame, Menu, RenderedPage } from '../common/types'; import { Slug } from '../common/slug'; import { clearNotices, notify } from './notices'; import { anchorHeaderFix, addAnchorLinks } from './anchors'; @@ -94,9 +94,9 @@ async function navigate(slug, replace = false, loadData = true) { return; } - const data = await req.json().catch(() => { + const data: RenderedPage = (await req.json().catch(() => { throw new Error('Error parsing page data'); - }); + })) as RenderedPage; document.querySelector('#content').innerHTML = data.content; console.log('NAV RESULT', data); @@ -125,7 +125,7 @@ async function navigate(slug, replace = false, loadData = true) { if (showExclusiveNotice) { notify( - `This page contains sections that are irrelevant to your selected game. If you're missing a section, consider changing your game.`, + `This page contains sections that are irrelevant to your selected game. If you're missing a section, consider changing your game.`, 'eye-off' ); } @@ -150,7 +150,7 @@ async function navigate(slug, replace = false, loadData = true) { document.querySelector('html').className = 'theme-' + info.game; - document.title = `${data.title || 'Page not found'} - ${games[info.game].name} Wiki`; + document.title = `${data.meta.title || 'Page not found'} - ${games[info.game].name} Wiki`; document.querySelector('#current-game').innerText = games[info.game].name; document.querySelector('link[rel=icon]').href = games[info.game].favicon || games[info.game].icon; @@ -159,10 +159,10 @@ async function navigate(slug, replace = false, loadData = true) { document.querySelector('.top-nav .game a').href = `/${info.game}`; - if (loadData || data.file) { + if (loadData || data.path) { // Update the edit button to reflect our current page document.querySelector('.edit a').href = `https://github.com/StrataSource/Wiki/edit/main/${ - data.file || '404.md' + data.path || '404.md' }`; } diff --git a/src/common/types.ts b/src/common/types.ts index 2404542..a7a4929 100644 --- a/src/common/types.ts +++ b/src/common/types.ts @@ -34,40 +34,22 @@ export interface MetaGame { features: string[]; } -export interface Article { - id: string; - content: HTMLString; - name: string; - slug: Slug; - file: string; - meta: PageMeta; -} - export interface PageMeta { title?: string; features?: string[]; + example?: string; } export interface RenderedPage { + path: string; content: HTMLString; meta: PageMeta; } -export interface Index { - [gameID: string]: { - id: string; - categories: { - [categoryID: string]: { - topics: { - [topicID: string]: { - articles: { - [articleID: string]: Article; - }; - }; - }; - }; - }; - }; +export interface Article { + id: string; + slug: Slug; + page: RenderedPage; } export interface MenuArticle { diff --git a/src/exporter/export.ts b/src/exporter/export.ts index 7e252cb..051a0b7 100644 --- a/src/exporter/export.ts +++ b/src/exporter/export.ts @@ -1,43 +1,16 @@ import fs from 'fs-extra'; + import { PageHandler } from './pages'; import { Templater } from './template'; -import { Renderer } from './render'; import { Slug } from '../common/slug'; -import { MetaGame } from '../common/types'; +import { MetaGame, Article, HTMLString } from '../common/types'; export class Exporter { pageHandler: PageHandler; - templater: Templater; - renderer: Renderer; games: MetaGame[]; constructor() { - this.pageHandler = new PageHandler(this); - this.templater = new Templater(); - this.renderer = new Renderer(); - - // Read the pages folder, anything with a meta.json is a "game" - this.games = fs - .readdirSync('../pages') - .filter((game) => fs.existsSync(`../pages/${game}/meta.json`)) - .map( - (game) => - ({ - ...fs.readJSONSync(`../pages/${game}/meta.json`), - id: game - } as MetaGame) - ); - - // For each game, register up a handler for its game exclusive block - for (const game of this.games) { - this.renderer.registerGame(game.id, game.nameShort || game.name || game.id); - } - - // For each game, cache all articles - console.log('Caching articles...'); - for (const game of this.games) { - this.pageHandler.cacheArticles(game.id); - } + this.pageHandler = new PageHandler(); } export() { @@ -52,28 +25,20 @@ export class Exporter { step(); this.clean(); - - step(); this.copyAssets(); - - step(); this.copyResources(); step(); - this.templater.generateNav(this.games); + this.findGameMeta(); + this.copyGameMeta(); step(); this.pageHandler.buildIndex(this.games); + fs.writeFileSync('public/ajax/menu.json', JSON.stringify(this.pageHandler.menu)); step(); this.saveAllPages(); - step(); - this.generateSpecialPages(); - - step(); - this.copyGameMeta(); - const endTime = performance.now(); console.log('Done!'); @@ -100,44 +65,87 @@ export class Exporter { fs.copySync('static', 'public'); } - saveAllPages() { - for (const gameMeta of this.games) - for (const category of Object.values(this.pageHandler.index[gameMeta.id].categories)) - for (const topic of Object.values(category.topics)) - for (const article of Object.values(topic.articles)) { - this.pageHandler.savePage(gameMeta, article); - } + findGameMeta() { + // Read the pages folder, anything with a meta.json is a "game" + this.games = fs + .readdirSync('../pages') + .filter((game) => fs.existsSync(`../pages/${game}/meta.json`)) + .map( + (game) => + ({ + ...fs.readJSONSync(`../pages/${game}/meta.json`), + id: game + } as MetaGame) + ); } copyGameMeta() { + // Copy over the game meta data const games = {}; - for (const game of this.games) { - games[game.id] = game; - } - + for (const game of this.games) games[game.id] = game; fs.writeFileSync('public/ajax/games.json', JSON.stringify(games)); } - generateSpecialPages() { - for (const game of this.games) { - const content = this.renderer.renderPage(`../pages/${game.id}/index.md`); + /** + * Saves an article to the right directories + * @param templater The target template + * @param metaGame The game meta data + * @param article The article + */ + savePage(templater: Templater, metaGame: MetaGame, article: Article): void { + const path = article.slug.toString().split('/').slice(0, -1).join('/'); + + // Write article JSON meta to file + fs.mkdirSync('public/ajax/article/' + path, { recursive: true }); + fs.writeFileSync('public/ajax/article/' + path + '/' + article.id + '.json', JSON.stringify(article.page)); + + // Generate HTML content + const content = templater.applyTemplate({ + metaGame: metaGame, + html: article.page.content, + title: article.page.meta.title || article.id, + menuTopics: this.pageHandler.menu[metaGame.id][article.slug.category] + }); + + // Writing HTML to file + // console.log('public/' + path); + fs.mkdirSync('public/' + path, { recursive: true }); + fs.writeFileSync('public/' + path + '/' + article.id + '.html', content); + } - this.pageHandler.savePage(game, { - ...content, + saveAllPages() { + // Read template HTML + const templateMain: HTMLString = fs.readFileSync('templates/main.html', 'utf8'); + const template404: HTMLString = fs.readFileSync('templates/404.html', 'utf8'); + + // Create the templater for articles + const templater = new Templater(templateMain); + + // Apply templates for all pages + for (const game of this.games) { + // Generate the nav bar links + templater.generateNav(game); + + // Save the Home page + const content = this.pageHandler.articleCache[game.id]['index']; + content.meta.title = 'Home'; + this.savePage(templater, game, { + id: 'index', slug: new Slug(game.id), - name: content.meta.title || 'Home', - file: `/pages/${game.id}/index.md`, - id: 'index' + page: content }); - this.pageHandler.savePage(game, { - ...content, - slug: new Slug(game.id), - name: 'Home', - file: '', + // Save the 404 page + this.savePage(templater, game, { id: '404', - content: fs.readFileSync('templates/404.html', 'utf8') + slug: new Slug(game.id), + page: { content: template404, meta: { title: '404' }, path: '' } }); + + // Save all articles + for (const article of this.pageHandler.gameArticles[game.id]) { + this.savePage(templater, game, article); + } } } } diff --git a/src/exporter/pages.ts b/src/exporter/pages.ts index 1034421..e012824 100644 --- a/src/exporter/pages.ts +++ b/src/exporter/pages.ts @@ -1,19 +1,22 @@ -import { Slug } from '../common/slug'; -import { Article, MetaGame, Index, Menu, MenuArticle, MenuTopic, RenderedPage } from '../common/types'; import fs from 'fs-extra'; -import { Exporter } from './export'; + +import { Slug } from '../common/slug'; +import { Article, MetaGame, Menu, MenuArticle, MenuTopic, RenderedPage } from '../common/types'; +import { Renderer } from './render'; export class PageHandler { - private exporter: Exporter; + renderer: Renderer; menu: Menu; - index: Index; articleCache: { [path: string]: { [article: string]: RenderedPage } }; + gameArticles: { [game: string]: Article[] }; - constructor(exporter) { - this.exporter = exporter; + constructor() { + this.renderer = new Renderer(); + this.menu = {}; this.articleCache = {}; + this.gameArticles = {}; } /** @@ -43,39 +46,41 @@ export class PageHandler { const articleID = articleName.slice(0, ext); // Render and cache it! - const result = this.exporter.renderer.renderPage(topicPath + '/' + articleName); + const result = this.renderer.renderPage(topicPath + '/' + articleName); this.articleCache[topicDir][articleID] = result; } } buildIndex(games: MetaGame[]): void { - const index: Index = {}; - const menu: Menu = {}; + // For each game, register up a handler for its game exclusive block + for (const game of games) { + this.renderer.registerGame(game.id, game.nameShort || game.name || game.id); + } - // For each game, for each category, for each topic, render all articles + // For each game, cache all articles + console.log('Caching articles...'); for (const game of games) { - index[game.id] = { - id: game.id, - categories: {} - }; + this.cacheArticles(game.id); + } - menu[game.id] = {}; + // For each game, for each category, for each topic, build a list of articles + console.log('Building article index...'); + for (const game of games) { + this.gameArticles[game.id] = []; + this.menu[game.id] = {}; for (const category of game.categories) { - // Insert the category into the index - index[game.id].categories[category.id] = { - topics: {} - }; - // If this category is just a redirect, we don't need to render any pages if (category.redirect) continue; // It's a normal category, so we'll need to fill in its topics const menuTopics: MenuTopic[] = []; - menu[game.id][category.id] = menuTopics; + this.menu[game.id][category.id] = menuTopics; for (const topic of category.topics) { + // If the topic lacks a specific path, we'll use its id if (!topic.path) topic.path = topic.id; + // All articles on the topic // Array of tuples of [article, page] const articles: [string, RenderedPage][] = []; @@ -87,7 +92,7 @@ export class PageHandler { continue; } - // Pull in the articles we want from the topic + // Pull in only the articles we want from the topic const topic = this.articleCache[path]; for (const articleFile of Object.keys(topic)) { const page = topic[articleFile]; @@ -101,17 +106,14 @@ export class PageHandler { ) continue; - // Push the parent and the name + // Push the article articles.push([articleFile, page]); } } + // At this point, the topic should have articles if (articles.length === 0) throw new Error(`Could not locate articles: ${game.id}/${topic.path}/`); - index[game.id].categories[category.id].topics[topic.id] = { - articles: {} - }; - // Add topic to menu const articleList: MenuArticle[] = []; const menuTopic: MenuTopic = { @@ -128,16 +130,12 @@ export class PageHandler { const slug = new Slug(game.id, category.id, topic.id, articleID); - const article: Article = { + // Add article to index + this.gameArticles[game.id].push({ id: articleID, - content: page.content, - name: meta.title || articleID, slug: slug, - meta: meta - }; - - // Add article to index - index[game.id].categories[category.id].topics[topic.id].articles[articleID] = article; + page: page + }); // Add the article to menu const entry: MenuArticle = { @@ -153,41 +151,10 @@ export class PageHandler { } // Add to collection of all articles - console.log(`Pushed article ${article.id}`); + // console.log(`Pushed article ${articleID}`); } } } } - - fs.writeFileSync('public/ajax/menu.json', JSON.stringify(menu)); - - this.menu = menu; - this.index = index; - } - - /** - * Saves an article to the right directories - * @param {Object} article The article object - */ - savePage(metaGame: MetaGame, article: Article): void { - const path = article.slug.toString().split('/').slice(0, -1).join('/'); - - // Writing JSON meta to file - console.log('public/ajax/article/' + path); - fs.mkdirSync('public/ajax/article/' + path, { recursive: true }); - fs.writeFileSync('public/ajax/article/' + path + '/' + article.id + '.json', JSON.stringify(article)); - - // Writing HTML to file - console.log('public/' + path); - fs.mkdirSync('public/' + path, { recursive: true }); - fs.writeFileSync( - 'public/' + path + '/' + article.id + '.html', - this.exporter.templater.applyTemplate({ - metaGame: metaGame, - html: article.content, - title: article.name, - menuTopics: this.menu[metaGame.id][article.slug.category] - }) - ); } } diff --git a/src/exporter/render.ts b/src/exporter/render.ts index 7810fd5..f885d16 100644 --- a/src/exporter/render.ts +++ b/src/exporter/render.ts @@ -66,6 +66,7 @@ export class Renderer { render(str: MarkdownString): RenderedPage { this.tempMetaValue = {}; return { + path: '', content: this.md.render(str), meta: this.tempMetaValue }; @@ -78,11 +79,16 @@ export class Renderer { */ renderPage(path: string): RenderedPage { console.log(`Rendering file ${path}`); - - return this.render( + const page = this.render( fs.readFileSync(path, { encoding: 'utf8' }) ); + + // Store paths relative to root directory + if (path.startsWith('../')) path = path.slice(3); + page.path = path; + + return page; } } diff --git a/src/exporter/template.ts b/src/exporter/template.ts index aa2d943..fc72ce2 100644 --- a/src/exporter/template.ts +++ b/src/exporter/template.ts @@ -1,4 +1,3 @@ -import fs from 'fs-extra'; import { HTMLString, MenuTopic, MetaGame } from '../common/types'; export interface TemplaterArgs { @@ -10,6 +9,11 @@ export interface TemplaterArgs { export class Templater { navs: Record = {}; + templateContent: HTMLString; + + constructor(templateContent: HTMLString) { + this.templateContent = templateContent; + } applyTemplate({ metaGame, html, title, menuTopics }: TemplaterArgs): HTMLString { const replacers: Record = {}; @@ -36,7 +40,7 @@ export class Templater { replacers.branch = process.env.CF_PAGES_BRANCH || 'UNAVAILABLE'; // Read template HTML - let res: HTMLString = fs.readFileSync('templates/main.html', 'utf8'); + let res: HTMLString = this.templateContent; // Replacing values from opts in HTML for (const [key, value] of Object.entries(replacers)) { @@ -69,17 +73,15 @@ export class Templater { /** * Generates the top nav links for all pages - * @param games Array of all game meta data + * @param game Game metadata */ - generateNav(games: MetaGame[]) { - for (const game of games) { - // For each of the game's categories, shove a link into the nav bar to it - let str = ''; - for (const category of game.categories) { - const link = category.redirect || `/${game.id}/${category.id}/${category.home}`; - str += `${category.label}`; - } - this.navs[game.id] = str; + generateNav(game: MetaGame) { + // For each of the game's categories, shove a link into the nav bar to it + let str = ''; + for (const category of game.categories) { + const link = category.redirect || `/${game.id}/${category.id}/${category.home}`; + str += `${category.label}`; } + this.navs[game.id] = str; } }