This commit is contained in:
TheGiddyLimit
2024-01-01 19:34:49 +00:00
parent 332769043f
commit 8117ebddc5
1748 changed files with 2544409 additions and 1 deletions

184
node/.eslintrc.cjs Normal file
View File

@@ -0,0 +1,184 @@
module.exports = {
"extends": "eslint:recommended",
"env": {
"browser": true,
"es6": true,
"jquery": true,
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
},
"rules": {
"accessor-pairs": "off",
"arrow-spacing": ["error", {"before": true, "after": true}],
"block-spacing": ["error", "always"],
"brace-style": ["error", "1tbs", {"allowSingleLine": true}],
"comma-dangle": ["error", {
"arrays": "always-multiline",
"objects": "always-multiline",
"imports": "always-multiline",
"exports": "always-multiline",
"functions": "always-multiline",
}],
"comma-spacing": ["error", {"before": false, "after": true}],
"comma-style": ["error", "last"],
"constructor-super": "error",
"curly": ["error", "multi-line"],
"dot-location": ["error", "property"],
"eqeqeq": ["error", "always", {"null": "ignore"}],
"func-call-spacing": ["error", "never"],
"generator-star-spacing": ["error", {"before": false, "after": true}],
"handle-callback-err": ["error", "^(err|error)$"],
"indent": [
"error",
"tab",
{
"SwitchCase": 1,
},
],
"key-spacing": ["error", {"beforeColon": false, "afterColon": true}],
"keyword-spacing": ["error", {"before": true, "after": true}],
"new-cap": ["error", {"newIsCap": true, "capIsNew": false}],
"new-parens": "error",
"no-array-constructor": "error",
"no-caller": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-condition": ["error", {"checkLoops": false}],
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-args": "error",
"no-dupe-class-members": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-eval": "error",
"no-ex-assign": "error",
"no-extra-bind": "error",
"no-extra-boolean-cast": "error",
"no-extra-parens": ["error", "functions"],
"no-fallthrough": "error",
"no-floating-decimal": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-implied-eval": "error",
"no-inner-declarations": ["error", "functions"],
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-iterator": "error",
"no-label-var": "error",
"no-labels": ["error", {"allowLoop": true, "allowSwitch": false}],
"no-lone-blocks": "error",
"no-mixed-operators": ["error", {
"groups": [
["==", "!=", "===", "!==", ">", ">=", "<", "<="],
["&&", "||"],
["in", "instanceof"],
],
"allowSamePrecedence": true,
}],
"no-mixed-spaces-and-tabs": "error",
"no-multi-spaces": "error",
"no-multi-str": "error",
"no-multiple-empty-lines": ["error", {"max": 1, "maxEOF": 0}],
"no-negated-in-lhs": "error",
"no-new": "error",
"no-new-func": "error",
"no-new-object": "error",
"no-new-require": "error",
"no-new-symbol": "error",
"no-new-wrappers": "error",
"no-obj-calls": "error",
"no-octal": "error",
"no-octal-escape": "error",
"no-path-concat": "error",
"no-proto": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-return-await": "error",
"no-self-assign": "error",
"no-self-compare": "error",
"no-sequences": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-template-curly-in-string": "error",
"no-this-before-super": "error",
"no-throw-literal": "error",
"no-trailing-spaces": "error",
"no-undef": "off",
"no-undef-init": "error",
"no-unexpected-multiline": "error",
"no-unmodified-loop-condition": "error",
"no-unneeded-ternary": ["error", {"defaultAssignment": false}],
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unused-expressions": ["error", {
"allowShortCircuit": true,
"allowTernary": true,
"allowTaggedTemplates": true,
}],
"no-unused-vars": "off",
"no-use-before-define": ["error", {"functions": false, "classes": false, "variables": false}],
"no-useless-call": "error",
"no-useless-computed-key": "error",
"no-useless-constructor": "error",
"no-useless-escape": "error",
"no-useless-rename": "error",
"no-useless-return": "error",
"no-whitespace-before-property": "error",
"no-with": "error",
"object-property-newline": ["error", {"allowMultiplePropertiesPerLine": true}],
"one-var": ["error", {"initialized": "never"}],
"operator-linebreak": ["error", "after", {
"overrides": {
"?": "before",
":": "before",
"+": "before",
"-": "before",
"*": "before",
"/": "before",
"||": "before",
"&&": "before",
},
}],
"padded-blocks": ["error", {"blocks": "never", "switches": "never", "classes": "never"}],
"prefer-promise-reject-errors": "error",
"rest-spread-spacing": ["error", "never"],
"semi": ["warn", "always"],
"semi-spacing": ["error", {"before": false, "after": true}],
"space-before-blocks": ["error", "always"],
"space-before-function-paren": ["error", "always"],
"space-in-parens": ["error", "never"],
"space-infix-ops": "error",
"space-unary-ops": ["error", {"words": true, "nonwords": false}],
"spaced-comment": ["error", "always", {
"line": {"markers": ["*package", "!", "/", ",", "="]},
"block": {
"balanced": true,
"markers": ["*package", "!", ",", ":", "::", "flow-include"],
"exceptions": ["*"],
},
}],
"symbol-description": "error",
"template-curly-spacing": ["error", "never"],
"template-tag-spacing": ["error", "never"],
"unicode-bom": ["error", "never"],
"use-isnan": "error",
"valid-typeof": ["error", {"requireStringLiterals": true}],
"wrap-iife": ["error", "any", {"functionPrototypeMethods": true}],
"yield-star-spacing": ["error", "both"],
"yoda": ["error", "never"],
"no-prototype-builtins": "off",
"require-atomic-updates": "off",
"no-console": 0,
"prefer-template": "error",
"quotes": ["error", "double", {"allowTemplateLiterals": true}],
"no-var": "error",
},
};

109
node/build-sw.mjs Normal file
View File

@@ -0,0 +1,109 @@
import {injectManifest} from "workbox-build";
import esbuild from "esbuild";
const args = process.argv.slice(2);
const prod = args[0] === "prod";
/**
* convert from bytes to mb and label the units
* @param {number} bytes
* @returns String of the mb conversion with label
*/
const bytesToMb = (bytes) => `${(bytes / 1e6).toPrecision(3)} mb`;
const buildResultLog = (label, buildResult) => {
console.log(`\n${label}:`);
console.log(buildResult);
};
// we need to build the injector first so the glob matches and hashes the newest file
const esbuildBuildResultSwInjector = await esbuild.build({
entryPoints: ["sw-injector-template.js"],
bundle: true,
minify: prod,
drop: prod ? ["console"] : undefined,
allowOverwrite: true,
outfile: "sw-injector.js",
});
buildResultLog("esbuild bundling sw-injector-template.js", esbuildBuildResultSwInjector);
const workboxPrecacheBuildResult = await injectManifest({
swSrc: "sw-template.js",
swDest: "sw.js",
injectionPoint: "self.__WB_PRECACHE_MANIFEST",
maximumFileSizeToCacheInBytes: 5 /* mb */ * 1e6,
globDirectory: "", // use the current directory - run this script from project root.
globPatterns: [
"js/**/*.js", // all js needs to be loaded
"lib/**/*.js", // js in lib needs to be loaded
"css/**/*.css", // all css needs to be loaded
"homebrew/**/*.json", // presumably if there is homebrew data it should also be loaded
"prerelease/**/*.json", // as above
// we want to match all data unless its for an adventure
"data/*.json", // root level data
"data/**/!(adventure)/*.json", // matches all json in data unless it is a file inside a directory called adventure
"*.html", // all html pages need to be loaded
"search/*.json", // search data is needed
"manifest.webmanifest", // we should make sure we have the manifest, although its not strictly needed...
// we want to store fonts to make things styled nicely
"fonts/glyphicons-halflings-regular.woff2",
"fonts/Convergence-Regular.woff2",
"fonts/Roboto-Regular.woff2",
// we need to cache the sw-injector or we won't be injected
"sw-injector.js",
],
});
buildResultLog(
`workbox manifest "self.__WB_PRECACHE_MANIFEST" injection`,
{...workboxPrecacheBuildResult, size: bytesToMb(workboxPrecacheBuildResult.size)},
);
const workboxRuntimeBuildResult = await injectManifest({
swSrc: "sw.js",
swDest: "sw.js",
injectionPoint: "self.__WB_RUNTIME_MANIFEST",
maximumFileSizeToCacheInBytes: 50 /* mb */ * 1e6,
globDirectory: "", // use the current directory - run this script from project root.
/*
it is less then ideal for these globs to match files that were already matched for pre-caching, but it wont break anything
route precedence goes to pre-cache, so they won't fight and double cache the file
however, doubly included files bloat the manifest, so ideal to avoid
*/
globPatterns: [
"data/adventure/**/*.json", // matches all adventure json
"img/**/*", // matches all images
"icon/*.png", // all icons
"*.png", // root images
"*.svg", // root svg
],
manifestTransforms: [
(manifest) =>
({manifest: manifest.map(
entry =>
[
entry.url
// sanitize spaces
.replaceAll(" ", "%20"),
entry.revision,
],
)}),
],
});
buildResultLog(
`workbox manifest "self.__WB_RUNTIME_MANIFEST" injection`,
{...workboxRuntimeBuildResult, size: bytesToMb(workboxRuntimeBuildResult.size)},
);
const esbuildBuildResultSw = await esbuild.build({
entryPoints: ["sw.js"],
bundle: true,
minify: prod,
drop: prod ? ["console"] : undefined,
allowOverwrite: true,
outfile: "sw.js",
});
buildResultLog("esbuild bundling sw-template.js", esbuildBuildResultSw);

162
node/clean-images.js Normal file
View File

@@ -0,0 +1,162 @@
/**
* Rename bestiary fluff images to match the source they originate from.
*
* This script assumes the user has a symlink to the image repo as "img".
*/
import * as fs from "fs";
import * as ut from "./util.js";
function cleanBestiaryFluffImages () {
console.log(`##### Cleaning bestiary fluff images #####`);
// read all the image dirs and track which images are actually in use
const _ALL_IMAGE_PATHS = new Set();
const PATH_BESTIARY_IMAGES = `./img/bestiary/`;
fs.readdirSync(PATH_BESTIARY_IMAGES).forEach(f => {
const path = `${PATH_BESTIARY_IMAGES}/${f}`;
if (fs.lstatSync(path).isDirectory()) {
fs.readdirSync(path).forEach(img => _ALL_IMAGE_PATHS.add(`bestiary/${f}/${img}`));
}
});
function getCleanName (name) {
return name
.replace(/"/g, "")
.replace(/\//g, " ");
}
const ixFluff = JSON.parse(fs.readFileSync(`./data/bestiary/fluff-index.json`, "utf-8"));
const imageNameToCreatureName = {};
Object.entries(ixFluff).forEach(([k, f]) => {
const path = `./data/bestiary/${f}`;
const fluff = JSON.parse(fs.readFileSync(path, "utf-8"));
ixFluff[k] = {path, json: fluff}; // store the parsed data in the index object, for later writing
fluff.monster.forEach(mon => {
if (mon.images) {
mon.images.forEach(img => {
(imageNameToCreatureName[img.href.path] =
imageNameToCreatureName[img.href.path] || []).push({name: mon.name, fluff: mon});
_ALL_IMAGE_PATHS.delete(img.href.path);
});
}
});
});
if (_ALL_IMAGE_PATHS.size) {
console.error(`The following images were unused:`);
_ALL_IMAGE_PATHS.forEach(it => console.error(it));
throw new Error(`Could not clean bestiary fluff!`);
}
// a value of "true" in the mapping represents "map to the shortest available name in the array"
const MAP_TO_SHORTEST = {
"bestiary/GoS/Locathah.webp": true,
"bestiary/MM/Gnoll.webp": true,
"bestiary/MM/Orc.webp": true,
"bestiary/MTF/Deathlock.webp": true,
"bestiary/MTF/Tortle.webp": true,
"bestiary/VGM/Darkling.webp": true,
"bestiary/VGM/Grung.webp": true,
"bestiary/VGM/Neogi.webp": true,
"bestiary/VGM/Shadow-Mastiff.webp": true,
};
// a value of "true" in the mapping represents "map directly"
const MAP_TO_VALUE = {
"bestiary/GGR/Rakdos Performer.webp": true,
"bestiary/GoS/Drowned-One.webp": true,
"bestiary/MM/Dinosaurs.webp": true,
"bestiary/MM/Giants.webp": true,
"bestiary/MM/Fungi.webp": true,
"bestiary/MM/Blights.webp": true,
"bestiary/MM/Myconids.webp": true,
"bestiary/MTF/Kruthik.webp": true,
"bestiary/MTF/Oblex.webp": true,
"bestiary/MTF/Steeder.webp": true,
"bestiary/MTF/Gith.webp": true,
"bestiary/MTF/Sword-Wraith.webp": true,
"bestiary/VGM/Alhoon.webp": true,
"bestiary/VGM/Firenewt.webp": true,
"bestiary/VGM/Vegepygmy.webp": true,
"bestiary/PotA/Crushing-Wave-Cultists.webp": true,
"bestiary/ToA/Albino Dwarf.webp": true,
"bestiary/WDH/The-Gralhunds.webp": true,
};
const multipleNames = Object.entries(imageNameToCreatureName)
.filter(([img, metas]) => !MAP_TO_SHORTEST[img] && !MAP_TO_VALUE[img] && metas.length > 1);
if (multipleNames.length) {
console.error(`The following images could be mapped to multiple names:`);
multipleNames.forEach(([img, metas]) => {
console.error(`\t${`${img}\t`.padEnd(55, " ")} ${metas.map(m => m.name).join(", ")}`);
});
throw new Error(`Could not clean bestiary fluff!`);
}
// handle any special renames first
Object.entries(imageNameToCreatureName).forEach(([img, metas]) => {
if (MAP_TO_SHORTEST[img]) {
const nameLen = Math.min(...metas.map(it => it.name.length));
const cleanShortName = getCleanName(metas.find(it => it.name.length === nameLen).name);
const pathParts = img.split("/");
const namePart = pathParts.pop();
const spl = namePart.split(".");
if (spl.length > 2) throw new Error(`Could not extract extension from name "${namePart}"`);
const nuPath = [...pathParts, `${cleanShortName}.${spl[1]}`].join("/");
fs.renameSync(`./img/${img}`, `./img/${nuPath}`);
metas.forEach(meta => {
meta.fluff.images
.filter(it => it.href.path === img && !it._IS_RENAMED)
.forEach(it => {
it.href.path = nuPath;
it._IS_RENAMED = true;
});
});
} else if (MAP_TO_VALUE[img]) {
const nuPath = MAP_TO_VALUE[img] === true ? img.replace(/-/g, " ") : MAP_TO_VALUE[img];
if (nuPath !== img) fs.renameSync(`./img/${img}`, `./img/${nuPath}`);
metas.forEach(meta => {
meta.fluff.images
.filter(it => it.href.path === img && !it._IS_RENAMED)
.forEach(it => {
it.href.path = nuPath;
it._IS_RENAMED = true;
});
});
}
});
// handle straight renames
Object.entries(imageNameToCreatureName)
.filter(([img, metas]) => !MAP_TO_SHORTEST[img] && !MAP_TO_VALUE[img])
.forEach(([img, metas]) => {
const meta = metas[0];
const cleanName = getCleanName(meta.name);
meta.fluff.images
.filter(it => !it._IS_RENAMED)
.map((it, i) => {
const pathParts = it.href.path.split("/");
const namePart = pathParts.pop();
const spl = namePart.split(".");
if (spl.length > 2) throw new Error(`Could not extract extension from name "${namePart}"`);
const nuPath = [...pathParts, `${cleanName}${i > 0 ? ` ${`${i}`.padStart(3, "0")}` : ""}.${spl[1]}`].join("/");
fs.renameSync(`img/${it.href.path}`, `img/${nuPath}`);
it.href.path = nuPath;
it._IS_RENAMED = true;
});
});
// cleanup
Object.values(imageNameToCreatureName)
.forEach(metas => metas.forEach(meta => meta.fluff.images.forEach(it => delete it._IS_RENAMED)));
// write JSON files
Object.values(ixFluff).forEach(meta => fs.writeFileSync(meta.path, CleanUtil.getCleanJson(meta.json), "utf-8"));
console.log(`Done!`);
}
cleanBestiaryFluffImages();

27
node/clean-jsons.js Normal file
View File

@@ -0,0 +1,27 @@
"use strict";
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
function cleanFolder (folder, {isFast = false} = {}) {
console.log(`Cleaning directory ${folder}...`);
const files = ut.listFiles({
dir: folder,
blocklistFilePrefixes: ut.FILE_PREFIX_BLOCKLIST
.filter(it => it !== "foundry-"),
});
files
.filter(file => file.endsWith(".json"))
.forEach(file => {
console.log(`\tCleaning ${file}...`);
fs.writeFileSync(file, CleanUtil.getCleanJson(ut.readJson(file), {isFast}), "utf-8");
});
}
cleanFolder(`./data`);
cleanFolder(`./homebrew`, {isFast: true});
console.log("Cleaning complete.");

68
node/clean-tags.js Normal file
View File

@@ -0,0 +1,68 @@
"use strict";
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/utils.js";
const BLOCKLIST_FILE_PREFIXES = [
...ut.FILE_PREFIX_BLOCKLIST,
// specific files
"demo.json",
];
const TAGS_TO_CHECK = new Set([
"spell",
"item",
"creature",
"condition",
"disease",
"background",
"race",
"optfeature",
"reward",
"feat",
"psionic",
"object",
"cult",
"boon",
"trap",
"hazard",
"variantrule",
"vehicle",
]);
const files = ut.listFiles({dir: `./data`, blocklistFilePrefixes: BLOCKLIST_FILE_PREFIXES});
files.forEach(f => {
const compressedTags = fs.readFileSync(f, "utf-8").replace(/{@([a-zA-Z]+) ([^|}]+)\|([^|}]*)\|([^|}]+)}/g, (...m) => {
const [all, tag, ref, src, txt] = m;
if (!TAGS_TO_CHECK.has(tag.toLowerCase())) return all;
if (ref.toLowerCase() === txt.toLowerCase()) {
if (!src || Parser.getTagSource(tag.toLowerCase()) === src.toLowerCase()) {
return `{@${tag} ${txt}${src ? `|${src}` : ""}}`;
}
}
return all;
});
const compressedSources = compressedTags.replace(/{@([a-zA-Z]+) ([^|}]+)\|([^|}]+)\|([^|}]+)}/g, (...m) => {
const [all, tag, ref, src, txt] = m;
if (!TAGS_TO_CHECK.has(tag.toLowerCase())) return all;
if (Parser.getTagSource(tag) === src.toLowerCase()) {
return `{@${tag} ${ref}||${txt}}`;
}
return all;
});
const skippedDefaultSources = compressedSources.replace(/{@([a-zA-Z]+) ([^|}]+)\|([^|}]+)}/g, (...m) => {
const [all, tag, ref, src] = m;
if (!TAGS_TO_CHECK.has(tag.toLowerCase())) return all;
if (Parser.getTagSource(tag) === src.toLowerCase()) {
return `{@${tag} ${ref}}`;
}
return all;
});
const out = CleanUtil.getCleanJson(skippedDefaultSources);
fs.writeFileSync(f, JSON.parse(out), "utf-8");
});

View File

@@ -0,0 +1,44 @@
/**
* Can be used to inspect which HTML files depend on which scripts.
*/
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/utils.js";
const files = ut.listFiles({
dir: ".",
allowlistDirs: [],
allowlistFileExts: [".html"],
});
const ALL_JS_FILES = new Set([]);
const FILE_TO_JS_FILES = {};
files.forEach(file => {
const cleanFilename = file.replace("./", "");
const html = fs.readFileSync(file, "utf-8");
html.replace(/src="js\/(.*?\.js)"/g, (...m) => {
ALL_JS_FILES.add(m[1]);
(FILE_TO_JS_FILES[cleanFilename] = FILE_TO_JS_FILES[cleanFilename] || [])
.push(m[1]);
});
});
const out = [];
const numFiles = Object.keys(FILE_TO_JS_FILES).length;
[...ALL_JS_FILES].sort(SortUtil.ascSortLower).forEach(file => {
const total = Object.values(FILE_TO_JS_FILES).filter(it => it.includes(file)).length;
out.push({file, total});
});
out.sort((a, b) => SortUtil.ascSort(b.total, a.total))
.forEach(it => {
if (it.total > numFiles / 2 && it.total < numFiles) {
const notSeenIn = Object.keys(FILE_TO_JS_FILES).filter(k => !FILE_TO_JS_FILES[k].includes(it.file));
console.log(`${it.total}/${numFiles} ${it.file} -- missing from:`);
notSeenIn.forEach(it => console.log(`\t${it}`));
} else {
console.log(`${it.total}/${numFiles} ${it.file}`);
}
});

33
node/generate-all-maps.js Normal file
View File

@@ -0,0 +1,33 @@
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/maps-util.js";
const out = {};
console.log("Updating maps...");
[
{
prop: "adventure",
index: `./data/adventures.json`,
dir: `./data/adventure`,
},
{
prop: "book",
index: `./data/books.json`,
dir: `./data/book`,
},
].forEach(({prop, index, dir}) => {
ut.readJson(index)[prop].forEach(head => {
console.log(`\tGenerating map data for ${head.id}`);
const body = ut.readJson(`${dir}/${prop}-${head.id.toLowerCase()}.json`).data;
const imageData = MapsUtil.getImageData({prop, head, body});
if (imageData) Object.assign(out, imageData);
});
});
fs.writeFileSync("data/generated/gendata-maps.json", JSON.stringify(out), "utf8");
console.log("Updated maps.");

15
node/generate-all.js Normal file
View File

@@ -0,0 +1,15 @@
async function main () {
await import("./generate-dmscreen-reference.js");
await import("./generate-quick-reference.js");
await (await import("./generate-tables-data.js")).default;
await import("./generate-subclass-lookup.js");
await (await import("./generate-spell-source-lookup.js")).default;
await import("./generate-nav-adventure-book-index.js");
await import("./generate-all-maps.js");
// await import("./generate-wotc-homebrew.js"); // unused
// Generate the search index at the end, as it catches data generated earlier
await (await import("./generate-search-index.js")).default;
}
main().catch(e => { throw e; });

View File

@@ -0,0 +1,79 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import * as utB from "./util-book-reference.js";
const index = utB.UtilBookReference.getIndex(
{
name: "Quick Reference",
id: "bookref-quick",
tag: "quickref",
},
{
name: "DM Reference",
id: "bookref-dmscreen",
tag: "dmref",
},
);
fs.writeFileSync("data/generated/bookref-dmscreen.json", CleanUtil.getCleanJson(index, {isMinify: true}), "utf8");
function flattenReferenceIndex (ref, skipHeaders) {
const outMeta = {
name: {},
id: {},
section: {},
};
const meta = {
name: {},
id: {},
section: {},
};
const out = [];
let nameId = 1;
let idId = 1;
let sectionId = 1;
let indexId = 1;
Object.values(ref).forEach(book => {
if (!meta.name[book.name]) {
outMeta.name[nameId] = book.name;
meta.name[book.name] = nameId++;
}
if (!meta.id[book.id]) {
outMeta.id[idId] = book.id;
meta.id[book.id] = idId++;
}
book.contents.forEach((c, i) => {
if (!meta.section[c.name]) {
outMeta.section[sectionId] = c.name;
meta.section[c.name] = sectionId++;
}
if (skipHeaders) return;
(c.headers || []).forEach(h => {
out.push({
id: indexId++,
b: meta.id[book.id], // book
s: meta.section[c.name], // section name
p: i, // section index
h, // header name
});
});
});
});
return {
_meta: outMeta,
data: out,
};
}
fs.writeFileSync("data/generated/bookref-dmscreen-index.json", JSON.stringify(flattenReferenceIndex(index.reference)), "utf8");
console.log("Updated DM Screen references.");

View File

@@ -0,0 +1,94 @@
import * as fs from "fs";
import sharp from "sharp";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
const _NUM_WORKERS = 16;
const _QUALITY = 70;
const _SZ_THUMBNAIL_PX = 320;
async function getFileInput ({pathName, ext}) {
// See: https://github.com/lovell/sharp/issues/806
if (ext === "bmp") throw new Error(`Generating thumbnails from .bmp is not supported!`);
return sharp(pathName, {limitInputPixels: false});
}
async function webpFile ({pathInput, pathOutput, extInput}) {
const x = await getFileInput({pathName: pathInput, ext: extInput});
// See: https://sharp.pixelplumbing.com/api-output#webp
await x
.resize(_SZ_THUMBNAIL_PX, _SZ_THUMBNAIL_PX, {fit: "contain", background: {r: 0, g: 0, b: 0, alpha: 0}})
.webp({quality: _QUALITY})
.toFile(pathOutput);
}
async function pMain () {
const tasksGenThumbnail = [];
const walker = MiscUtil.getWalker({isNoModification: true});
[
{index: "adventures", dir: "adventure", prop: "adventure"},
{index: "books", dir: "book", prop: "book"},
]
.map(meta => {
const indexData = ut.readJson(`data/${meta.index}.json`);
indexData[meta.prop]
.forEach(contents => {
const filePath = `data/${meta.dir}/${meta.prop}-${contents.id.toLowerCase()}.json`;
const data = ut.readJson(filePath);
walker.walk(
data.data,
{
object: (obj) => {
if (obj.type !== "image" || !obj.mapRegions) return;
const imgPath = obj.href.path;
const pathParts = imgPath.split("/");
const thumbPath = [...pathParts.slice(0, -1), "thumbnail", pathParts.last().replace(/\.[^.]+$/g, ".webp")].join("/");
obj.hrefThumbnail = {
type: "internal",
path: thumbPath,
};
tasksGenThumbnail.push({imgPath, thumbPath});
},
},
);
fs.writeFileSync(filePath, CleanUtil.getCleanJson(data), "utf8");
});
});
console.log(`Generating thumbnails for ${tasksGenThumbnail.length} map${tasksGenThumbnail.length === 1 ? "" : "s"}...`);
tasksGenThumbnail.map(({thumbPath}) => {
const thumbDir = ["img", ...thumbPath.split("/").slice(0, -1)].join("/");
if (!fs.existsSync(thumbDir)) fs.mkdirSync(thumbDir, {recursive: true});
});
let cnt = 0;
const workers = [...new Array(_NUM_WORKERS)]
.map(async () => {
while (tasksGenThumbnail.length) {
const {imgPath, thumbPath} = tasksGenThumbnail.pop();
const extInput = imgPath.toLowerCase().split(".").last();
await webpFile({pathInput: `img/${imgPath}`, pathOutput: `img/${thumbPath}`, extInput});
cnt++;
if ((cnt % 25) === 0) console.log(`Generated ${cnt}...`);
}
});
await Promise.all(workers);
console.log(`Generated ${cnt} thumbnails!`);
}
pMain()
.then(() => console.log("Regenerated map thumbnails."))
.catch(e => { throw e; });

View File

@@ -0,0 +1,42 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import * as ut from "./util.js";
const _PROPS_TO_INDEX = [
"name",
"id",
"source",
"parentSource",
"group",
"level",
"storyline",
"published",
"publishedOrder",
];
const _METAS = [
{
file: `data/adventures.json`,
prop: "adventure",
},
{
file: `data/books.json`,
prop: "book",
},
];
const out = _METAS.mergeMap(({file, prop}) => {
return {[prop]: ut.readJson(file)[prop]
.map(it => _PROPS_TO_INDEX.mergeMap(prop => {
const sub = {[prop]: it[prop]};
// Expand the parent source, as the navbar doesn't wait for load completion.
// (Although in this instance, it actually does, but this keeps the API sane.)
if (prop === "parentSource" && it[prop]) sub.parentName = Parser.sourceJsonToFull(it[prop]);
return sub;
})),
};
});
fs.writeFileSync("data/generated/gendata-nav-adventure-book-index.json", JSON.stringify(out), "utf8");
console.log("Generated navbar adventure/book index.");

494
node/generate-pages.js Normal file
View File

@@ -0,0 +1,494 @@
import Handlebars from "handlebars";
import "../js/parser.js";
import "../js/utils.js";
import fs from "fs";
class _HtmlGenerator {
static _getAttrClass (str, {classListAdditional = null} = {}) {
const pts = [
str,
classListAdditional?.length ? classListAdditional.join(" ") : "",
]
.filter(Boolean)
.join(" ");
if (!pts) return null;
return `class="${pts}"`;
}
}
class _HtmlGeneratorListButtons extends _HtmlGenerator {
static getBtnPreviewToggle () {
return `<button class="col-0-3 btn btn-default btn-xs p-0 lst__btn-collapse-all-previews" name="list-toggle-all-previews">[+]</button>`;
}
static getBtnSource () {
return `<button class="sort btn btn-default btn-xs ve-grow" data-sort="source">Source</button>`;
}
/**
* @param {string} width
* @param {?string} sortIdent
* @param {string} text
* @param {?boolean} isDisabled
* @param {?Array<string>} classListAdditional
* @return {string}
*/
static getBtn (
{
width,
sortIdent = null,
text,
isDisabled = false,
classListAdditional = null,
},
) {
const attrs = [
this._getAttrClass(`col-${width} sort btn btn-default btn-xs`, {classListAdditional}),
sortIdent ? `data-sort="${sortIdent}"` : null,
isDisabled ? `disabled` : null,
]
.filter(Boolean)
.join(" ");
return `<button ${attrs}>${text}</button>`;
}
}
class _HtmlGeneratorListToken extends _HtmlGenerator {
/**
* @param {?Array<string>} classListAdditional
* @return {string}
*/
static getWrpToken ({classListAdditional = null} = {}) {
const attrs = [
`id="float-token"`,
this._getAttrClass(`relative`, {classListAdditional}),
]
.filter(Boolean)
.join(" ");
return `<div ${attrs}></div>`;
}
}
/** @abstract */
class _PageGeneratorBase {
_filename;
_page;
init () {
this._registerPartials();
return this;
}
_registerPartial ({ident, filename}) {
Handlebars.registerPartial(ident, this.constructor._getLoadedSource({filename}));
}
_registerPartials () {
this._registerPartial({ident: "head", filename: "head/template-head.hbs"});
this._registerPartial({ident: "adRhs", filename: "ad/template-ad-rhs.hbs"});
this._registerPartial({ident: "adLeaderboard", filename: "ad/template-ad-leaderboard.hbs"});
this._registerPartial({ident: "adMobile1", filename: "ad/template-ad-mobile-1.hbs"});
this._registerPartial({ident: "adFooter", filename: "ad/template-ad-footer.hbs"});
this._registerPartial({ident: "navbar", filename: "navbar/template-navbar.hbs"});
this._registerPartial({ident: "blank", filename: "misc/template-blank.hbs"});
}
/**
* @abstract
* @return {object}
*/
_getData () { throw new Error("Unimplemented!"); }
generatePage () {
const template = Handlebars.compile(this.constructor._getLoadedSource({filename: this._filename}));
const rendered = template(this._getData())
.split("\n")
.map(l => l.trimEnd())
.join("\n");
fs.writeFileSync(this._page, rendered, "utf-8");
}
static _getLoadedSource ({filename}) {
return fs.readFileSync(`./node/generate-pages/${filename}`, "utf-8");
}
}
class _PageGeneratorListBase extends _PageGeneratorBase {
_filename = "list/template-list.hbs";
_page;
_titlePage;
_navbarTitle;
_isFontAwesome = false;
_stylesheets;
_isStyleBook = false;
_scriptIdentList;
_scriptsUtilsAdditional;
_scriptsPrePageAdditional;
_isModule = false;
_isMultisource = false;
_btnsList;
_btnsSublist;
_wrpToken;
_styleListContainerAdditional;
_styleContentWrapperAdditional;
_isPrinterView = false;
_registerPartials () {
super._registerPartials();
this._registerPartial({ident: "listListcontainer", filename: "list/template-list-listcontainer.hbs"});
this._registerPartial({ident: "listFilterSearchGroup", filename: "list/template-list-filter-search-group.hbs"});
this._registerPartial({ident: "listFiltertools", filename: "list/template-list-filtertools.hbs"});
this._registerPartial({ident: "listList", filename: "list/template-list-list.hbs"});
this._registerPartial({ident: "listContentwrapper", filename: "list/template-list-contentwrapper.hbs"});
this._registerPartial({ident: "listSublistContainer", filename: "list/template-list-sublist-container.hbs"});
this._registerPartial({ident: "listSublist", filename: "list/template-list-sublist.hbs"});
this._registerPartial({ident: "listSublistsort", filename: "list/template-list-sublistsort.hbs"});
this._registerPartial({ident: "listStatsTabs", filename: "list/template-list-stats-tabs.hbs"});
this._registerPartial({ident: "listWrpPagecontent", filename: "list/template-list-wrp-pagecontent.hbs"});
this._registerPartial({ident: "listRhsWrpFooterControls", filename: "list/template-list-rhs-wrp-footer-controls.hbs"});
this._registerPartial({ident: "listRhsWrpToken", filename: "list/template-list-rhs-wrp-token.hbs"});
this._registerPartial({ident: "listScripts", filename: "list/template-list-scripts.hbs"});
}
_getData () {
return {
titlePage: this._titlePage,
navbarTitle: this._navbarTitle ?? this._titlePage,
navbarDescription: "Search by name on the left, click a name to display on the right.",
isFontAwesome: this._isFontAwesome,
stylesheets: this._stylesheets,
scriptIdentList: this._scriptIdentList,
scriptsUtilsAdditional: this._scriptsUtilsAdditional,
scriptsPrePageAdditional: this._scriptsPrePageAdditional,
isModule: this._isModule,
isMultisource: this._isMultisource,
btnsList: this._btnsList,
btnsSublist: this._btnsSublist,
wrpToken: this._wrpToken,
isStyleBook: this._isStyleBook,
styleListContainerAdditional: this._styleListContainerAdditional,
styleContentWrapperAdditional: this._styleContentWrapperAdditional,
identPartialListListcontainer: "listListcontainer",
identPartialListContentwrapper: "listContentwrapper",
isPrinterView: this._isPrinterView,
};
}
}
class _PageGeneratorListActions extends _PageGeneratorListBase {
_page = UrlUtil.PG_ACTIONS;
_titlePage = "Actions";
_scriptIdentList = "actions";
_btnsList = [
_HtmlGeneratorListButtons.getBtnPreviewToggle(),
_HtmlGeneratorListButtons.getBtn({width: "5-7", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "time", text: "Time"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "8", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "time", text: "Time"}),
];
}
class _PageGeneratorListBackgrounds extends _PageGeneratorListBase {
_page = UrlUtil.PG_BACKGROUNDS;
_titlePage = "Backgrounds";
_scriptIdentList = "backgrounds";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "6", sortIdent: "skills", text: "Skill Proficiencies"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "8", sortIdent: "skills", text: "Skills"}),
];
_isPrinterView = true;
}
class _PageGeneratorListBestiary extends _PageGeneratorListBase {
_page = UrlUtil.PG_BESTIARY;
_titlePage = "Bestiary";
_stylesheets = [
"bestiary",
"encounterbuilder-bundle",
];
_scriptIdentList = "bestiary";
_scriptsUtilsAdditional = [
"utils-tableview.js",
];
_isModule = true;
_isMultisource = true;
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "4-2", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "4-1", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "1-7", sortIdent: "cr", text: "CR"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "5", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "3-8", classListAdditional: ["best-ecgen__hidden"], sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "3-8", classListAdditional: ["best-ecgen__visible"], isDisabled: true, text: "&nbsp;"}),
_HtmlGeneratorListButtons.getBtn({width: "1-2", sortIdent: "cr", text: "CR"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "count", text: "Number"}),
];
_registerPartials () {
super._registerPartials();
this._registerPartial({
ident: "listContentwrapperBestiary",
filename: "list/template-list-contentwrapper--bestiary.hbs",
});
this._registerPartial({
ident: "listSublistContainerBestiary",
filename: "list/template-list-sublist-container--bestiary.hbs",
});
}
_getData () {
return {
...super._getData(),
identPartialListContentwrapper: "listContentwrapperBestiary",
};
}
_isPrinterView = true;
}
class _PageGeneratorListCharCreationOptions extends _PageGeneratorListBase {
_page = UrlUtil.PG_CHAR_CREATION_OPTIONS;
_titlePage = "Other Character Creation Options";
_scriptIdentList = "charcreationoptions";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "5", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "5", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "5", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "7", sortIdent: "name", text: "Name"}),
];
}
class _PageGeneratorListConditionsDiseases extends _PageGeneratorListBase {
_page = UrlUtil.PG_CONDITIONS_DISEASES;
_titlePage = "Conditions & Diseases";
_scriptIdentList = "conditionsdiseases";
_btnsList = [
_HtmlGeneratorListButtons.getBtnPreviewToggle(),
_HtmlGeneratorListButtons.getBtn({width: "3", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "6-7", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "10", sortIdent: "name", text: "Name"}),
];
}
class _PageGeneratorListCultsBoons extends _PageGeneratorListBase {
_page = UrlUtil.PG_CULTS_BOONS;
_titlePage = "Cults & Supernatural Boons";
_scriptIdentList = "cultsboons";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "subType", text: "Subtype"}),
_HtmlGeneratorListButtons.getBtn({width: "6", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "type", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "subType", text: "Subtype"}),
_HtmlGeneratorListButtons.getBtn({width: "8", sortIdent: "name", text: "Name"}),
];
}
class _PageGeneratorListDecks extends _PageGeneratorListBase {
_page = UrlUtil.PG_DECKS;
_titlePage = "Decks";
_isFontAwesome = true;
_stylesheets = [
"decks",
];
_isStyleBook = true;
_scriptIdentList = "decks";
_styleListContainerAdditional = "ve-flex-4";
_styleContentWrapperAdditional = "ve-flex-7";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "10", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "12", sortIdent: "name", text: "Name"}),
];
}
class _PageGeneratorListDeities extends _PageGeneratorListBase {
_page = UrlUtil.PG_DEITIES;
_titlePage = "Deities";
_scriptIdentList = "deities";
_styleListContainerAdditional = "ve-flex-6";
_styleContentWrapperAdditional = "ve-flex-4";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "3", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "pantheon", text: "Pantheon"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "alignment", text: "Alignment"}),
_HtmlGeneratorListButtons.getBtn({width: "3", sortIdent: "domains", text: "Domains"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "pantheon", text: "Pantheon"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "alignment", text: "Alignment"}),
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "domains", text: "Domains"}),
];
}
class _PageGeneratorListFeats extends _PageGeneratorListBase {
_page = UrlUtil.PG_FEATS;
_titlePage = "Feats";
_scriptIdentList = "feats";
_btnsList = [
_HtmlGeneratorListButtons.getBtnPreviewToggle(),
_HtmlGeneratorListButtons.getBtn({width: "3-5", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "3-5", sortIdent: "ability", text: "Ability"}),
_HtmlGeneratorListButtons.getBtn({width: "3", sortIdent: "prerequisite", text: "Prerequisite"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "ability", text: "Ability"}),
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "prerequisite", text: "Prerequisite"}),
];
_isPrinterView = true;
}
class _PageGeneratorListItems extends _PageGeneratorListBase {
_page = UrlUtil.PG_ITEMS;
_titlePage = "Items";
_stylesheets = [
"items",
];
_scriptIdentList = "items";
_scriptsUtilsAdditional = [
"utils-tableview.js",
];
_styleContentWrapperAdditional = "itm__wrp-stats";
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "6", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "weight", text: "Weight"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "cost", text: "Cost"}),
_HtmlGeneratorListButtons.getBtn({width: "2", sortIdent: "count", text: "Number"}),
];
_registerPartials () {
super._registerPartials();
this._registerPartial({
ident: "listListcontainerItems",
filename: "list/template-list-listcontainer--items.hbs",
});
this._registerPartial({
ident: "listContentwrapperItems",
filename: "list/template-list-contentwrapper--items.hbs",
});
this._registerPartial({
ident: "listSublistContainerItems",
filename: "list/template-list-sublist-container--items.hbs",
});
}
_getData () {
return {
...super._getData(),
identPartialListListcontainer: "listListcontainerItems",
identPartialListContentwrapper: "listContentwrapperItems",
};
}
_isPrinterView = true;
}
class _PageGeneratorListTrapsHazards extends _PageGeneratorListBase {
_page = UrlUtil.PG_TRAPS_HAZARDS;
_titlePage = "Traps & Hazards";
_scriptIdentList = "trapshazards";
_btnsList = [
_HtmlGeneratorListButtons.getBtn({width: "3", sortIdent: "trapType", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "7", sortIdent: "name", text: "Name"}),
_HtmlGeneratorListButtons.getBtnSource(),
];
_btnsSublist = [
_HtmlGeneratorListButtons.getBtn({width: "4", sortIdent: "trapType", text: "Type"}),
_HtmlGeneratorListButtons.getBtn({width: "8", sortIdent: "name", text: "Name"}),
];
}
const generators = [
new _PageGeneratorListActions(),
new _PageGeneratorListBackgrounds(),
new _PageGeneratorListBestiary(),
new _PageGeneratorListCharCreationOptions(),
new _PageGeneratorListConditionsDiseases(),
new _PageGeneratorListCultsBoons(),
new _PageGeneratorListDecks(),
new _PageGeneratorListDeities(),
new _PageGeneratorListFeats(),
new _PageGeneratorListItems(),
new _PageGeneratorListTrapsHazards(),
];
generators
.map(gen => gen.init())
.forEach(generator => generator.generatePage());

View File

@@ -0,0 +1,2 @@
<!--5ETOOLS_SCRIPT_ANCHOR-->
<!--5ETOOLS_AD_ADHESION-->

View File

@@ -0,0 +1 @@
<div class="cancer__wrp-leaderboard cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__wrp-leaderboard-inner"><!--5ETOOLS_AD_LEADERBOARD--></div></div>

View File

@@ -0,0 +1 @@
<div class="cancer__wrp-mobile-1 cancer__anchor"><div class="cancer__disp-cancer"></div><!--5ETOOLS_AD_MOB_PLAYER_1--></div>

View File

@@ -0,0 +1 @@
<div class="cancer__wrp-sidebar-rhs cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--top"><!--5ETOOLS_AD_RIGHT_1--></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--bottom"><!--5ETOOLS_AD_RIGHT_2--></div></div>

View File

@@ -0,0 +1,58 @@
<head>
<!--5ETOOLS_CMP-->
<!--5ETOOLS_ANALYTICS-->
<!--5ETOOLS_ADCODE-->
<meta charset="utf-8">
<meta name="description" content="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>{{titlePage}} - 5etools</title>
<link rel="stylesheet" href="css/bootstrap.css">
{{#if isFontAwesome}} <link rel="stylesheet" href="css/fontawesome.css">
{{/if}}
<link rel="stylesheet" href="css/main.css">
{{#each stylesheets}} <link rel="stylesheet" href="css/{{this}}.css">
{{/each}}
<!-- Favicons -->
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="icon" type="image/png" sizes="256x256" href="favicon-256x256.png">
<link rel="icon" type="image/png" sizes="144x144" href="favicon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="favicon-128x128.png">
<link rel="icon" type="image/png" sizes="64x64" href="favicon-64x64.png">
<link rel="icon" type="image/png" sizes="48x48" href="favicon-48x48.png">
<link rel="icon" type="image/png" sizes="32x32" href="favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="favicon-16x16.png">
<!-- Chrome Web App Icons -->
<link rel="manifest" href="manifest.webmanifest">
<meta name="application-name" content="5etools">
<meta name="theme-color" content="#006bc4">
<!-- Windows Start Menu tiles -->
<meta name="msapplication-config" content="browserconfig.xml"/>
<meta name="msapplication-TileColor" content="#006bc4">
<!-- Apple Touch Icons -->
<link rel="apple-touch-icon" sizes="180x180" href="apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="360x360" href="apple-touch-icon-360x360.png">
<link rel="apple-touch-icon" sizes="167x167" href="apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="152x152" href="apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="apple-touch-icon-120x120.png">
<meta name="apple-mobile-web-app-title" content="5etools">
<!-- macOS Safari Pinned Tab and Touch Bar -->
<link rel="mask-icon" href="safari-pinned-tab.svg" color="#006bc4">
<!-- OpenSearch -->
<link rel="search" href="open-search.xml" title="Search 5etools" type="application/opensearchdescription+xml">
<script type="text/javascript" src="sw-injector.js"></script>
<script type="text/javascript" src="js/styleswitch.js"></script>
<script type="text/javascript" src="js/navigation.js"></script>
<script type="text/javascript" src="js/browsercheck.js"></script>
</head>

View File

@@ -0,0 +1,27 @@
<div id="contentwrapper" class="view-col{{#if styleContentWrapperAdditional}} {{styleContentWrapperAdditional}}{{/if}}">
<div class="best-ecgen__visible mb-3" id="wrp-encounterbuild-random-and-adjust"></div>
<div class="best-ecgen__visible mb-4" id="best-ecgen__wrp-save-controls"></div>
{{> "listSublistContainerBestiary" }}
{{> "listStatsTabs" }}
{{> "listRhsWrpToken" }}
{{> "listWrpPagecontent" }}
<div class="ve-text-center mt-2 no-print best-ecgen__hidden">
<button class="btn btn-success btn-xs" id="btn-encounterbuild">Encounter Builder</button>
<div id="wrp-profbonusdice" class="ve-inline-block">
<button class="btn btn-default btn-xs" id="profbonusdice"
title="See the Dungeon Master&apos;s Guide, p263.">Use Proficiency Dice
</button>
</div>
<button class="btn btn-default btn-xs" id="btn-book">Printer View</button>
<button class="btn btn-default btn-xs" id="btn-show-table" title="View and Download Creatures in Tabular Format">Table View</button>
<button class="btn btn-xs btn-info" id="manage-brew">Manage Homebrew</button>
</div>
<div class="best-ecgen__visible--flex-col best-ecgen__wrp pb-1 px-2" id="wrp-encounterbuild-group-and-difficulty"></div>
</div>

View File

@@ -0,0 +1,13 @@
<div id="contentwrapper" class="view-col{{#if styleContentWrapperAdditional}} {{styleContentWrapperAdditional}}{{/if}}">
{{> "listSublistContainerItems" }}
{{> "listStatsTabs" }}
{{> "listWrpPagecontent" }}
<div class="ve-text-center mt-2 no-print">
<button class="btn btn-default btn-xs" id="btn-book">Printer View</button>
<button class="btn btn-default btn-xs" id="btn-show-table" title="View and Download Items in Tabular Format">Table View</button>
<button class="btn btn-xs btn-info" id="manage-brew">Manage Homebrew</button>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div id="contentwrapper" class="view-col{{#if styleContentWrapperAdditional}} {{styleContentWrapperAdditional}}{{/if}}">
{{> "listSublistContainer" }}
{{> "listStatsTabs" }}
{{> "listWrpPagecontent" }}
{{> "listRhsWrpFooterControls" }}
</div>

View File

@@ -0,0 +1,8 @@
<div class="lst__form-top" id="filter-search-group">
<div class="w-100 relative">
<input type="search" id="lst__search" autocomplete="off" autocapitalize="off" spellcheck="false" class="search form-control lst__search lst__search--no-border-h">
<div id="lst__search-glass" class="lst__wrp-search-glass no-events ve-flex-vh-center"><span class="glyphicon glyphicon-search"></span></div>
<div class="lst__wrp-search-visible no-events ve-flex-vh-center"></div>
</div>
<button class="btn btn-default" id="reset">Reset</button>
</div>

View File

@@ -0,0 +1,5 @@
<div id="filtertools" class="input-group input-group--bottom ve-flex no-shrink">
{{#each btnsList}}
{{{this}}}
{{/each}}
</div>

View File

@@ -0,0 +1 @@
<div id="list" class="list list--stats"></div>

View File

@@ -0,0 +1,39 @@
<div class="view-col itm__wrp-lists" id="listcontainer">
<div class="ve-flex-col min-h-0 w-100 itm__wrp-list itm__wrp-list--empty itm__wrp-list--mundane">
<div class="lst__form-top" id="filter-search-group">
<div class="w-100 relative">
<input type="search" id="lst__search" autocomplete="off" autocapitalize="off" spellcheck="false" class="search form-control lst__search lst__search--no-border-h">
<div id="lst__search-glass" class="lst__wrp-search-glass no-events ve-flex-vh-center"><span class="glyphicon glyphicon-search"></span></div>
<div class="lst__wrp-search-visible no-events ve-flex-vh-center"></div>
</div>
<button class="btn btn-default" id="reset">Reset</button>
</div>
<div id="filtertools-mundane" class="ele-mundane input-group input-group--bottom ve-flex no-shrink">
<button class="col-3-5 sort btn btn-default btn-xs" data-sort="name" data-sortby="asc">Name</button>
<button class="col-4-5 sort btn btn-default btn-xs" data-sort="type" data-sortby="asc">Type</button>
<button class="col-1-5 sort btn btn-default btn-xs" data-sort="cost" data-sortby="asc">Cost</button>
<button class="col-1-5 sort btn btn-default btn-xs" data-sort="weight" data-sortby="asc">Weight</button>
<button class="sort btn btn-default btn-xs ve-grow" data-sort="source" data-sortby="asc">Source</button>
</div>
<div class="list list--stats mundane ele-mundane"></div>
<h3 class="ele-mundane"><span class="side-label side-label--mundane clickable"></span></h3>
</div>
<div class="ve-flex-col min-h-0 w-100 itm__wrp-list itm__wrp-list--empty itm__wrp-list--magic">
<div class="no-shrink itm__list-divider ele-mundane-and-magic"></div>
<div id="filtertools-magic" class="ele-magic input-group input-group--bottom ve-flex no-shrink">
<button class="fullborder col-3-5 sort btn btn-default btn-xs" data-sort="name" data-sortby="asc">Name</button>
<button class="fullborder col-4 sort btn btn-default btn-xs" data-sort="type" data-sortby="asc">Type</button>
<button class="fullborder col-1-5 sort btn btn-default btn-xs" data-sort="weight" data-sortby="asc">Weight</button>
<button class="col-0-6 sort btn btn-default btn-xs" data-sort="attunement" title="Can Be Attuned">A.</button>
<button class="fullborder col-1-4 sort btn btn-default btn-xs" data-sort="rarity" data-sortby="asc">Rarity</button>
<button class="fullborder sort btn btn-default btn-xs ve-grow" data-sort="source" data-sortby="asc">Source</button>
</div>
<div class="list list--stats magic ele-magic"></div>
<h3 class="ele-magic"><span class="side-label side-label--magic clickable"></span></h3>
</div>
</div>

View File

@@ -0,0 +1,7 @@
<div class="view-col{{#if styleListContainerAdditional}} {{styleListContainerAdditional}}{{/if}}" id="listcontainer">
{{> "listFilterSearchGroup" }}
{{> "listFiltertools" }}
{{> "listList" }}
</div>

View File

@@ -0,0 +1,6 @@
<div class="ve-text-center mt-2 no-print">
{{#if isPrinterView}}
<button class="btn btn-default btn-xs" id="btn-book">Printer View</button>
{{/if}}
<button class="btn btn-xs btn-info" id="manage-brew">Manage Homebrew</button>
</div>

View File

@@ -0,0 +1 @@
<div id="float-token" class="relative"></div>

View File

@@ -0,0 +1,31 @@
<script type="text/javascript" src="lib/jquery.js"></script>
<script type="text/javascript" src="js/parser.js"></script>
<script type="text/javascript" src="js/utils.js"></script>
<script type="text/javascript" src="js/utils-ui.js"></script>
<script type="text/javascript" src="js/utils-list.js"></script>
{{#each scriptsUtilsAdditional}}<script type="text/javascript" src="js/{{this}}"></script>
{{/each}}
<script type="text/javascript" src="lib/localforage.js"></script>
<script type="text/javascript" src="js/omnidexer.js"></script>
<script type="text/javascript" src="js/omnisearch.js"></script>
<script type="text/javascript" src="js/filter.js"></script>
<script type="text/javascript" src="js/utils-dataloader.js"></script>
<script type="text/javascript" src="js/utils-brew.js"></script>
<script type="text/javascript" src="js/render.js"></script>
<script type="text/javascript" src="js/render-dice.js"></script>
<script type="text/javascript" src="js/render-markdown.js"></script>
<script type="text/javascript" src="js/render-{{scriptIdentList}}.js"></script>
<script type="text/javascript" src="js/scalecreature.js"></script>
<script type="text/javascript" src="js/hist.js"></script>
<script type="text/javascript" src="js/listpage.js"></script>
{{#if isMultisource}}<script type="text/javascript" src="js/multisource.js"></script>
{{/if}}
<script type="text/javascript" src="js/filter-common.js"></script>
<script type="text/javascript" src="js/filter-{{scriptIdentList}}.js"></script>
{{#each scriptsPrePageAdditional}}<script type="text/javascript" src="js/{{this}}"></script>
{{/each}}
{{#if isModule}}<script type="module" src="js/{{scriptIdentList}}.js"></script>
{{else}}<script type="text/javascript" src="js/{{scriptIdentList}}.js"></script>
{{/if}}
<script type="text/javascript" src="js/list2.js"></script>
<script type="text/javascript" src="lib/elasticlunr.js"></script>

View File

@@ -0,0 +1,3 @@
<div class="wrp-stat-tab" id="stat-tabs">
<div id="tabs-right"></div>
</div>

View File

@@ -0,0 +1,10 @@
<div id="sublistcontainer" class="sublist sublist--resizable no-print">
{{> "listSublistsort" }}
{{> "listSublist" }}
<div class="row best-ecgen__hidden ve-small">
<span class="col-6 text-right pr-2">Total:</span>
<span class="col-6 no-wrap pl-0" id="totalcr"></span>
</div>
</div>

View File

@@ -0,0 +1,12 @@
<div id="sublistcontainer" class="sublist sublist--resizable no-print">
{{> "listSublistsort" }}
{{> "listSublist" }}
<div class="row ve-small ve-flex-v-center">
<span class="col-6 text-right pr-2">Total:</span>
<span class="col-2 ve-text-center no-wrap" id="totalweight"></span>
<span class="col-2 ve-text-center no-wrap clickable" id="totalvalue"></span>
<span class="col-2 ve-text-center no-wrap pr-0" id="totalitems"></span>
</div>
</div>

View File

@@ -0,0 +1,5 @@
<div id="sublistcontainer" class="sublist sublist--resizable no-print">
{{> "listSublistsort" }}
{{> "listSublist" }}
</div>

View File

@@ -0,0 +1 @@
<div id="sublist" class="list"></div>

View File

@@ -0,0 +1,5 @@
<div id="sublistsort" class="btn-group sublist__wrp-cols">
{{#each btnsSublist}}
{{{this}}}
{{/each}}
</div>

View File

@@ -0,0 +1,7 @@
<div id="wrp-pagecontent" class="wrp-stats-table{{#if isStyleBook}} wrp-stats-table--book{{/if}}">
<table id="pagecontent" class="w-100 stats{{#if isStyleBook}} stats--book{{/if}}">
<tr><th class="border" colspan="6"></th></tr>
<tr><td colspan="6" class="initial-message">Select an entry from the list to view it here</td></tr>
<tr><th class="border" colspan="6"></th></tr>
</table>
</div>

View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
{{> "head" }}
<body>
{{> "adRhs" }}
<div class="viewport-wrapper">
{{> "adLeaderboard" }}
{{> "navbar" }}
<div class="view-col-group--cancer h-100 mh-0">
<div class="container view-col-wrapper view-col-wrapper--cancer">
{{> (lookup . "identPartialListListcontainer") }}
{{> "adMobile1" }}
{{> (lookup . "identPartialListContentwrapper") }}
</div>
</div>
</div>
{{> "adFooter" }}
{{> "listScripts" }}
</body>
</html>

View File

@@ -0,0 +1,9 @@
<header class="hidden-xs hidden-sm page__header">
<div class="container ve-flex-v-baseline">
<h1 class="page__title no-wrap my-0">{{navbarTitle}}</h1>
<p class="page__subtitle no-wrap my-0" id="page__subtitle">{{navbarDescription}}</p>
</div>
</header>
<nav class="container page__nav" id="navigation">
<ul class="nav nav-pills page__nav-inner" id="navbar"></ul>
</nav>

View File

@@ -0,0 +1,6 @@
import * as fs from "fs";
import "../js/parser.js";
import * as utB from "./util-book-reference.js";
fs.writeFileSync("data/generated/bookref-quick.json", JSON.stringify(utB.UtilBookReference.getIndex({name: "Quick Reference", id: "bookref-quick", tag: "quickref"})).replace(/\s*\u2014\s*?/g, "\\u2014"), "utf8");
console.log("Updated quick references.");

View File

@@ -0,0 +1,27 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
import "../js/render-dice.js";
import "../js/hist.js";
import * as utS from "./util-search-index.js";
import {Timer} from "./util.js";
async function main () {
const t = Timer.start();
console.log("##### Creating primary index... #####");
const index = await utS.UtilSearchIndex.pGetIndex();
fs.writeFileSync("search/index.json", JSON.stringify(index), "utf8");
console.log("##### Creating secondary index: Items... #####");
const indexItems = await utS.UtilSearchIndex.pGetIndexAdditionalItem();
fs.writeFileSync("search/index-item.json", JSON.stringify(indexItems), "utf8");
console.log("##### Creating alternate index: Spells... #####");
const indexAltSpells = await utS.UtilSearchIndex.pGetIndexAlternate("spell");
fs.writeFileSync("search/index-alt-spell.json", JSON.stringify(indexAltSpells), "utf8");
console.log("##### Creating Foundry index... #####");
const indexFoundry = await utS.UtilSearchIndex.pGetIndexFoundry();
fs.writeFileSync("search/index-foundry.json", JSON.stringify(indexFoundry), "utf8");
console.log(`Created indexes in ${Timer.stop(t)}`);
}
export default main();

231
node/generate-seo.js Normal file
View File

@@ -0,0 +1,231 @@
/**
* Generator script which creates stub per-entity pages for SEO.
*/
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import "../js/utils-dataloader.js";
import "../js/render.js";
import "../js/render-dice.js";
import * as ut from "./util.js";
const IS_DEV_MODE = true; // !!process.env.VET_SEO_IS_DEV_MODE; // N.b.: disabled as all known deployments are "dev"
const BASE_SITE_URL = process.env.VET_BASE_SITE_URL || "https://5e.tools/";
const isSkipUaEtc = !!process.env.VET_SEO_IS_SKIP_UA_ETC;
const isOnlyVanilla = !!process.env.VET_SEO_IS_ONLY_VANILLA;
const version = ut.readJson("package.json").version;
const lastMod = (() => {
const date = new Date();
return `${date.getFullYear()}-${`${date.getMonth() + 1}`.padStart(2, "0")}-${`${date.getDate()}`.padStart(2, "0")}`;
})();
const baseSitemapData = (() => {
const out = {};
// Scrape all the links from navigation.js -- avoid any unofficial HTML files which might exist
const navText = fs.readFileSync("./js/navigation.js", "utf-8");
navText.replace(/(?:"([^"]+\.html)"|'([^']+)\.html'|`([^`]+)\.html`)/gi, (...m) => {
const str = m[1] || m[2] || m[3];
if (str.includes("${")) return;
out[str] = true;
});
return out;
})();
const getTemplate = (page, source, hash, textStyle, isFluff) => `<!DOCTYPE html><html lang="en"><head>
<!--5ETOOLS_CMP-->
<!--5ETOOLS_ANALYTICS-->
<!--5ETOOLS_ADCODE-->
<meta charset="utf-8"><meta name="description" content=""><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><meta name="apple-mobile-web-app-capable" content="yes"><title>5etools</title><link rel="stylesheet" href="/css/bootstrap.css?v=${version}"><link rel="stylesheet" href="/css/main.css"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><link rel="icon" type="image/png" sizes="256x256" href="/favicon-256x256.png"><link rel="icon" type="image/png" sizes="144x144" href="/favicon-144x144.png"><link rel="icon" type="image/png" sizes="128x128" href="/favicon-128x128.png"><link rel="icon" type="image/png" sizes="64x64" href="/favicon-64x64.png"><link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png"><link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"><link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"><link rel="manifest" href="/manifest.webmanifest"><meta name="application-name" content="5etools"><meta name="theme-color" content="#006bc4"><meta name="msapplication-config" content="browserconfig.xml"/><meta name="msapplication-TileColor" content="#006bc4"><link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png"><link rel="apple-touch-icon" sizes="360x360" href="/apple-touch-icon-360x360.png"><link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png"><link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png"><link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png"><meta name="apple-mobile-web-app-title" content="5etools"><link rel="mask-icon" href="/safari-pinned-tab.svg" color="#006bc4"><link rel="search" href="/open-search.xml" title="Search 5etools" type="application/opensearchdescription+xml"><script type="text/javascript" src="/js/header.js?v=${VERSION_NUMBER}"></script><script>_SEO_PAGE="${page}";_SEO_SOURCE="${source}";_SEO_HASH="${hash}";_SEO_STYLE=${textStyle};_SEO_FLUFF=${isFluff}</script></head><body><div class="cancer__wrp-sidebar-rhs cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--top"><!--5ETOOLS_AD_RIGHT_1--></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--bottom"><!--5ETOOLS_AD_RIGHT_2--></div></div><div class="cancer__wrp-leaderboard cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__wrp-leaderboard-inner"><!--5ETOOLS_AD_LEADERBOARD--></div></div><header class="hidden-xs hidden-sm page__header"><div class="container ve-flex-v-baseline"><h1 class="page__title no-wrap my-0"></h1></div></header><nav class="container page__nav" id="navigation"><ul class="nav nav-pills page__nav-inner" id="navbar"></ul></nav><main class="container"><div class="row"><div id="wrp-pagecontent"><table id="pagecontent" class="w-100 stats"><tr><th class="border" colspan="6"></th></tr><tr><td colspan="6" class="initial-message">Loading...</td></tr><tr><th class="border" colspan="6"></th></tr></table></div></div><div class="row" id="link-page"></div></main><script type="text/javascript" src="https://cdn.jsdelivr.net/combine/npm/jquery@3.4.1/dist/jquery.min.js,gh/weixsong/elasticlunr.js@0.9/elasticlunr.min.js"></script><script type="text/javascript" src="/lib/localforage.js"></script></script></script><script type="text/javascript" src="/js/shared.js?v=${VERSION_NUMBER}"></script><script type="text/javascript" src="/js/render-${page}.js?v=${VERSION_NUMBER}"></script><script type="text/javascript" src="/js/seo-loader.js?v=${VERSION_NUMBER}"></script></body></html>`;
const getTemplateDev = (page, source, hash, textStyle, isFluff) => `<!DOCTYPE html><html lang="en"><head>
<!--5ETOOLS_CMP-->
<!--5ETOOLS_ANALYTICS-->
<!--5ETOOLS_ADCODE-->
<meta charset="utf-8">
<meta name="description" content="">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="apple-mobile-web-app-capable" content="yes">
<title>5etools</title>
<link rel="stylesheet" href="/css/bootstrap.css">
<link rel="stylesheet" href="/css/main.css">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="icon" type="image/png" sizes="256x256" href="/favicon-256x256.png">
<link rel="icon" type="image/png" sizes="144x144" href="/favicon-144x144.png">
<link rel="icon" type="image/png" sizes="128x128" href="/favicon-128x128.png">
<link rel="icon" type="image/png" sizes="64x64" href="/favicon-64x64.png">
<link rel="icon" type="image/png" sizes="48x48" href="/favicon-48x48.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/manifest.webmanifest">
<meta name="application-name" content="5etools">
<meta name="theme-color" content="#006bc4">
<meta name="msapplication-config" content="browserconfig.xml"/>
<meta name="msapplication-TileColor" content="#006bc4">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon-180x180.png">
<link rel="apple-touch-icon" sizes="360x360" href="/apple-touch-icon-360x360.png">
<link rel="apple-touch-icon" sizes="167x167" href="/apple-touch-icon-167x167.png">
<link rel="apple-touch-icon" sizes="152x152" href="/apple-touch-icon-152x152.png">
<link rel="apple-touch-icon" sizes="120x120" href="/apple-touch-icon-120x120.png">
<meta name="apple-mobile-web-app-title" content="5etools">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#006bc4">
<script type="text/javascript" src="/js/styleswitch.js"></script>
<script type="text/javascript" src="/js/navigation.js"></script>
<script type="text/javascript" src="/js/browsercheck.js"></script>
<script>_SEO_PAGE="${page}";_SEO_SOURCE="${source}";_SEO_HASH="${hash}";_SEO_STYLE=${textStyle};_SEO_FLUFF=${isFluff}</script>
</head>
<body>
<div class="cancer__wrp-sidebar-rhs cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--top"><!--5ETOOLS_AD_RIGHT_1--></div><div class="cancer__sidebar-rhs-inner cancer__sidebar-rhs-inner--bottom"><!--5ETOOLS_AD_RIGHT_2--></div></div>
<div class="cancer__wrp-leaderboard cancer__anchor"><div class="cancer__disp-cancer"></div><div class="cancer__wrp-leaderboard-inner"><!--5ETOOLS_AD_LEADERBOARD--></div></div>
<header class="hidden-xs hidden-sm page__header"><div class="container ve-flex-v-baseline"><h1 class="page__title no-wrap my-0"></h1></div></header><nav class="container page__nav" id="navigation"><ul class="nav nav-pills page__nav-inner" id="navbar"></ul></nav>
<main class="container"><div class="row"><div id="wrp-pagecontent"><table id="pagecontent" class="w-100 stats"><tr><th class="border" colspan="6"></th></tr><tr><td colspan="6" class="initial-message">Loading...</td></tr><tr><th class="border" colspan="6"></th></tr></table></div></div><div class="row" id="link-page"></div></main>
<script type="text/javascript" src="/lib/jquery.js"></script>
<script type="text/javascript" src="/lib/localforage.js"></script>
<script type="text/javascript" src="/lib/elasticlunr.js"></script>
<script type="text/javascript" src="/js/parser.js"></script>
<script type="text/javascript" src="/js/utils.js"></script>
<script type="text/javascript" src="/js/utils-ui.js"></script>
<script type="text/javascript" src="/js/omnidexer.js"></script>
<script type="text/javascript" src="/js/omnisearch.js"></script>
<script type="text/javascript" src="/js/filter.js"></script>
<script type="text/javascript" src="/js/utils-dataloader.js"></script>
<script type="text/javascript" src="/js/utils-brew.js"></script>
<script type="text/javascript" src="/js/render.js"></script>
<script type="text/javascript" src="/js/render-dice.js"></script>
<script type="text/javascript" src="/js/scalecreature.js"></script>
<script type="text/javascript" src="/js/hist.js"></script>
<script type="text/javascript" src="/js/render-${page}.js"></script>
<script type="text/javascript" src="/js/seo-loader.js"></script></body></html>`;
const toGenerate = [
{
page: "spells",
pGetEntries: () => {
const index = ut.readJson(`data/spells/index.json`);
const fileData = Object.entries(index)
.filter(([source]) => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(source))
.filter(([source]) => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(source))
.map(([_, filename]) => ut.readJson(`data/spells/${filename}`));
return fileData.map(it => MiscUtil.copy(it.spell)).reduce((a, b) => a.concat(b));
},
style: 1,
isFluff: 1,
},
{
page: "bestiary",
pGetEntries: () => {
const index = ut.readJson(`data/bestiary/index.json`);
const fileData = Object.entries(index)
.filter(([source]) => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(source))
.filter(([source]) => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(source))
.map(([source, filename]) => ({source: source, json: ut.readJson(`data/bestiary/${filename}`)}));
// Filter to prevent duplicates from "otherSources" copies
return fileData.map(it => MiscUtil.copy(it.json.monster.filter(mon => mon.source === it.source))).reduce((a, b) => a.concat(b));
},
style: 2,
isFluff: 1,
},
{
page: "items",
pGetEntries: async () => {
const out = (await Renderer.item.pBuildList()).filter(it => !it._isItemGroup);
return out
.filter(it => !isSkipUaEtc || !SourceUtil.isNonstandardSourceWotc(it.source))
.filter(it => !isOnlyVanilla || Parser.SOURCES_VANILLA.has(it.source));
},
style: 1,
isFluff: 1,
},
// TODO expand this as required
];
const siteMapData = {};
async function main () {
ut.patchLoadJson();
let total = 0;
console.log(`Generating SEO pages...`);
await Promise.all(toGenerate.map(async meta => {
try {
fs.mkdirSync(`./${meta.page}`, { recursive: true });
} catch (err) {
if (err.code !== "EEXIST") throw err;
}
const entries = await meta.pGetEntries();
const builder = UrlUtil.URL_TO_HASH_BUILDER[`${meta.page}.html`];
entries.forEach(ent => {
let offset = 0;
let html;
let path;
while (true) {
const hash = builder(ent);
const sluggedHash = UrlUtil.getSluggedHash(hash);
path = `${meta.page}/${sluggedHash}${offset ? `-${offset}` : ""}.html`;
if (siteMapData[path]) {
++offset;
continue;
}
html = (IS_DEV_MODE ? getTemplateDev : getTemplate)(meta.page, ent.source, hash, meta.style, meta.isFluff);
siteMapData[path] = true;
break;
}
if (offset > 0) console.warn(`\tDeduplicated URL using suffix: ${path}`);
fs.writeFileSync(`./${path}`, html, "utf-8");
total++;
if (total % 100 === 0) console.log(`Wrote ${total} files...`);
});
}));
console.log(`Wrote ${total} files.`);
let sitemapLinkCount = 0;
let sitemap = `<?xml version="1.0" encoding="UTF-8"?>\n`;
sitemap += `<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">\n`;
sitemap += `<url>
<loc>${BASE_SITE_URL}</loc>
<lastmod>${lastMod}</lastmod>
<changefreq>monthly</changefreq>
</url>\n`;
sitemapLinkCount++;
Object.keys(baseSitemapData).forEach(url => {
sitemap += `<url>
<loc>${BASE_SITE_URL}${url}</loc>
<lastmod>${lastMod}</lastmod>
<changefreq>monthly</changefreq>
</url>\n`;
sitemapLinkCount++;
});
Object.keys(siteMapData).forEach(url => {
sitemap += `<url>
<loc>${BASE_SITE_URL}${url}</loc>
<lastmod>${lastMod}</lastmod>
<changefreq>weekly</changefreq>
</url>\n`;
sitemapLinkCount++;
});
sitemap += `</urlset>\n`;
fs.writeFileSync("./sitemap.xml", sitemap, "utf-8");
console.log(`Wrote ${sitemapLinkCount.toLocaleString()} URL${sitemapLinkCount === 1 ? "" : "s"} to sitemap.xml`);
ut.unpatchLoadJson();
}
main().then(() => console.log(`SEO page generation complete.`)).catch(e => console.error(e));

View File

@@ -0,0 +1,41 @@
// TODO this replaces `generate-subclass-lookup.js` in effect
// TODO make a system for generating the same data on homebrew docs
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/utils-ui.js";
import "../js/utils-proporder.js";
import "../js/filter.js";
import "../js/filter-spells.js";
import "../js/utils-dataloader.js";
import "../js/render.js";
import "../js/hist.js";
import {SpellSourceLookupBuilder} from "../js/converterutils-spell-sources.js";
async function pMain () {
ut.patchLoadJson();
const index = ut.readJson("./data/spells/index.json");
const jsonMetas = Object.values(index)
.map(filename => {
const path = `./data/spells/${filename}`;
return {
path,
json: ut.readJson(path),
};
});
const spellDatas = jsonMetas.flatMap(({json}) => json.spell);
const lookup = await SpellSourceLookupBuilder.pGetLookup({spells: spellDatas});
fs.writeFileSync(`./data/generated/gendata-spell-source-lookup.json`, CleanUtil.getCleanJson(lookup, {isMinify: true}));
ut.unpatchLoadJson();
console.log("Regenerated spell source lookup.");
}
export default pMain();

View File

@@ -0,0 +1,15 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
const out = {};
const classIndex = JSON.parse(fs.readFileSync("./data/class/index.json", "utf-8"));
Object.values(classIndex).forEach(f => {
const data = JSON.parse(fs.readFileSync(`./data/class/${f}`, "utf-8"));
(data.subclass || []).forEach(sc => {
MiscUtil.set(out, sc.classSource, sc.className, sc.source, sc.shortName, {name: sc.name, isReprinted: sc.isReprinted});
});
});
fs.writeFileSync(`./data/generated/gendata-subclass-lookup.json`, CleanUtil.getCleanJson(out, {isMinify: true}));
console.log("Regenerated subclass lookup.");

View File

@@ -0,0 +1,195 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
import * as ut from "./util.js";
import "../js/utils-generate-tables-data.js";
import "../js/utils-dataloader.js";
import "../js/hist.js";
class GenTables {
_doLoadAdventureData () {
return ut.readJson(`./data/adventures.json`).adventure
.map(idx => {
if (!GenTables.ADVENTURE_BLOCKLIST[idx.id]) {
return {
adventure: idx,
adventureData: JSON.parse(fs.readFileSync(`./data/adventure/adventure-${idx.id.toLowerCase()}.json`, "utf-8")),
};
}
})
.filter(it => it);
}
_doLoadBookData () {
return ut.readJson(`./data/books.json`).book
.map(idx => {
if (!GenTables.BOOK_BLOCKLIST[idx.id]) {
return {
book: idx,
bookData: JSON.parse(fs.readFileSync(`./data/book/book-${idx.id.toLowerCase()}.json`, "utf-8")),
};
}
})
.filter(it => it);
}
async pRun () {
const output = {tables: [], tableGroups: []};
this._addBookAndAdventureData(output);
await this._pAddClassData(output);
await this._pAddVariantRuleData(output);
await this._pAddBackgroundData(output);
await this._pAddEncountersData(output);
await this._pAddNamesData(output);
const toSave = JSON.stringify({table: output.tables, tableGroup: output.tableGroups});
fs.writeFileSync(`./data/generated/gendata-tables.json`, toSave, "utf-8");
console.log("Regenerated table data.");
}
_addBookAndAdventureData (output) {
const advDocs = this._doLoadAdventureData();
const bookDocs = this._doLoadBookData();
advDocs.forEach(doc => {
const {
table: foundTables,
tableGroup: foundTableGroups,
} = UtilGenTables.getAdventureBookTables(
doc,
{
headProp: "adventure",
bodyProp: "adventureData",
isRequireIncludes: true,
},
);
output.tables.push(...foundTables);
output.tableGroups.push(...foundTableGroups);
});
bookDocs.forEach(doc => {
const {
table: foundTables,
tableGroup: foundTableGroups,
} = UtilGenTables.getAdventureBookTables(
doc,
{
headProp: "book",
bodyProp: "bookData",
},
);
output.tables.push(...foundTables);
output.tableGroups.push(...foundTableGroups);
});
}
async _pAddClassData (output) {
ut.patchLoadJson();
const classData = await DataUtil.class.loadJSON();
ut.unpatchLoadJson();
classData.class.forEach(cls => {
const {table: foundTables} = UtilGenTables.getClassTables(cls);
output.tables.push(...foundTables);
});
classData.subclass.forEach(sc => {
const {table: foundTables} = UtilGenTables.getSubclassTables(sc);
output.tables.push(...foundTables);
});
}
async _pAddVariantRuleData (output) {
return this._pAddGenericEntityData({
output,
path: `./data/variantrules.json`,
props: ["variantrule"],
});
}
async _pAddBackgroundData (output) {
return this._pAddGenericEntityData({
output,
path: `./data/backgrounds.json`,
props: ["background"],
});
}
async _pAddGenericEntityData (
{
output,
path,
props,
},
) {
ut.patchLoadJson();
const jsonData = await DataUtil.loadJSON(path);
ut.unpatchLoadJson();
props.forEach(prop => {
jsonData[prop].forEach(it => {
// Note that this implicitly requires each table to have a `"tableInclude"`
const {table: foundTables} = UtilGenTables.getGenericTables(it, prop, "entries");
output.tables.push(...foundTables);
});
});
}
// -----------------------
async _pAddEncountersData (output) {
return this._pAddEncounterOrNamesData({
output,
path: `./data/encounters.json`,
prop: "encounter",
fnGetNameCaption: Renderer.table.getConvertedEncounterTableName.bind(Renderer.table),
colLabel1: "Encounter",
});
}
async _pAddNamesData (output) {
return this._pAddEncounterOrNamesData({
output,
path: `./data/names.json`,
prop: "name",
fnGetNameCaption: Renderer.table.getConvertedNameTableName.bind(Renderer.table),
colLabel1: "Name",
});
}
async _pAddEncounterOrNamesData (
{
output,
path,
prop,
fnGetNameCaption,
colLabel1,
},
) {
ut.patchLoadJson();
const jsonData = await DataUtil.loadJSON(path);
ut.unpatchLoadJson();
jsonData[prop].forEach(group => {
group.tables.forEach(tableRaw => {
output.tables.push(Renderer.table.getConvertedEncounterOrNamesTable({
group,
tableRaw,
fnGetNameCaption,
colLabel1,
}));
});
});
}
// -----------------------
}
GenTables.BOOK_BLOCKLIST = {};
GenTables.ADVENTURE_BLOCKLIST = {};
const generator = new GenTables();
export default generator.pRun();

View File

@@ -0,0 +1,124 @@
import * as fs from "fs";
import "../js/utils.js";
import * as ut from "./util.js";
class GenVariantrules {
_doLoadAdventureData () {
return ut.readJson(`./data/adventures.json`).adventure
.map(idx => {
if (GenVariantrules.ADVENTURE_ALLOWLIST[idx.id]) {
return {
adventure: idx,
adventureData: JSON.parse(fs.readFileSync(`./data/adventure/adventure-${idx.id.toLowerCase()}.json`, "utf-8")),
};
}
})
.filter(it => it);
}
_doLoadBookData () {
return ut.readJson(`./data/books.json`).book
.map(idx => {
if (!GenVariantrules.BOOK_BLOCKLIST[idx.id]) {
return {
book: idx,
bookData: JSON.parse(fs.readFileSync(`./data/book/book-${idx.id.toLowerCase()}.json`, "utf-8")),
};
}
})
.filter(it => it);
}
async pRun () {
GenVariantrules._WALKER = MiscUtil.getWalker({keyBlocklist: MiscUtil.GENERIC_WALKER_ENTRIES_KEY_BLOCKLIST, isNoModification: true});
const output = {variantrule: []};
this._addBookAndAdventureData(output);
const toSave = JSON.stringify({variantrule: output.variantrule});
fs.writeFileSync(`./data/generated/gendata-variantrules.json`, toSave, "utf-8");
console.log("Regenerated variant rules data.");
}
_addBookAndAdventureData (output) {
const advDocs = this._doLoadAdventureData();
const bookDocs = this._doLoadBookData();
advDocs.forEach(doc => {
const foundVariantrules = this._getAdventureBookVariantRules(
doc,
{
headProp: "adventure",
bodyProp: "adventureData",
isRequireIncludes: true,
},
);
if (!foundVariantrules) return;
output.variantrule.push(...foundVariantrules);
});
bookDocs.forEach(doc => {
const foundVariantrules = this._getAdventureBookVariantRules(
doc,
{
headProp: "book",
bodyProp: "bookData",
},
);
if (!foundVariantrules) return;
output.variantrule.push(...foundVariantrules);
});
}
/**
* @param doc
* @param opts
* @param opts.headProp
* @param opts.bodyProp
* @param [opts.isRequireIncludes]
*/
_getAdventureBookVariantRules (doc, opts) {
if (!doc[opts.bodyProp]) return;
const out = [];
GenVariantrules._WALKER.walk(
doc[opts.bodyProp],
{
object: (obj) => {
if (!obj.data?.variantRuleInclude) return;
const variantRuleMeta = obj.data.variantRuleInclude;
const cpy = MiscUtil.copy(obj);
// region Cleanup
delete cpy.data;
GenVariantrules._WALKER.walk(
cpy,
{
object: (obj) => {
delete obj.id;
},
},
);
// endregion
cpy.ruleType = variantRuleMeta.ruleType;
cpy.source = doc[opts.headProp].source;
out.push(cpy);
},
},
);
return out;
}
}
GenVariantrules.BOOK_BLOCKLIST = {};
GenVariantrules.ADVENTURE_ALLOWLIST = {};
GenVariantrules._WALKER = null;
const generator = new GenVariantrules();
export default generator.pRun();

View File

@@ -0,0 +1,26 @@
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/utils.js";
const toDump = [
{prop: "book", json: JSON.parse(fs.readFileSync("./data/books.json", "utf-8"))},
{prop: "adventure", json: JSON.parse(fs.readFileSync("./data/adventures.json", "utf-8"))},
];
toDump.forEach(it => {
const out = {};
Object.assign(out, it.json);
it.json[it.prop].forEach(meta => {
const json = JSON.parse(fs.readFileSync(`./data/${it.prop}/${it.prop}-${meta.id.toLowerCase()}.json`, "utf-8"));
// do the linking required by homebrew
json.source = meta.source;
json.id = meta.id;
const dataProp = `${it.prop}Data`;
(out[dataProp] = out[dataProp] || []).push(json);
});
const path = `./data/generated/gendata-wotc-${it.prop}.json`;
fs.writeFileSync(path, CleanUtil.getCleanJson(out, {isMinify: true}), "utf-8");
console.log(`Saved combined ${it.prop} file to "${path}".`);
});

14
node/minify-json.js Normal file
View File

@@ -0,0 +1,14 @@
"use strict";
import * as fs from "fs";
import * as ut from "./util.js";
function minifyFolder (folder) {
const files = ut.listFiles({dir: folder});
files
.filter(file => file.endsWith(".json"))
.forEach(file => fs.writeFileSync(file, JSON.stringify(ut.readJson(file)), "utf-8"));
}
minifyFolder(`./data`);
console.log("JSON minification complete.");

14
node/prettify-data.js Normal file
View File

@@ -0,0 +1,14 @@
import {Command} from "commander";
import {prettifyFile, prettifyFolder} from "./util-prettify-data.js";
const program = new Command()
.option("--file <file>", `Input file`)
.option("--dir <dir>", `Input directory ()`, "./data")
;
program.parse(process.argv);
const params = program.opts();
if (params.file) prettifyFile(params.file);
else prettifyFolder(params.dir);
console.log("Prettifying complete.");

7
node/rm.js Normal file
View File

@@ -0,0 +1,7 @@
import {rmDirRecursiveSync} from "./util.js";
if (process.argv.length < 3) throw new Error(`An argument is required!`);
const tgt = process.argv[2];
console.log(`Removing: ${tgt}`);
rmDirRecursiveSync(tgt);

View File

@@ -0,0 +1,79 @@
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/render.js";
class AreaTagger {
constructor (filePath) {
this._filePath = filePath;
this._data = ut.readJson(filePath);
this._maxTag = 0;
this._existingTags = null;
}
_getNewTag () {
let hexTag;
do {
if (this._maxTag >= 4095) throw new Error("Exhausted tags!");
hexTag = this._maxTag.toString(16).padStart(3, "0");
this._maxTag++;
} while (this._existingTags.has(hexTag));
this._existingTags.add(hexTag);
return hexTag;
}
_doPopulateExistingTags () {
const map = Renderer.adventureBook.getEntryIdLookup(this._data.data, "populateExistingIds");
this._existingTags = new Set(Object.keys(map));
}
_addNewTags () {
const handlers = {
object: (obj) => {
Renderer.ENTRIES_WITH_CHILDREN
.filter(meta => meta.key === "entries")
.forEach(meta => {
if (obj.type !== meta.type) return;
if (!obj.id) obj.id = this._getNewTag();
});
if (obj.id) return obj;
if (obj.type === "image" && !obj.id && obj.mapRegions) obj.id = this._getNewTag();
return obj;
},
};
this._data.data.forEach(chap => MiscUtil.getWalker().walk(chap, handlers));
}
_doWrite () {
const outStr = CleanUtil.getCleanJson(this._data);
fs.writeFileSync(this._filePath, outStr, "utf-8");
}
run () {
this._doPopulateExistingTags();
this._addNewTags();
this._doWrite();
}
}
console.log(`Running area tagging pass...`);
const adventureIndex = ut.readJson("./data/adventures.json");
const bookIndex = ut.readJson("./data/books.json");
const doPass = (arr, type) => {
arr.forEach(meta => {
const areaTagger = new AreaTagger(`./data/${type}/${type}-${meta.id.toLowerCase()}.json`);
areaTagger.run();
console.log(`\tTagged ${meta.id}...`);
});
};
doPass(adventureIndex.adventure, "adventure");
doPass(bookIndex.book, "book");
console.log(`Area tagging complete.`);

View File

@@ -0,0 +1,147 @@
import * as fs from "fs";
import "../js/parser.js";
import "../js/utils.js";
import probe from "probe-image-size";
import {ObjectWalker} from "5etools-utils";
import {Command} from "commander";
import * as ut from "./util.js";
function addDir (allFiles, dir) {
ut.listFiles({dir})
.filter(file => file.toLowerCase().endsWith(".json"))
.forEach(filePath => addFile(allFiles, filePath));
}
function addFile (allFiles, path) {
const json = ut.readJson(path, "utf-8");
allFiles.push({json, path});
}
function getFileProbeTarget (path) {
const target = fs.createReadStream(path);
return {
target,
location: path,
fnCleanup: () => target.destroy(), // stream cleanup
};
}
function getProbeTarget (imgEntry, {localBrewDir = null, isAllowExternal = false}) {
if (imgEntry.href.type === "internal") {
return getFileProbeTarget(`img/${imgEntry.href.path}`);
}
if (imgEntry.href.type === "external") {
if (localBrewDir && imgEntry.href.url.startsWith(`${VeCt.URL_ROOT_BREW}`)) {
return getFileProbeTarget(
decodeURI(imgEntry.href.url.replace(`${VeCt.URL_ROOT_BREW}_img`, `${localBrewDir}/_img`)),
);
}
if (!isAllowExternal) { // Local files are not truly "external"
console.warn(`Skipping "external" image (URL was "${imgEntry.href.url}"); run with the "--allow-external" option if you wish to probe external URLs.`);
return null;
}
return {
target: imgEntry.href.url,
location: imgEntry.href.url,
};
}
throw new Error(`Unhandled image href.type "${imgEntry.href.type}"!`);
}
function getImageEntries (imageEntries, obj) {
if (obj.type === "image" && obj.href) {
imageEntries.push(obj);
}
return obj;
}
async function pMutImageDimensions (imgEntry, {localBrewDir = null, isAllowExternal = false}) {
const probeMeta = getProbeTarget(imgEntry, {localBrewDir, isAllowExternal});
if (probeMeta == null) return;
const {target, location, fnCleanup} = probeMeta;
try {
const dimensions = await probe(target);
if (fnCleanup) fnCleanup();
imgEntry.width = dimensions.width;
imgEntry.height = dimensions.height;
} catch (e) {
console.error(`Failed to set dimensions for ${location} -- ${e.message}`);
}
}
async function main (
{
dirs,
files,
localBrewDir = null,
isAllowExternal = false,
},
) {
const tStart = Date.now();
const allFiles = [];
dirs.forEach(dir => addDir(allFiles, dir));
files.forEach(file => addFile(allFiles, file));
console.log(`Running on ${allFiles.length} JSON file${allFiles.length === 1 ? "" : "s"}...`);
const imageEntries = [];
allFiles.forEach(meta => {
ObjectWalker.walk({
filePath: meta.path,
obj: meta.json,
primitiveHandlers: {
object: getImageEntries.bind(this, imageEntries),
},
});
});
await Promise.all(
[...new Array(4)]
.map(async () => {
while (imageEntries.length) {
const imageEntry = imageEntries.pop();
await pMutImageDimensions(imageEntry, {localBrewDir, isAllowExternal});
}
}),
);
allFiles.forEach(meta => fs.writeFileSync(meta.path, CleanUtil.getCleanJson(meta.json), "utf-8"));
const tEnd = Date.now();
console.log(`Completed in ${((tEnd - tStart) / 1000).toFixed(2)}s.`);
}
const program = new Command()
.option("--file <file...>", `Input files`)
.option("--dir <dir...>", `Input directories`)
.option("--allow-external", "Allow external URLs to be probed.")
.option("--local-brew-dir <localBrewDir>", "Use local homebrew directory for relevant URLs.")
;
program.parse(process.argv);
const params = program.opts();
const dirs = [...(params.dir || [])];
const files = [...(params.file || [])];
// If no options specified, use default selection
if (!dirs.length && !files.length) {
dirs.push("./data/adventure");
dirs.push("./data/book");
files.push("./data/decks.json");
files.push("./data/fluff-recipes.json");
}
main({
dirs,
files,
localBrewDir: params.localBrewDir,
isAllowExternal: params.allowExternal,
})
.catch(e => console.error(e));

27
node/tag-jsons.js Normal file
View File

@@ -0,0 +1,27 @@
import "../js/parser.js";
import "../js/utils.js";
import "../js/utils-dataloader.js";
import "../js/render.js";
import "../js/render-dice.js";
import * as ut from "./util.js";
import {setUp, loadSpells, run, teardown} from "./util-tag-jsons.js";
/**
* Args:
* file="./data/my-file.json"
* filePrefix="./data/dir/"
* inplace
* bestiaryFile="./data/my-file.json"
*/
async function main () {
ut.ArgParser.parse();
setUp();
await TagJsons.pInit({
spells: loadSpells(),
});
run(ut.ArgParser.ARGS);
teardown();
console.log("Run complete.");
}
main();

143
node/util-book-reference.js Normal file
View File

@@ -0,0 +1,143 @@
import * as ut from "./util.js";
import "../js/utils.js";
import "../js/render.js";
const UtilBookReference = {
getSections (refId) {
switch (refId) {
case "bookref-quick":
return [
"Character Creation",
"Equipment",
"Playing the Game",
"Combat",
"Adventuring",
];
case "bookref-dmscreen":
return [
"Running the Game",
"Combat",
"Factions",
];
default:
throw new Error(`No sections defined for book id ${refId}`);
}
},
getIndex (...refTypes) {
const index = ut.readJson(`./data/books.json`);
const books = {};
index.book.forEach(b => {
books[b.id.toLowerCase()] = ut.readJson(`./data/book/book-${b.id.toLowerCase()}.json`);
});
const outJson = {
reference: {},
data: {},
};
refTypes.forEach(it => outJson.reference[it.id] = {
name: it.name,
id: it.id,
contents: [],
});
let bookData = [];
function reset () {
bookData = [];
index.book.forEach(b => {
const data = {source: b.id, file: MiscUtil.copy(books[b.id.toLowerCase()])};
bookData.push(data);
});
}
refTypes.forEach(refType => {
reset();
const out = {};
function recursiveSetSource (ent, source) {
if (ent instanceof Array) {
ent.forEach(e => recursiveSetSource(e, source));
} else if (typeof ent === "object") {
if (ent.page) ent.source = source;
Object.values(ent).forEach(v => recursiveSetSource(v, source));
}
}
function isDesiredSect (ent) {
return ent.entries && ent.data && ent.data[refType.tag];
}
function recursiveAdd (ent, source) {
if (ent.entries) {
ent.entries = ent.entries.filter(nxt => recursiveAdd(nxt, source));
}
if (isDesiredSect(ent)) {
const sect = ent.data[refType.tag];
if (!out[sect]) {
out[sect] = {
sectName: UtilBookReference.getSections(refType.id)[sect - 1],
sections: [],
};
}
const toAdd = MiscUtil.copy(ent);
toAdd.type = "section";
const discard = !!toAdd.data.allowRefDupe;
recursiveSetSource(toAdd, source);
out[sect].sections.push(toAdd);
return discard;
} else {
return true;
}
}
bookData.forEach(book => {
book.file.data.forEach(chap => {
if (chap.entries) {
recursiveAdd(chap, book.source);
}
});
});
Object.keys(out).sort().forEach(i => {
const sects = out[i].sections.sort((a, b) => SortUtil.ascSort(a.name, b.name));
const header = outJson.reference[refType.id];
header.contents.push({
name: out[i].sectName,
headers: sects.map(s => s.name),
});
const toAdd = {
type: "entries",
entries: sects,
};
if (!outJson.data[refType.id]) outJson.data[refType.id] = [];
outJson.data[refType.id].push(toAdd);
});
});
const walker = MiscUtil.getWalker({isAllowDeleteObjects: true, isDepthFirst: true});
walker.walk(
outJson.data,
{
object: (obj) => {
delete obj.id; // Remove IDs to avoid duplicates
if (obj.type === "image" && !obj.data?.["quickrefKeep"]) return undefined;
if (obj.type === "gallery" && !obj.images.length) return undefined;
return obj;
},
array: (obj) => {
return obj.filter(it => it != null);
},
},
);
return outJson;
},
};
export {UtilBookReference};

339
node/util-prettify-data.js Normal file
View File

@@ -0,0 +1,339 @@
import * as fs from "fs";
import * as ut from "./util.js";
import "../js/parser.js";
import "../js/utils.js";
import "../js/utils-proporder.js";
const FILE_BLOCKLIST = new Set([
"loot.json",
"msbcr.json",
"monsterfeatures.json",
"index.json",
"life.json",
"makecards.json",
"renderdemo.json",
"foundry.json",
"makebrew-creature.json",
"index-meta.json",
"index-props.json",
"index-sources.json",
"index-timestamps.json",
"package.json",
"package-lock.json",
]);
const _FILE_PROP_ORDER = [
"$schema",
"_meta",
// region Player options
"class",
"foundryClass",
"subclass",
"foundrySubclass",
"classFeature",
"foundryClassFeature",
"subclassFeature",
"foundrySubclassFeature",
"optionalfeature",
"background",
"backgroundFeature",
"backgroundFluff",
"race",
"subrace",
"foundryRace",
"foundryRaceFeature",
"raceFluff",
"raceFluffMeta",
"feat",
"foundryFeat",
"featFluff",
"reward",
"rewardFluff",
"charoption",
"charoptionFluff",
// endregion
// region General entities
"spell",
"spellFluff",
"foundrySpell",
"spellList",
"baseitem",
"item",
"itemGroup",
"magicvariant",
"itemFluff",
"itemProperty",
"reducedItemProperty",
"itemType",
"itemTypeAdditionalEntries",
"reducedItemType",
"itemEntry",
"itemMastery",
"linkedLootTables",
"deck",
"card",
"deity",
"language",
"languageFluff",
// endregion
// region GM-specific
"monster",
"monsterFluff",
"foundryMonster",
"legendaryGroup",
"object",
"objectFluff",
"vehicle",
"vehicleUpgrade",
"vehicleFluff",
"cult",
"boon",
"trap",
"trapFluff",
"hazard",
"hazardFluff",
"encounter",
"name",
// endregion
// region Rules
"variantrule",
"table",
"condition",
"conditionFluff",
"disease",
"status",
"action",
"skill",
"sense",
"citation",
"adventure",
"adventureData",
"book",
"bookData",
// endregion
// region Other
"recipe",
"recipeFluff",
// endregion
// region Legacy content
"psionic",
"psionicDisciplineFocus",
"psionicDisciplineActive",
// endregion
// region Tooling
"makebrewCreatureTrait",
"makebrewCreatureAction",
"monsterfeatures",
// endregion
// region Roll20-specific
"roll20Spell",
// endregion
// region Non-brew data
"blocklist",
// endregion
];
const KEY_BLOCKLIST = new Set(["data", "itemTypeAdditionalEntries", "itemType", "itemProperty", "itemEntry"]);
const PROPS_TO_UNHANDLED_KEYS = {};
function getFnListSort (prop) {
switch (prop) {
case "spell":
case "roll20Spell":
case "foundrySpell":
case "spellList":
case "monster":
case "foundryMonster":
case "monsterFluff":
case "monsterTemplate":
case "makebrewCreatureTrait":
case "makebrewCreatureAction":
case "action":
case "background":
case "legendaryGroup":
case "language":
case "languageScript":
case "name":
case "condition":
case "disease":
case "status":
case "cult":
case "boon":
case "feat":
case "foundryFeat":
case "vehicle":
case "vehicleUpgrade":
case "backgroundFluff":
case "featFluff":
case "conditionFluff":
case "spellFluff":
case "itemFluff":
case "languageFluff":
case "vehicleFluff":
case "objectFluff":
case "raceFluff":
case "item":
case "baseitem":
case "magicvariant":
case "itemGroup":
case "itemMastery":
case "object":
case "optionalfeature":
case "psionic":
case "reward":
case "rewardFluff":
case "variantrule":
case "race":
case "foundryRaceFeature":
case "table":
case "trap":
case "trapFluff":
case "hazard":
case "hazardFluff":
case "charoption":
case "charoptionFluff":
case "recipe":
case "recipeFluff":
case "sense":
case "skill":
case "deck":
case "citation":
return SortUtil.ascSortGenericEntity.bind(SortUtil);
case "deity":
return SortUtil.ascSortDeity.bind(SortUtil);
case "card":
return SortUtil.ascSortCard.bind(SortUtil);
case "class":
case "foundryClass":
return (a, b) => SortUtil.ascSortDateString(Parser.sourceJsonToDate(b.source), Parser.sourceJsonToDate(a.source)) || SortUtil.ascSortLower(a.name, b.name) || SortUtil.ascSortLower(a.source, b.source);
case "subclass":
return (a, b) => SortUtil.ascSortDateString(Parser.sourceJsonToDate(b.source), Parser.sourceJsonToDate(a.source)) || SortUtil.ascSortLower(a.name, b.name);
case "classFeature":
case "foundryClassFeature":
return (a, b) => SortUtil.ascSortLower(a.classSource, b.classSource)
|| SortUtil.ascSortLower(a.className, b.className)
|| SortUtil.ascSort(a.level, b.level)
|| SortUtil.ascSortLower(a.name, b.name)
|| SortUtil.ascSortLower(a.source, b.source);
case "subclassFeature":
case "foundrySubclassFeature":
return (a, b) => SortUtil.ascSortLower(a.classSource, b.classSource)
|| SortUtil.ascSortLower(a.className, b.className)
|| SortUtil.ascSortLower(a.subclassSource, b.subclassSource)
|| SortUtil.ascSortLower(a.subclassShortName, b.subclassShortName)
|| SortUtil.ascSort(a.level, b.level)
|| SortUtil.ascSort(a.header || 0, b.header || 0)
|| SortUtil.ascSortLower(a.name, b.name)
|| SortUtil.ascSortLower(a.source, b.source);
case "subrace": return (a, b) => SortUtil.ascSortLower(a.raceName, b.raceName)
|| SortUtil.ascSortLower(a.raceSource, b.raceSource)
|| SortUtil.ascSortLower(a.name || "", b.name || "")
|| SortUtil.ascSortLower(a.source, b.source);
case "encounter":
return SortUtil.ascSortEncounter.bind(SortUtil);
case "adventure": return SortUtil.ascSortAdventure.bind(SortUtil);
case "book": return SortUtil.ascSortBook.bind(SortUtil);
case "adventureData":
case "bookData":
return SortUtil.ascSortBookData.bind(SortUtil);
default: throw new Error(`Unhandled prop "${prop}"`);
}
}
export const getPrettified = json => {
let isModified = false;
// region Sort keys within entities
Object.entries(json)
.filter(([k, v]) => !KEY_BLOCKLIST.has(k) && v instanceof Array)
.forEach(([k, v]) => {
if (PropOrder.hasOrder(k)) {
PROPS_TO_UNHANDLED_KEYS[k] = PROPS_TO_UNHANDLED_KEYS[k] || new Set();
json[k] = v.map(it => PropOrder.getOrdered(it, k, {fnUnhandledKey: uk => PROPS_TO_UNHANDLED_KEYS[k].add(uk)}));
json[k].sort(getFnListSort(k));
isModified = true;
return;
}
console.warn(`\t\tUnhandled property: "${k}"`);
});
// endregion
// region Sort file-level properties
const keyOrder = Object.keys(json)
.sort((a, b) => {
const ixA = _FILE_PROP_ORDER.indexOf(a);
const ixB = _FILE_PROP_ORDER.indexOf(b);
return SortUtil.ascSort(~ixA ? ixA : Number.MAX_SAFE_INTEGER, ~ixB ? ixB : Number.MAX_SAFE_INTEGER);
});
const numUnhandledKeys = Object.keys(json).filter(it => !~_FILE_PROP_ORDER.indexOf(it));
if (numUnhandledKeys > 1) console.warn(`\t\tUnhandled file-level properties: "${numUnhandledKeys}"`);
if (!CollectionUtil.deepEquals(Object.keys(json), keyOrder)) {
const nxt = {};
keyOrder.forEach(k => nxt[k] = json[k]);
json = nxt;
isModified = true;
}
// endregion
return {json, isModified};
};
export const prettifyFile = file => {
console.log(`\tPrettifying ${file}...`);
const json = ut.readJson(file);
const {json: jsonPrettified, isModified} = getPrettified(json);
if (isModified) fs.writeFileSync(file, CleanUtil.getCleanJson(jsonPrettified), "utf-8");
};
export const prettifyFolder = folder => {
console.log(`Prettifying directory ${folder}...`);
const files = ut.listFiles({dir: folder});
files
.filter(file => file.endsWith(".json") && !FILE_BLOCKLIST.has(file.split("/").last()))
.forEach(file => prettifyFile(file));
Object.entries(PROPS_TO_UNHANDLED_KEYS)
.filter(([, set]) => set.size)
.forEach(([prop, set]) => {
console.warn(`Unhandled keys for data property "${prop}":`);
set.forEach(k => console.warn(`\t${k}`));
});
};

128
node/util-search-index.js Normal file
View File

@@ -0,0 +1,128 @@
import "../js/utils.js";
import "../js/utils-dataloader.js";
import "../js/render.js";
import "../js/omnidexer.js";
import * as ut from "./util.js";
class UtilSearchIndex {
/**
* Prefer "core" sources, then official sources, then others.
*/
static _sortSources (a, b) {
const aCore = Number(Parser.SOURCES_VANILLA.has(a));
const bCore = Number(Parser.SOURCES_VANILLA.has(b));
if (aCore !== bCore) return bCore - aCore;
const aStandard = Number(!SourceUtil.isNonstandardSource(a));
const bStandard = Number(!SourceUtil.isNonstandardSource(b));
return aStandard !== bStandard ? bStandard - aStandard : SortUtil.ascSortLower(a, b);
}
static async pGetIndex ({doLogging = true, noFilter = false} = {}) {
return UtilSearchIndex._pGetIndex({doLogging, noFilter});
}
static async pGetIndexAlternate (forProp, {doLogging = true, noFilter = false} = {}) {
const opts = {alternate: forProp};
return UtilSearchIndex._pGetIndex({opts, doLogging, noFilter});
}
static async pGetIndexFoundry ({doLogging = true, noFilter = false} = {}) {
const opts = {
isSkipSpecial: true,
};
const optsAddToIndex = {
isIncludeTag: true,
isIncludeUid: true,
isIncludeImg: true,
};
return UtilSearchIndex._pGetIndex({opts, optsAddToIndex, doLogging, noFilter});
}
static async _pGetIndex ({opts = {}, optsAddToIndex = {}, doLogging = true, noFilter = false} = {}) {
ut.patchLoadJson();
const indexer = new Omnidexer();
// region Index entities from directories, e.g. creatures and spells
const toIndexMultiPart = Omnidexer.TO_INDEX__FROM_INDEX_JSON
.filter(indexMeta => opts.alternate ? indexMeta.alternateIndexes && indexMeta.alternateIndexes[opts.alternate] : true);
for (const indexMeta of toIndexMultiPart) {
const dataIndex = ut.readJson(`./data/${indexMeta.dir}/index.json`);
const loadedFiles = Object.entries(dataIndex)
.sort(([kA], [kB]) => UtilSearchIndex._sortSources(kA, kB))
.map(([_, filename]) => filename);
for (const filename of loadedFiles) {
const filePath = `./data/${indexMeta.dir}/${filename}`;
const contents = ut.readJson(filePath);
if (doLogging) console.log(`\tindexing ${filePath}`);
const optsNxt = {isNoFilter: noFilter};
if (opts.alternate) optsNxt.alt = indexMeta.alternateIndexes[opts.alternate];
await indexer.pAddToIndex(indexMeta, contents, {...optsNxt, ...optsAddToIndex});
}
}
// endregion
// region Index entities from single files
const toIndexSingle = Omnidexer.TO_INDEX
.filter(indexMeta => opts.alternate ? indexMeta.alternateIndexes && indexMeta.alternateIndexes[opts.alternate] : true);
for (const indexMeta of toIndexSingle) {
const filePath = `./data/${indexMeta.file}`;
const data = ut.readJson(filePath);
if (indexMeta.postLoad) indexMeta.postLoad(data);
if (doLogging) console.log(`\tindexing ${filePath}`);
Object.values(data)
.filter(it => it instanceof Array)
.forEach(it => it.sort((a, b) => UtilSearchIndex._sortSources(SourceUtil.getEntitySource(a), SourceUtil.getEntitySource(b)) || SortUtil.ascSortLower(a.name || MiscUtil.get(a, "inherits", "name") || "", b.name || MiscUtil.get(b, "inherits", "name") || "")));
const optsNxt = {isNoFilter: noFilter};
if (opts.alternate) optsNxt.alt = indexMeta.alternateIndexes[opts.alternate];
await indexer.pAddToIndex(indexMeta, data, {...optsNxt, ...optsAddToIndex});
}
// endregion
// region Index special
if (!opts.alternate && !opts.isSkipSpecial) {
for (const indexMeta of Omnidexer.TO_INDEX__SPECIAL) {
const toIndex = await indexMeta.pGetIndex();
toIndex.forEach(it => indexer.pushToIndex(it));
}
}
// endregion
const out = indexer.getIndex();
ut.unpatchLoadJson();
return out;
}
// this should be generalised if further specific indexes are required
static async pGetIndexAdditionalItem ({baseIndex = 0, doLogging = true} = {}) {
ut.patchLoadJson();
const indexer = new Omnidexer(baseIndex);
await Promise.all(Omnidexer.TO_INDEX.filter(it => it.category === Parser.CAT_ID_ITEM).map(async ti => {
const filename = `./data/${ti.file}`;
const data = ut.readJson(filename);
if (ti.postLoad) ti.postLoad(data);
if (ti.additionalIndexes && ti.additionalIndexes.item) {
if (doLogging) console.log(`\tindexing ${filename}`);
const extra = await ti.additionalIndexes.item(indexer, data);
extra.forEach(add => indexer.pushToIndex(add));
}
}));
const out = indexer.getIndex();
ut.unpatchLoadJson();
return out;
}
}
export {UtilSearchIndex};

86
node/util-tag-jsons.js Normal file
View File

@@ -0,0 +1,86 @@
import * as fs from "fs";
import "../js/utils.js";
import "../js/render.js";
import "../js/render-dice.js";
import * as ut from "./util.js";
import "../js/converterutils.js";
import "../js/converterutils-entries.js";
function run (args) {
TagJsons._BLOCKLIST_FILE_PREFIXES = [
...ut.FILE_PREFIX_BLOCKLIST,
// specific files
"demo.json",
];
let files;
if (args.file) {
files = [args.file];
} else {
files = ut.listFiles({dir: `./data`, blocklistFilePrefixes: TagJsons._BLOCKLIST_FILE_PREFIXES});
if (args.filePrefix) {
files = files.filter(f => f.startsWith(args.filePrefix));
if (!files.length) throw new Error(`No file with prefix "${args.filePrefix}" found!`);
}
}
const creatureList = getTaggableCreatureList(args.bestiaryFile);
files.forEach(file => {
console.log(`Tagging file "${file}"`);
const json = ut.readJson(file);
if (json instanceof Array) return;
TagJsons.mutTagObject(json, {creaturesToTag: creatureList});
const outPath = args.inplace ? file : file.replace("./data/", "./trash/");
if (!args.inplace) {
const dirPart = outPath.split("/").slice(0, -1).join("/");
fs.mkdirSync(dirPart, {recursive: true});
}
fs.writeFileSync(outPath, CleanUtil.getCleanJson(json));
});
}
/**
* Return creatures from the provided bestiary which are one of:
* - A named creature
* - A copy of a creature
* - An NPC
* as these creatures are likely to be missed by the automated tagging during conversion.
*/
function getTaggableCreatureList (filename) {
if (!filename) return [];
const bestiaryJson = ut.readJson(filename);
return (bestiaryJson.monster || [])
.filter(it => it.isNamedCreature || it.isNpc || it._copy)
.map(it => ({name: it.name, source: it.source}));
}
function setUp () {
ut.patchLoadJson();
}
function teardown () {
ut.unpatchLoadJson();
}
function loadSpells () {
const spellIndex = ut.readJson(`./data/spells/index.json`);
return Object.entries(spellIndex).map(([source, filename]) => {
if (SourceUtil.isNonstandardSource(source)) return [];
return ut.readJson(`./data/spells/${filename}`).spell;
}).flat();
}
export {
setUp,
loadSpells,
run,
teardown,
getTaggableCreatureList,
};

191
node/util.js Normal file
View File

@@ -0,0 +1,191 @@
import * as fs from "fs";
import https from "https";
function readJson (path) {
try {
const data = fs.readFileSync(path, "utf8")
.replace(/^\uFEFF/, ""); // strip BOM
return JSON.parse(data);
} catch (e) {
e.message += ` (Path: ${path})`;
throw e;
}
}
function isDirectory (path) {
return fs.lstatSync(path).isDirectory();
}
const FILE_EXTENSION_ALLOWLIST = [
".json",
];
const FILE_PREFIX_BLOCKLIST = [
"bookref-",
"foundry-",
"gendata-",
];
const DIR_PREFIX_BLOCKLIST = [
".git",
".idea",
];
/**
* Recursively list all files in a directory.
*
* @param [opts] Options object.
* @param [opts.blocklistFilePrefixes] Blocklisted filename prefixes (case sensitive).
* @param [opts.blocklistDirPrefixes] Blocklisted directory prefixes (case sensitive).
* @param [opts.allowlistFileExts] Allowlisted filename extensions (case sensitive).
* @param [opts.dir] Directory to list.
* @param [opts.allowlistDirs] Directory allowlist.
*/
function listFiles (opts) {
opts = opts || {};
opts.dir = opts.dir ?? "./data";
opts.blocklistFilePrefixes = opts.blocklistFilePrefixes === undefined ? FILE_PREFIX_BLOCKLIST : opts.blocklistFilePrefixes;
opts.blocklistDirPrefixes = opts.blocklistDirPrefixes === undefined ? DIR_PREFIX_BLOCKLIST : opts.blocklistDirPrefixes;
opts.allowlistFileExts = opts.allowlistFileExts === undefined ? FILE_EXTENSION_ALLOWLIST : opts.allowlistFileExts;
opts.allowlistDirs = opts.allowlistDirs || null;
const dirContent = fs.readdirSync(opts.dir, "utf8")
.filter(file => {
const path = `${opts.dir}/${file}`;
if (isDirectory(path)) {
if (opts.blocklistDirPrefixes != null && opts.blocklistDirPrefixes.some(it => file.startsWith(it))) return false;
return opts.allowlistDirs ? opts.allowlistDirs.includes(path) : true;
}
return (opts.blocklistFilePrefixes == null || !opts.blocklistFilePrefixes.some(it => file.startsWith(it)))
&& (opts.allowlistFileExts == null || opts.allowlistFileExts.some(it => file.endsWith(it)));
})
.map(file => `${opts.dir}/${file}`);
return dirContent.reduce((acc, file) => {
if (isDirectory(file)) acc.push(...listFiles({...opts, dir: file}));
else acc.push(file);
return acc;
}, []);
}
function rmDirRecursiveSync (dir) {
if (fs.existsSync(dir)) {
fs.readdirSync(dir).forEach(file => {
const curPath = `${dir}/${file}`;
if (fs.lstatSync(curPath).isDirectory()) rmDirRecursiveSync(curPath);
else fs.unlinkSync(curPath);
});
fs.rmdirSync(dir);
}
}
class PatchLoadJson {
static _CACHED = null;
static _CACHED_RAW = null;
static _PATCH_STACK = 0;
static _CACHE_HTTP_REQUEST = {};
static patchLoadJson () {
if (this._PATCH_STACK++) return;
PatchLoadJson._CACHED = PatchLoadJson._CACHED || DataUtil.loadJSON.bind(DataUtil);
const pLoadUrl = async url => {
if (!url.startsWith("http")) return readJson(url);
return this._CACHE_HTTP_REQUEST[url] ||= new Promise((resolve, reject) => {
https
.get(
url,
resp => {
let stack = "";
resp.on("data", chunk => stack += chunk);
resp.on("end", () => resolve(JSON.parse(stack)));
},
)
.on("error", err => reject(err));
});
};
const loadJsonCache = {};
DataUtil.loadJSON = (url) => {
if (!loadJsonCache[url]) {
loadJsonCache[url] = (async () => {
const data = await pLoadUrl(url);
await DataUtil.pDoMetaMerge(url, data, {isSkipMetaMergeCache: true});
return data;
})();
}
return loadJsonCache[url];
};
PatchLoadJson._CACHED_RAW = PatchLoadJson._CACHED_RAW || DataUtil.loadRawJSON.bind(DataUtil);
DataUtil.loadRawJSON = async (url) => pLoadUrl(url);
}
static unpatchLoadJson () {
if (--this._PATCH_STACK) return;
if (PatchLoadJson._CACHED) DataUtil.loadJSON = PatchLoadJson._CACHED;
if (PatchLoadJson._CACHED_RAW) DataUtil.loadRawJSON = PatchLoadJson._CACHED_RAW;
}
}
class ArgParser {
static parse () {
process.argv
.slice(2)
.forEach(arg => {
let [k, v] = arg.split("=").map(it => it.trim()).filter(Boolean);
if (v == null) ArgParser.ARGS[k] = true;
else {
v = v
.replace(/^"(.*)"$/, "$1")
.replace(/^'(.*)'$/, "$1")
;
if (!isNaN(v)) ArgParser.ARGS[k] = Number(v);
else ArgParser.ARGS[k] = v;
}
});
}
}
ArgParser.ARGS = {};
class Timer {
static _ID = 0;
static _RUNNING = {};
static start () {
const id = this._ID++;
this._RUNNING[id] = this._getSecs();
return id;
}
static stop (id, {isFormat = true} = {}) {
const out = this._getSecs() - this._RUNNING[id];
delete this._RUNNING[id];
return isFormat ? `${out.toFixed(3)}s` : out;
}
static _getSecs () {
const [s, ns] = process.hrtime();
return s + (ns / 1000000000);
}
}
export const patchLoadJson = PatchLoadJson.patchLoadJson.bind(PatchLoadJson);
export const unpatchLoadJson = PatchLoadJson.unpatchLoadJson.bind(PatchLoadJson);
export {
readJson,
listFiles,
FILE_PREFIX_BLOCKLIST,
ArgParser,
rmDirRecursiveSync,
Timer,
};

28
node/version-bump.js Normal file
View File

@@ -0,0 +1,28 @@
import * as fs from "fs";
import {simpleGit} from "simple-git";
import "../js/parser.js";
import "../js/utils.js";
const git = simpleGit();
const FILES_TO_REPLACE_VERSION_IN = ["js/utils.js"];
const VERSION_MARKER_START = "/* 5ETOOLS_VERSION__OPEN */";
const VERSION_MARKER_END = "/* 5ETOOLS_VERSION__CLOSE */";
const VERSION_REPLACE_REGEXP = new RegExp(`${VERSION_MARKER_START.escapeRegexp()}.*?${VERSION_MARKER_END.escapeRegexp()}`, "g");
async function main () {
const version = JSON.parse(fs.readFileSync("package.json", "utf-8")).version;
const versionReplaceString = `${VERSION_MARKER_START}"${version}"${VERSION_MARKER_END}`;
console.log("Replacing version in files ", FILES_TO_REPLACE_VERSION_IN, " with ", version);
for (const fileName of FILES_TO_REPLACE_VERSION_IN) {
let fileContents = fs.readFileSync(fileName, "utf8");
const contentsWithReplacedVersion = fileContents.replace(VERSION_REPLACE_REGEXP, versionReplaceString);
fs.writeFileSync(fileName, contentsWithReplacedVersion, "utf8");
await git.add(fileName);
}
}
main()
.then(() => console.log("Replacing version in all files."))
.catch(e => { throw e; });