Muse owns muse-shaped files now
This commit is contained in:
parent
3e2ab6ec73
commit
bf444ccb1a
23 changed files with 1288 additions and 201 deletions
6
bun.lock
6
bun.lock
|
@ -7,6 +7,8 @@
|
||||||
"@proscenium/playbill": "link:@proscenium/playbill",
|
"@proscenium/playbill": "link:@proscenium/playbill",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"marked": "^15.0.7",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"zod": "^3.24.1",
|
"zod": "^3.24.1",
|
||||||
},
|
},
|
||||||
|
@ -57,6 +59,10 @@
|
||||||
|
|
||||||
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
"lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="],
|
||||||
|
|
||||||
|
"marked": ["marked@15.0.7", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg=="],
|
||||||
|
|
||||||
|
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
|
||||||
|
|
||||||
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
"typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
|
@ -15,6 +15,8 @@
|
||||||
"@proscenium/playbill": "link:@proscenium/playbill",
|
"@proscenium/playbill": "link:@proscenium/playbill",
|
||||||
"chalk": "^5.4.1",
|
"chalk": "^5.4.1",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"marked": "^15.0.7",
|
||||||
|
"slugify": "^1.6.6",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
|
|
361
src/define/index.ts
Normal file
361
src/define/index.ts
Normal file
|
@ -0,0 +1,361 @@
|
||||||
|
import {
|
||||||
|
type Ability,
|
||||||
|
type Blueprint,
|
||||||
|
type Item,
|
||||||
|
type Method,
|
||||||
|
type Resource,
|
||||||
|
type Rule,
|
||||||
|
type Species,
|
||||||
|
parseAbility,
|
||||||
|
parseBlueprint,
|
||||||
|
parseGlossary,
|
||||||
|
parseItem,
|
||||||
|
parseMethod,
|
||||||
|
parseResource,
|
||||||
|
parseRule,
|
||||||
|
parseSpecies,
|
||||||
|
} from "@proscenium/playbill";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const Base = z.object({
|
||||||
|
$define: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const Info = Base.extend({
|
||||||
|
id: z.string(), // TODO: Validate ID shapes
|
||||||
|
name: z.string().default("Unnamed Component"),
|
||||||
|
description: z.string().default("No description provided"),
|
||||||
|
categories: z.array(z.string()).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const AbilitySchema = Info.extend({
|
||||||
|
$define: z.literal("ability"),
|
||||||
|
type: z.string().default("action"),
|
||||||
|
ap: z.number().int().default(0),
|
||||||
|
hp: z.number().int().default(0),
|
||||||
|
ep: z.number().int().default(0),
|
||||||
|
xp: z.number().int().default(0),
|
||||||
|
damage: z.number().int().default(0),
|
||||||
|
damageType: z.string().default("phy"),
|
||||||
|
roll: z.string().default("none"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const BlueprintSchema = Info.extend({
|
||||||
|
$define: z.literal("blueprint"),
|
||||||
|
species: z.string(),
|
||||||
|
items: z.array(z.string()).default([]),
|
||||||
|
abilities: z.array(z.string()).default([]),
|
||||||
|
ap: z.number().int().default(0),
|
||||||
|
hp: z.number().int().default(0),
|
||||||
|
ep: z.number().int().default(0),
|
||||||
|
xp: z.number().int().default(0),
|
||||||
|
muscle: z.string().default("novice"),
|
||||||
|
focus: z.string().default("novice"),
|
||||||
|
knowledge: z.string().default("novice"),
|
||||||
|
charm: z.string().default("novice"),
|
||||||
|
cunning: z.string().default("novice"),
|
||||||
|
spark: z.string().default("novice"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const GlossarySchema = Base.extend({
|
||||||
|
$define: z.literal("glossary"),
|
||||||
|
terms: z.record(z.string(), z.string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
const BaseItem = Info.extend({
|
||||||
|
$define: z.literal("item"),
|
||||||
|
rarity: z.string().default("common"),
|
||||||
|
affinity: z.string().default("none"),
|
||||||
|
damage: z.number().int().default(0),
|
||||||
|
damageType: z.string().default("phy"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ItemSchema = z.discriminatedUnion("type", [
|
||||||
|
BaseItem.extend({
|
||||||
|
type: z.literal("wielded"),
|
||||||
|
hands: z.number().int().default(1),
|
||||||
|
}),
|
||||||
|
BaseItem.extend({
|
||||||
|
type: z.literal("worn"),
|
||||||
|
hands: z.number().int().default(1),
|
||||||
|
}),
|
||||||
|
BaseItem.extend({
|
||||||
|
type: z.literal("trinket"),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const MethodSchema = Info.extend({
|
||||||
|
$define: z.literal("method"),
|
||||||
|
curator: z.string().default("Someone"),
|
||||||
|
abilities: z.array(z.array(z.string())).default([]),
|
||||||
|
});
|
||||||
|
|
||||||
|
const ResourceSchema = z.discriminatedUnion("type", [
|
||||||
|
Info.extend({
|
||||||
|
$define: z.literal("resource"),
|
||||||
|
type: z.literal("text"),
|
||||||
|
}),
|
||||||
|
Info.extend({
|
||||||
|
$define: z.literal("resource"),
|
||||||
|
type: z.literal("image"),
|
||||||
|
url: z.string(),
|
||||||
|
}),
|
||||||
|
Info.extend({
|
||||||
|
$define: z.literal("resource"),
|
||||||
|
type: z.literal("table"),
|
||||||
|
data: z.array(z.array(z.string())),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const RuleSchema = Info.extend({
|
||||||
|
$define: z.literal("rule"),
|
||||||
|
overrule: z.nullable(z.string()).default(null),
|
||||||
|
order: z.number().int().default(Number.POSITIVE_INFINITY),
|
||||||
|
});
|
||||||
|
|
||||||
|
const SpeciesSchema = Info.extend({
|
||||||
|
$define: z.literal("species"),
|
||||||
|
hands: z.number().int().default(2),
|
||||||
|
abilities: z.array(z.string()).default([]),
|
||||||
|
ap: z.number().int().default(0),
|
||||||
|
hp: z.number().int().default(0),
|
||||||
|
ep: z.number().int().default(0),
|
||||||
|
xp: z.number().int().default(0),
|
||||||
|
muscle: z.string().default("novice"),
|
||||||
|
focus: z.string().default("novice"),
|
||||||
|
knowledge: z.string().default("novice"),
|
||||||
|
charm: z.string().default("novice"),
|
||||||
|
cunning: z.string().default("novice"),
|
||||||
|
spark: z.string().default("novice"),
|
||||||
|
});
|
||||||
|
|
||||||
|
const SchemaMapping = {
|
||||||
|
ability: AbilitySchema,
|
||||||
|
blueprint: BlueprintSchema,
|
||||||
|
glossary: GlossarySchema,
|
||||||
|
item: ItemSchema,
|
||||||
|
method: MethodSchema,
|
||||||
|
resource: ResourceSchema,
|
||||||
|
rule: RuleSchema,
|
||||||
|
species: SpeciesSchema,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type ComponentType = keyof typeof SchemaMapping;
|
||||||
|
|
||||||
|
const parseComponentType = (val: unknown): ComponentType => {
|
||||||
|
if (typeof val !== "string") {
|
||||||
|
throw new Error("Component type must be a string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(val in SchemaMapping)) {
|
||||||
|
throw new Error(`Unknown component type: ${val}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return val as ComponentType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ParsedComponent =
|
||||||
|
| {
|
||||||
|
type: "ability";
|
||||||
|
component: Ability;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "blueprint";
|
||||||
|
component: Blueprint;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "glossary";
|
||||||
|
component: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "item";
|
||||||
|
component: Item;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "method";
|
||||||
|
component: Method;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "resource";
|
||||||
|
component: Resource;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "rule";
|
||||||
|
component: Rule;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "species";
|
||||||
|
component: Species;
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseAbilityDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = AbilitySchema.parse(obj);
|
||||||
|
|
||||||
|
const ability = parseAbility({
|
||||||
|
id: parsed.id,
|
||||||
|
name: parsed.name,
|
||||||
|
description: parsed.description,
|
||||||
|
categories: parsed.categories,
|
||||||
|
type: parsed.type,
|
||||||
|
ap: parsed.ap,
|
||||||
|
ep: parsed.ep,
|
||||||
|
hp: parsed.hp,
|
||||||
|
xp: parsed.xp,
|
||||||
|
damage:
|
||||||
|
parsed.damage > 0
|
||||||
|
? {
|
||||||
|
amount: parsed.damage,
|
||||||
|
type: parsed.damageType,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
roll: parsed.roll,
|
||||||
|
boons: [],
|
||||||
|
banes: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "ability",
|
||||||
|
component: ability,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseBlueprintDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = BlueprintSchema.parse(obj);
|
||||||
|
|
||||||
|
const blueprint = parseBlueprint({
|
||||||
|
id: parsed.id,
|
||||||
|
name: parsed.name,
|
||||||
|
description: parsed.description,
|
||||||
|
categories: parsed.categories,
|
||||||
|
species: parsed.species,
|
||||||
|
items: parsed.items,
|
||||||
|
abilities: parsed.abilities,
|
||||||
|
baggage: [],
|
||||||
|
ap: parsed.ap,
|
||||||
|
hp: parsed.hp,
|
||||||
|
ep: parsed.ep,
|
||||||
|
xp: parsed.xp,
|
||||||
|
muscle: parsed.muscle,
|
||||||
|
focus: parsed.focus,
|
||||||
|
knowledge: parsed.knowledge,
|
||||||
|
charm: parsed.charm,
|
||||||
|
cunning: parsed.cunning,
|
||||||
|
spark: parsed.spark,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "blueprint",
|
||||||
|
component: blueprint,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseGlossaryDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const { terms } = GlossarySchema.parse(obj);
|
||||||
|
|
||||||
|
// Create a new object with each definition as an array
|
||||||
|
const curatedTerms: Record<string, string[]> = {};
|
||||||
|
for (const [term, definition] of Object.entries(terms)) {
|
||||||
|
curatedTerms[term] = [definition];
|
||||||
|
}
|
||||||
|
|
||||||
|
const glossary = parseGlossary(curatedTerms);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "glossary",
|
||||||
|
component: glossary,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseItemDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = ItemSchema.parse(obj);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "item",
|
||||||
|
component: parseItem({
|
||||||
|
...parsed,
|
||||||
|
damage:
|
||||||
|
parsed.damage > 0
|
||||||
|
? {
|
||||||
|
amount: parsed.damage,
|
||||||
|
type: parsed.damageType,
|
||||||
|
}
|
||||||
|
: null,
|
||||||
|
tweak: "",
|
||||||
|
temper: "",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMethodDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = MethodSchema.parse(obj);
|
||||||
|
|
||||||
|
const method = {
|
||||||
|
id: parsed.id,
|
||||||
|
name: parsed.name,
|
||||||
|
description: parsed.description,
|
||||||
|
categories: parsed.categories,
|
||||||
|
curator: parsed.curator,
|
||||||
|
abilities: parsed.abilities,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "method",
|
||||||
|
component: parseMethod(method),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseResourceDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = ResourceSchema.parse(obj);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "resource",
|
||||||
|
component: parseResource(parsed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseRuleDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = RuleSchema.parse(obj);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "rule",
|
||||||
|
component: parseRule(parsed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSpeciesDefinition(obj: unknown): ParsedComponent {
|
||||||
|
const parsed = SpeciesSchema.parse(obj);
|
||||||
|
|
||||||
|
return {
|
||||||
|
type: "species",
|
||||||
|
component: parseSpecies(parsed),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parsePlaybillComponent(obj: unknown): ParsedComponent {
|
||||||
|
const baseParse = Base.parse(obj);
|
||||||
|
|
||||||
|
const type = parseComponentType(baseParse.$define);
|
||||||
|
const schema = SchemaMapping[type];
|
||||||
|
const component = schema.parse(obj);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "ability":
|
||||||
|
return parseAbilityDefinition(component);
|
||||||
|
case "blueprint":
|
||||||
|
return parseBlueprintDefinition(component);
|
||||||
|
case "glossary":
|
||||||
|
return parseGlossaryDefinition(component);
|
||||||
|
case "item":
|
||||||
|
return parseItemDefinition(component);
|
||||||
|
case "method":
|
||||||
|
return parseMethodDefinition(component);
|
||||||
|
case "resource":
|
||||||
|
return parseResourceDefinition(component);
|
||||||
|
case "rule":
|
||||||
|
return parseRuleDefinition(component);
|
||||||
|
case "species":
|
||||||
|
return parseSpeciesDefinition(component);
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown component type: ${type}`);
|
||||||
|
}
|
||||||
|
}
|
48
src/index.ts
48
src/index.ts
|
@ -1,16 +1,13 @@
|
||||||
import { watch } from "node:fs/promises";
|
import { watch } from "node:fs/promises";
|
||||||
import {
|
|
||||||
PlaybillSchema,
|
|
||||||
renderPlaybillToHTML,
|
|
||||||
renderPlaybillToJSON,
|
|
||||||
} from "@proscenium/playbill";
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { loadFromBinding, resolveBindingPath } from "#lib/binding";
|
import { CLIError, MuseError, loadFromBinding, resolveBindingPath } from "#lib";
|
||||||
import { CLIError, FileNotFoundError, MuseError } from "#lib/errors";
|
import { renderPlaybillToHTML } from "#render/html";
|
||||||
import { type CLIArguments, parseCLIArguments } from "#util/args";
|
import {
|
||||||
import { getAbsoluteDirname } from "#util/files";
|
type CLIArguments,
|
||||||
|
getAbsoluteDirname,
|
||||||
|
parseCLIArguments,
|
||||||
|
} from "#util";
|
||||||
import { version } from "../package.json" with { type: "json" };
|
import { version } from "../package.json" with { type: "json" };
|
||||||
import path from "node:path";
|
|
||||||
|
|
||||||
const usage = `
|
const usage = `
|
||||||
${chalk.bold("muse")} - Compile and validate Playbills for Proscenium
|
${chalk.bold("muse")} - Compile and validate Playbills for Proscenium
|
||||||
|
@ -39,47 +36,22 @@ async function processBinding({ inputFilePath, options }: CLIArguments) {
|
||||||
const bindingPath = await resolveBindingPath(inputFilePath);
|
const bindingPath = await resolveBindingPath(inputFilePath);
|
||||||
const binding = await loadFromBinding(bindingPath);
|
const binding = await loadFromBinding(bindingPath);
|
||||||
|
|
||||||
// -- VALDATE PLAYBILL -- //
|
|
||||||
const validatedPlaybill = PlaybillSchema.safeParse(binding.playbill);
|
|
||||||
|
|
||||||
if (!validatedPlaybill.success) {
|
|
||||||
console.error("Error validating playbill");
|
|
||||||
console.error(validatedPlaybill.error.errors);
|
|
||||||
return ExitCode.Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- EXIT EARLY IF JUST CHECKING VALIDATION --//
|
// -- EXIT EARLY IF JUST CHECKING VALIDATION --//
|
||||||
if (options.check) {
|
if (options.check) {
|
||||||
console.log(chalk.green("Playbill validated successfully"));
|
console.log(chalk.green("Playbill validated successfully"));
|
||||||
return ExitCode.Success;
|
return ExitCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
let css = "";
|
|
||||||
|
|
||||||
if (binding.styles && binding.styles.length > 0) {
|
|
||||||
for (const style of binding.styles) {
|
|
||||||
const cssFilePath = path.resolve(binding.bindingFileDirname, style);
|
|
||||||
const cssFile = Bun.file(cssFilePath);
|
|
||||||
const cssFileExists = await cssFile.exists();
|
|
||||||
|
|
||||||
if (cssFileExists) {
|
|
||||||
css += `${await cssFile.text()}\n`;
|
|
||||||
} else {
|
|
||||||
throw new FileNotFoundError(cssFilePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// -- SERIALIZE USING RENDERER --//
|
// -- SERIALIZE USING RENDERER --//
|
||||||
let serializedPlaybill = "";
|
let serializedPlaybill = "";
|
||||||
|
|
||||||
switch (options.renderer) {
|
switch (options.renderer) {
|
||||||
case "json":
|
case "json":
|
||||||
serializedPlaybill = await renderPlaybillToJSON(validatedPlaybill.data);
|
serializedPlaybill = binding.playbill.serialize();
|
||||||
break;
|
break;
|
||||||
case "html":
|
case "html":
|
||||||
serializedPlaybill = await renderPlaybillToHTML(validatedPlaybill.data, {
|
serializedPlaybill = await renderPlaybillToHTML(binding.playbill, {
|
||||||
styles: css,
|
styles: binding.styles,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
|
|
@ -1,19 +1,24 @@
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {
|
import {
|
||||||
UnvalidatedPlaybillSchema,
|
type Ability,
|
||||||
type UnknownComponent,
|
type Blueprint,
|
||||||
type UnvalidatedPlaybill,
|
type Item,
|
||||||
|
type Method,
|
||||||
|
Playbill,
|
||||||
|
type Resource,
|
||||||
|
type Rule,
|
||||||
|
type Species,
|
||||||
} from "@proscenium/playbill";
|
} from "@proscenium/playbill";
|
||||||
import { Glob } from "bun";
|
import { Glob } from "bun";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { FileNotFoundError } from "#lib/errors";
|
import { loadYAMLFileOrFail } from "#util";
|
||||||
import { loadYAMLFileOrFail } from "#util/files";
|
import { ComponentFile } from "./component-file";
|
||||||
import { loadResourceFile } from "./resource";
|
import { FileNotFoundError } from "./errors";
|
||||||
|
|
||||||
const BindingFileSchema = z.object({
|
const BindingSchema = z.object({
|
||||||
extend: z.string().optional(),
|
extend: z.string().optional(),
|
||||||
|
|
||||||
id: z.string(),
|
id: z.string().default("playbill"),
|
||||||
name: z.string().default("Unnamed playbill"),
|
name: z.string().default("Unnamed playbill"),
|
||||||
author: z.string().optional(),
|
author: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
|
@ -23,10 +28,10 @@ const BindingFileSchema = z.object({
|
||||||
styles: z.array(z.string()).optional(),
|
styles: z.array(z.string()).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type BindingFileContent = z.infer<typeof BindingFileSchema>;
|
type Binding = z.infer<typeof BindingSchema>;
|
||||||
|
|
||||||
export interface PlaybillBinding {
|
export interface BoundPlaybill {
|
||||||
_raw: BindingFileContent;
|
_raw: Binding;
|
||||||
|
|
||||||
// File information
|
// File information
|
||||||
bindingFilePath: string;
|
bindingFilePath: string;
|
||||||
|
@ -40,11 +45,20 @@ export interface PlaybillBinding {
|
||||||
version: string;
|
version: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
styles?: string[];
|
styles?: string;
|
||||||
|
|
||||||
playbill: UnvalidatedPlaybill;
|
playbill: Playbill;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a path, find the binding.yaml file
|
||||||
|
* If the path is to a directory, check for a binding.yaml inside
|
||||||
|
* If the path is to a file, check if it exists and is a binding.yaml
|
||||||
|
* Otherwise throw an error
|
||||||
|
*
|
||||||
|
* @param bindingPath - The path to the binding file
|
||||||
|
* @returns The absolute resolved path to the binding file
|
||||||
|
*/
|
||||||
export async function resolveBindingPath(bindingPath: string): Promise<string> {
|
export async function resolveBindingPath(bindingPath: string): Promise<string> {
|
||||||
// If the path does not specify a filename, use binding.yaml
|
// If the path does not specify a filename, use binding.yaml
|
||||||
const inputLocation = Bun.file(bindingPath);
|
const inputLocation = Bun.file(bindingPath);
|
||||||
|
@ -66,29 +80,40 @@ export async function resolveBindingPath(bindingPath: string): Promise<string> {
|
||||||
|
|
||||||
export async function loadFromBinding(
|
export async function loadFromBinding(
|
||||||
bindingPath: string,
|
bindingPath: string,
|
||||||
): Promise<PlaybillBinding> {
|
): Promise<BoundPlaybill> {
|
||||||
const resolvedBindingPath = await resolveBindingPath(bindingPath);
|
const resolvedBindingPath = await resolveBindingPath(bindingPath);
|
||||||
const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath);
|
const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath);
|
||||||
const binding = BindingFileSchema.parse(yamlContent);
|
const binding = BindingSchema.parse(yamlContent);
|
||||||
const fileGlobs = binding.files;
|
const fileGlobs = binding.files;
|
||||||
const bindingFileDirname = path.dirname(resolvedBindingPath);
|
const bindingFileDirname = path.dirname(resolvedBindingPath);
|
||||||
|
|
||||||
let playbill: UnvalidatedPlaybill = UnvalidatedPlaybillSchema.parse({
|
const playbill = new Playbill();
|
||||||
$define: "playbill",
|
|
||||||
id: "blank",
|
|
||||||
name: "Unnamed playbill",
|
|
||||||
description: "Unnamed Playbill",
|
|
||||||
version: "0.0.1",
|
|
||||||
});
|
|
||||||
|
|
||||||
// If this is extending another binding, load that first
|
// If this is extending another binding, load that first
|
||||||
if (binding.extend) {
|
if (binding.extend) {
|
||||||
const pathFromHere = path.resolve(bindingFileDirname, binding.extend);
|
const pathFromHere = path.resolve(bindingFileDirname, binding.extend);
|
||||||
const extendBindingPath = await resolveBindingPath(pathFromHere);
|
const extendBindingPath = await resolveBindingPath(pathFromHere);
|
||||||
const loadedBinding = await loadFromBinding(extendBindingPath);
|
const loadedBinding = await loadFromBinding(extendBindingPath);
|
||||||
playbill = loadedBinding.playbill;
|
playbill.extend(loadedBinding.playbill);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load any specified stylesheets
|
||||||
|
let styles = "";
|
||||||
|
if (binding.styles && binding.styles.length > 0) {
|
||||||
|
for (const style of binding.styles) {
|
||||||
|
const cssFilePath = path.resolve(bindingFileDirname, style);
|
||||||
|
const cssFile = Bun.file(cssFilePath);
|
||||||
|
const cssFileExists = await cssFile.exists();
|
||||||
|
|
||||||
|
if (cssFileExists) {
|
||||||
|
styles += `${await cssFile.text()}\n`;
|
||||||
|
} else {
|
||||||
|
throw new FileNotFoundError(cssFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan for all files matching the globs
|
||||||
const allFilePaths: string[] = [];
|
const allFilePaths: string[] = [];
|
||||||
|
|
||||||
for (const thisGlob of fileGlobs) {
|
for (const thisGlob of fileGlobs) {
|
||||||
|
@ -104,31 +129,110 @@ export async function loadFromBinding(
|
||||||
allFilePaths.push(...results);
|
allFilePaths.push(...results);
|
||||||
}
|
}
|
||||||
|
|
||||||
const bindingFileAbsolutePath = path.resolve(bindingPath);
|
// Exclude the binding.yaml file
|
||||||
const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => {
|
const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => {
|
||||||
return filePath !== bindingFileAbsolutePath;
|
return filePath !== resolvedBindingPath;
|
||||||
});
|
});
|
||||||
|
|
||||||
// -- LOAD ASSOCIATED RESOURCE FILES -- //
|
// Load discovered component files
|
||||||
const loadedResources: UnknownComponent[] = [];
|
const componentFiles: ComponentFile[] = [];
|
||||||
|
const fileLoadPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
for (const filepath of filePathsWithoutBindingFile) {
|
for (const filepath of filePathsWithoutBindingFile) {
|
||||||
const loaded = await loadResourceFile(filepath);
|
const componentFile = new ComponentFile(filepath);
|
||||||
loadedResources.push(...loaded);
|
componentFiles.push(componentFile);
|
||||||
|
fileLoadPromises.push(componentFile.load());
|
||||||
}
|
}
|
||||||
|
|
||||||
playbill.id = binding.id;
|
await Promise.all(fileLoadPromises);
|
||||||
|
|
||||||
|
// Decorate the playbill with the binding metadata
|
||||||
playbill.name = binding.name;
|
playbill.name = binding.name;
|
||||||
playbill.author = binding.author ?? "Anonymous";
|
playbill.author = binding.author ?? "Anonymous";
|
||||||
playbill.version = binding.version;
|
playbill.version = binding.version;
|
||||||
playbill.description = binding.description ?? "";
|
playbill.description = binding.description ?? "";
|
||||||
|
|
||||||
for (const resource of loadedResources) {
|
const abilities: Ability[] = [];
|
||||||
const resourceType = resource.$define;
|
const bluprints: Blueprint[] = [];
|
||||||
const resourceMap = playbill[resourceType] as Record<
|
const glossaries: Record<string, string[]>[] = [];
|
||||||
string,
|
const methods: Method[] = [];
|
||||||
typeof resource
|
const species: Species[] = [];
|
||||||
>;
|
const items: Item[] = [];
|
||||||
resourceMap[resource.id] = resource;
|
const rules: Rule[] = [];
|
||||||
|
const resources: Resource[] = [];
|
||||||
|
|
||||||
|
// Aggregate all components before adding to playbill to allow for
|
||||||
|
// ensuring components are added in a valid order
|
||||||
|
for (const file of componentFiles) {
|
||||||
|
// Load components from the file into the playbill
|
||||||
|
for (const component of file.components) {
|
||||||
|
switch (component.type) {
|
||||||
|
case "ability":
|
||||||
|
abilities.push(component.component);
|
||||||
|
break;
|
||||||
|
case "blueprint":
|
||||||
|
bluprints.push(component.component);
|
||||||
|
break;
|
||||||
|
case "glossary":
|
||||||
|
glossaries.push(component.component);
|
||||||
|
break;
|
||||||
|
case "method":
|
||||||
|
methods.push(component.component);
|
||||||
|
break;
|
||||||
|
case "species":
|
||||||
|
species.push(component.component);
|
||||||
|
break;
|
||||||
|
case "item":
|
||||||
|
items.push(component.component);
|
||||||
|
break;
|
||||||
|
case "rule":
|
||||||
|
rules.push(component.component);
|
||||||
|
break;
|
||||||
|
case "resource":
|
||||||
|
resources.push(component.component);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Unknown component type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add resources first
|
||||||
|
for (const resource of resources) {
|
||||||
|
playbill.addResource(resource);
|
||||||
|
}
|
||||||
|
// Add abilities before methods, species, and blueprints
|
||||||
|
for (const ability of abilities) {
|
||||||
|
playbill.addAbility(ability);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add species before blueprints
|
||||||
|
for (const specie of species) {
|
||||||
|
playbill.addSpecies(specie);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add methods after abilities
|
||||||
|
for (const method of methods) {
|
||||||
|
playbill.addMethod(method);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add items
|
||||||
|
for (const item of items) {
|
||||||
|
playbill.addItem(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add blueprints after methods, species, items, and abilities
|
||||||
|
for (const blueprint of bluprints) {
|
||||||
|
playbill.addBlueprint(blueprint);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all definitions
|
||||||
|
for (const glossary of glossaries) {
|
||||||
|
for (const [term, definitions] of Object.entries(glossary)) {
|
||||||
|
for (const definition of definitions) {
|
||||||
|
playbill.addDefinition(term, definition);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -143,7 +247,7 @@ export async function loadFromBinding(
|
||||||
description: binding.description ?? "",
|
description: binding.description ?? "",
|
||||||
version: binding.version,
|
version: binding.version,
|
||||||
|
|
||||||
styles: binding.styles,
|
styles: styles.length > 0 ? styles : undefined,
|
||||||
|
|
||||||
playbill,
|
playbill,
|
||||||
};
|
};
|
||||||
|
|
129
src/lib/component-file.ts
Normal file
129
src/lib/component-file.ts
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
import path from "node:path";
|
||||||
|
import { type ParsedComponent, parsePlaybillComponent } from "define";
|
||||||
|
import slugify from "slugify";
|
||||||
|
import YAML, { YAMLParseError } from "yaml";
|
||||||
|
import { ZodError } from "zod";
|
||||||
|
import { loadFileOrFail } from "#util";
|
||||||
|
import { MalformedResourceFileError } from "./errors";
|
||||||
|
import { extractFrontmatter, extractMarkdown } from "./markdown";
|
||||||
|
|
||||||
|
type FileFormat = "yaml" | "markdown";
|
||||||
|
|
||||||
|
function parseYAMLResourceFile(
|
||||||
|
filePath: string,
|
||||||
|
text: string,
|
||||||
|
): ParsedComponent[] {
|
||||||
|
const parsedDocs = YAML.parseAllDocuments(text);
|
||||||
|
|
||||||
|
if (parsedDocs.some((doc) => doc.toJS() === null)) {
|
||||||
|
throw new MalformedResourceFileError("Encountered NULL resource", filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = parsedDocs.flatMap((doc) => doc.errors);
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
throw new MalformedResourceFileError(
|
||||||
|
"Error parsing YAML resource",
|
||||||
|
filePath,
|
||||||
|
errors.map((e) => e.message).join(", "),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const collection: ParsedComponent[] = [];
|
||||||
|
|
||||||
|
for (const doc of parsedDocs) {
|
||||||
|
const raw = doc.toJS();
|
||||||
|
collection.push(parsePlaybillComponent(raw));
|
||||||
|
}
|
||||||
|
|
||||||
|
return collection;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseMarkdownResourceFile(
|
||||||
|
filePath: string,
|
||||||
|
text: string,
|
||||||
|
): ParsedComponent[] {
|
||||||
|
try {
|
||||||
|
const defaultName = path.basename(filePath, ".md");
|
||||||
|
const defaultId = slugify(defaultName, { lower: true });
|
||||||
|
const frontmatter = extractFrontmatter(text);
|
||||||
|
const markdown = extractMarkdown(text);
|
||||||
|
|
||||||
|
const together = parsePlaybillComponent({
|
||||||
|
// Use the file name, allow it to be overridden by frontmatter
|
||||||
|
name: defaultName,
|
||||||
|
id: defaultId,
|
||||||
|
...frontmatter,
|
||||||
|
description: markdown,
|
||||||
|
});
|
||||||
|
|
||||||
|
return [together];
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof YAMLParseError) {
|
||||||
|
throw new MalformedResourceFileError(
|
||||||
|
"Error parsing Markdown frontmatter",
|
||||||
|
filePath,
|
||||||
|
e.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e instanceof ZodError) {
|
||||||
|
throw new MalformedResourceFileError(
|
||||||
|
"Error parsing resource file",
|
||||||
|
filePath,
|
||||||
|
e.message,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ComponentFile {
|
||||||
|
private _filePath: string;
|
||||||
|
private _raw = "";
|
||||||
|
private _format: FileFormat;
|
||||||
|
private _components: ParsedComponent[] = [];
|
||||||
|
|
||||||
|
constructor(filePath: string) {
|
||||||
|
this._filePath = path.resolve(filePath);
|
||||||
|
|
||||||
|
const extension = path.extname(filePath).slice(1).toLowerCase();
|
||||||
|
|
||||||
|
switch (extension) {
|
||||||
|
case "yaml":
|
||||||
|
this._format = "yaml";
|
||||||
|
break;
|
||||||
|
case "md":
|
||||||
|
this._format = "markdown";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported file format: ${extension}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load() {
|
||||||
|
this._raw = await loadFileOrFail(this._filePath);
|
||||||
|
switch (this._format) {
|
||||||
|
case "yaml":
|
||||||
|
this._components = parseYAMLResourceFile(this._filePath, this._raw);
|
||||||
|
break;
|
||||||
|
case "markdown":
|
||||||
|
this._components = parseMarkdownResourceFile(this._filePath, this._raw);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of well-formed components defined in this file
|
||||||
|
*/
|
||||||
|
get components() {
|
||||||
|
return this._components;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The aboslute file path of this file
|
||||||
|
*/
|
||||||
|
get path() {
|
||||||
|
return this._filePath;
|
||||||
|
}
|
||||||
|
}
|
14
src/lib/html.ts
Normal file
14
src/lib/html.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
export function html(
|
||||||
|
strings: TemplateStringsArray,
|
||||||
|
...values: unknown[]
|
||||||
|
): string {
|
||||||
|
const [first, ...rest] = strings;
|
||||||
|
let str = first;
|
||||||
|
|
||||||
|
for (let i = 0; i < rest.length; i++) {
|
||||||
|
str += values[i];
|
||||||
|
str += rest[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return str.trim();
|
||||||
|
}
|
5
src/lib/index.ts
Normal file
5
src/lib/index.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export * from "./errors";
|
||||||
|
export * from "./markdown";
|
||||||
|
export * from "./component-file";
|
||||||
|
export * from "./binding";
|
||||||
|
export * from "./html";
|
|
@ -1,5 +1,32 @@
|
||||||
|
import type { MarkedExtension } from "marked";
|
||||||
|
import { marked } from "marked";
|
||||||
import YAML from "yaml";
|
import YAML from "yaml";
|
||||||
|
|
||||||
|
const plugin: MarkedExtension = {
|
||||||
|
renderer: {
|
||||||
|
heading({ tokens, depth }) {
|
||||||
|
const text = this.parser.parseInline(tokens);
|
||||||
|
const level = Math.max(depth, 3);
|
||||||
|
return `<h${level}>${text}</h${level}>`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
marked.use(plugin);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a string of markdown, convert it to HTML
|
||||||
|
*
|
||||||
|
* @param markdown The markdown to convert
|
||||||
|
* @returns A promise resolving to the HTML representation of the markdown
|
||||||
|
*/
|
||||||
|
export async function renderMarkdown(markdown: string): Promise<string> {
|
||||||
|
// TODO: Custom render tags for links, emphasis, and strong
|
||||||
|
return await marked.parse(markdown, {
|
||||||
|
async: true,
|
||||||
|
gfm: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,118 +0,0 @@
|
||||||
import {
|
|
||||||
ComponentMap,
|
|
||||||
type UnknownComponent,
|
|
||||||
UnknownComponentSchema,
|
|
||||||
} from "@proscenium/playbill";
|
|
||||||
import YAML, { YAMLParseError } from "yaml";
|
|
||||||
import { ZodError } from "zod";
|
|
||||||
import { MalformedResourceFileError } from "#lib/errors";
|
|
||||||
import { loadFileOrFail } from "#util/files";
|
|
||||||
import { extractFrontmatter, extractMarkdown } from "./markdown";
|
|
||||||
|
|
||||||
type FileFormat = "yaml" | "markdown";
|
|
||||||
|
|
||||||
function determineFileFormat(filePath: string): FileFormat {
|
|
||||||
return filePath.split(".").pop() === "md" ? "markdown" : "yaml";
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadYamlResourceFile(
|
|
||||||
filePath: string,
|
|
||||||
text: string,
|
|
||||||
): UnknownComponent[] {
|
|
||||||
const parsedDocs = YAML.parseAllDocuments(text);
|
|
||||||
|
|
||||||
if (parsedDocs.some((doc) => doc.toJS() === null)) {
|
|
||||||
throw new MalformedResourceFileError("Encountered NULL resource", filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = parsedDocs.flatMap((doc) => doc.errors);
|
|
||||||
|
|
||||||
if (errors.length > 0) {
|
|
||||||
throw new MalformedResourceFileError(
|
|
||||||
"Error parsing YAML resource",
|
|
||||||
filePath,
|
|
||||||
errors.map((e) => e.message).join(", "),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const collection: UnknownComponent[] = [];
|
|
||||||
|
|
||||||
for (const doc of parsedDocs) {
|
|
||||||
const raw = doc.toJS();
|
|
||||||
const parsed = UnknownComponentSchema.safeParse(raw);
|
|
||||||
|
|
||||||
if (!parsed.success) {
|
|
||||||
throw new MalformedResourceFileError(
|
|
||||||
"Failed to parse resource",
|
|
||||||
filePath,
|
|
||||||
`Schema validation failed: ${parsed.error.message}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedResource = parsed.data;
|
|
||||||
const type = parsedResource.$define;
|
|
||||||
const schemaToUse = ComponentMap[type];
|
|
||||||
const validated = schemaToUse.parse(parsedResource);
|
|
||||||
collection.push(validated);
|
|
||||||
}
|
|
||||||
|
|
||||||
return collection;
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadMarkdownResourceFile(
|
|
||||||
filePath: string,
|
|
||||||
text: string,
|
|
||||||
): UnknownComponent[] {
|
|
||||||
try {
|
|
||||||
const frontmatter = extractFrontmatter(text);
|
|
||||||
const markdown = extractMarkdown(text);
|
|
||||||
|
|
||||||
const together = {
|
|
||||||
...frontmatter,
|
|
||||||
description: markdown,
|
|
||||||
};
|
|
||||||
|
|
||||||
const parsed = UnknownComponentSchema.parse(together);
|
|
||||||
return [parsed];
|
|
||||||
} catch (e) {
|
|
||||||
if (e instanceof YAMLParseError) {
|
|
||||||
throw new MalformedResourceFileError(
|
|
||||||
"Error parsing Markdown frontmatter",
|
|
||||||
filePath,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e instanceof ZodError) {
|
|
||||||
throw new MalformedResourceFileError(
|
|
||||||
"Error parsing resource file",
|
|
||||||
filePath,
|
|
||||||
e.message,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Load and process all documents in a yaml file at the given path
|
|
||||||
* @param filePath - The path to the yaml file
|
|
||||||
* @returns An array of validated resources
|
|
||||||
*
|
|
||||||
* @throws ResourceFileError
|
|
||||||
* @throws NullResourceError
|
|
||||||
*/
|
|
||||||
export async function loadResourceFile(
|
|
||||||
filePath: string,
|
|
||||||
): Promise<UnknownComponent[]> {
|
|
||||||
const text = await loadFileOrFail(filePath);
|
|
||||||
const format = determineFileFormat(filePath);
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case "yaml":
|
|
||||||
return loadYamlResourceFile(filePath, text);
|
|
||||||
case "markdown":
|
|
||||||
return loadMarkdownResourceFile(filePath, text);
|
|
||||||
}
|
|
||||||
}
|
|
41
src/render/html/component/ability.ts
Normal file
41
src/render/html/component/ability.ts
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
import type { Ability } from "@proscenium/playbill";
|
||||||
|
import { html, renderMarkdown } from "#lib";
|
||||||
|
|
||||||
|
export async function AbilityCard(ability: Ability): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(ability.description);
|
||||||
|
|
||||||
|
const costs: string[] = [];
|
||||||
|
|
||||||
|
if (ability.ap > 0) {
|
||||||
|
costs.push(`<li class="ability-cost-ap">${ability.ap} AP</li>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ability.hp > 0) {
|
||||||
|
costs.push(`<li class="ability-cost-hp">${ability.hp} HP</li>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ability.ep > 0) {
|
||||||
|
costs.push(`<li class="ability-cost-ep">${ability.ep} EP</li>`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const costList =
|
||||||
|
costs.length > 0
|
||||||
|
? `<ul class="ability-costs">${costs.join("\n")}</ul>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const classList = `ability ability-${ability.type}`;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<div class="${classList}" data-component-id="${ability.id}">
|
||||||
|
<div class="ability-header">
|
||||||
|
<h4>${ability.name}</h4>
|
||||||
|
<span class="ability-type">${ability.type}</span>
|
||||||
|
<span class="ability-cost-xp">${ability.xp} XP</span>
|
||||||
|
${costList}
|
||||||
|
</div>
|
||||||
|
<div class="ability-content">
|
||||||
|
${renderedMarkdown}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
39
src/render/html/component/base/section.ts
Normal file
39
src/render/html/component/base/section.ts
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import { html } from "#lib";
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
type: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
preInfo?: string;
|
||||||
|
info?: string;
|
||||||
|
componentId?: string;
|
||||||
|
leftCornerTag?: string;
|
||||||
|
rightCornerTag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function Section({
|
||||||
|
type,
|
||||||
|
title,
|
||||||
|
content,
|
||||||
|
preInfo = "",
|
||||||
|
info = "",
|
||||||
|
componentId,
|
||||||
|
leftCornerTag = "",
|
||||||
|
rightCornerTag = "",
|
||||||
|
}: SectionProps): Promise<string> {
|
||||||
|
const dataTags = componentId ? `data-component-id="${componentId}"` : "";
|
||||||
|
return html`
|
||||||
|
<section class="section ${type}" ${dataTags}>
|
||||||
|
<div class="section-header ${type}-header">
|
||||||
|
${preInfo}
|
||||||
|
<h2>${title}</h2>
|
||||||
|
${info}
|
||||||
|
</div>
|
||||||
|
<div class="section-tag section-tag--left">${leftCornerTag}</div>
|
||||||
|
<div class="section-tag section-tag--right">${rightCornerTag}</div>
|
||||||
|
<div class="section-content ${type}-content">
|
||||||
|
${content}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
25
src/render/html/component/glossary.ts
Normal file
25
src/render/html/component/glossary.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { html } from "#lib";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
export async function GlossaryTable(
|
||||||
|
glossary: Record<string, string[]>,
|
||||||
|
): Promise<string> {
|
||||||
|
const terms = Object.entries(glossary).map(([term, defs]) => {
|
||||||
|
return html`<dt>${term}</dt>${defs.map((d) => html`<dd>${d}</dd>`).join("\n")}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const content = html`
|
||||||
|
<div class="glossary-content">
|
||||||
|
<dl>
|
||||||
|
${terms.join("\n")}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "glossary",
|
||||||
|
componentId: "glossary",
|
||||||
|
title: "Glossary",
|
||||||
|
content,
|
||||||
|
});
|
||||||
|
}
|
12
src/render/html/component/header.ts
Normal file
12
src/render/html/component/header.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { Playbill } from "@proscenium/playbill";
|
||||||
|
import { html } from "#lib";
|
||||||
|
|
||||||
|
export async function PageHeader(playbill: Playbill): Promise<string> {
|
||||||
|
return html`
|
||||||
|
<header>
|
||||||
|
<h1 class="header-title">${playbill.name}</h1>
|
||||||
|
<p class="header-byline">A Playbill by ${playbill.author}</p>
|
||||||
|
<p><small class="header-info">Version ${playbill.version}</small></p>
|
||||||
|
</header>
|
||||||
|
`;
|
||||||
|
}
|
51
src/render/html/component/item.ts
Normal file
51
src/render/html/component/item.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import type { Item } from "@proscenium/playbill";
|
||||||
|
import { capitalize } from "lodash-es";
|
||||||
|
import { html, renderMarkdown } from "#lib";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
export async function ItemCard(item: Item): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(item.description);
|
||||||
|
|
||||||
|
let itemTypeDescriptor = "";
|
||||||
|
|
||||||
|
switch (item.type) {
|
||||||
|
case "wielded":
|
||||||
|
itemTypeDescriptor = `Wielded (${item.hands} ${item.hands === 1 ? "Hand" : "Hands"})`;
|
||||||
|
break;
|
||||||
|
case "worn":
|
||||||
|
itemTypeDescriptor = `Worn (${item.slot})`;
|
||||||
|
break;
|
||||||
|
case "trinket":
|
||||||
|
itemTypeDescriptor = "Trinket";
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const affinity =
|
||||||
|
item.affinity !== "none"
|
||||||
|
? html`<p class="item-affinity ${item.affinity}">${item.affinity} Affinity</p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const damageType = item.damage?.type === "arc" ? "Arcane" : "Physical";
|
||||||
|
const damageAmount = item.damage?.amount ?? 0;
|
||||||
|
const damageRating =
|
||||||
|
damageAmount > 0
|
||||||
|
? html`<p class="item-damage ${damageType}">${damageAmount} ${damageType} Damage</p>`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const info = html`
|
||||||
|
<div class="item-info">
|
||||||
|
${affinity}
|
||||||
|
${damageRating}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "item",
|
||||||
|
componentId: item.id,
|
||||||
|
title: item.name,
|
||||||
|
content: renderedMarkdown,
|
||||||
|
info,
|
||||||
|
rightCornerTag: capitalize(item.rarity),
|
||||||
|
leftCornerTag: itemTypeDescriptor,
|
||||||
|
});
|
||||||
|
}
|
38
src/render/html/component/method.ts
Normal file
38
src/render/html/component/method.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import type { Method, Playbill } from "@proscenium/playbill";
|
||||||
|
import { html, renderMarkdown } from "#lib";
|
||||||
|
import { AbilityCard } from "./ability";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
export async function MethodSection(method: Method, playbill: Playbill) {
|
||||||
|
const descriptionHTML = await renderMarkdown(method.description);
|
||||||
|
let ranksHTML = "";
|
||||||
|
let rankNumber = 1;
|
||||||
|
|
||||||
|
for (const rank of method.abilities) {
|
||||||
|
ranksHTML += `<div class="method-rank"><h3>Rank ${rankNumber}</h3><div class="method-rank-content">`;
|
||||||
|
|
||||||
|
for (const ability of rank) {
|
||||||
|
const html = await AbilityCard(playbill.abilities[ability]);
|
||||||
|
ranksHTML += html;
|
||||||
|
}
|
||||||
|
|
||||||
|
ranksHTML += "</div></div>";
|
||||||
|
|
||||||
|
rankNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const methodContent = html`
|
||||||
|
<div class="method-ranks">
|
||||||
|
${ranksHTML}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "method",
|
||||||
|
componentId: method.id,
|
||||||
|
title: method.name,
|
||||||
|
preInfo: html`<p class="method-curator">${method.curator}'s Method of</p>`,
|
||||||
|
info: html`<p class="method-description">${descriptionHTML}</p>`,
|
||||||
|
content: methodContent,
|
||||||
|
});
|
||||||
|
}
|
102
src/render/html/component/resource.ts
Normal file
102
src/render/html/component/resource.ts
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
import type {
|
||||||
|
ImageResource,
|
||||||
|
Resource,
|
||||||
|
TableResource,
|
||||||
|
TextResource,
|
||||||
|
} from "@proscenium/playbill";
|
||||||
|
import { html, renderMarkdown } from "#lib";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
async function TextResourceSection(resource: TextResource): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(resource.description);
|
||||||
|
|
||||||
|
const info = resource.categories
|
||||||
|
.map((category) => {
|
||||||
|
return html`<li>${category}</li>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "resource",
|
||||||
|
componentId: resource.id,
|
||||||
|
title: resource.name,
|
||||||
|
content: renderedMarkdown,
|
||||||
|
info: `<ul class="resource-categories">${info}</ul>`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ImageResourceSection(resource: ImageResource): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(resource.description);
|
||||||
|
|
||||||
|
const info = resource.categories
|
||||||
|
.map((category) => {
|
||||||
|
return html`<li>${category}</li>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const fig = html`
|
||||||
|
<figure class="resource-image">
|
||||||
|
<img src="${resource.url}" alt="${resource.name}" />
|
||||||
|
<figcaption>${renderedMarkdown}</figcaption>
|
||||||
|
</figure>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "resource",
|
||||||
|
componentId: resource.id,
|
||||||
|
title: resource.name,
|
||||||
|
content: fig,
|
||||||
|
info: `<ul class="resource-categories">${info}</ul>`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function TableResourceSection(resource: TableResource): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(resource.description);
|
||||||
|
|
||||||
|
const info = resource.categories
|
||||||
|
.map((category) => {
|
||||||
|
return html`<li>${category}</li>`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
const tableData = resource.data;
|
||||||
|
|
||||||
|
const [headerRow, ...rows] = tableData;
|
||||||
|
|
||||||
|
let tableHTML = html`<table class="resource-table"><thead><tr>`;
|
||||||
|
|
||||||
|
for (const header of headerRow) {
|
||||||
|
tableHTML += html`<th>${await renderMarkdown(header)}</th>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += html`</tr></thead><tbody>`;
|
||||||
|
|
||||||
|
for (const row of rows) {
|
||||||
|
tableHTML += html`<tr>`;
|
||||||
|
for (const cell of row) {
|
||||||
|
tableHTML += html`<td>${await renderMarkdown(cell)}</td>`;
|
||||||
|
}
|
||||||
|
tableHTML += html`</tr>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
tableHTML += html`</tbody></table>`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "resource",
|
||||||
|
componentId: resource.id,
|
||||||
|
title: resource.name,
|
||||||
|
content: renderedMarkdown + tableHTML,
|
||||||
|
info: `<ul class="resource-categories">${info}</ul>`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ResourceSection(resource: Resource): Promise<string> {
|
||||||
|
switch (resource.type) {
|
||||||
|
case "text":
|
||||||
|
return TextResourceSection(resource);
|
||||||
|
case "image":
|
||||||
|
return ImageResourceSection(resource);
|
||||||
|
case "table":
|
||||||
|
return TableResourceSection(resource);
|
||||||
|
}
|
||||||
|
}
|
14
src/render/html/component/rule.ts
Normal file
14
src/render/html/component/rule.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Rule } from "@proscenium/playbill";
|
||||||
|
import { renderMarkdown } from "#lib";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
export async function RuleSection(rule: Rule): Promise<string> {
|
||||||
|
const renderedMarkdown = await renderMarkdown(rule.description);
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
title: rule.name,
|
||||||
|
componentId: rule.id,
|
||||||
|
content: renderedMarkdown,
|
||||||
|
type: "rule",
|
||||||
|
});
|
||||||
|
}
|
97
src/render/html/component/species.ts
Normal file
97
src/render/html/component/species.ts
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
import type { Playbill, Species } from "@proscenium/playbill";
|
||||||
|
import { html, renderMarkdown } from "#lib";
|
||||||
|
import { AbilityCard } from "./ability";
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
export async function SpeciesSection(species: Species, playbill: Playbill) {
|
||||||
|
const descriptionHTML = await renderMarkdown(species.description);
|
||||||
|
let abilitiesHTML = "";
|
||||||
|
|
||||||
|
for (const ability of species.abilities) {
|
||||||
|
abilitiesHTML += await AbilityCard(playbill.abilities[ability]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hpString = species.hp >= 0 ? `+${species.hp}` : `-${species.hp}`;
|
||||||
|
const apString = species.ap >= 0 ? `+${species.ap}` : `-${species.ap}`;
|
||||||
|
const epString = species.ep >= 0 ? `+${species.ep}` : `-${species.ep}`;
|
||||||
|
|
||||||
|
const hasAbilities = species.abilities.length > 0;
|
||||||
|
|
||||||
|
const abilitySection = hasAbilities
|
||||||
|
? html`
|
||||||
|
<div class="species-abilities">
|
||||||
|
<h3>Innate Abilities</h3>
|
||||||
|
${abilitiesHTML}
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const sectionContent = html`
|
||||||
|
<div class="species-stats">
|
||||||
|
|
||||||
|
<div class="species-stat-hp">
|
||||||
|
<h4 class="hp species-stat-hp-title" title="Health Points">HP</h4>
|
||||||
|
<p class="species-stat-hp-value">${hpString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-stat-ap">
|
||||||
|
<h4 class="ap species-stat-ap-title" title="Action Points">AP</h4>
|
||||||
|
<p class="species-stat-ap-value">${apString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-stat-ep">
|
||||||
|
<h4 class="ep species-stat-ep-title" title="Exhaustion Points">EP</h4>
|
||||||
|
<p class="species-stat-ep-value">${epString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talents">
|
||||||
|
|
||||||
|
<div class="species-talent-muscle">
|
||||||
|
<h4 class="species-talent-muscle-title muscle">Muscle</h4>
|
||||||
|
<p class="species-talent-muscle-value ${species.muscle}">${species.muscle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talent-focus">
|
||||||
|
<h4 class="species-talent-focus-title focus">Focus</h4>
|
||||||
|
<p class="species-talent-focus-value ${species.focus}">${species.focus}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talent-knowledge">
|
||||||
|
<h4 class="species-talent-knowledge-title knowledge">Knowledge</h4>
|
||||||
|
<p class="species-talent-knowledge-value ${species.knowledge}">${species.knowledge}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talent-charm">
|
||||||
|
<h4 class="species-talent-charm-title charm">Charm</h4>
|
||||||
|
<p class="species-talent-charm-value ${species.charm}">${species.charm}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talent-cunning">
|
||||||
|
<h4 class="species-talent-cunning-title cunning">Cunning</h4>
|
||||||
|
<p class="species-talent-cunning-value ${species.cunning}">${species.cunning}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="species-talent-spark">
|
||||||
|
<h4 class="species-talent-spark-title spark">Spark</h4>
|
||||||
|
<p class="species-talent-spark-value ${species.spark}">${species.spark}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="species-content">
|
||||||
|
${descriptionHTML}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
${abilitySection}
|
||||||
|
`;
|
||||||
|
|
||||||
|
return Section({
|
||||||
|
type: "species",
|
||||||
|
componentId: species.id,
|
||||||
|
title: species.name,
|
||||||
|
content: sectionContent,
|
||||||
|
info: "<p>Species</p>",
|
||||||
|
});
|
||||||
|
}
|
164
src/render/html/index.ts
Normal file
164
src/render/html/index.ts
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
import type { Playbill } from "@proscenium/playbill";
|
||||||
|
import sortBy from "lodash-es/sortBy";
|
||||||
|
import { html } from "#lib";
|
||||||
|
import { GlossaryTable } from "./component/glossary";
|
||||||
|
import { PageHeader } from "./component/header";
|
||||||
|
import { ItemCard } from "./component/item";
|
||||||
|
import { MethodSection } from "./component/method";
|
||||||
|
import { ResourceSection } from "./component/resource";
|
||||||
|
import { RuleSection } from "./component/rule";
|
||||||
|
import { SpeciesSection } from "./component/species";
|
||||||
|
|
||||||
|
interface HTMLRenderOptions {
|
||||||
|
styles?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderPlaybillToHTML(
|
||||||
|
playbill: Playbill,
|
||||||
|
opts: HTMLRenderOptions = {},
|
||||||
|
): Promise<string> {
|
||||||
|
// Render various resources
|
||||||
|
const resources = (
|
||||||
|
await Promise.all(Object.values(playbill.resources).map(ResourceSection))
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
const methods = (
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(playbill.methods).map((method) => {
|
||||||
|
return MethodSection(method, playbill);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
const species = (
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(playbill.species).map((species) => {
|
||||||
|
return SpeciesSection(species, playbill);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
const items = (
|
||||||
|
await Promise.all(
|
||||||
|
Object.values(playbill.items).map((item) => {
|
||||||
|
return ItemCard(item);
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
).join("\n");
|
||||||
|
|
||||||
|
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
|
||||||
|
const rules = (await Promise.all(rulesByOrder.map(RuleSection))).join("\n");
|
||||||
|
const glossaryHTML = await GlossaryTable(playbill.glossary);
|
||||||
|
|
||||||
|
// Prepare stylesheet
|
||||||
|
const css = opts.styles ? html`<style>${opts.styles}</style>` : "";
|
||||||
|
|
||||||
|
// Main document
|
||||||
|
const doc = html`
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Playbill: ${playbill.name}</title>
|
||||||
|
${css}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function removeHash() {
|
||||||
|
history.pushState("", document.title, window.location.pathname);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHash(hash) {
|
||||||
|
if (hash.length > 0) {
|
||||||
|
history.pushState(null, null, "#" + hash);
|
||||||
|
} else {
|
||||||
|
removeHash();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAllViews() {
|
||||||
|
document.querySelectorAll(".view").forEach((view) => {
|
||||||
|
view.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAllViews() {
|
||||||
|
document.querySelectorAll(".view").forEach((view) => {
|
||||||
|
view.style.display = "block";
|
||||||
|
});
|
||||||
|
setHash("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function showView(viewId) {
|
||||||
|
document.getElementById(viewId).style.display = "block";
|
||||||
|
setHash(viewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function hideAllAndShow(viewId) {
|
||||||
|
hideAllViews();
|
||||||
|
showView(viewId);
|
||||||
|
}
|
||||||
|
|
||||||
|
function navigateFromHash() {
|
||||||
|
const hash = window.location.hash.slice(1);
|
||||||
|
if (hash.length > 0) {
|
||||||
|
const view = document.getElementById(hash);
|
||||||
|
|
||||||
|
if (view !== null) {
|
||||||
|
hideAllAndShow(hash);
|
||||||
|
} else {
|
||||||
|
showAllViews();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showAllViews();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", navigateFromHash());
|
||||||
|
window.addEventListener("hashchange", navigateFromHash())
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
${await PageHeader(playbill)}
|
||||||
|
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
|
||||||
|
<nav>
|
||||||
|
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
|
||||||
|
<button onclick="hideAllAndShow('methods')" id="methods-nav">Methods</button>
|
||||||
|
<button onclick="hideAllAndShow('items')" id="items-nav">Items</button>
|
||||||
|
<button onclick="hideAllAndShow('rules')" id="rules-nav">Rulebook</button>
|
||||||
|
<button onclick="hideAllAndShow('glossary')" id="glossary-nav">Glossary</button>
|
||||||
|
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
|
||||||
|
<button onclick="showAllViews()" id="all-nav">Show All</button>
|
||||||
|
</nav>
|
||||||
|
<article id="species" class="view">
|
||||||
|
${species}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="methods" class="view">
|
||||||
|
${methods}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="items" class="view">
|
||||||
|
${items}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="rules" class="view">
|
||||||
|
${rules}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="glossary" class="view">
|
||||||
|
${glossaryHTML}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="resources" class="view">
|
||||||
|
${resources}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</article>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
return doc;
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { CLIError, MuseError } from "#lib/errors";
|
import { CLIError, MuseError } from "#lib";
|
||||||
|
|
||||||
const Renderers = ["json", "html"] as const;
|
const Renderers = ["json", "html"] as const;
|
||||||
const RendererSchema = z.enum(Renderers);
|
const RendererSchema = z.enum(Renderers);
|
||||||
|
|
2
src/util/index.ts
Normal file
2
src/util/index.ts
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
export * from "./args";
|
||||||
|
export * from "./files";
|
|
@ -25,14 +25,14 @@
|
||||||
"noPropertyAccessFromIndexSignature": false,
|
"noPropertyAccessFromIndexSignature": false,
|
||||||
"baseUrl": "./src",
|
"baseUrl": "./src",
|
||||||
"paths": {
|
"paths": {
|
||||||
"#lib/*": [
|
"#lib": [
|
||||||
"./lib/*"
|
"./lib/index.ts"
|
||||||
],
|
],
|
||||||
"#renderers/*": [
|
"#render/*": [
|
||||||
"./renderers/*"
|
"./render/*"
|
||||||
],
|
],
|
||||||
"#util/*": [
|
"#util": [
|
||||||
"./util/*"
|
"./util/index.ts"
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue