mirror of
https://github.com/Kornstalx/5etools-mirror-2.github.io.git
synced 2026-01-14 05:47:50 -06:00
v1.198.1
This commit is contained in:
184
node/.eslintrc.cjs
Normal file
184
node/.eslintrc.cjs
Normal 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
109
node/build-sw.mjs
Normal 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
162
node/clean-images.js
Normal 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
27
node/clean-jsons.js
Normal 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
68
node/clean-tags.js
Normal 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");
|
||||
});
|
||||
44
node/find-mergable-scripts.js
Normal file
44
node/find-mergable-scripts.js
Normal 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
33
node/generate-all-maps.js
Normal 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
15
node/generate-all.js
Normal 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; });
|
||||
79
node/generate-dmscreen-reference.js
Normal file
79
node/generate-dmscreen-reference.js
Normal 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.");
|
||||
94
node/generate-map-thumbnails.js
Normal file
94
node/generate-map-thumbnails.js
Normal 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; });
|
||||
42
node/generate-nav-adventure-book-index.js
Normal file
42
node/generate-nav-adventure-book-index.js
Normal 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
494
node/generate-pages.js
Normal 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: " "}),
|
||||
|
||||
_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());
|
||||
2
node/generate-pages/ad/template-ad-footer.hbs
Normal file
2
node/generate-pages/ad/template-ad-footer.hbs
Normal file
@@ -0,0 +1,2 @@
|
||||
<!--5ETOOLS_SCRIPT_ANCHOR-->
|
||||
<!--5ETOOLS_AD_ADHESION-->
|
||||
1
node/generate-pages/ad/template-ad-leaderboard.hbs
Normal file
1
node/generate-pages/ad/template-ad-leaderboard.hbs
Normal 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>
|
||||
1
node/generate-pages/ad/template-ad-mobile-1.hbs
Normal file
1
node/generate-pages/ad/template-ad-mobile-1.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<div class="cancer__wrp-mobile-1 cancer__anchor"><div class="cancer__disp-cancer"></div><!--5ETOOLS_AD_MOB_PLAYER_1--></div>
|
||||
1
node/generate-pages/ad/template-ad-rhs.hbs
Normal file
1
node/generate-pages/ad/template-ad-rhs.hbs
Normal 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>
|
||||
58
node/generate-pages/head/template-head.hbs
Normal file
58
node/generate-pages/head/template-head.hbs
Normal 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>
|
||||
@@ -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'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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,9 @@
|
||||
<div id="contentwrapper" class="view-col{{#if styleContentWrapperAdditional}} {{styleContentWrapperAdditional}}{{/if}}">
|
||||
{{> "listSublistContainer" }}
|
||||
|
||||
{{> "listStatsTabs" }}
|
||||
|
||||
{{> "listWrpPagecontent" }}
|
||||
|
||||
{{> "listRhsWrpFooterControls" }}
|
||||
</div>
|
||||
@@ -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>
|
||||
5
node/generate-pages/list/template-list-filtertools.hbs
Normal file
5
node/generate-pages/list/template-list-filtertools.hbs
Normal file
@@ -0,0 +1,5 @@
|
||||
<div id="filtertools" class="input-group input-group--bottom ve-flex no-shrink">
|
||||
{{#each btnsList}}
|
||||
{{{this}}}
|
||||
{{/each}}
|
||||
</div>
|
||||
1
node/generate-pages/list/template-list-list.hbs
Normal file
1
node/generate-pages/list/template-list-list.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<div id="list" class="list list--stats"></div>
|
||||
@@ -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>
|
||||
7
node/generate-pages/list/template-list-listcontainer.hbs
Normal file
7
node/generate-pages/list/template-list-listcontainer.hbs
Normal file
@@ -0,0 +1,7 @@
|
||||
<div class="view-col{{#if styleListContainerAdditional}} {{styleListContainerAdditional}}{{/if}}" id="listcontainer">
|
||||
{{> "listFilterSearchGroup" }}
|
||||
|
||||
{{> "listFiltertools" }}
|
||||
|
||||
{{> "listList" }}
|
||||
</div>
|
||||
@@ -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>
|
||||
1
node/generate-pages/list/template-list-rhs-wrp-token.hbs
Normal file
1
node/generate-pages/list/template-list-rhs-wrp-token.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<div id="float-token" class="relative"></div>
|
||||
31
node/generate-pages/list/template-list-scripts.hbs
Normal file
31
node/generate-pages/list/template-list-scripts.hbs
Normal 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>
|
||||
3
node/generate-pages/list/template-list-stats-tabs.hbs
Normal file
3
node/generate-pages/list/template-list-stats-tabs.hbs
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="wrp-stat-tab" id="stat-tabs">
|
||||
<div id="tabs-right"></div>
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
<div id="sublistcontainer" class="sublist sublist--resizable no-print">
|
||||
{{> "listSublistsort" }}
|
||||
|
||||
{{> "listSublist" }}
|
||||
</div>
|
||||
1
node/generate-pages/list/template-list-sublist.hbs
Normal file
1
node/generate-pages/list/template-list-sublist.hbs
Normal file
@@ -0,0 +1 @@
|
||||
<div id="sublist" class="list"></div>
|
||||
5
node/generate-pages/list/template-list-sublistsort.hbs
Normal file
5
node/generate-pages/list/template-list-sublistsort.hbs
Normal file
@@ -0,0 +1,5 @@
|
||||
<div id="sublistsort" class="btn-group sublist__wrp-cols">
|
||||
{{#each btnsSublist}}
|
||||
{{{this}}}
|
||||
{{/each}}
|
||||
</div>
|
||||
@@ -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>
|
||||
31
node/generate-pages/list/template-list.hbs
Normal file
31
node/generate-pages/list/template-list.hbs
Normal 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>
|
||||
0
node/generate-pages/misc/template-blank.hbs
Normal file
0
node/generate-pages/misc/template-blank.hbs
Normal file
9
node/generate-pages/navbar/template-navbar.hbs
Normal file
9
node/generate-pages/navbar/template-navbar.hbs
Normal 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>
|
||||
6
node/generate-quick-reference.js
Normal file
6
node/generate-quick-reference.js
Normal 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.");
|
||||
27
node/generate-search-index.js
Normal file
27
node/generate-search-index.js
Normal 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
231
node/generate-seo.js
Normal 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));
|
||||
41
node/generate-spell-source-lookup.js
Normal file
41
node/generate-spell-source-lookup.js
Normal 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();
|
||||
15
node/generate-subclass-lookup.js
Normal file
15
node/generate-subclass-lookup.js
Normal 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.");
|
||||
195
node/generate-tables-data.js
Normal file
195
node/generate-tables-data.js
Normal 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();
|
||||
124
node/generate-variantrules-data.js
Normal file
124
node/generate-variantrules-data.js
Normal 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();
|
||||
26
node/generate-wotc-homebrew.js
Normal file
26
node/generate-wotc-homebrew.js
Normal 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
14
node/minify-json.js
Normal 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
14
node/prettify-data.js
Normal 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
7
node/rm.js
Normal 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);
|
||||
79
node/tag-adventure-book-areas.js
Normal file
79
node/tag-adventure-book-areas.js
Normal 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.`);
|
||||
147
node/tag-image-dimensions.js
Normal file
147
node/tag-image-dimensions.js
Normal 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
27
node/tag-jsons.js
Normal 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
143
node/util-book-reference.js
Normal 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
339
node/util-prettify-data.js
Normal 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
128
node/util-search-index.js
Normal 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
86
node/util-tag-jsons.js
Normal 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
191
node/util.js
Normal 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
28
node/version-bump.js
Normal 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; });
|
||||
Reference in New Issue
Block a user