From c1166680a8639dff92db8899f7904964db4b0180 Mon Sep 17 00:00:00 2001
From: Endeavorance
Date: Tue, 1 Apr 2025 15:43:48 -0400
Subject: [PATCH 01/14] Generic muse
---
.gitignore | 1 +
bun.lock | 3 +
package.json | 1 +
src/args.ts | 56 +++
src/binding.ts | 164 +++++++
src/css.d.ts | 2 -
src/define/index.ts | 326 -------------
src/{lib => }/errors.ts | 0
src/index.ts | 95 +---
src/lib/binding.ts | 177 --------
src/lib/component-file.ts | 177 --------
src/lib/index.ts | 4 -
src/lib/pfm.ts | 79 ----
src/lib/slug.ts | 5 -
src/muse-file.ts | 168 +++++++
src/render/html/component/ability.tsx | 67 ---
src/render/html/component/base/html.tsx | 10 -
src/render/html/component/base/section.tsx | 41 --
src/render/html/component/glossary.tsx | 26 --
src/render/html/component/header.tsx | 18 -
src/render/html/component/item.tsx | 55 ---
src/render/html/component/method.tsx | 49 --
src/render/html/component/resource.tsx | 115 -----
src/render/html/component/rule.tsx | 15 -
src/render/html/component/species.tsx | 120 -----
src/render/html/default.css | 505 ---------------------
src/render/html/index.tsx | 160 -------
src/util/args.ts | 111 -----
src/util/files.ts | 65 ---
src/util/index.ts | 2 -
tsconfig.json | 16 +-
31 files changed, 412 insertions(+), 2221 deletions(-)
create mode 100644 src/args.ts
create mode 100644 src/binding.ts
delete mode 100644 src/css.d.ts
delete mode 100644 src/define/index.ts
rename src/{lib => }/errors.ts (100%)
delete mode 100644 src/lib/binding.ts
delete mode 100644 src/lib/component-file.ts
delete mode 100644 src/lib/index.ts
delete mode 100644 src/lib/pfm.ts
delete mode 100644 src/lib/slug.ts
create mode 100644 src/muse-file.ts
delete mode 100644 src/render/html/component/ability.tsx
delete mode 100644 src/render/html/component/base/html.tsx
delete mode 100644 src/render/html/component/base/section.tsx
delete mode 100644 src/render/html/component/glossary.tsx
delete mode 100644 src/render/html/component/header.tsx
delete mode 100644 src/render/html/component/item.tsx
delete mode 100644 src/render/html/component/method.tsx
delete mode 100644 src/render/html/component/resource.tsx
delete mode 100644 src/render/html/component/rule.tsx
delete mode 100644 src/render/html/component/species.tsx
delete mode 100644 src/render/html/default.css
delete mode 100644 src/render/html/index.tsx
delete mode 100644 src/util/args.ts
delete mode 100644 src/util/files.ts
delete mode 100644 src/util/index.ts
diff --git a/.gitignore b/.gitignore
index 4cda367..a5f06e1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -176,3 +176,4 @@ dist
demo-data
demo.html
+demo
diff --git a/bun.lock b/bun.lock
index b85882f..e428c07 100644
--- a/bun.lock
+++ b/bun.lock
@@ -20,6 +20,7 @@
"remark-rehype": "^11.1.1",
"remark-wiki-link": "^2.0.1",
"slugify": "^1.6.6",
+ "smol-toml": "^1.3.1",
"unified": "^11.0.5",
"yaml": "^2.7.0",
"zod": "^3.24.1",
@@ -283,6 +284,8 @@
"slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="],
+ "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
+
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
diff --git a/package.json b/package.json
index a1bf80f..4ba9691 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"remark-rehype": "^11.1.1",
"remark-wiki-link": "^2.0.1",
"slugify": "^1.6.6",
+ "smol-toml": "^1.3.1",
"unified": "^11.0.5",
"yaml": "^2.7.0",
"zod": "^3.24.1"
diff --git a/src/args.ts b/src/args.ts
new file mode 100644
index 0000000..9e9f8d1
--- /dev/null
+++ b/src/args.ts
@@ -0,0 +1,56 @@
+import { parseArgs } from "node:util";
+import chalk from "chalk";
+import { version } from "../package.json" with { type: "json" };
+
+export const USAGE = `
+${chalk.bold("muse")} - Compile and process troves of data
+${chalk.dim(`v${version}`)}
+
+Usage:
+ muse [/path/to/binding.yaml]
+
+Options:
+ --help, -h Show this help message
+`.trim();
+
+/**
+ * A shape representing the arguments passed to the CLI
+ */
+export interface CLIArguments {
+ inputFilePath: string;
+
+ options: {
+ help: boolean;
+ };
+}
+
+/**
+ * Given an array of CLI arguments, parse them into a structured object
+ *
+ * @param argv The arguments to parse
+ * @returns The parsed CLI arguments
+ *
+ * @throws {CLIError} if the arguments are invalid
+ */
+export function parseCLIArguments(argv: string[]): CLIArguments {
+ const { values: options, positionals: args } = parseArgs({
+ args: argv,
+ options: {
+ help: {
+ short: "h",
+ default: false,
+ type: "boolean",
+ },
+ },
+ strict: true,
+ allowPositionals: true,
+ });
+
+ return {
+ inputFilePath: args[0] ?? "./binding.yaml",
+
+ options: {
+ help: options.help,
+ },
+ };
+}
diff --git a/src/binding.ts b/src/binding.ts
new file mode 100644
index 0000000..6803251
--- /dev/null
+++ b/src/binding.ts
@@ -0,0 +1,164 @@
+import path from "node:path";
+import { Glob } from "bun";
+import z from "zod";
+import { FileNotFoundError, MuseError } from "./errors";
+import { MuseFile, type MuseFileContent } from "./muse-file";
+
+// binding.yaml file schema
+const BindingSchema = z.object({
+ include: z.array(z.string()).default([]),
+
+ name: z.string().default("Unnamed playbill"),
+ author: z.string().default("Someone"),
+ description: z.string().default("No description provided"),
+ version: z.string().default("0.0.0"),
+
+ files: z
+ .array(z.string())
+ .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]),
+
+ options: z.record(z.string(), z.any()).default({}),
+ processors: z.array(z.string()).default([]),
+});
+
+type ParsedBinding = z.infer;
+
+/**
+ * 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 {
+ // If the path does not specify a filename, use binding.yaml
+ const inputLocation = Bun.file(bindingPath);
+
+ // If it is a directory, try again seeking a binding.yaml inside
+ const stat = await inputLocation.stat();
+ if (stat.isDirectory()) {
+ return resolveBindingPath(path.resolve(bindingPath, "binding.yaml"));
+ }
+
+ // If it doesnt exist, bail
+ if (!(await inputLocation.exists())) {
+ throw new FileNotFoundError(bindingPath);
+ }
+
+ // Resolve the path
+ return path.resolve(bindingPath);
+}
+
+export interface BindingMetadata {
+ name: string;
+ author: string;
+ description: string;
+ version: string;
+}
+
+export interface MuseProcessor {
+ readonly filePath: string;
+ process(binding: Binding): Promise;
+}
+
+export interface Binding {
+ readonly _raw: ParsedBinding;
+ readonly bindingPath: string;
+ readonly includedFiles: string[];
+ readonly entries: MuseFileContent[];
+ readonly metadata: BindingMetadata;
+ readonly options: Record;
+ readonly processors: MuseProcessor[];
+ readonly imports: Binding[];
+}
+
+export async function loadBinding(initialPath: string): Promise {
+ // Resolve the file location if possible
+ const bindingPath = await resolveBindingPath(initialPath);
+ const bindingFile = new MuseFile(bindingPath);
+
+ // Load and parse the Binding YAML content
+ const bindingFileContent = await bindingFile.read();
+ const parsedBinding = BindingSchema.parse(bindingFileContent.data);
+
+ // Prepare collections to load into
+ const extendsFrom: Binding[] = [];
+ const entries: MuseFileContent[] = [];
+
+ // Process imported bindings and bring in their entries
+ for (const includePath of parsedBinding.include) {
+ const pathFromHere = path.resolve(bindingFile.dirname, includePath);
+ const extendBindingPath = await resolveBindingPath(pathFromHere);
+ const loadedBinding = await loadBinding(extendBindingPath);
+ extendsFrom.push(loadedBinding);
+ entries.push(...loadedBinding.entries);
+ }
+
+ // Collect file manifest from globs
+ const allFilePaths: string[] = [];
+ for (const thisGlob of parsedBinding.files) {
+ const glob = new Glob(thisGlob);
+
+ const results = glob.scanSync({
+ cwd: bindingFile.dirname,
+ absolute: true,
+ followSymlinks: true,
+ onlyFiles: true,
+ });
+
+ allFilePaths.push(...results);
+ }
+
+ // Exclude the binding.yaml file
+ const includedFilePaths = allFilePaths.filter((filePath) => {
+ return filePath !== bindingFile.path;
+ });
+
+ const fileLoadPromises: Promise[] = [];
+ for (const filePath of includedFilePaths) {
+ const file = new MuseFile(filePath);
+ fileLoadPromises.push(file.read());
+ }
+
+ const mainEntries = await Promise.all(fileLoadPromises);
+ entries.push(...mainEntries);
+
+ // Load and check processors
+ const processors: MuseProcessor[] = [];
+ for (const processorPath of parsedBinding.processors) {
+ const resolvedProcessorPath = path.resolve(
+ bindingFile.dirname,
+ processorPath,
+ );
+ const { process } = await import(resolvedProcessorPath);
+
+ if (!process || typeof process !== "function") {
+ throw new MuseError(
+ `Processor at ${processorPath} does not export a process() function`,
+ );
+ }
+
+ processors.push({
+ filePath: resolvedProcessorPath,
+ process: process as MuseProcessor["process"],
+ });
+ }
+
+ return {
+ _raw: parsedBinding,
+ bindingPath,
+ includedFiles: includedFilePaths,
+ entries,
+ options: parsedBinding.options,
+ processors: processors,
+ imports: extendsFrom,
+ metadata: {
+ name: parsedBinding.name,
+ author: parsedBinding.author,
+ description: parsedBinding.description,
+ version: parsedBinding.version,
+ },
+ };
+}
diff --git a/src/css.d.ts b/src/css.d.ts
deleted file mode 100644
index cb02ac0..0000000
--- a/src/css.d.ts
+++ /dev/null
@@ -1,2 +0,0 @@
-declare module '*.css' {
-}
diff --git a/src/define/index.ts b/src/define/index.ts
deleted file mode 100644
index 68b0aa6..0000000
--- a/src/define/index.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-import {
- type Ability,
- type AnyPlaybillComponent,
- type Blueprint,
- type Item,
- type Method,
- type Resource,
- type Rule,
- type Species,
- parseAbility,
- parseBlueprint,
- parseItem,
- parseMethod,
- parseResource,
- parseRule,
- parseSpecies,
-} from "@proscenium/playbill";
-import { z } from "zod";
-
-const Base = z.object({
- $define: z.string(),
- $hidden: z.boolean().default(false),
- 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 = Base.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(1),
- damage: z.number().int().default(0),
- damageType: z.string().default("phy"),
- roll: z.string().default("none"),
-});
-
-const BlueprintSchema = Base.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(4),
- hp: z.number().int().default(5),
- ep: z.number().int().default(1),
- 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 = Base.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"),
- abilities: z.array(z.string()).default([]),
-});
-
-const ItemSchema = z.discriminatedUnion("type", [
- BaseItem.extend({
- type: z.literal("wielded"),
- hands: z.number().int().default(1),
- }),
- BaseItem.extend({
- type: z.literal("worn"),
- slot: z.string().default("unique"),
- }),
- BaseItem.extend({
- type: z.literal("trinket"),
- }),
-]);
-
-const MethodSchema = Base.extend({
- $define: z.literal("method"),
- curator: z.string().default("Someone"),
- abilities: z.array(z.array(z.string())).default([]),
- rank1: z.array(z.string()).default([]),
- rank2: z.array(z.string()).default([]),
- rank3: z.array(z.string()).default([]),
- rank4: z.array(z.string()).default([]),
- rank5: z.array(z.string()).default([]),
- rank6: z.array(z.string()).default([]),
- rank7: z.array(z.string()).default([]),
- rank8: z.array(z.string()).default([]),
- rank9: z.array(z.string()).default([]),
-});
-
-const ResourceSchema = z.discriminatedUnion("type", [
- Base.extend({
- $define: z.literal("resource"),
- type: z.literal("text"),
- }),
- Base.extend({
- $define: z.literal("resource"),
- type: z.literal("image"),
- url: z.string(),
- }),
- Base.extend({
- $define: z.literal("resource"),
- type: z.literal("table"),
- data: z.array(z.array(z.string())),
- }),
-]);
-
-const RuleSchema = Base.extend({
- $define: z.literal("rule"),
- overrule: z.nullable(z.string()).default(null),
- order: z.number().int().default(Number.POSITIVE_INFINITY),
-});
-
-const SpeciesSchema = Base.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;
-};
-
-function parseAbilityDefinition(obj: unknown): Ability {
- 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,
- });
-
- return ability;
-}
-
-function parseBlueprintDefinition(obj: unknown): Blueprint {
- 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 blueprint;
-}
-
-function parseItemDefinition(obj: unknown): Item {
- const parsed = ItemSchema.parse(obj);
-
- return parseItem({
- ...parsed,
- damage:
- parsed.damage > 0
- ? {
- amount: parsed.damage,
- type: parsed.damageType,
- }
- : null,
- abilities: parsed.abilities,
- tweak: "",
- temper: "",
- });
-}
-
-function parseMethodDefinition(obj: unknown): Method {
- const parsed = MethodSchema.parse(obj);
-
- // Prefer `abilities` if defined, otherwise
- // construct `abilities` using the rankX properties
- const abilities = parsed.abilities;
-
- if (abilities.length === 0) {
- const allRanks = [
- parsed.rank1,
- parsed.rank2,
- parsed.rank3,
- parsed.rank4,
- parsed.rank5,
- parsed.rank6,
- parsed.rank7,
- parsed.rank8,
- parsed.rank9,
- ];
-
- // Add all ranks until an empty rank is found
- for (const rank of allRanks) {
- if (rank.length > 0) {
- abilities.push(rank);
- } else {
- break;
- }
- }
- }
-
- const method = {
- id: parsed.id,
- name: parsed.name,
- description: parsed.description,
- categories: parsed.categories,
- curator: parsed.curator,
- abilities: abilities,
- };
-
- return parseMethod(method);
-}
-
-function parseResourceDefinition(obj: unknown): Resource {
- const parsed = ResourceSchema.parse(obj);
- return parseResource(parsed);
-}
-
-function parseRuleDefinition(obj: unknown): Rule {
- const parsed = RuleSchema.parse(obj);
-
- return parseRule(parsed);
-}
-
-function parseSpeciesDefinition(obj: unknown): Species {
- const parsed = SpeciesSchema.parse(obj);
-
- return parseSpecies(parsed);
-}
-
-export function parsePlaybillComponent(
- obj: unknown,
-): AnyPlaybillComponent | null {
- const baseParse = Base.parse(obj);
-
- if (baseParse.$hidden) {
- return null;
- }
-
- 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 "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}`);
- }
-}
diff --git a/src/lib/errors.ts b/src/errors.ts
similarity index 100%
rename from src/lib/errors.ts
rename to src/errors.ts
diff --git a/src/index.ts b/src/index.ts
index 23d166c..32e0823 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,62 +1,29 @@
-import { watch } from "node:fs/promises";
import chalk from "chalk";
-import {
- CLIError,
- MuseError,
- compileMarkdownInPlaybill,
- loadFromBinding,
- resolveBindingPath,
-} from "#lib";
-import { renderPlaybillToHTML } from "#render/html";
-import {
- type CLIArguments,
- getAbsoluteDirname,
- parseCLIArguments,
- USAGE,
-} from "#util";
+import { MuseError } from "./errors";
+import { loadBinding, type Binding } from "./binding";
+import { type CLIArguments, parseCLIArguments, USAGE } from "./args";
enum ExitCode {
Success = 0,
Error = 1,
}
-async function processBinding({ inputFilePath, options }: CLIArguments) {
+async function processBinding({ inputFilePath }: CLIArguments) {
// Load the binding
- const bindingPath = await resolveBindingPath(inputFilePath);
- const binding = await loadFromBinding(bindingPath);
+ const binding = await loadBinding(inputFilePath);
- // If --check is specified, exit early
- if (options.check) {
- console.log(chalk.green("Playbill validated successfully"));
- return ExitCode.Success;
+ // Run the data through all processors
+ const processedSteps: Binding[] = [binding];
+ for (const processor of binding.processors) {
+ const processedStep = await processor.process(binding);
+ processedSteps.push(processedStep);
}
- if (options.markdown) {
- await compileMarkdownInPlaybill(binding);
- }
+ const finalState = processedSteps[processedSteps.length - 1];
+ const serialized = JSON.stringify(finalState.entries, null, 2);
- // Serialize (default: JSON)
- let serializedPlaybill = "";
-
- switch (options.renderer) {
- case "json":
- serializedPlaybill = binding.playbill.serialize();
- break;
- case "html":
- serializedPlaybill = await renderPlaybillToHTML(binding);
- break;
- default:
- throw new CLIError(`Unknown renderer: ${options.renderer}`);
- }
-
- // Write to disk if --outfile is specified
- if (options.outfile !== "") {
- await Bun.write(options.outfile, serializedPlaybill);
- return ExitCode.Success;
- }
-
- // Otherwise, write to stdout
- console.log(serializedPlaybill);
+ // Otherwise
+ console.log(serialized);
return ExitCode.Success;
}
@@ -70,39 +37,9 @@ async function main(): Promise {
return ExitCode.Success;
}
- let lastProcessResult = await processBinding(cliArguments);
+ const lastProcessResult = await processBinding(cliArguments);
- if (options.watch) {
- const watchDir = getAbsoluteDirname(cliArguments.inputFilePath);
-
- console.log(`Watching ${watchDir} for changes...`);
-
- const watcher = watch(watchDir, {
- recursive: true,
- });
-
- for await (const event of watcher) {
- console.log(
- `Detected ${event.eventType} on ${event.filename}. Reprocessing...`,
- );
-
- try {
- lastProcessResult = await processBinding(cliArguments);
-
- if (lastProcessResult === ExitCode.Error) {
- console.error(`Error processing ${event.filename}`);
- } else {
- console.log("Reprocessed changes");
- }
- } catch (error) {
- console.error(`Error processing ${event.filename}`);
- console.error(error);
- }
- }
- return ExitCode.Success;
- }
-
- return ExitCode.Success;
+ return lastProcessResult;
}
try {
diff --git a/src/lib/binding.ts b/src/lib/binding.ts
deleted file mode 100644
index 9e75fc8..0000000
--- a/src/lib/binding.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import path from "node:path";
-import { type AnyPlaybillComponent, Playbill } from "@proscenium/playbill";
-import { Glob } from "bun";
-import z from "zod";
-import { loadYAMLFileOrFail } from "#util";
-import { ComponentFile } from "./component-file";
-import { FileNotFoundError } from "./errors";
-
-// binding.yaml file schema
-const BindingSchema = z.object({
- include: z.array(z.string()).default([]),
-
- name: z.string().default("Unnamed playbill"),
- author: z.string().default("Someone"),
- description: z.string().default("No description provided"),
- version: z.string().default("0.0.0"),
-
- files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
- omitDefaultStyles: z.boolean().default(false),
- styles: z
- .union([z.string(), z.array(z.string())])
- .transform((val) => (Array.isArray(val) ? val : [val]))
- .default([]),
- terms: z.record(z.string(), z.string()).default({}),
-});
-
-export interface BoundPlaybill {
- // File information
- bindingFilePath: string;
- bindingFileDirname: string;
- files: string[];
- componentFiles: ComponentFile[];
-
- // Binding properties
- name: string;
- author: string;
- version: string;
- description: string;
-
- omitDefaultStyles: boolean;
- styles: string;
-
- 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 {
- // If the path does not specify a filename, use binding.yaml
- const inputLocation = Bun.file(bindingPath);
-
- // If it is a directory, try again seeking a binding.yaml inside
- const stat = await inputLocation.stat();
- if (stat.isDirectory()) {
- return resolveBindingPath(path.resolve(bindingPath, "binding.yaml"));
- }
-
- // If it doesnt exist, bail
- if (!(await inputLocation.exists())) {
- throw new FileNotFoundError(bindingPath);
- }
-
- // Resolve the path
- return path.resolve(bindingPath);
-}
-
-export async function loadFromBinding(
- bindingPath: string,
-): Promise {
- const resolvedBindingPath = await resolveBindingPath(bindingPath);
- const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath);
- const binding = BindingSchema.parse(yamlContent);
- const fileGlobs = binding.files;
- const bindingFileDirname = path.dirname(resolvedBindingPath);
-
- const playbill = new Playbill(
- binding.name,
- binding.author,
- binding.description,
- binding.version,
- );
-
- // If this is extending another binding, load that first
- for (const includePath of binding.include) {
- const pathFromHere = path.resolve(bindingFileDirname, includePath);
- const extendBindingPath = await resolveBindingPath(pathFromHere);
- const loadedBinding = await loadFromBinding(extendBindingPath);
- playbill.extend(loadedBinding.playbill);
- }
-
- // Load any specified stylesheets
- const loadedStyles: string[] = [];
- for (const style of binding.styles) {
- const cssFilePath = path.resolve(bindingFileDirname, style);
- const cssFile = Bun.file(cssFilePath);
- const cssFileExists = await cssFile.exists();
-
- if (cssFileExists) {
- loadedStyles.push(await cssFile.text());
- } else {
- throw new FileNotFoundError(cssFilePath);
- }
- }
- const styles = loadedStyles.join("\n");
-
- // Scan for all files matching the globs
- const allFilePaths: string[] = [];
-
- for (const thisGlob of fileGlobs) {
- const glob = new Glob(thisGlob);
-
- const results = glob.scanSync({
- cwd: bindingFileDirname,
- absolute: true,
- followSymlinks: true,
- onlyFiles: true,
- });
-
- allFilePaths.push(...results);
- }
-
- // Exclude the binding.yaml file
- const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => {
- return filePath !== resolvedBindingPath;
- });
-
- // Load discovered component files
- const componentFiles: ComponentFile[] = [];
- const fileLoadPromises: Promise[] = [];
-
- for (const filepath of filePathsWithoutBindingFile) {
- const componentFile = new ComponentFile(filepath);
- componentFiles.push(componentFile);
- fileLoadPromises.push(componentFile.load());
- }
-
- await Promise.all(fileLoadPromises);
-
- // Aggregate all loaded components
- const loadedComponents: AnyPlaybillComponent[] = [];
- for (const file of componentFiles) {
- loadedComponents.push(...file.components);
- }
-
- // Add all components to the playbill
- // (This method ensures proper addition order maintain correctness)
- playbill.addManyComponents(loadedComponents);
-
- // Add all definitions
- for (const [term, definition] of Object.entries(binding.terms)) {
- playbill.addDefinition(term, definition);
- }
-
- return {
- bindingFilePath: bindingPath,
- bindingFileDirname,
- files: filePathsWithoutBindingFile,
- componentFiles,
-
- name: binding.name,
- author: binding.author ?? "Anonymous",
- description: binding.description ?? "",
- version: binding.version,
-
- omitDefaultStyles: binding.omitDefaultStyles,
- styles,
-
- playbill,
- };
-}
diff --git a/src/lib/component-file.ts b/src/lib/component-file.ts
deleted file mode 100644
index 0dd6ea4..0000000
--- a/src/lib/component-file.ts
+++ /dev/null
@@ -1,177 +0,0 @@
-import path from "node:path";
-import { parsePlaybillComponent } from "define";
-import YAML, { YAMLParseError } from "yaml";
-import { ZodError } from "zod";
-import { loadFileOrFail } from "#util";
-import { MalformedResourceFileError } from "./errors";
-import { toSlug } from "./slug";
-import type { AnyPlaybillComponent } from "@proscenium/playbill";
-
-type FileFormat = "yaml" | "markdown";
-
-const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
-
-/**
- * Attempt to parse YAML frontmatter from a mixed yaml/md doc
- * @param content The raw markdown content
- * @returns Any successfully parsed frontmatter
- */
-function extractFrontmatter(content: string) {
- // If it does not start with `---`, it is invalid for frontmatter
- if (content.trim().indexOf("---") !== 0) {
- return {};
- }
-
- if (FRONTMATTER_REGEX.test(content)) {
- const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? "";
- const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
- return YAML.parse(cleanFrontmatter);
- }
-
- return {};
-}
-
-/**
- * Given a string of a markdown document, extract the markdown content
- * @param content The raw markdown content
- * @returns The markdown content without frontmatter
- */
-function extractMarkdown(content: string): string {
- if (content.trim().indexOf("---") !== 0) {
- return content;
- }
- return content.replace(FRONTMATTER_REGEX, "").trim();
-}
-
-function parseYAMLResourceFile(
- filePath: string,
- text: string,
-): AnyPlaybillComponent[] {
- 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: AnyPlaybillComponent[] = [];
-
- for (const doc of parsedDocs) {
- const raw = doc.toJS();
- const parsedComponent = parsePlaybillComponent(raw);
- if (parsedComponent !== null) {
- collection.push(parsedComponent);
- }
- }
-
- return collection;
-}
-
-function parseMarkdownResourceFile(
- filePath: string,
- text: string,
-): AnyPlaybillComponent[] {
- try {
- const defaultName = path.basename(filePath, ".md");
- const defaultId = toSlug(defaultName);
- 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,
- });
-
- // Null means hidden
- if (together === null) {
- return [];
- }
-
- 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: AnyPlaybillComponent[] = [];
- private _basename: string;
-
- constructor(filePath: string) {
- this._filePath = path.resolve(filePath);
-
- const extension = path.extname(filePath).slice(1).toLowerCase();
- this._basename = path.basename(filePath, `.${extension}`);
-
- 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;
- }
-
- get baseName() {
- return this._basename;
- }
-}
diff --git a/src/lib/index.ts b/src/lib/index.ts
deleted file mode 100644
index e5a4496..0000000
--- a/src/lib/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./errors";
-export * from "./pfm";
-export * from "./component-file";
-export * from "./binding";
diff --git a/src/lib/pfm.ts b/src/lib/pfm.ts
deleted file mode 100644
index 9584462..0000000
--- a/src/lib/pfm.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-import rehypeStringify from "rehype-stringify";
-import remarkParse from "remark-parse";
-import remarkRehype from "remark-rehype";
-import { unified } from "unified";
-import type { BoundPlaybill } from "./binding";
-import remarkWikiLink from "remark-wiki-link";
-import { toSlug } from "./slug";
-import rehypeRaw from "rehype-raw";
-import remarkGfm from "remark-gfm";
-import rehypeShiftHeading from "rehype-shift-heading";
-import {
- parseProsceniumScript,
- type AnyPlaybillComponent,
-} from "@proscenium/playbill";
-
-export type MarkdownParserFunction = (input: string) => Promise;
-
-export function createMarkdownRenderer(
- binding: BoundPlaybill,
-): MarkdownParserFunction {
- // TODO: Allow specifying links to other files via file paths
- // In this scenario, assume its a markdown-defined file and find the ID, use that for the link
- // to allow for more natural Obsidian-like linking
- // For now, this will mostly work, but if you need to specify a unique ID, it wont work in Obsidian
- // despite being valid for Muse
-
- const parser = unified()
- .use(remarkParse)
- .use(remarkGfm)
- .use(remarkWikiLink, {
- aliasDivider: "|",
- permalinks: binding.playbill.allComponentIds,
- pageResolver: (permalink: string) => {
- return [toSlug(permalink)];
- },
- hrefTemplate: (permalink: string) => `#component/${permalink}`,
- })
- .use(remarkRehype, { allowDangerousHtml: true })
- .use(rehypeRaw)
- .use(rehypeShiftHeading, { shift: 1 })
- .use(rehypeStringify);
-
- // TODO: Add sanitization
-
- return async (input: string): Promise => {
- const parsed = await parser.process(input);
- return String(parsed);
- };
-}
-
-export async function compileMarkdownInPlaybill(
- boundPlaybill: BoundPlaybill,
-): Promise {
- const renderMarkdown = createMarkdownRenderer(boundPlaybill);
- const playbill = boundPlaybill.playbill;
-
- // Define a processor function to iterate over all components and process their markdown
- const processMarkdownInComponent = async (entry: AnyPlaybillComponent) => {
- const preparsed = parseProsceniumScript(entry.description, playbill);
- entry.description = await renderMarkdown(preparsed);
-
- if (entry._component === "resource" && entry.type === "table") {
- const newData: string[][] = [];
- for (const row of entry.data) {
- const newRow: string[] = [];
- for (const cell of row) {
- newRow.push(await renderMarkdown(cell));
- }
- newData.push(newRow);
- }
- entry.data = newData;
- }
- };
-
- // Process all components
- await playbill.processComponents(processMarkdownInComponent);
-
- return boundPlaybill;
-}
diff --git a/src/lib/slug.ts b/src/lib/slug.ts
deleted file mode 100644
index b6a0de4..0000000
--- a/src/lib/slug.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import slugify from "slugify";
-
-export function toSlug(str: string): string {
- return slugify(str, { lower: true });
-}
diff --git a/src/muse-file.ts b/src/muse-file.ts
new file mode 100644
index 0000000..f8fd51e
--- /dev/null
+++ b/src/muse-file.ts
@@ -0,0 +1,168 @@
+import type { BunFile } from "bun";
+import { normalize, basename, extname, dirname } from "node:path";
+import YAML from "yaml";
+import TOML from "smol-toml";
+import { FileError } from "./errors";
+
+export type MuseFileType = "md" | "yaml" | "json" | "toml";
+export interface MuseFileContent {
+ type: MuseFileType;
+ text: string;
+ data: Record;
+}
+
+function extensionToFiletype(ext: string): MuseFileType {
+ switch (ext) {
+ case "md":
+ return "md";
+ case "yaml":
+ return "yaml";
+ case "json":
+ return "json";
+ case "toml":
+ return "toml";
+ default:
+ throw new Error(`Unsupported file format: ${ext}`);
+ }
+}
+
+const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
+/**
+ * Attempt to parse YAML frontmatter from a mixed yaml/md doc
+ * @param content The raw markdown content
+ * @returns Any successfully parsed frontmatter
+ */
+function extractFrontmatter(content: string) {
+ // If it does not start with `---`, it is invalid for frontmatter
+ if (content.trim().indexOf("---") !== 0) {
+ return {};
+ }
+
+ if (FRONTMATTER_REGEX.test(content)) {
+ const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? "";
+ const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim();
+ return YAML.parse(cleanFrontmatter);
+ }
+
+ return {};
+}
+
+/**
+ * Given a string of a markdown document, extract the markdown content
+ * @param content The raw markdown content
+ * @returns The markdown content without frontmatter
+ */
+function extractMarkdown(content: string): string {
+ if (content.trim().indexOf("---") !== 0) {
+ return content;
+ }
+ return content.replace(FRONTMATTER_REGEX, "").trim();
+}
+
+export class MuseFile {
+ protected _path: string;
+ protected _file: BunFile;
+ protected _fileType: MuseFileType;
+
+ constructor(filePath: string) {
+ this._path = normalize(filePath);
+ this._file = Bun.file(this._path);
+ this._fileType = extensionToFiletype(this.extension.slice(1));
+ }
+
+ get dirname(): string {
+ return dirname(this._path);
+ }
+
+ get extension(): string {
+ return extname(this._path);
+ }
+
+ get basename(): string {
+ return basename(this._path);
+ }
+
+ get filename(): string {
+ return basename(this._path, this.extension);
+ }
+
+ get path(): string {
+ return this._path;
+ }
+
+ public async readJSON(): Promise {
+ try {
+ const text = await this._file.text();
+
+ return {
+ type: "json",
+ text: text,
+ data: JSON.parse(text),
+ };
+ } catch (error) {
+ throw new FileError(`Failed to read JSON file: ${error}`, this._path);
+ }
+ }
+
+ public async readYAML(): Promise {
+ try {
+ const text = await this._file.text();
+ return {
+ type: "yaml",
+ text,
+ data: YAML.parse(text),
+ };
+ } catch (error) {
+ throw new FileError(`Failed to read YAML file: ${error}`, this._path);
+ }
+ }
+
+ public async readTOML(): Promise {
+ try {
+ const text = await this._file.text();
+ return {
+ type: "toml",
+ text,
+ data: TOML.parse(text),
+ };
+ } catch (error) {
+ throw new FileError(`Failed to read TOML file: ${error}`, this._path);
+ }
+ }
+
+ public async readMarkdown(): Promise {
+ try {
+ const text = await this._file.text();
+ const frontmatter = extractFrontmatter(text);
+ const markdown = extractMarkdown(text);
+
+ return {
+ type: "md",
+ text: markdown,
+ data: {
+ ...frontmatter,
+ },
+ };
+ } catch (error) {
+ throw new FileError(`Failed to read Markdown file: ${error}`, this._path);
+ }
+ }
+
+ public async read(): Promise {
+ switch (this._fileType) {
+ case "json":
+ return this.readJSON();
+ case "yaml":
+ return this.readYAML();
+ case "toml":
+ return this.readTOML();
+ case "md":
+ return this.readMarkdown();
+ default:
+ throw new FileError(
+ `No reader for file type ${this._fileType}`,
+ this._path,
+ );
+ }
+ }
+}
diff --git a/src/render/html/component/ability.tsx b/src/render/html/component/ability.tsx
deleted file mode 100644
index 09537da..0000000
--- a/src/render/html/component/ability.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import type { Ability } from "@proscenium/playbill";
-import { HTML } from "./base/html";
-
-interface AbilityCardProps {
- ability: Ability;
-}
-
-export function AbilityCard({ ability }: AbilityCardProps) {
- const costs: React.ReactNode[] = [];
-
- if (ability.ap > 0) {
- costs.push(
-
- {ability.ap} AP
- ,
- );
- }
-
- if (ability.hp > 0) {
- costs.push(
-
- {ability.hp} HP
- ,
- );
- }
-
- if (ability.ep > 0) {
- costs.push(
-
- {ability.ep} EP
- ,
- );
- }
-
- const damage =
- ability.damage !== null ? (
-
- {ability.damage.amount} {ability.damage.type === "phy" ? "Phy" : "Arc"}
-
- ) : null;
-
- const roll =
- ability.roll !== "none" ? (
- {ability.roll}
- ) : null;
-
- const type = (
- {ability.type}
- );
-
- const classList = `ability ability-${ability.type}`;
-
- return (
-
-
-
{ability.name}
-
- {type}
- {roll}
- {damage}
- {costs}
-
-
-
-
- );
-}
diff --git a/src/render/html/component/base/html.tsx b/src/render/html/component/base/html.tsx
deleted file mode 100644
index 761e2f1..0000000
--- a/src/render/html/component/base/html.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-interface HTMLWrapperProps {
- html: string;
- className?: string;
-}
-
-export function HTML({ html, className }: HTMLWrapperProps) {
- return (
-
- );
-}
diff --git a/src/render/html/component/base/section.tsx b/src/render/html/component/base/section.tsx
deleted file mode 100644
index 2c85f28..0000000
--- a/src/render/html/component/base/section.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import classNames from "classnames";
-import type React from "react";
-
-interface SectionProps {
- type: string;
- title: string;
- children: React.ReactNode;
- preInfo?: React.ReactNode;
- info?: React.ReactNode;
- componentId?: string;
- leftCornerTag?: React.ReactNode;
- rightCornerTag?: React.ReactNode;
-}
-
-export function Section({
- type,
- title,
- children,
- preInfo,
- info,
- componentId,
- leftCornerTag,
- rightCornerTag,
-}: SectionProps) {
- const sectionClasses = classNames("section", type);
- const headerClasses = classNames("section-header", `${type}-header`);
- const contentClasses = classNames("section-content", `${type}-content`);
-
- return (
-
-
- {preInfo}
-
{title}
- {info}
-
- {leftCornerTag}
- {rightCornerTag}
- {children}
-
- );
-}
diff --git a/src/render/html/component/glossary.tsx b/src/render/html/component/glossary.tsx
deleted file mode 100644
index a139dc4..0000000
--- a/src/render/html/component/glossary.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Section } from "./base/section";
-
-interface GlossaryTableProps {
- terms: [string, string[]][];
-}
-
-export function GlossaryTable({ terms }: GlossaryTableProps) {
- return (
-
-
-
- {terms.map(([term, defs]) => {
- return (
-
-
- {term}
- {defs.map((d, i) => (
- - {d}
- ))}
-
- );
- })}
-
-
-
- );
-}
diff --git a/src/render/html/component/header.tsx b/src/render/html/component/header.tsx
deleted file mode 100644
index 4c0535e..0000000
--- a/src/render/html/component/header.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Playbill } from "@proscenium/playbill";
-
-interface PlaybillHeaderProps {
- playbill: Playbill;
-}
-
-export function PageHeader({ playbill }: PlaybillHeaderProps) {
- return (
-
- );
-}
diff --git a/src/render/html/component/item.tsx b/src/render/html/component/item.tsx
deleted file mode 100644
index ebe63b7..0000000
--- a/src/render/html/component/item.tsx
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { Item } from "@proscenium/playbill";
-import { capitalize } from "lodash-es";
-import { Section } from "./base/section";
-import { HTML } from "./base/html";
-
-interface ItemSectionProps {
- item: Item;
-}
-
-export function ItemCard({ item }: ItemSectionProps) {
- 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 infoParts: React.ReactNode[] = [];
-
- if (item.affinity !== "none") {
- infoParts.push(
-
- {item.affinity} Affinity
-
,
- );
- }
-
- if (item.damage !== null && item.damage.amount > 0) {
- infoParts.push(
-
- {item.damage.amount} {capitalize(item.damage.type)} Damage
-
,
- );
- }
-
- return (
- {infoParts}}
- rightCornerTag={capitalize(item.rarity)}
- leftCornerTag={itemTypeDescriptor}
- >
-
-
- );
-}
diff --git a/src/render/html/component/method.tsx b/src/render/html/component/method.tsx
deleted file mode 100644
index e39106b..0000000
--- a/src/render/html/component/method.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import type { Method, Playbill } from "@proscenium/playbill";
-import { AbilityCard } from "./ability";
-import { Section } from "./base/section";
-
-interface MethodSectionProps {
- method: Method;
- playbill: Playbill;
-}
-
-export function MethodSection({ method, playbill }: MethodSectionProps) {
- const ranks = method.abilities.map((rank, i) => {
- const gridTemplateColumns = new Array(Math.min(rank.length, 3))
- .fill("1fr")
- .join(" ");
- return (
-
-
Rank {i + 1}
-
- {rank.map((abilityId) => {
- const ability = playbill.getAbility(abilityId);
-
- if (ability === null) {
- throw new Error(`Ability not found: ${abilityId}`);
- }
-
- return
;
- })}
-
-
- );
- });
-
- return (
- {method.curator}'s Method of}
- info={
-
- }
- >
- {ranks}
-
- );
-}
diff --git a/src/render/html/component/resource.tsx b/src/render/html/component/resource.tsx
deleted file mode 100644
index cd07910..0000000
--- a/src/render/html/component/resource.tsx
+++ /dev/null
@@ -1,115 +0,0 @@
-import type {
- ImageResource,
- Resource,
- TableResource,
- TextResource,
-} from "@proscenium/playbill";
-import { Section } from "./base/section";
-import { HTML } from "./base/html";
-
-interface TextResourceSectionProps {
- resource: TextResource;
-}
-
-function TextResourceSection({ resource }: TextResourceSectionProps) {
- return (
-
- {resource.categories.map((category) => (
- {category}
- ))}
-
- }
- >
-
-
- );
-}
-
-interface ImageResourceSectionProps {
- resource: ImageResource;
-}
-
-function ImageResourceSection({ resource }: ImageResourceSectionProps) {
- return (
-
- {resource.categories.map((category) => (
- {category}
- ))}
-
- }
- >
-
-
-
-
-
- );
-}
-
-interface TableResourceSectionProps {
- resource: TableResource;
-}
-
-function TableResourceSection({ resource }: TableResourceSectionProps) {
- const [headers, ...rows] = resource.data;
- return (
-
- {resource.categories.map((category) => (
- {category}
- ))}
-
- }
- >
-
-
-
- {headers.map((header) => (
- |
- ))}
-
-
-
- {rows.map((row, i) => (
-
- {row.map((cell, j) => (
- |
- ))}
-
- ))}
-
-
-
- );
-}
-
-interface ResourceSectionProps {
- resource: Resource;
-}
-
-export function ResourceSection({ resource }: ResourceSectionProps) {
- switch (resource.type) {
- case "text":
- return ;
- case "image":
- return ;
- case "table":
- return ;
- }
-}
diff --git a/src/render/html/component/rule.tsx b/src/render/html/component/rule.tsx
deleted file mode 100644
index 7971be1..0000000
--- a/src/render/html/component/rule.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import type { Rule } from "@proscenium/playbill";
-import { Section } from "./base/section";
-import { HTML } from "./base/html";
-
-interface RuleSectionProps {
- rule: Rule;
-}
-
-export function RuleSection({ rule }: RuleSectionProps) {
- return (
-
-
- );
-}
diff --git a/src/render/html/component/species.tsx b/src/render/html/component/species.tsx
deleted file mode 100644
index 6c11831..0000000
--- a/src/render/html/component/species.tsx
+++ /dev/null
@@ -1,120 +0,0 @@
-import type { Playbill, Species } from "@proscenium/playbill";
-import { Section } from "./base/section";
-import { AbilityCard } from "./ability";
-import { HTML } from "./base/html";
-
-interface SpeciesSectionProps {
- species: Species;
- playbill: Playbill;
-}
-
-export function SpeciesSection({ species, playbill }: SpeciesSectionProps) {
- 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 ? (
-
-
Innate Abilities
- {species.abilities.map((abilityId) => {
- const ability = playbill.getAbility(abilityId);
-
- if (ability === null) {
- throw new Error(`Ability not found: ${abilityId}`);
- }
-
- return
;
- })}
-
- ) : (
- ""
- );
-
- const sectionContent = (
- <>
-
-
-
-
-
Muscle
-
- {species.muscle}
-
-
-
-
-
Focus
-
- {species.focus}
-
-
-
-
-
- Knowledge
-
-
- {species.knowledge}
-
-
-
-
-
Charm
-
- {species.charm}
-
-
-
-
-
Cunning
-
- {species.cunning}
-
-
-
-
-
Spark
-
- {species.spark}
-
-
-
-