diff --git a/src/define/index.ts b/src/define/index.ts index 83c3410..23cc8b7 100644 --- a/src/define/index.ts +++ b/src/define/index.ts @@ -8,7 +8,6 @@ import { type Species, parseAbility, parseBlueprint, - parseGlossary, parseItem, parseMethod, parseResource, @@ -18,6 +17,7 @@ import { import { z } from "zod"; const Base = z.object({ + // Required -- defines the type of component $define: z.string(), }); @@ -34,7 +34,7 @@ const AbilitySchema = Info.extend({ ap: z.number().int().default(0), hp: z.number().int().default(0), ep: z.number().int().default(0), - xp: 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"), @@ -68,6 +68,7 @@ const BaseItem = Info.extend({ 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", [ @@ -163,10 +164,6 @@ export type ParsedComponent = type: "blueprint"; component: Blueprint; } - | { - type: "glossary"; - component: Record; - } | { type: "item"; component: Item; @@ -209,8 +206,6 @@ function parseAbilityDefinition(obj: unknown): ParsedComponent { } : null, roll: parsed.roll, - boons: [], - banes: [], }); return { @@ -249,23 +244,6 @@ function parseBlueprintDefinition(obj: unknown): ParsedComponent { }; } -function parseGlossaryDefinition(obj: unknown): ParsedComponent { - const { terms } = GlossarySchema.parse(obj); - - // Create a new object with each definition as an array - const curatedTerms: Record = {}; - for (const [term, definition] of Object.entries(terms)) { - curatedTerms[term] = [definition]; - } - - const glossary = parseGlossary(curatedTerms); - - return { - type: "glossary", - component: glossary, - }; -} - function parseItemDefinition(obj: unknown): ParsedComponent { const parsed = ItemSchema.parse(obj); @@ -280,6 +258,7 @@ function parseItemDefinition(obj: unknown): ParsedComponent { type: parsed.damageType, } : null, + abilities: parsed.abilities, tweak: "", temper: "", }), @@ -343,8 +322,6 @@ export function parsePlaybillComponent(obj: unknown): ParsedComponent { return parseAbilityDefinition(component); case "blueprint": return parseBlueprintDefinition(component); - case "glossary": - return parseGlossaryDefinition(component); case "item": return parseItemDefinition(component); case "method": diff --git a/src/index.ts b/src/index.ts index cb96466..4d838f0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,23 +6,8 @@ import { type CLIArguments, getAbsoluteDirname, parseCLIArguments, + USAGE, } from "#util"; -import { version } from "../package.json" with { type: "json" }; - -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 - --help, -h Show this help message -`.trim(); enum ExitCode { Success = 0, @@ -72,7 +57,7 @@ async function main(): Promise { // -- HELP TEXT -- // if (options.help) { - console.log(usage); + console.log(USAGE); return ExitCode.Success; } diff --git a/src/lib/binding.ts b/src/lib/binding.ts index 98aa824..147c48a 100644 --- a/src/lib/binding.ts +++ b/src/lib/binding.ts @@ -15,17 +15,21 @@ import { loadYAMLFileOrFail } from "#util"; import { ComponentFile } from "./component-file"; import { FileNotFoundError } from "./errors"; +// binding.yaml file schema const BindingSchema = z.object({ extend: z.string().optional(), - id: z.string().default("playbill"), name: z.string().default("Unnamed playbill"), - author: z.string().optional(), - description: z.string().optional(), + 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"]), - styles: z.array(z.string()).optional(), + 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({}), }); type Binding = z.infer; @@ -37,15 +41,15 @@ export interface BoundPlaybill { bindingFilePath: string; bindingFileDirname: string; includedFiles: string[]; + componentFiles: ComponentFile[]; // Binding properties - id: string; name: string; author: string; version: string; description: string; - styles?: string; + styles: string; playbill: Playbill; } @@ -98,20 +102,19 @@ export async function loadFromBinding( } // Load any specified stylesheets - let styles = ""; - if (binding.styles && binding.styles.length > 0) { - for (const style of binding.styles) { - const cssFilePath = path.resolve(bindingFileDirname, style); - const cssFile = Bun.file(cssFilePath); - const cssFileExists = await cssFile.exists(); + 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) { - styles += `${await cssFile.text()}\n`; - } else { - throw new FileNotFoundError(cssFilePath); - } + 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[] = []; @@ -154,7 +157,6 @@ export async function loadFromBinding( const abilities: Ability[] = []; const bluprints: Blueprint[] = []; - const glossaries: Record[] = []; const methods: Method[] = []; const species: Species[] = []; const items: Item[] = []; @@ -173,9 +175,6 @@ export async function loadFromBinding( case "blueprint": bluprints.push(component.component); break; - case "glossary": - glossaries.push(component.component); - break; case "method": methods.push(component.component); break; @@ -232,12 +231,8 @@ export async function loadFromBinding( } // Add all definitions - for (const glossary of glossaries) { - for (const [term, definitions] of Object.entries(glossary)) { - for (const definition of definitions) { - playbill.addDefinition(term, definition); - } - } + for (const [term, definition] of Object.entries(binding.terms)) { + playbill.addDefinition(term, definition); } return { @@ -245,14 +240,14 @@ export async function loadFromBinding( bindingFilePath: bindingPath, bindingFileDirname, includedFiles: filePathsWithoutBindingFile, + componentFiles, - id: binding.id, name: binding.name, author: binding.author ?? "Anonymous", description: binding.description ?? "", version: binding.version, - styles: styles.length > 0 ? styles : undefined, + styles, playbill, }; diff --git a/src/lib/html.ts b/src/lib/html.ts deleted file mode 100644 index 90c42ce..0000000 --- a/src/lib/html.ts +++ /dev/null @@ -1,14 +0,0 @@ -export function html( - strings: TemplateStringsArray, - ...values: unknown[] -): string { - const [first, ...rest] = strings; - let str = first; - - for (let i = 0; i < rest.length; i++) { - str += values[i]; - str += rest[i]; - } - - return str.trim(); -} diff --git a/src/lib/index.ts b/src/lib/index.ts index 957897f..6e62318 100644 --- a/src/lib/index.ts +++ b/src/lib/index.ts @@ -2,4 +2,3 @@ export * from "./errors"; export * from "./markdown"; export * from "./component-file"; export * from "./binding"; -export * from "./html"; diff --git a/src/lib/markdown.ts b/src/lib/markdown.ts index 4daaeb6..4090f69 100644 --- a/src/lib/markdown.ts +++ b/src/lib/markdown.ts @@ -21,7 +21,6 @@ marked.use(plugin); * @returns A promise resolving to the HTML representation of the markdown */ export async function renderMarkdown(markdown: string): Promise { - // TODO: Custom render tags for links, emphasis, and strong return await marked.parse(markdown, { async: true, gfm: true, @@ -60,23 +59,3 @@ export function extractMarkdown(content: string): string { } return content.replace(FRONTMATTER_REGEX, "").trim(); } - -/** - * Combine yaml frontmatter and markdown into a single string - * @param markdown The markdown to combine with frontmatter - * @param frontmatter The frontmatter shape to combine with markdown - * @returns A combined document with yaml frontmatter and markdown below - */ -export function combineMarkdownAndFrontmatter( - markdown: string, - frontmatter: unknown, -) { - const frontmatterToWrite: unknown = structuredClone(frontmatter); - - return `--- -${YAML.stringify(frontmatterToWrite)} ---- - -${markdown.trim()} -`; -} diff --git a/src/render/html/component/ability.tsx b/src/render/html/component/ability.tsx index 3efa648..4e8d404 100644 --- a/src/render/html/component/ability.tsx +++ b/src/render/html/component/ability.tsx @@ -1,4 +1,5 @@ import type { Ability } from "@proscenium/playbill"; +import { HTML } from "./base/html"; interface AbilityCardProps { ability: Ability; @@ -21,18 +22,18 @@ export function AbilityCard({ ability }: AbilityCardProps) { const costList = costs.length > 0 - ? `
    ${costs.join("\n")}
` + ?
    {costs}
: ""; const classList = `ability ability-${ability.type}`; return
-

${ability.name}

- ${ability.type} - ${ability.xp} XP - ${costList} +

{ability.name}

+ {ability.type} + {ability.xp} XP + {costList}
-
+
} diff --git a/src/render/html/component/base/html.tsx b/src/render/html/component/base/html.tsx new file mode 100644 index 0000000..b3daa36 --- /dev/null +++ b/src/render/html/component/base/html.tsx @@ -0,0 +1,8 @@ +interface HTMLWrapperProps { + html: string; + className?: string; +} + +export function HTML({ html, className }: HTMLWrapperProps) { + return
; +} diff --git a/src/render/html/component/base/section.ts b/src/render/html/component/base/section.ts deleted file mode 100644 index 6366661..0000000 --- a/src/render/html/component/base/section.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { html } from "#lib"; - -interface SectionProps { - type: string; - title: string; - content: string; - preInfo?: string; - info?: string; - componentId?: string; - leftCornerTag?: string; - rightCornerTag?: string; -} - -export async function Section({ - type, - title, - content, - preInfo = "", - info = "", - componentId, - leftCornerTag = "", - rightCornerTag = "", -}: SectionProps): Promise { - const dataTags = componentId ? `data-component-id="${componentId}"` : ""; - return html` -
-
- ${preInfo} -

${title}

- ${info} -
- - -
- ${content} -
-
- `; -} diff --git a/src/render/html/component/base/react-section.tsx b/src/render/html/component/base/section.tsx similarity index 79% rename from src/render/html/component/base/react-section.tsx rename to src/render/html/component/base/section.tsx index c5f8a52..f6200a7 100644 --- a/src/render/html/component/base/react-section.tsx +++ b/src/render/html/component/base/section.tsx @@ -4,7 +4,7 @@ import React from "react"; interface SectionProps { type: string; title: string; - content: React.ReactNode; + children: React.ReactNode; preInfo?: React.ReactNode; info?: React.ReactNode; componentId?: string; @@ -15,7 +15,7 @@ interface SectionProps { export function Section({ type, title, - content, + children, preInfo, info, componentId, @@ -26,8 +26,6 @@ export function Section({ const headerClasses = classNames("section-header", `${type}-header`); const contentClasses = classNames("section-content", `${type}-content`); - const contentDiv = typeof content === "string" ?
:
{content}
; - return
{preInfo} @@ -36,7 +34,9 @@ export function Section({
{leftCornerTag}
{rightCornerTag}
- {contentDiv} +
+ {children} +
} diff --git a/src/render/html/component/glossary.ts b/src/render/html/component/glossary.ts deleted file mode 100644 index 731e53c..0000000 --- a/src/render/html/component/glossary.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { html } from "#lib"; -import { Section } from "./base/section"; - -export async function GlossaryTable( - glossary: Record, -): Promise { - const terms = Object.entries(glossary).map(([term, defs]) => { - return html`
${term}
${defs.map((d) => html`
${d}
`).join("\n")}`; - }); - - const content = html` -
-
- ${terms.join("\n")} -
-
- `; - - return Section({ - type: "glossary", - componentId: "glossary", - title: "Glossary", - content, - }); -} diff --git a/src/render/html/component/glossary.tsx b/src/render/html/component/glossary.tsx new file mode 100644 index 0000000..0a0e7c0 --- /dev/null +++ b/src/render/html/component/glossary.tsx @@ -0,0 +1,23 @@ +import { Section } from "./base/section"; + +interface GlossaryTableProps { + glossary: Record; +} + +export function GlossaryTable( + { glossary }: GlossaryTableProps, +) { + return
+
+
+ {Object.entries(glossary).map(([term, defs]) => { + return
+
{term}
+ {defs.map((d, i) =>
{d}
)} +
+ })} +
+
+
+} + diff --git a/src/render/html/component/header.ts b/src/render/html/component/header.ts deleted file mode 100644 index f953d28..0000000 --- a/src/render/html/component/header.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { Playbill } from "@proscenium/playbill"; -import { html } from "#lib"; - -export async function PageHeader(playbill: Playbill): Promise { - return html` -
-

${playbill.name}

- -

Version ${playbill.version}

-
- `; -} diff --git a/src/render/html/component/header.tsx b/src/render/html/component/header.tsx new file mode 100644 index 0000000..b7a4fdb --- /dev/null +++ b/src/render/html/component/header.tsx @@ -0,0 +1,13 @@ +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.ts b/src/render/html/component/item.ts deleted file mode 100644 index 4c08f09..0000000 --- a/src/render/html/component/item.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { Item } from "@proscenium/playbill"; -import { capitalize } from "lodash-es"; -import { html, renderMarkdown } from "#lib"; -import { Section } from "./base/section"; - -export async function ItemCard(item: Item): Promise { - const renderedMarkdown = await renderMarkdown(item.description); - - let itemTypeDescriptor = ""; - - switch (item.type) { - case "wielded": - itemTypeDescriptor = `Wielded (${item.hands} ${item.hands === 1 ? "Hand" : "Hands"})`; - break; - case "worn": - itemTypeDescriptor = `Worn (${item.slot})`; - break; - case "trinket": - itemTypeDescriptor = "Trinket"; - break; - } - - const affinity = - item.affinity !== "none" - ? html`

${item.affinity} Affinity

` - : ""; - - const damageType = item.damage?.type === "arc" ? "Arcane" : "Physical"; - const damageAmount = item.damage?.amount ?? 0; - const damageRating = - damageAmount > 0 - ? html`

${damageAmount} ${damageType} Damage

` - : ""; - - const info = html` -
- ${affinity} - ${damageRating} -
- `; - - return Section({ - type: "item", - componentId: item.id, - title: item.name, - content: renderedMarkdown, - info, - rightCornerTag: capitalize(item.rarity), - leftCornerTag: itemTypeDescriptor, - }); -} diff --git a/src/render/html/component/item.tsx b/src/render/html/component/item.tsx new file mode 100644 index 0000000..fc0d453 --- /dev/null +++ b/src/render/html/component/item.tsx @@ -0,0 +1,53 @@ +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 index 6b52ca1..b759c25 100644 --- a/src/render/html/component/method.tsx +++ b/src/render/html/component/method.tsx @@ -1,8 +1,13 @@ import type { Method, Playbill } from "@proscenium/playbill"; import { AbilityCard } from "./ability"; -import { Section } from "./base/react-section"; +import { Section } from "./base/section"; -export function MethodSection(method: Method, playbill: Playbill) { +interface MethodSectionProps { + method: Method; + playbill: Playbill; +} + +export function MethodSection({ method, playbill }: MethodSectionProps) { const ranks = method.abilities.map((rank, i) => { return

Rank {i + 1}

@@ -20,9 +25,9 @@ export function MethodSection(method: Method, playbill: Playbill) { type="method" componentId={method.id} title={method.name} - preInfo={

{method.curator}

} - info={

{method.description}

} - content={
{ranks}
} - /> - + preInfo={

{method.curator}'s Method of

} + info={

} + > +
{ranks}
+ } diff --git a/src/render/html/component/resource.ts b/src/render/html/component/resource.ts deleted file mode 100644 index 73fdce0..0000000 --- a/src/render/html/component/resource.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { - ImageResource, - Resource, - TableResource, - TextResource, -} from "@proscenium/playbill"; -import { html, renderMarkdown } from "#lib"; -import { Section } from "./base/section"; - -async function TextResourceSection(resource: TextResource): Promise { - const renderedMarkdown = await renderMarkdown(resource.description); - - const info = resource.categories - .map((category) => { - return html`
  • ${category}
  • `; - }) - .join("\n"); - - return Section({ - type: "resource", - componentId: resource.id, - title: resource.name, - content: renderedMarkdown, - info: `
      ${info}
    `, - }); -} - -async function ImageResourceSection(resource: ImageResource): Promise { - const renderedMarkdown = await renderMarkdown(resource.description); - - const info = resource.categories - .map((category) => { - return html`
  • ${category}
  • `; - }) - .join("\n"); - - const fig = html` -
    - ${resource.name} -
    ${renderedMarkdown}
    -
    - `; - - return Section({ - type: "resource", - componentId: resource.id, - title: resource.name, - content: fig, - info: `
      ${info}
    `, - }); -} - -async function TableResourceSection(resource: TableResource): Promise { - const renderedMarkdown = await renderMarkdown(resource.description); - - const info = resource.categories - .map((category) => { - return html`
  • ${category}
  • `; - }) - .join("\n"); - - const tableData = resource.data; - - const [headerRow, ...rows] = tableData; - - let tableHTML = html``; - - for (const header of headerRow) { - tableHTML += html``; - } - - tableHTML += html``; - - for (const row of rows) { - tableHTML += html``; - for (const cell of row) { - tableHTML += html``; - } - tableHTML += html``; - } - - tableHTML += html`
    ${await renderMarkdown(header)}
    ${await renderMarkdown(cell)}
    `; - - return Section({ - type: "resource", - componentId: resource.id, - title: resource.name, - content: renderedMarkdown + tableHTML, - info: `
      ${info}
    `, - }); -} - -export async function ResourceSection(resource: Resource): Promise { - switch (resource.type) { - case "text": - return TextResourceSection(resource); - case "image": - return ImageResourceSection(resource); - case "table": - return TableResourceSection(resource); - } -} diff --git a/src/render/html/component/resource.tsx b/src/render/html/component/resource.tsx new file mode 100644 index 0000000..4066bfa --- /dev/null +++ b/src/render/html/component/resource.tsx @@ -0,0 +1,87 @@ +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 index b569305..3546f87 100644 --- a/src/render/html/component/rule.tsx +++ b/src/render/html/component/rule.tsx @@ -1,11 +1,17 @@ import type { Rule } from "@proscenium/playbill"; -import { Section } from "./base/react-section"; +import { Section } from "./base/section"; +import { HTML } from "./base/html"; -export function RuleSection(rule: Rule) { +interface RuleSectionProps { + rule: Rule; +} + +export function RuleSection({ rule }: RuleSectionProps) { return
    ; + > + +
    } diff --git a/src/render/html/component/species.ts b/src/render/html/component/species.ts deleted file mode 100644 index 81c4881..0000000 --- a/src/render/html/component/species.ts +++ /dev/null @@ -1,90 +0,0 @@ -import type { Playbill, Species } from "@proscenium/playbill"; -import { html, renderMarkdown } from "#lib"; -import { AbilityCard } from "./ability"; -import { Section } from "./base/section"; - -export async function SpeciesSection(species: Species, playbill: Playbill) { - const descriptionHTML = await renderMarkdown(species.description); - const hpString = species.hp >= 0 ? `+${species.hp}` : `-${species.hp}`; - const apString = species.ap >= 0 ? `+${species.ap}` : `-${species.ap}`; - const epString = species.ep >= 0 ? `+${species.ep}` : `-${species.ep}`; - - const hasAbilities = species.abilities.length > 0; - - const abilitySection = hasAbilities - ? html` -
    -

    Innate Abilities

    -
    - ` - : ""; - - const sectionContent = html` -
    - -
    -

    HP

    -

    ${hpString}

    -
    - -
    -

    AP

    -

    ${apString}

    -
    - -
    -

    EP

    -

    ${epString}

    -
    - -
    - -
    - -
    -

    Muscle

    -

    ${species.muscle}

    -
    - -
    -

    Focus

    -

    ${species.focus}

    -
    - -
    -

    Knowledge

    -

    ${species.knowledge}

    -
    - -
    -

    Charm

    -

    ${species.charm}

    -
    - -
    -

    Cunning

    -

    ${species.cunning}

    -
    - -
    -

    Spark

    -

    ${species.spark}

    -
    - -
    -
    - ${descriptionHTML} -
    - - - ${abilitySection} - `; - - return Section({ - type: "species", - componentId: species.id, - title: species.name, - content: sectionContent, - info: "

    Species

    ", - }); -} diff --git a/src/render/html/component/species.tsx b/src/render/html/component/species.tsx new file mode 100644 index 0000000..b849b60 --- /dev/null +++ b/src/render/html/component/species.tsx @@ -0,0 +1,91 @@ +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) => { + 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/index.tsx b/src/render/html/index.tsx index 038d83e..1457128 100644 --- a/src/render/html/index.tsx +++ b/src/render/html/index.tsx @@ -1,6 +1,6 @@ -import { Playbill } from "@proscenium/playbill"; +import { Playbill, type TaggedComponent } from "@proscenium/playbill"; import sortBy from "lodash-es/sortBy"; -import { html, renderMarkdown } from "#lib"; +import { renderMarkdown } from "#lib"; import { GlossaryTable } from "./component/glossary"; import { PageHeader } from "./component/header"; import { ItemCard } from "./component/item"; @@ -14,6 +14,22 @@ interface HTMLRenderOptions { styles?: string; } +async function processMarkdownInComponent(entry: TaggedComponent) { + entry.component.description = await renderMarkdown(entry.component.description); + + if (entry.type === "resource" && entry.component.type === "table") { + const newData: string[][] = []; + for (const row of entry.component.data) { + const newRow: string[] = []; + for (const cell of row) { + newRow.push(await renderMarkdown(cell)); + } + newData.push(newRow); + } + entry.component.data = newData; + } +} + export async function renderPlaybillToHTML( playbillToRender: Playbill, opts: HTMLRenderOptions = {}, @@ -21,46 +37,40 @@ export async function renderPlaybillToHTML( // Make a copy of the playbill to avoid modifying the original // and compile all description markdown const playbill = Playbill.fromSerialized(playbillToRender.serialize()); - await playbill.processComponents(renderMarkdown); - - // Render various resources - const resources = ( - await Promise.all(Object.values(playbill.resources).map(ResourceSection)) - ).join("\n"); - - const methods = ( - await Promise.all( - Object.values(playbill.methods).map((method) => { - return MethodSection(method, playbill); - }), - ) - ).join("\n"); - - const species = ( - await Promise.all( - Object.values(playbill.species).map((species) => { - return SpeciesSection(species, playbill); - }), - ) - ).join("\n"); - - const items = ( - await Promise.all( - Object.values(playbill.items).map((item) => { - return ItemCard(item); - }), - ) - ).join("\n"); - + await playbill.processComponents(processMarkdownInComponent); const rulesByOrder = sortBy(Object.values(playbill.rules), "order"); - const rules = rulesByOrder.map(RuleSection); - const glossaryHTML = await GlossaryTable(playbill.glossary); // Prepare stylesheet - const css = opts.styles ? html`` : ""; + const css = opts.styles ? `` : ""; + + const body = renderToString(<> +
    + {Object.values(playbill.species).map((species) => )} +
    + +
    + {Object.values(playbill.methods).map((method) => )} +
    + +
    + {Object.values(playbill.items).map((item) => )} +
    + +
    + {rulesByOrder.map((rule) => )} +
    + +
    + {} +
    + +
    + {Object.values(playbill.resources).map((resource) => )} +
    + ); // Main document - const doc = html` + const doc = ` @@ -126,7 +136,7 @@ export async function renderPlaybillToHTML( - ${await PageHeader(playbill)} + ${renderToString()}

    ${playbill.description}

    -
    - ${species} -
    - -
    - ${methods} -
    - -
    - ${items} -
    - -
    - ${renderToString(rules)} -
    - -
    - ${glossaryHTML} -
    - -
    - ${resources} -
    - - + ${body} `; diff --git a/src/util/args.ts b/src/util/args.ts index 75dedcf..5b95881 100644 --- a/src/util/args.ts +++ b/src/util/args.ts @@ -1,6 +1,23 @@ 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 + --help, -h Show this help message +`.trim(); const Renderers = ["json", "html"] as const; const RendererSchema = z.enum(Renderers); diff --git a/src/util/files.ts b/src/util/files.ts index 952b8cc..cd6c865 100644 --- a/src/util/files.ts +++ b/src/util/files.ts @@ -37,6 +37,23 @@ export async function loadYAMLFileOrFail(filePath: string): Promise { } } +/** + * 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 *