Use new playbill updates

This commit is contained in:
Endeavorance 2025-03-21 16:40:47 -04:00
parent c1f3c6cade
commit f7f861a1cb
8 changed files with 81 additions and 210 deletions

View file

@ -1,5 +1,6 @@
import { import {
type Ability, type Ability,
type AnyPlaybillComponent,
type Blueprint, type Blueprint,
type Item, type Item,
type Method, type Method,
@ -161,37 +162,7 @@ const parseComponentType = (val: unknown): ComponentType => {
return val as ComponentType; return val as ComponentType;
}; };
export type ParsedComponent = function parseAbilityDefinition(obj: unknown): Ability {
| {
type: "ability";
component: Ability;
}
| {
type: "blueprint";
component: Blueprint;
}
| {
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 parsed = AbilitySchema.parse(obj);
const ability = parseAbility({ const ability = parseAbility({
@ -214,13 +185,10 @@ function parseAbilityDefinition(obj: unknown): ParsedComponent {
roll: parsed.roll, roll: parsed.roll,
}); });
return { return ability;
type: "ability",
component: ability,
};
} }
function parseBlueprintDefinition(obj: unknown): ParsedComponent { function parseBlueprintDefinition(obj: unknown): Blueprint {
const parsed = BlueprintSchema.parse(obj); const parsed = BlueprintSchema.parse(obj);
const blueprint = parseBlueprint({ const blueprint = parseBlueprint({
@ -244,34 +212,28 @@ function parseBlueprintDefinition(obj: unknown): ParsedComponent {
spark: parsed.spark, spark: parsed.spark,
}); });
return { return blueprint;
type: "blueprint",
component: blueprint,
};
} }
function parseItemDefinition(obj: unknown): ParsedComponent { function parseItemDefinition(obj: unknown): Item {
const parsed = ItemSchema.parse(obj); const parsed = ItemSchema.parse(obj);
return { return parseItem({
type: "item", ...parsed,
component: parseItem({ damage:
...parsed, parsed.damage > 0
damage: ? {
parsed.damage > 0 amount: parsed.damage,
? { type: parsed.damageType,
amount: parsed.damage, }
type: parsed.damageType, : null,
} abilities: parsed.abilities,
: null, tweak: "",
abilities: parsed.abilities, temper: "",
tweak: "", });
temper: "",
}),
};
} }
function parseMethodDefinition(obj: unknown): ParsedComponent { function parseMethodDefinition(obj: unknown): Method {
const parsed = MethodSchema.parse(obj); const parsed = MethodSchema.parse(obj);
// Prefer `abilities` if defined, otherwise // Prefer `abilities` if defined, otherwise
@ -310,40 +272,29 @@ function parseMethodDefinition(obj: unknown): ParsedComponent {
abilities: abilities, abilities: abilities,
}; };
return { return parseMethod(method);
type: "method",
component: parseMethod(method),
};
} }
function parseResourceDefinition(obj: unknown): ParsedComponent { function parseResourceDefinition(obj: unknown): Resource {
const parsed = ResourceSchema.parse(obj); const parsed = ResourceSchema.parse(obj);
return parseResource(parsed);
return {
type: "resource",
component: parseResource(parsed),
};
} }
function parseRuleDefinition(obj: unknown): ParsedComponent { function parseRuleDefinition(obj: unknown): Rule {
const parsed = RuleSchema.parse(obj); const parsed = RuleSchema.parse(obj);
return { return parseRule(parsed);
type: "rule",
component: parseRule(parsed),
};
} }
function parseSpeciesDefinition(obj: unknown): ParsedComponent { function parseSpeciesDefinition(obj: unknown): Species {
const parsed = SpeciesSchema.parse(obj); const parsed = SpeciesSchema.parse(obj);
return { return parseSpecies(parsed);
type: "species",
component: parseSpecies(parsed),
};
} }
export function parsePlaybillComponent(obj: unknown): ParsedComponent | null { export function parsePlaybillComponent(
obj: unknown,
): AnyPlaybillComponent | null {
const baseParse = Base.parse(obj); const baseParse = Base.parse(obj);
if (baseParse.$hidden) { if (baseParse.$hidden) {

View file

@ -21,17 +21,17 @@ enum ExitCode {
} }
async function processBinding({ inputFilePath, options }: CLIArguments) { async function processBinding({ inputFilePath, options }: CLIArguments) {
// -- BINDING FILE -- // // Load the binding
const bindingPath = await resolveBindingPath(inputFilePath); const bindingPath = await resolveBindingPath(inputFilePath);
const binding = await loadFromBinding(bindingPath); const binding = await loadFromBinding(bindingPath);
// -- EXIT EARLY IF JUST CHECKING VALIDATION --// // If --check is specified, exit early
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;
} }
// -- SERIALIZE USING RENDERER --// // Serialize (default: JSON)
let serializedPlaybill = ""; let serializedPlaybill = "";
switch (options.renderer) { switch (options.renderer) {
@ -48,12 +48,13 @@ async function processBinding({ inputFilePath, options }: CLIArguments) {
throw new CLIError(`Unknown renderer: ${options.renderer}`); throw new CLIError(`Unknown renderer: ${options.renderer}`);
} }
// Write to disk if an outfile is specified // Write to disk if --outfile is specified
if (options.outfile !== "") { if (options.outfile !== "") {
await Bun.write(options.outfile, serializedPlaybill); await Bun.write(options.outfile, serializedPlaybill);
return ExitCode.Success; return ExitCode.Success;
} }
// Otherwise, write to stdout
console.log(serializedPlaybill); console.log(serializedPlaybill);
return ExitCode.Success; return ExitCode.Success;
} }
@ -62,26 +63,27 @@ async function main(): Promise<number> {
const cliArguments = parseCLIArguments(Bun.argv.slice(2)); const cliArguments = parseCLIArguments(Bun.argv.slice(2));
const { options } = cliArguments; const { options } = cliArguments;
// -- HELP TEXT -- // // If --help is specified, print usage and exit
if (options.help) { if (options.help) {
console.log(USAGE); console.log(USAGE);
return ExitCode.Success; return ExitCode.Success;
} }
console.log(`Processing ${cliArguments.inputFilePath}`);
let lastProcessResult = await processBinding(cliArguments); let lastProcessResult = await processBinding(cliArguments);
if (options.watch) { if (options.watch) {
const watchDir = getAbsoluteDirname(cliArguments.inputFilePath); const watchDir = getAbsoluteDirname(cliArguments.inputFilePath);
console.log(`Watching ${watchDir} for changes`); console.log(`Watching ${watchDir} for changes...`);
const watcher = watch(watchDir, { const watcher = watch(watchDir, {
recursive: true, recursive: true,
}); });
for await (const event of watcher) { for await (const event of watcher) {
console.log(`Detected ${event.eventType} on ${event.filename}`); console.log(
`Detected ${event.eventType} on ${event.filename}. Reprocessing...`,
);
try { try {
lastProcessResult = await processBinding(cliArguments); lastProcessResult = await processBinding(cliArguments);

View file

@ -1,14 +1,5 @@
import path from "node:path"; import path from "node:path";
import { import { type AnyPlaybillComponent, Playbill } from "@proscenium/playbill";
type Ability,
type Blueprint,
type Item,
type Method,
Playbill,
type Resource,
type Rule,
type Species,
} from "@proscenium/playbill";
import { Glob } from "bun"; import { Glob } from "bun";
import z from "zod"; import z from "zod";
import { loadYAMLFileOrFail } from "#util"; import { loadYAMLFileOrFail } from "#util";
@ -33,11 +24,7 @@ const BindingSchema = z.object({
terms: z.record(z.string(), z.string()).default({}), terms: z.record(z.string(), z.string()).default({}),
}); });
type Binding = z.infer<typeof BindingSchema>;
export interface BoundPlaybill { export interface BoundPlaybill {
_raw: Binding;
// File information // File information
bindingFilePath: string; bindingFilePath: string;
bindingFileDirname: string; bindingFileDirname: string;
@ -93,7 +80,12 @@ export async function loadFromBinding(
const fileGlobs = binding.files; const fileGlobs = binding.files;
const bindingFileDirname = path.dirname(resolvedBindingPath); const bindingFileDirname = path.dirname(resolvedBindingPath);
const playbill = new Playbill(); const playbill = new Playbill(
binding.name,
binding.author,
binding.description,
binding.version,
);
// If this is extending another binding, load that first // If this is extending another binding, load that first
for (const includePath of binding.include) { for (const includePath of binding.include) {
@ -151,86 +143,15 @@ export async function loadFromBinding(
await Promise.all(fileLoadPromises); await Promise.all(fileLoadPromises);
// Decorate the playbill with the binding metadata // Aggregate all loaded components
playbill.name = binding.name; const loadedComponents: AnyPlaybillComponent[] = [];
playbill.author = binding.author ?? "Anonymous";
playbill.version = binding.version;
playbill.description = binding.description ?? "";
const abilities: Ability[] = [];
const bluprints: Blueprint[] = [];
const methods: Method[] = [];
const species: Species[] = [];
const items: Item[] = [];
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) { for (const file of componentFiles) {
// Load components from the file into the playbill loadedComponents.push(...file.components);
for (const component of file.components) {
switch (component.type) {
case "ability":
abilities.push(component.component);
break;
case "blueprint":
bluprints.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 // Add all components to the playbill
for (const resource of resources) { // (This method ensures proper addition order maintain correctness)
playbill.addResource(resource); playbill.addManyComponents(loadedComponents);
}
// 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 rules
for (const rule of rules) {
playbill.addRule(rule);
}
// Add all definitions // Add all definitions
for (const [term, definition] of Object.entries(binding.terms)) { for (const [term, definition] of Object.entries(binding.terms)) {
@ -238,7 +159,6 @@ export async function loadFromBinding(
} }
return { return {
_raw: binding,
bindingFilePath: bindingPath, bindingFilePath: bindingPath,
bindingFileDirname, bindingFileDirname,
files: filePathsWithoutBindingFile, files: filePathsWithoutBindingFile,

View file

@ -1,10 +1,11 @@
import path from "node:path"; import path from "node:path";
import { type ParsedComponent, parsePlaybillComponent } from "define"; import { parsePlaybillComponent } from "define";
import YAML, { YAMLParseError } from "yaml"; import YAML, { YAMLParseError } from "yaml";
import { ZodError } from "zod"; import { ZodError } from "zod";
import { loadFileOrFail } from "#util"; import { loadFileOrFail } from "#util";
import { MalformedResourceFileError } from "./errors"; import { MalformedResourceFileError } from "./errors";
import { toSlug } from "./slug"; import { toSlug } from "./slug";
import type { AnyPlaybillComponent } from "@proscenium/playbill";
type FileFormat = "yaml" | "markdown"; type FileFormat = "yaml" | "markdown";
@ -45,7 +46,7 @@ function extractMarkdown(content: string): string {
function parseYAMLResourceFile( function parseYAMLResourceFile(
filePath: string, filePath: string,
text: string, text: string,
): ParsedComponent[] { ): AnyPlaybillComponent[] {
const parsedDocs = YAML.parseAllDocuments(text); const parsedDocs = YAML.parseAllDocuments(text);
if (parsedDocs.some((doc) => doc.toJS() === null)) { if (parsedDocs.some((doc) => doc.toJS() === null)) {
@ -62,7 +63,7 @@ function parseYAMLResourceFile(
); );
} }
const collection: ParsedComponent[] = []; const collection: AnyPlaybillComponent[] = [];
for (const doc of parsedDocs) { for (const doc of parsedDocs) {
const raw = doc.toJS(); const raw = doc.toJS();
@ -78,7 +79,7 @@ function parseYAMLResourceFile(
function parseMarkdownResourceFile( function parseMarkdownResourceFile(
filePath: string, filePath: string,
text: string, text: string,
): ParsedComponent[] { ): AnyPlaybillComponent[] {
try { try {
const defaultName = path.basename(filePath, ".md"); const defaultName = path.basename(filePath, ".md");
const defaultId = toSlug(defaultName); const defaultId = toSlug(defaultName);
@ -124,7 +125,7 @@ export class ComponentFile {
private _filePath: string; private _filePath: string;
private _raw = ""; private _raw = "";
private _format: FileFormat; private _format: FileFormat;
private _components: ParsedComponent[] = []; private _components: AnyPlaybillComponent[] = [];
private _basename: string; private _basename: string;
constructor(filePath: string) { constructor(filePath: string) {

View file

@ -8,7 +8,7 @@ import { toSlug } from "./slug";
import rehypeRaw from "rehype-raw"; import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
import rehypeShiftHeading from "rehype-shift-heading"; import rehypeShiftHeading from "rehype-shift-heading";
import type { TaggedComponent } from "@proscenium/playbill"; import type { AnyPlaybillComponent } from "@proscenium/playbill";
export type MarkdownParserFunction = (input: string) => Promise<string>; export type MarkdownParserFunction = (input: string) => Promise<string>;
@ -26,9 +26,7 @@ export function createMarkdownRenderer(
.use(remarkGfm) .use(remarkGfm)
.use(remarkWikiLink, { .use(remarkWikiLink, {
aliasDivider: "|", aliasDivider: "|",
permalinks: binding.playbill permalinks: binding.playbill.allComponents().map((c) => c.id),
.allComponents()
.map((c) => `${c.component.id}`),
pageResolver: (permalink: string) => { pageResolver: (permalink: string) => {
return [toSlug(permalink)]; return [toSlug(permalink)];
}, },
@ -54,21 +52,19 @@ export async function compileMarkdownInPlaybill(
const playbill = boundPlaybill.playbill; const playbill = boundPlaybill.playbill;
// Define a processor function to iterate over all components and process their markdown // Define a processor function to iterate over all components and process their markdown
const processMarkdownInComponent = async (entry: TaggedComponent) => { const processMarkdownInComponent = async (entry: AnyPlaybillComponent) => {
entry.component.description = await renderMarkdown( entry.description = await renderMarkdown(entry.description);
entry.component.description,
);
if (entry.type === "resource" && entry.component.type === "table") { if (entry._component === "resource" && entry.type === "table") {
const newData: string[][] = []; const newData: string[][] = [];
for (const row of entry.component.data) { for (const row of entry.data) {
const newRow: string[] = []; const newRow: string[] = [];
for (const cell of row) { for (const cell of row) {
newRow.push(await renderMarkdown(cell)); newRow.push(await renderMarkdown(cell));
} }
newData.push(newRow); newData.push(newRow);
} }
entry.component.data = newData; entry.data = newData;
} }
}; };

View file

@ -1,15 +1,15 @@
import { Section } from "./base/section"; import { Section } from "./base/section";
interface GlossaryTableProps { interface GlossaryTableProps {
glossary: Record<string, string[]>; terms: [string, string[]][];
} }
export function GlossaryTable({ glossary }: GlossaryTableProps) { export function GlossaryTable({ terms }: GlossaryTableProps) {
return ( return (
<Section type="glossary" componentId="glossary" title="Glossary"> <Section type="glossary" componentId="glossary" title="Glossary">
<div className="glossary-content"> <div className="glossary-content">
<dl> <dl>
{Object.entries(glossary).map(([term, defs]) => { {terms.map(([term, defs]) => {
return ( return (
<div key={term}> <div key={term}>
<dt>{term}</dt> <dt>{term}</dt>

View file

@ -17,7 +17,12 @@ export function MethodSection({ method, playbill }: MethodSectionProps) {
<h3>Rank {i + 1}</h3> <h3>Rank {i + 1}</h3>
<div className="method-rank-content" style={{ gridTemplateColumns }}> <div className="method-rank-content" style={{ gridTemplateColumns }}>
{rank.map((abilityId) => { {rank.map((abilityId) => {
const ability = playbill.abilities[abilityId]; const ability = playbill.getAbility(abilityId);
if (ability === null) {
throw new Error(`Ability not found: ${abilityId}`);
}
return <AbilityCard ability={ability} key={abilityId} />; return <AbilityCard ability={ability} key={abilityId} />;
})} })}
</div> </div>

View file

@ -1,5 +1,4 @@
import { Playbill } from "@proscenium/playbill"; import { Playbill } from "@proscenium/playbill";
import sortBy from "lodash-es/sortBy";
import { compileMarkdownInPlaybill, type BoundPlaybill } from "#lib"; import { compileMarkdownInPlaybill, type BoundPlaybill } from "#lib";
import { GlossaryTable } from "./component/glossary"; import { GlossaryTable } from "./component/glossary";
import { PageHeader } from "./component/header"; import { PageHeader } from "./component/header";
@ -19,9 +18,6 @@ export async function renderPlaybillToHTML(
const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize()); const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize());
await compileMarkdownInPlaybill(boundPlaybill); await compileMarkdownInPlaybill(boundPlaybill);
// Soprt rules by their order prop
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
// Prepare stylesheet // Prepare stylesheet
const cssParts: string[] = []; const cssParts: string[] = [];
@ -38,7 +34,7 @@ export async function renderPlaybillToHTML(
const body = renderToString( const body = renderToString(
<> <>
<article id="species" className="view"> <article id="species" className="view">
{Object.values(playbill.species).map((species) => ( {playbill.allSpecies.map((species) => (
<SpeciesSection <SpeciesSection
key={species.id} key={species.id}
species={species} species={species}
@ -48,29 +44,29 @@ export async function renderPlaybillToHTML(
</article> </article>
<article id="methods" className="view" style={{ display: "none" }}> <article id="methods" className="view" style={{ display: "none" }}>
{Object.values(playbill.methods).map((method) => ( {playbill.allMethods.map((method) => (
<MethodSection key={method.id} method={method} playbill={playbill} /> <MethodSection key={method.id} method={method} playbill={playbill} />
))} ))}
</article> </article>
<article id="items" className="view" style={{ display: "none" }}> <article id="items" className="view" style={{ display: "none" }}>
{Object.values(playbill.items).map((item) => ( {playbill.allItems.map((item) => (
<ItemCard key={item.id} item={item} /> <ItemCard key={item.id} item={item} />
))} ))}
</article> </article>
<article id="rules" className="view" style={{ display: "none" }}> <article id="rules" className="view" style={{ display: "none" }}>
{rulesByOrder.map((rule) => ( {playbill.allRules.map((rule) => (
<RuleSection key={rule.id} rule={rule} /> <RuleSection key={rule.id} rule={rule} />
))} ))}
</article> </article>
<article id="glossary" className="view" style={{ display: "none" }}> <article id="glossary" className="view" style={{ display: "none" }}>
{<GlossaryTable glossary={playbill.glossary} />} {<GlossaryTable terms={playbill.allDefinitions} />}
</article> </article>
<article id="resources" className="view" style={{ display: "none" }}> <article id="resources" className="view" style={{ display: "none" }}>
{Object.values(playbill.resources).map((resource) => ( {playbill.allResources.map((resource) => (
<ResourceSection key={resource.id} resource={resource} /> <ResourceSection key={resource.id} resource={resource} />
))} ))}
</article> </article>