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 ( -
-

{playbill.name}

-

A Playbill by {playbill.author}

-

- {" "} - Version {playbill.version} -

-
- ); -} 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}
  • - ))} - - } - > -
    - {resource.name} -
    -
    -
    - ); -} - -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 = ( - <> -
    -
    -

    - HP -

    -

    {hpString}

    -
    - -
    -

    - AP -

    -

    {apString}

    -
    - -
    -

    - EP -

    -

    {epString}

    -
    -
    - -
    -
    -

    Muscle

    -

    - {species.muscle} -

    -
    - -
    -

    Focus

    -

    - {species.focus} -

    -
    - -
    -

    - Knowledge -

    -

    - {species.knowledge} -

    -
    - -
    -

    Charm

    -

    - {species.charm} -

    -
    - -
    -

    Cunning

    -

    - {species.cunning} -

    -
    - -
    -

    Spark

    -

    - {species.spark} -

    -
    -
    - - {abilitySection} - - ); - - return ( -
    Species

    } - > - {sectionContent} -
    - ); -} diff --git a/src/render/html/default.css b/src/render/html/default.css deleted file mode 100644 index f7c5ab5..0000000 --- a/src/render/html/default.css +++ /dev/null @@ -1,505 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap"); - -:root { - /* Spacing and sizing */ - --max-width: 800px; - --padding: 0.5rem; - --margin: 1rem; - - /* Typography */ - --line-height: 1.5; - --font-size: 18px; - --font-body: "Open Sans", sans-serif; - --font-header: "Urbanist", sans-serif; - - /* Page Colors */ - --color-background: hsl(34deg 18 15); - --color-background--dark: hsl(34deg 18 10); - --color-background--light: hsl(34deg 18 20); - --color-text: hsl(35deg 37 73); - --color-text--dim: hsl(35deg 17 50); - --color-heading: hsl(32deg 48 85); - --color-emphasis: hsl(56deg 59 56); - --color-strong: hsl(4deg 88 61); - - /* Concept Colors */ - --color-muscle: var(--color-heading); - --color-focus: var(--color-heading); - --color-knowledge: var(--color-heading); - --color-charm: var(--color-heading); - --color-cunning: var(--color-heading); - --color-spark: var(--color-heading); - - --color-novice: gray; - --color-adept: pink; - --color-master: papayawhip; - --color-theatrical: red; - - --color-phy: red; - --color-arc: hsl(56deg 59 56); - - --color-ap: hsl(215deg 91 75); - --color-hp: hsl(15deg 91 75); - --color-ep: hsl(115deg 40 75); - --color-xp: hsl(0deg 0 45); -} - -/* Normalize box sizing */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -/* Strip default margins */ -* { - margin: 0; -} - -/* Global style classes */ -.phy { - color: var(--color-phy); -} - -.arc { - color: var(--color-arc); -} - -.ap { - color: var(--color-ap); -} - -.hp { - color: var(--color-hp); -} - -.ep { - color: var(--color-ep); -} - -.muscle { - color: var(--color-muscle); -} - -.focus { - color: var(--color-focus); -} - -.knowledge { - color: var(--color-knowledge); -} - -.charm { - color: var(--color-charm); -} - -.cunning { - color: var(--color-cunning); -} - -.spark { - color: var(--color-spark); -} - -.novice { - color: var(--color-novice); -} - -.adept { - color: var(--color-adept); -} - -.master { - color: var(--color-master); -} - -.theatrical { - color: var(--color-theatrical); -} - -/* Sections */ - -section { - position: relative; - max-width: var(--max-width); - margin: 0 auto var(--margin); - border: 2px solid var(--color-background--dark); -} - -.section-header { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - min-height: 8rem; - - text-align: center; - background-color: var(--color-background--dark); -} - -.section-header p { - margin: 0; -} - -.section-content { - padding: var(--padding); -} - -.section-content :last-child { - margin-bottom: 0; -} - -.section-tag { - position: absolute; - top: var(--padding); - color: var(--color-text--dim); - font-weight: bold; - max-width: 45%; -} - -.section-tag--left { - left: var(--padding); -} - -.section-tag--right { - right: var(--padding); -} - -body { - background-color: var(--color-background); - color: var(--color-text); - font-family: var(--font-body); - font-size: var(--font-size); -} - -p { - margin-bottom: var(--margin); - line-height: var(--line-height); -} - -em { - color: var(--color-emphasis); -} - -strong { - color: var(--color-strong); -} - -/* Basic Typography */ -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: var(--font-header); - color: var(--color-heading); -} - -h1 { - font-size: 4rem; - text-align: center; -} - -h2 { - font-size: 2rem; -} - -h3 { - font-size: 1.75rem; -} - -h4 { - font-size: 1.5rem; -} - -h5 { - font-size: 1.25rem; -} - -h6 { - font-weight: bold; - font-size: 1rem; -} - -table { - width: 100%; - margin-bottom: var(--margin); -} - -thead { - background-color: var(--color-background--light); -} - -td { - padding: var(--padding); - text-align: left; - vertical-align: top; -} - -tr:nth-child(even) { - background-color: var(--color-background--light); -} - -tr:hover { - background-color: var(--color-background--dark); -} - -article blockquote { - font-style: italic; - border-left: 3px solid var(--color-emphasis); - background-color: var(--color-background--light); - margin-bottom: var(--margin); - padding: 0.5rem 0.5rem 0.5rem 2rem; -} - -article blockquote p:last-child { - margin-bottom: 0; -} - -article img { - display: block; - margin: 0 auto; - max-width: 70%; - max-height: 35vh; -} - -@media screen and (min-width: 480px) { - article img { - float: right; - margin-left: var(--margin); - margin-right: 0; - max-width: 45%; - } - - article img.left { - float: left; - float: left; - margin-right: var(--margin); - } -} - -/* Main Header */ -header { - min-height: 20vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.header-byline { - font-size: 1.2rem; -} - -/* Overview */ -.overview { - padding: 1rem; - background-color: var(--color-background--dark); - text-align: center; -} - -.overview p { - margin: 0 auto; -} - -/* Nav */ - -nav { - max-width: var(--max-width); - padding: var(--padding); - margin: 0 auto; - display: flex; - flex-direction: row; - justify-content: space-between; - flex-wrap: wrap; - gap: var(--padding); -} - -nav button { - padding: var(--padding); - font-size: inherit; - color: inherit; - background-color: var(--color-background--light); - border: 1px solid var(--color-background--dark); - cursor: pointer; - flex: 1; -} - -nav button:hover { - background-color: var(--color-background--dark); -} - -/* Ability */ - -.ability { - padding: var(--padding); - background-color: var(--color-background--light); -} - -.ability-details { - display: flex; - flex-direction: row; - gap: var(--padding); - flex-wrap: wrap; - text-transform: capitalize; - font-weight: 300; - font-style: italic; -} - -.ability-header { - position: relative; - margin-bottom: var(--margin); -} - -/* Method */ -.method-rank { - margin-bottom: var(--margin); - overflow-x: auto; -} - -.method-curator { - font-style: italic; -} - -.method-description { - margin-top: var(--margin); -} - -.method-rank-content { - display: grid; - gap: var(--padding); -} - -.method-rank-content .ability { - width: 100%; - height: 100%; -} - -/* Species */ - -.species { - margin-bottom: var(--margin); -} - -.species-stats { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - text-align: center; - gap: var(--padding); -} - -.species-talents .adept { - font-weight: 600; -} - -.species-talents .master { - font-weight: 700; -} - -.species-stats h4, -.species-talents h4 { - font-size: var(--font-size); -} - -.species-talents { - text-transform: capitalize; -} - -.species-talents { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: var(--padding); - text-align: center; - margin-bottom: var(--margin); -} - -.species-abilities .ability-cost-xp { - display: none; -} - -/* Items */ -.item { - position: relative; - background-color: var(--color-background--light); -} - -.item-info { - display: flex; - flex-direction: row; - gap: var(--padding); -} - -.item-affinity { - text-transform: capitalize; -} - -.item-rarity { - position: absolute; - top: var(--padding); - right: var(--padding); - text-transform: capitalize; -} - -/* Glossary */ - -.glossary-content > dl > div { - margin-bottom: var(--margin); -} - -dt { - color: var(--color-strong); - font-weight: bold; -} - -dd { - margin-left: 2ch; -} - -dd + dd { - margin-top: var(--padding); -} - -dd + dt { - margin-top: var(--margin); -} - -/* Resources */ -.resource-categories { - list-style: none; - padding: 0; - display: flex; - flex-direction: row; - text-transform: capitalize; - gap: var(--padding); - color: var(--color-text--dim); -} - -.resource-categories li { - font-style: italic; -} - -.resource-categories li:before { - content: "#"; - opacity: 0.5; -} - -.resource-image { - display: block; -} - -.resource-image img { - float: none; - margin: 0 auto; - - max-width: 90%; - max-height: 100vh; - - margin-bottom: var(--padding); -} - -.resource-image figcaption { - font-style: italic; - text-align: center; -} diff --git a/src/render/html/index.tsx b/src/render/html/index.tsx deleted file mode 100644 index bb26ddd..0000000 --- a/src/render/html/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Playbill } from "@proscenium/playbill"; -import { compileMarkdownInPlaybill, type BoundPlaybill } 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"; -import { renderToString } from "react-dom/server"; -import defaultCSS from "./default.css" with { type: "text" }; - -export async function renderPlaybillToHTML( - boundPlaybill: BoundPlaybill, -): Promise { - // Make a copy of the playbill to avoid modifying the original - // and compile all description markdown - const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize()); - await compileMarkdownInPlaybill(boundPlaybill); - - // Prepare stylesheet - const cssParts: string[] = []; - - if (!boundPlaybill.omitDefaultStyles) { - cssParts.push(``); - } - - if (boundPlaybill.styles.length > 0) { - cssParts.push(``); - } - - const css = cssParts.join("\n"); - - const body = renderToString( - <> -
    - {playbill.allSpecies.map((species) => ( - - ))} -
    - -
    - {playbill.allMethods.map((method) => ( - - ))} -
    - -
    - {playbill.allItems.map((item) => ( - - ))} -
    - -
    - {playbill.allRules.map((rule) => ( - - ))} -
    - -
    - {} -
    - -
    - {playbill.allResources.map((resource) => ( - - ))} -
    - , - ); - - // Main document - const doc = ` - - - - - - Playbill: ${playbill.name} - ${css} - - - - - ${renderToString()} -

    ${playbill.description}

    - - ${body} - - - `; - - return doc; -} diff --git a/src/util/args.ts b/src/util/args.ts deleted file mode 100644 index 25d2d0f..0000000 --- a/src/util/args.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { parseArgs } from "node:util"; -import { z } from "zod"; -import { CLIError, MuseError } from "#lib"; -import chalk from "chalk"; -import { version } from "../../package.json" with { type: "json" }; - -export const USAGE = ` -${chalk.bold("muse")} - Compile and validate Playbills for Proscenium -${chalk.dim(`v${version}`)} - -Usage: - muse [/path/to/binding.yaml] - -Options: - --check Only load and check the current binding and resources, but do not compile - --outfile, -o Specify the output file path. If not specified, output to stdout - --watch, -w Watch the directory for changes and recompile - --renderer, -r Specify the output renderer. Options: json, html - --markdown Compile markdown in descriptions when rendering to JSON - --help, -h Show this help message -`.trim(); - -const Renderers = ["json", "html"] as const; -const RendererSchema = z.enum(Renderers); -type Renderer = z.infer; - -/** - * A shape representing the arguments passed to the CLI - */ -export interface CLIArguments { - inputFilePath: string; - - options: { - check: boolean; - outfile: string; - help: boolean; - renderer: Renderer; - watch: boolean; - markdown: 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: { - check: { - short: "c", - type: "boolean", - default: false, - }, - outfile: { - short: "o", - type: "string", - default: "", - }, - help: { - short: "h", - default: false, - type: "boolean", - }, - renderer: { - short: "r", - default: "json", - type: "string", - }, - watch: { - default: false, - type: "boolean", - }, - markdown: { - default: false, - type: "boolean", - }, - }, - strict: true, - allowPositionals: true, - }); - - // -- ARG VALIDATION -- // - if (options.check && options.outfile !== "") { - throw new CLIError("Cannot use --check and --outfile together"); - } - - const parsedRenderer = RendererSchema.safeParse(options.renderer); - - if (!parsedRenderer.success) { - throw new MuseError(`Invalid renderer: ${parsedRenderer.data}`); - } - - return { - inputFilePath: args[0] ?? "./binding.yaml", - - options: { - check: options.check, - outfile: options.outfile, - help: options.help, - renderer: parsedRenderer.data, - watch: options.watch, - markdown: options.markdown, - }, - }; -} diff --git a/src/util/files.ts b/src/util/files.ts deleted file mode 100644 index cd6c865..0000000 --- a/src/util/files.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "node:path"; -import YAML from "yaml"; -import { FileError, FileNotFoundError } from "../lib/errors"; - -/** - * Load a file from the filesystem or throw an error if it does not exist. - * @param filePath The path to the file to load - * @returns The contents of the file as a string - * - * @throws {FileNotFoundError} if the file does not exist - */ -export async function loadFileOrFail(filePath: string): Promise { - const fileToLoad = Bun.file(filePath); - const fileExists = await fileToLoad.exists(); - - if (!fileExists) { - throw new FileNotFoundError(`File not found: ${filePath}`); - } - - return await fileToLoad.text(); -} - -/** - * Load and parse a YAML file or throw an error if the file doesnt exist - * or the yaml fails to parse - * @param filePath The path to the file to load - * @returns The parsed contents of the file - * - * @throws {FileNotFoundError} if the file does not exist - * @throws {FileError} if the file is not valid YAML - */ -export async function loadYAMLFileOrFail(filePath: string): Promise { - try { - return YAML.parse(await loadFileOrFail(filePath)); - } catch (error) { - throw new FileError("Failed to parse YAML file", filePath, `${error}`); - } -} - -/** - * Load and parse a JSON file or throw an error if the file doesnt exist - * or the JSON fails to parse - * @param filePath The path to the file to load - * @returns The parsed contents of the file - * - * @throws {FileNotFoundError} if the file does not exist - * @throws {FileError} if the file is not valid JSON - */ -export async function loadJSONFileOrFail(filePath: string): Promise { - try { - return JSON.parse(await loadFileOrFail(filePath)); - } catch (error) { - throw new FileError("Failed to parse JSON file", filePath, `${error}`); - } -} - -/** - * Returns the absolute path of the directory containing the given file - * - * @param filePath The path to the file - * @returns The absolute path of the directory containing the file - */ -export function getAbsoluteDirname(filePath: string): string { - return path.resolve(path.dirname(filePath)); -} diff --git a/src/util/index.ts b/src/util/index.ts deleted file mode 100644 index 1d10193..0000000 --- a/src/util/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./args"; -export * from "./files"; diff --git a/tsconfig.json b/tsconfig.json index cc9ee32..1e3b4b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "moduleResolution": "bundler", "allowArbitraryExtensions": true, "verbatimModuleSyntax": true, - "noEmit": true, + "declaration": true, + "emitDeclarationOnly": true, // Best practices "strict": true, "skipLibCheck": true, @@ -24,21 +25,10 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", - "paths": { - "#lib": [ - "./lib/index.ts" - ], - "#render/*": [ - "./render/*" - ], - "#util": [ - "./util/index.ts" - ], - } + "paths": {} }, "include": [ "src/**/*.ts", "src/**/*.tsx", - "src/**/*.css", ] }