Convert rendering to jsx
This commit is contained in:
parent
0f72d048ee
commit
d1b632e83c
25 changed files with 422 additions and 513 deletions
|
@ -8,7 +8,6 @@ import {
|
||||||
type Species,
|
type Species,
|
||||||
parseAbility,
|
parseAbility,
|
||||||
parseBlueprint,
|
parseBlueprint,
|
||||||
parseGlossary,
|
|
||||||
parseItem,
|
parseItem,
|
||||||
parseMethod,
|
parseMethod,
|
||||||
parseResource,
|
parseResource,
|
||||||
|
@ -18,6 +17,7 @@ import {
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const Base = z.object({
|
const Base = z.object({
|
||||||
|
// Required -- defines the type of component
|
||||||
$define: z.string(),
|
$define: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const AbilitySchema = Info.extend({
|
||||||
ap: z.number().int().default(0),
|
ap: z.number().int().default(0),
|
||||||
hp: z.number().int().default(0),
|
hp: z.number().int().default(0),
|
||||||
ep: 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),
|
damage: z.number().int().default(0),
|
||||||
damageType: z.string().default("phy"),
|
damageType: z.string().default("phy"),
|
||||||
roll: z.string().default("none"),
|
roll: z.string().default("none"),
|
||||||
|
@ -68,6 +68,7 @@ const BaseItem = Info.extend({
|
||||||
affinity: z.string().default("none"),
|
affinity: z.string().default("none"),
|
||||||
damage: z.number().int().default(0),
|
damage: z.number().int().default(0),
|
||||||
damageType: z.string().default("phy"),
|
damageType: z.string().default("phy"),
|
||||||
|
abilities: z.array(z.string()).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
const ItemSchema = z.discriminatedUnion("type", [
|
const ItemSchema = z.discriminatedUnion("type", [
|
||||||
|
@ -163,10 +164,6 @@ export type ParsedComponent =
|
||||||
type: "blueprint";
|
type: "blueprint";
|
||||||
component: Blueprint;
|
component: Blueprint;
|
||||||
}
|
}
|
||||||
| {
|
|
||||||
type: "glossary";
|
|
||||||
component: Record<string, string[]>;
|
|
||||||
}
|
|
||||||
| {
|
| {
|
||||||
type: "item";
|
type: "item";
|
||||||
component: Item;
|
component: Item;
|
||||||
|
@ -209,8 +206,6 @@ function parseAbilityDefinition(obj: unknown): ParsedComponent {
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
roll: parsed.roll,
|
roll: parsed.roll,
|
||||||
boons: [],
|
|
||||||
banes: [],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
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<string, string[]> = {};
|
|
||||||
for (const [term, definition] of Object.entries(terms)) {
|
|
||||||
curatedTerms[term] = [definition];
|
|
||||||
}
|
|
||||||
|
|
||||||
const glossary = parseGlossary(curatedTerms);
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "glossary",
|
|
||||||
component: glossary,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseItemDefinition(obj: unknown): ParsedComponent {
|
function parseItemDefinition(obj: unknown): ParsedComponent {
|
||||||
const parsed = ItemSchema.parse(obj);
|
const parsed = ItemSchema.parse(obj);
|
||||||
|
|
||||||
|
@ -280,6 +258,7 @@ function parseItemDefinition(obj: unknown): ParsedComponent {
|
||||||
type: parsed.damageType,
|
type: parsed.damageType,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
|
abilities: parsed.abilities,
|
||||||
tweak: "",
|
tweak: "",
|
||||||
temper: "",
|
temper: "",
|
||||||
}),
|
}),
|
||||||
|
@ -343,8 +322,6 @@ export function parsePlaybillComponent(obj: unknown): ParsedComponent {
|
||||||
return parseAbilityDefinition(component);
|
return parseAbilityDefinition(component);
|
||||||
case "blueprint":
|
case "blueprint":
|
||||||
return parseBlueprintDefinition(component);
|
return parseBlueprintDefinition(component);
|
||||||
case "glossary":
|
|
||||||
return parseGlossaryDefinition(component);
|
|
||||||
case "item":
|
case "item":
|
||||||
return parseItemDefinition(component);
|
return parseItemDefinition(component);
|
||||||
case "method":
|
case "method":
|
||||||
|
|
19
src/index.ts
19
src/index.ts
|
@ -6,23 +6,8 @@ import {
|
||||||
type CLIArguments,
|
type CLIArguments,
|
||||||
getAbsoluteDirname,
|
getAbsoluteDirname,
|
||||||
parseCLIArguments,
|
parseCLIArguments,
|
||||||
|
USAGE,
|
||||||
} from "#util";
|
} 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>
|
|
||||||
|
|
||||||
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 {
|
enum ExitCode {
|
||||||
Success = 0,
|
Success = 0,
|
||||||
|
@ -72,7 +57,7 @@ async function main(): Promise<number> {
|
||||||
|
|
||||||
// -- HELP TEXT -- //
|
// -- HELP TEXT -- //
|
||||||
if (options.help) {
|
if (options.help) {
|
||||||
console.log(usage);
|
console.log(USAGE);
|
||||||
return ExitCode.Success;
|
return ExitCode.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,17 +15,21 @@ import { loadYAMLFileOrFail } from "#util";
|
||||||
import { ComponentFile } from "./component-file";
|
import { ComponentFile } from "./component-file";
|
||||||
import { FileNotFoundError } from "./errors";
|
import { FileNotFoundError } from "./errors";
|
||||||
|
|
||||||
|
// binding.yaml file schema
|
||||||
const BindingSchema = z.object({
|
const BindingSchema = z.object({
|
||||||
extend: z.string().optional(),
|
extend: z.string().optional(),
|
||||||
|
|
||||||
id: z.string().default("playbill"),
|
|
||||||
name: z.string().default("Unnamed playbill"),
|
name: z.string().default("Unnamed playbill"),
|
||||||
author: z.string().optional(),
|
author: z.string().default("Someone"),
|
||||||
description: z.string().optional(),
|
description: z.string().default("No description provided"),
|
||||||
version: z.string().default("0.0.0"),
|
version: z.string().default("0.0.0"),
|
||||||
|
|
||||||
files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
|
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<typeof BindingSchema>;
|
type Binding = z.infer<typeof BindingSchema>;
|
||||||
|
@ -37,15 +41,15 @@ export interface BoundPlaybill {
|
||||||
bindingFilePath: string;
|
bindingFilePath: string;
|
||||||
bindingFileDirname: string;
|
bindingFileDirname: string;
|
||||||
includedFiles: string[];
|
includedFiles: string[];
|
||||||
|
componentFiles: ComponentFile[];
|
||||||
|
|
||||||
// Binding properties
|
// Binding properties
|
||||||
id: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
author: string;
|
author: string;
|
||||||
version: string;
|
version: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
||||||
styles?: string;
|
styles: string;
|
||||||
|
|
||||||
playbill: Playbill;
|
playbill: Playbill;
|
||||||
}
|
}
|
||||||
|
@ -98,20 +102,19 @@ export async function loadFromBinding(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load any specified stylesheets
|
// Load any specified stylesheets
|
||||||
let styles = "";
|
const loadedStyles: string[] = [];
|
||||||
if (binding.styles && binding.styles.length > 0) {
|
|
||||||
for (const style of binding.styles) {
|
for (const style of binding.styles) {
|
||||||
const cssFilePath = path.resolve(bindingFileDirname, style);
|
const cssFilePath = path.resolve(bindingFileDirname, style);
|
||||||
const cssFile = Bun.file(cssFilePath);
|
const cssFile = Bun.file(cssFilePath);
|
||||||
const cssFileExists = await cssFile.exists();
|
const cssFileExists = await cssFile.exists();
|
||||||
|
|
||||||
if (cssFileExists) {
|
if (cssFileExists) {
|
||||||
styles += `${await cssFile.text()}\n`;
|
loadedStyles.push(await cssFile.text());
|
||||||
} else {
|
} else {
|
||||||
throw new FileNotFoundError(cssFilePath);
|
throw new FileNotFoundError(cssFilePath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
const styles = loadedStyles.join("\n");
|
||||||
|
|
||||||
// Scan for all files matching the globs
|
// Scan for all files matching the globs
|
||||||
const allFilePaths: string[] = [];
|
const allFilePaths: string[] = [];
|
||||||
|
@ -154,7 +157,6 @@ export async function loadFromBinding(
|
||||||
|
|
||||||
const abilities: Ability[] = [];
|
const abilities: Ability[] = [];
|
||||||
const bluprints: Blueprint[] = [];
|
const bluprints: Blueprint[] = [];
|
||||||
const glossaries: Record<string, string[]>[] = [];
|
|
||||||
const methods: Method[] = [];
|
const methods: Method[] = [];
|
||||||
const species: Species[] = [];
|
const species: Species[] = [];
|
||||||
const items: Item[] = [];
|
const items: Item[] = [];
|
||||||
|
@ -173,9 +175,6 @@ export async function loadFromBinding(
|
||||||
case "blueprint":
|
case "blueprint":
|
||||||
bluprints.push(component.component);
|
bluprints.push(component.component);
|
||||||
break;
|
break;
|
||||||
case "glossary":
|
|
||||||
glossaries.push(component.component);
|
|
||||||
break;
|
|
||||||
case "method":
|
case "method":
|
||||||
methods.push(component.component);
|
methods.push(component.component);
|
||||||
break;
|
break;
|
||||||
|
@ -232,27 +231,23 @@ export async function loadFromBinding(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add all definitions
|
// Add all definitions
|
||||||
for (const glossary of glossaries) {
|
for (const [term, definition] of Object.entries(binding.terms)) {
|
||||||
for (const [term, definitions] of Object.entries(glossary)) {
|
|
||||||
for (const definition of definitions) {
|
|
||||||
playbill.addDefinition(term, definition);
|
playbill.addDefinition(term, definition);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
_raw: binding,
|
_raw: binding,
|
||||||
bindingFilePath: bindingPath,
|
bindingFilePath: bindingPath,
|
||||||
bindingFileDirname,
|
bindingFileDirname,
|
||||||
includedFiles: filePathsWithoutBindingFile,
|
includedFiles: filePathsWithoutBindingFile,
|
||||||
|
componentFiles,
|
||||||
|
|
||||||
id: binding.id,
|
|
||||||
name: binding.name,
|
name: binding.name,
|
||||||
author: binding.author ?? "Anonymous",
|
author: binding.author ?? "Anonymous",
|
||||||
description: binding.description ?? "",
|
description: binding.description ?? "",
|
||||||
version: binding.version,
|
version: binding.version,
|
||||||
|
|
||||||
styles: styles.length > 0 ? styles : undefined,
|
styles,
|
||||||
|
|
||||||
playbill,
|
playbill,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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();
|
|
||||||
}
|
|
|
@ -2,4 +2,3 @@ export * from "./errors";
|
||||||
export * from "./markdown";
|
export * from "./markdown";
|
||||||
export * from "./component-file";
|
export * from "./component-file";
|
||||||
export * from "./binding";
|
export * from "./binding";
|
||||||
export * from "./html";
|
|
||||||
|
|
|
@ -21,7 +21,6 @@ marked.use(plugin);
|
||||||
* @returns A promise resolving to the HTML representation of the markdown
|
* @returns A promise resolving to the HTML representation of the markdown
|
||||||
*/
|
*/
|
||||||
export async function renderMarkdown(markdown: string): Promise<string> {
|
export async function renderMarkdown(markdown: string): Promise<string> {
|
||||||
// TODO: Custom render tags for links, emphasis, and strong
|
|
||||||
return await marked.parse(markdown, {
|
return await marked.parse(markdown, {
|
||||||
async: true,
|
async: true,
|
||||||
gfm: true,
|
gfm: true,
|
||||||
|
@ -60,23 +59,3 @@ export function extractMarkdown(content: string): string {
|
||||||
}
|
}
|
||||||
return content.replace(FRONTMATTER_REGEX, "").trim();
|
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()}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Ability } from "@proscenium/playbill";
|
import type { Ability } from "@proscenium/playbill";
|
||||||
|
import { HTML } from "./base/html";
|
||||||
|
|
||||||
interface AbilityCardProps {
|
interface AbilityCardProps {
|
||||||
ability: Ability;
|
ability: Ability;
|
||||||
|
@ -21,18 +22,18 @@ export function AbilityCard({ ability }: AbilityCardProps) {
|
||||||
|
|
||||||
const costList =
|
const costList =
|
||||||
costs.length > 0
|
costs.length > 0
|
||||||
? `<ul class="ability-costs">${costs.join("\n")}</ul>`
|
? <ul className="ability-costs">{costs}</ul>
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
const classList = `ability ability-${ability.type}`;
|
const classList = `ability ability-${ability.type}`;
|
||||||
|
|
||||||
return <div className={classList} data-component-id={ability.id}>
|
return <div className={classList} data-component-id={ability.id}>
|
||||||
<div className="ability-header">
|
<div className="ability-header">
|
||||||
<h4>${ability.name}</h4>
|
<h4>{ability.name}</h4>
|
||||||
<span className="ability-type">${ability.type}</span>
|
<span className="ability-type">{ability.type}</span>
|
||||||
<span className="ability-cost-xp">${ability.xp} XP</span>
|
<span className="ability-cost-xp">{ability.xp} XP</span>
|
||||||
${costList}
|
{costList}
|
||||||
</div>
|
</div>
|
||||||
<div className="ability-content" dangerouslySetInnerHTML={{ __html: ability.description }} />
|
<HTML className="ability-content" html={ability.description} />
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
8
src/render/html/component/base/html.tsx
Normal file
8
src/render/html/component/base/html.tsx
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
interface HTMLWrapperProps {
|
||||||
|
html: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function HTML({ html, className }: HTMLWrapperProps) {
|
||||||
|
return <div className={className} dangerouslySetInnerHTML={{ __html: html }} />;
|
||||||
|
}
|
|
@ -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<string> {
|
|
||||||
const dataTags = componentId ? `data-component-id="${componentId}"` : "";
|
|
||||||
return html`
|
|
||||||
<section class="section ${type}" ${dataTags}>
|
|
||||||
<div class="section-header ${type}-header">
|
|
||||||
${preInfo}
|
|
||||||
<h2>${title}</h2>
|
|
||||||
${info}
|
|
||||||
</div>
|
|
||||||
<div class="section-tag section-tag--left">${leftCornerTag}</div>
|
|
||||||
<div class="section-tag section-tag--right">${rightCornerTag}</div>
|
|
||||||
<div class="section-content ${type}-content">
|
|
||||||
${content}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
`;
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ import React from "react";
|
||||||
interface SectionProps {
|
interface SectionProps {
|
||||||
type: string;
|
type: string;
|
||||||
title: string;
|
title: string;
|
||||||
content: React.ReactNode;
|
children: React.ReactNode;
|
||||||
preInfo?: React.ReactNode;
|
preInfo?: React.ReactNode;
|
||||||
info?: React.ReactNode;
|
info?: React.ReactNode;
|
||||||
componentId?: string;
|
componentId?: string;
|
||||||
|
@ -15,7 +15,7 @@ interface SectionProps {
|
||||||
export function Section({
|
export function Section({
|
||||||
type,
|
type,
|
||||||
title,
|
title,
|
||||||
content,
|
children,
|
||||||
preInfo,
|
preInfo,
|
||||||
info,
|
info,
|
||||||
componentId,
|
componentId,
|
||||||
|
@ -26,8 +26,6 @@ export function Section({
|
||||||
const headerClasses = classNames("section-header", `${type}-header`);
|
const headerClasses = classNames("section-header", `${type}-header`);
|
||||||
const contentClasses = classNames("section-content", `${type}-content`);
|
const contentClasses = classNames("section-content", `${type}-content`);
|
||||||
|
|
||||||
const contentDiv = typeof content === "string" ? <div className={contentClasses} dangerouslySetInnerHTML={{ __html: content }} /> : <div className={contentClasses}>{content}</div>;
|
|
||||||
|
|
||||||
return <section className={sectionClasses} data-component-id={componentId}>
|
return <section className={sectionClasses} data-component-id={componentId}>
|
||||||
<div className={headerClasses}>
|
<div className={headerClasses}>
|
||||||
{preInfo}
|
{preInfo}
|
||||||
|
@ -36,7 +34,9 @@ export function Section({
|
||||||
</div>
|
</div>
|
||||||
<div className="section-tag section-tag--left">{leftCornerTag}</div>
|
<div className="section-tag section-tag--left">{leftCornerTag}</div>
|
||||||
<div className="section-tag section-tag--right">{rightCornerTag}</div>
|
<div className="section-tag section-tag--right">{rightCornerTag}</div>
|
||||||
{contentDiv}
|
<div className={contentClasses}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,25 +0,0 @@
|
||||||
import { html } from "#lib";
|
|
||||||
import { Section } from "./base/section";
|
|
||||||
|
|
||||||
export async function GlossaryTable(
|
|
||||||
glossary: Record<string, string[]>,
|
|
||||||
): Promise<string> {
|
|
||||||
const terms = Object.entries(glossary).map(([term, defs]) => {
|
|
||||||
return html`<dt>${term}</dt>${defs.map((d) => html`<dd>${d}</dd>`).join("\n")}`;
|
|
||||||
});
|
|
||||||
|
|
||||||
const content = html`
|
|
||||||
<div class="glossary-content">
|
|
||||||
<dl>
|
|
||||||
${terms.join("\n")}
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "glossary",
|
|
||||||
componentId: "glossary",
|
|
||||||
title: "Glossary",
|
|
||||||
content,
|
|
||||||
});
|
|
||||||
}
|
|
23
src/render/html/component/glossary.tsx
Normal file
23
src/render/html/component/glossary.tsx
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { Section } from "./base/section";
|
||||||
|
|
||||||
|
interface GlossaryTableProps {
|
||||||
|
glossary: Record<string, string[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function GlossaryTable(
|
||||||
|
{ glossary }: GlossaryTableProps,
|
||||||
|
) {
|
||||||
|
return <Section type="glossary" componentId="glossary" title="Glossary">
|
||||||
|
<div className="glossary-content">
|
||||||
|
<dl>
|
||||||
|
{Object.entries(glossary).map(([term, defs]) => {
|
||||||
|
return <div key={term}>
|
||||||
|
<dt>{term}</dt>
|
||||||
|
{defs.map((d, i) => <dd key={i}>{d}</dd>)}
|
||||||
|
</div>
|
||||||
|
})}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
|
|
@ -1,12 +0,0 @@
|
||||||
import type { Playbill } from "@proscenium/playbill";
|
|
||||||
import { html } from "#lib";
|
|
||||||
|
|
||||||
export async function PageHeader(playbill: Playbill): Promise<string> {
|
|
||||||
return html`
|
|
||||||
<header>
|
|
||||||
<h1 class="header-title">${playbill.name}</h1>
|
|
||||||
<p class="header-byline">A Playbill by ${playbill.author}</p>
|
|
||||||
<p><small class="header-info">Version ${playbill.version}</small></p>
|
|
||||||
</header>
|
|
||||||
`;
|
|
||||||
}
|
|
13
src/render/html/component/header.tsx
Normal file
13
src/render/html/component/header.tsx
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { Playbill } from "@proscenium/playbill";
|
||||||
|
|
||||||
|
interface PlaybillHeaderProps {
|
||||||
|
playbill: Playbill;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PageHeader({ playbill }: PlaybillHeaderProps) {
|
||||||
|
return <header>
|
||||||
|
<h1 className="header-title">{playbill.name}</h1>
|
||||||
|
<p className="header-byline">A Playbill by {playbill.author} </p>
|
||||||
|
<p> <small className="header-info"> Version {playbill.version}</small></p>
|
||||||
|
</header>
|
||||||
|
}
|
|
@ -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<string> {
|
|
||||||
const renderedMarkdown = await renderMarkdown(item.description);
|
|
||||||
|
|
||||||
let itemTypeDescriptor = "";
|
|
||||||
|
|
||||||
switch (item.type) {
|
|
||||||
case "wielded":
|
|
||||||
itemTypeDescriptor = `Wielded (${item.hands} ${item.hands === 1 ? "Hand" : "Hands"})`;
|
|
||||||
break;
|
|
||||||
case "worn":
|
|
||||||
itemTypeDescriptor = `Worn (${item.slot})`;
|
|
||||||
break;
|
|
||||||
case "trinket":
|
|
||||||
itemTypeDescriptor = "Trinket";
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const affinity =
|
|
||||||
item.affinity !== "none"
|
|
||||||
? html`<p class="item-affinity ${item.affinity}">${item.affinity} Affinity</p>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const damageType = item.damage?.type === "arc" ? "Arcane" : "Physical";
|
|
||||||
const damageAmount = item.damage?.amount ?? 0;
|
|
||||||
const damageRating =
|
|
||||||
damageAmount > 0
|
|
||||||
? html`<p class="item-damage ${damageType}">${damageAmount} ${damageType} Damage</p>`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const info = html`
|
|
||||||
<div class="item-info">
|
|
||||||
${affinity}
|
|
||||||
${damageRating}
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "item",
|
|
||||||
componentId: item.id,
|
|
||||||
title: item.name,
|
|
||||||
content: renderedMarkdown,
|
|
||||||
info,
|
|
||||||
rightCornerTag: capitalize(item.rarity),
|
|
||||||
leftCornerTag: itemTypeDescriptor,
|
|
||||||
});
|
|
||||||
}
|
|
53
src/render/html/component/item.tsx
Normal file
53
src/render/html/component/item.tsx
Normal file
|
@ -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(
|
||||||
|
<p key="affinity" className={`item-affinity ${item.affinity}`}>
|
||||||
|
{item.affinity} Affinity
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item.damage !== null && item.damage.amount > 0) {
|
||||||
|
infoParts.push(
|
||||||
|
<p key="damage" className={`item-damage ${item.damage.type}`}>
|
||||||
|
{item.damage.amount} {capitalize(item.damage.type)} Damage
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Section
|
||||||
|
type="item"
|
||||||
|
componentId={item.id}
|
||||||
|
title={item.name}
|
||||||
|
info={<div className="item-info">{infoParts}</div>}
|
||||||
|
rightCornerTag={capitalize(item.rarity)}
|
||||||
|
leftCornerTag={itemTypeDescriptor}
|
||||||
|
>
|
||||||
|
<HTML html={item.description} />
|
||||||
|
</Section>
|
||||||
|
}
|
|
@ -1,8 +1,13 @@
|
||||||
import type { Method, Playbill } from "@proscenium/playbill";
|
import type { Method, Playbill } from "@proscenium/playbill";
|
||||||
import { AbilityCard } from "./ability";
|
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) => {
|
const ranks = method.abilities.map((rank, i) => {
|
||||||
return <div className="method-rank" key={i}>
|
return <div className="method-rank" key={i}>
|
||||||
<h3>Rank {i + 1}</h3>
|
<h3>Rank {i + 1}</h3>
|
||||||
|
@ -20,9 +25,9 @@ export function MethodSection(method: Method, playbill: Playbill) {
|
||||||
type="method"
|
type="method"
|
||||||
componentId={method.id}
|
componentId={method.id}
|
||||||
title={method.name}
|
title={method.name}
|
||||||
preInfo={<p className="method-curator">{method.curator}</p>}
|
preInfo={<p className="method-curator">{method.curator}'s Method of</p>}
|
||||||
info={<p className="method-description">{method.description}</p>}
|
info={<p className="method-description" dangerouslySetInnerHTML={{ __html: method.description }}></p>}
|
||||||
content={<div className="method-ranks">{ranks}</div>}
|
>
|
||||||
/>
|
<div className="method-ranks">{ranks}</div>
|
||||||
|
</Section>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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<string> {
|
|
||||||
const renderedMarkdown = await renderMarkdown(resource.description);
|
|
||||||
|
|
||||||
const info = resource.categories
|
|
||||||
.map((category) => {
|
|
||||||
return html`<li>${category}</li>`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "resource",
|
|
||||||
componentId: resource.id,
|
|
||||||
title: resource.name,
|
|
||||||
content: renderedMarkdown,
|
|
||||||
info: `<ul class="resource-categories">${info}</ul>`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function ImageResourceSection(resource: ImageResource): Promise<string> {
|
|
||||||
const renderedMarkdown = await renderMarkdown(resource.description);
|
|
||||||
|
|
||||||
const info = resource.categories
|
|
||||||
.map((category) => {
|
|
||||||
return html`<li>${category}</li>`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
const fig = html`
|
|
||||||
<figure class="resource-image">
|
|
||||||
<img src="${resource.url}" alt="${resource.name}" />
|
|
||||||
<figcaption>${renderedMarkdown}</figcaption>
|
|
||||||
</figure>
|
|
||||||
`;
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "resource",
|
|
||||||
componentId: resource.id,
|
|
||||||
title: resource.name,
|
|
||||||
content: fig,
|
|
||||||
info: `<ul class="resource-categories">${info}</ul>`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function TableResourceSection(resource: TableResource): Promise<string> {
|
|
||||||
const renderedMarkdown = await renderMarkdown(resource.description);
|
|
||||||
|
|
||||||
const info = resource.categories
|
|
||||||
.map((category) => {
|
|
||||||
return html`<li>${category}</li>`;
|
|
||||||
})
|
|
||||||
.join("\n");
|
|
||||||
|
|
||||||
const tableData = resource.data;
|
|
||||||
|
|
||||||
const [headerRow, ...rows] = tableData;
|
|
||||||
|
|
||||||
let tableHTML = html`<table class="resource-table"><thead><tr>`;
|
|
||||||
|
|
||||||
for (const header of headerRow) {
|
|
||||||
tableHTML += html`<th>${await renderMarkdown(header)}</th>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tableHTML += html`</tr></thead><tbody>`;
|
|
||||||
|
|
||||||
for (const row of rows) {
|
|
||||||
tableHTML += html`<tr>`;
|
|
||||||
for (const cell of row) {
|
|
||||||
tableHTML += html`<td>${await renderMarkdown(cell)}</td>`;
|
|
||||||
}
|
|
||||||
tableHTML += html`</tr>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
tableHTML += html`</tbody></table>`;
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "resource",
|
|
||||||
componentId: resource.id,
|
|
||||||
title: resource.name,
|
|
||||||
content: renderedMarkdown + tableHTML,
|
|
||||||
info: `<ul class="resource-categories">${info}</ul>`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ResourceSection(resource: Resource): Promise<string> {
|
|
||||||
switch (resource.type) {
|
|
||||||
case "text":
|
|
||||||
return TextResourceSection(resource);
|
|
||||||
case "image":
|
|
||||||
return ImageResourceSection(resource);
|
|
||||||
case "table":
|
|
||||||
return TableResourceSection(resource);
|
|
||||||
}
|
|
||||||
}
|
|
87
src/render/html/component/resource.tsx
Normal file
87
src/render/html/component/resource.tsx
Normal file
|
@ -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 <Section
|
||||||
|
type="resource"
|
||||||
|
componentId={resource.id}
|
||||||
|
title={resource.name}
|
||||||
|
info={<ul className="resource-categories">{
|
||||||
|
resource.categories.map((category) => <li key={category}>{category}</li>)
|
||||||
|
}</ul>}
|
||||||
|
><HTML html={resource.description} /></Section>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ImageResourceSectionProps {
|
||||||
|
resource: ImageResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImageResourceSection({ resource }: ImageResourceSectionProps) {
|
||||||
|
return <Section
|
||||||
|
type="resource"
|
||||||
|
componentId={resource.id}
|
||||||
|
title={resource.name}
|
||||||
|
info={<ul className="resource-categories">{
|
||||||
|
resource.categories.map((category) => <li key={category}>{category}</li>)
|
||||||
|
}</ul>}
|
||||||
|
>
|
||||||
|
<figure className="resource-image">
|
||||||
|
<img src={resource.url} alt={resource.name} />
|
||||||
|
<figcaption dangerouslySetInnerHTML={{ __html: resource.description }} />
|
||||||
|
</figure>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableResourceSectionProps {
|
||||||
|
resource: TableResource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TableResourceSection({ resource }: TableResourceSectionProps) {
|
||||||
|
const [headers, ...rows] = resource.data;
|
||||||
|
return <Section
|
||||||
|
type="resource"
|
||||||
|
componentId={resource.id}
|
||||||
|
title={resource.name}
|
||||||
|
info={<ul className="resource-categories">{
|
||||||
|
resource.categories.map((category) => <li key={category}>{category}</li>)
|
||||||
|
}</ul>}
|
||||||
|
>
|
||||||
|
<table className="resource-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{headers.map((header) => <th key={header} dangerouslySetInnerHTML={{ __html: header }} />)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{rows.map((row, i) => <tr key={i}>{
|
||||||
|
row.map((cell, j) => <td key={j} dangerouslySetInnerHTML={{ __html: cell }} />)
|
||||||
|
}</tr>)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</Section>
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ResourceSectionProps {
|
||||||
|
resource: Resource;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ResourceSection({ resource }: ResourceSectionProps) {
|
||||||
|
switch (resource.type) {
|
||||||
|
case "text":
|
||||||
|
return <TextResourceSection resource={resource} />;
|
||||||
|
case "image":
|
||||||
|
return <ImageResourceSection resource={resource} />;
|
||||||
|
case "table":
|
||||||
|
return <TableResourceSection resource={resource} />;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,17 @@
|
||||||
import type { Rule } from "@proscenium/playbill";
|
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 <Section
|
return <Section
|
||||||
title={rule.name}
|
title={rule.name}
|
||||||
componentId={rule.id}
|
componentId={rule.id}
|
||||||
content={rule.description}
|
|
||||||
type="rule"
|
type="rule"
|
||||||
/>;
|
>
|
||||||
|
<HTML html={rule.description} />
|
||||||
|
</Section>
|
||||||
}
|
}
|
||||||
|
|
|
@ -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`
|
|
||||||
<div class="species-abilities">
|
|
||||||
<h3>Innate Abilities</h3>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const sectionContent = html`
|
|
||||||
<div class="species-stats">
|
|
||||||
|
|
||||||
<div class="species-stat-hp">
|
|
||||||
<h4 class="hp species-stat-hp-title" title="Health Points">HP</h4>
|
|
||||||
<p class="species-stat-hp-value">${hpString}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-stat-ap">
|
|
||||||
<h4 class="ap species-stat-ap-title" title="Action Points">AP</h4>
|
|
||||||
<p class="species-stat-ap-value">${apString}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-stat-ep">
|
|
||||||
<h4 class="ep species-stat-ep-title" title="Exhaustion Points">EP</h4>
|
|
||||||
<p class="species-stat-ep-value">${epString}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talents">
|
|
||||||
|
|
||||||
<div class="species-talent-muscle">
|
|
||||||
<h4 class="species-talent-muscle-title muscle">Muscle</h4>
|
|
||||||
<p class="species-talent-muscle-value ${species.muscle}">${species.muscle}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talent-focus">
|
|
||||||
<h4 class="species-talent-focus-title focus">Focus</h4>
|
|
||||||
<p class="species-talent-focus-value ${species.focus}">${species.focus}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talent-knowledge">
|
|
||||||
<h4 class="species-talent-knowledge-title knowledge">Knowledge</h4>
|
|
||||||
<p class="species-talent-knowledge-value ${species.knowledge}">${species.knowledge}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talent-charm">
|
|
||||||
<h4 class="species-talent-charm-title charm">Charm</h4>
|
|
||||||
<p class="species-talent-charm-value ${species.charm}">${species.charm}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talent-cunning">
|
|
||||||
<h4 class="species-talent-cunning-title cunning">Cunning</h4>
|
|
||||||
<p class="species-talent-cunning-value ${species.cunning}">${species.cunning}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="species-talent-spark">
|
|
||||||
<h4 class="species-talent-spark-title spark">Spark</h4>
|
|
||||||
<p class="species-talent-spark-value ${species.spark}">${species.spark}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<div class="species-content">
|
|
||||||
${descriptionHTML}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
${abilitySection}
|
|
||||||
`;
|
|
||||||
|
|
||||||
return Section({
|
|
||||||
type: "species",
|
|
||||||
componentId: species.id,
|
|
||||||
title: species.name,
|
|
||||||
content: sectionContent,
|
|
||||||
info: "<p>Species</p>",
|
|
||||||
});
|
|
||||||
}
|
|
91
src/render/html/component/species.tsx
Normal file
91
src/render/html/component/species.tsx
Normal file
|
@ -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
|
||||||
|
? (<div className="species-abilities">
|
||||||
|
<h3>Innate Abilities</h3>
|
||||||
|
{species.abilities.map((abilityId) => {
|
||||||
|
return <AbilityCard ability={playbill.abilities[abilityId]} key={abilityId} />;
|
||||||
|
})}
|
||||||
|
</div>)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const sectionContent = <>
|
||||||
|
<div className="species-stats">
|
||||||
|
|
||||||
|
<div className="species-stat-hp">
|
||||||
|
<h4 className="hp species-stat-hp-title" title="Health Points">HP</h4>
|
||||||
|
<p className="species-stat-hp-value">{hpString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-stat-ap">
|
||||||
|
<h4 className="ap species-stat-ap-title" title="Action Points">AP</h4>
|
||||||
|
<p className="species-stat-ap-value">{apString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-stat-ep">
|
||||||
|
<h4 className="ep species-stat-ep-title" title="Exhaustion Points">EP</h4>
|
||||||
|
<p className="species-stat-ep-value">{epString}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div >
|
||||||
|
|
||||||
|
<div className="species-talents">
|
||||||
|
|
||||||
|
<div className="species-talent-muscle">
|
||||||
|
<h4 className="species-talent-muscle-title muscle">Muscle</h4>
|
||||||
|
<p className={`species-talent-muscle-value ${species.muscle}`}>{species.muscle}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-talent-focus">
|
||||||
|
<h4 className="species-talent-focus-title focus">Focus</h4>
|
||||||
|
<p className={`species-talent-focus-value ${species.focus}`}>{species.focus}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-talent-knowledge">
|
||||||
|
<h4 className="species-talent-knowledge-title knowledge">Knowledge</h4>
|
||||||
|
<p className={`species-talent-knowledge-value ${species.knowledge}`}>{species.knowledge}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-talent-charm">
|
||||||
|
<h4 className="species-talent-charm-title charm">Charm</h4>
|
||||||
|
<p className={`species-talent-charm-value ${species.charm}`}>{species.charm}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-talent-cunning">
|
||||||
|
<h4 className="species-talent-cunning-title cunning">Cunning</h4>
|
||||||
|
<p className={`species-talent-cunning-value ${species.cunning}`}>{species.cunning}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="species-talent-spark">
|
||||||
|
<h4 className="species-talent-spark-title spark">Spark</h4>
|
||||||
|
<p className={`species-talent-spark-value ${species.spark}`}>{species.spark}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<HTML className="species-content" html={species.description} />
|
||||||
|
{abilitySection}
|
||||||
|
</>
|
||||||
|
|
||||||
|
return <Section
|
||||||
|
type="species"
|
||||||
|
componentId={species.id}
|
||||||
|
title={species.name}
|
||||||
|
info={<p>Species</p>}
|
||||||
|
>{sectionContent}</Section>
|
||||||
|
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { Playbill } from "@proscenium/playbill";
|
import { Playbill, type TaggedComponent } from "@proscenium/playbill";
|
||||||
import sortBy from "lodash-es/sortBy";
|
import sortBy from "lodash-es/sortBy";
|
||||||
import { html, renderMarkdown } from "#lib";
|
import { renderMarkdown } from "#lib";
|
||||||
import { GlossaryTable } from "./component/glossary";
|
import { GlossaryTable } from "./component/glossary";
|
||||||
import { PageHeader } from "./component/header";
|
import { PageHeader } from "./component/header";
|
||||||
import { ItemCard } from "./component/item";
|
import { ItemCard } from "./component/item";
|
||||||
|
@ -14,6 +14,22 @@ interface HTMLRenderOptions {
|
||||||
styles?: string;
|
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(
|
export async function renderPlaybillToHTML(
|
||||||
playbillToRender: Playbill,
|
playbillToRender: Playbill,
|
||||||
opts: HTMLRenderOptions = {},
|
opts: HTMLRenderOptions = {},
|
||||||
|
@ -21,46 +37,40 @@ export async function renderPlaybillToHTML(
|
||||||
// Make a copy of the playbill to avoid modifying the original
|
// Make a copy of the playbill to avoid modifying the original
|
||||||
// and compile all description markdown
|
// and compile all description markdown
|
||||||
const playbill = Playbill.fromSerialized(playbillToRender.serialize());
|
const playbill = Playbill.fromSerialized(playbillToRender.serialize());
|
||||||
await playbill.processComponents(renderMarkdown);
|
await playbill.processComponents(processMarkdownInComponent);
|
||||||
|
|
||||||
// Render various resources
|
|
||||||
const resources = (
|
|
||||||
await Promise.all(Object.values(playbill.resources).map(ResourceSection))
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const methods = (
|
|
||||||
await Promise.all(
|
|
||||||
Object.values(playbill.methods).map((method) => {
|
|
||||||
return MethodSection(method, playbill);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const species = (
|
|
||||||
await Promise.all(
|
|
||||||
Object.values(playbill.species).map((species) => {
|
|
||||||
return SpeciesSection(species, playbill);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const items = (
|
|
||||||
await Promise.all(
|
|
||||||
Object.values(playbill.items).map((item) => {
|
|
||||||
return ItemCard(item);
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
).join("\n");
|
|
||||||
|
|
||||||
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
|
const rulesByOrder = sortBy(Object.values(playbill.rules), "order");
|
||||||
const rules = rulesByOrder.map(RuleSection);
|
|
||||||
const glossaryHTML = await GlossaryTable(playbill.glossary);
|
|
||||||
|
|
||||||
// Prepare stylesheet
|
// Prepare stylesheet
|
||||||
const css = opts.styles ? html`<style>${opts.styles}</style>` : "";
|
const css = opts.styles ? `<style>${opts.styles}</style>` : "";
|
||||||
|
|
||||||
|
const body = renderToString(<>
|
||||||
|
<article id="species" className="view">
|
||||||
|
{Object.values(playbill.species).map((species) => <SpeciesSection key={species.id} species={species} playbill={playbill} />)}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="methods" className="view">
|
||||||
|
{Object.values(playbill.methods).map((method) => <MethodSection key={method.id} method={method} playbill={playbill} />)}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="items" className="view">
|
||||||
|
{Object.values(playbill.items).map((item) => <ItemCard key={item.id} item={item} />)}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="rules" className="view">
|
||||||
|
{rulesByOrder.map((rule) => <RuleSection key={rule.id} rule={rule} />)}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="glossary" className="view">
|
||||||
|
{<GlossaryTable glossary={playbill.glossary} />}
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="resources" className="view">
|
||||||
|
{Object.values(playbill.resources).map((resource) => <ResourceSection key={resource.id} resource={resource} />)}
|
||||||
|
</article>
|
||||||
|
</>);
|
||||||
|
|
||||||
// Main document
|
// Main document
|
||||||
const doc = html`
|
const doc = `
|
||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
@ -126,7 +136,7 @@ export async function renderPlaybillToHTML(
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
${await PageHeader(playbill)}
|
${renderToString(<PageHeader playbill={playbill} />)}
|
||||||
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
|
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
|
||||||
<nav>
|
<nav>
|
||||||
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
|
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
|
||||||
|
@ -137,31 +147,7 @@ export async function renderPlaybillToHTML(
|
||||||
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
|
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
|
||||||
<button onclick="showAllViews()" id="all-nav">Show All</button>
|
<button onclick="showAllViews()" id="all-nav">Show All</button>
|
||||||
</nav>
|
</nav>
|
||||||
<article id="species" class="view">
|
${body}
|
||||||
${species}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article id="methods" class="view">
|
|
||||||
${methods}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article id="items" class="view">
|
|
||||||
${items}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article id="rules" class="view">
|
|
||||||
${renderToString(rules)}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article id="glossary" class="view">
|
|
||||||
${glossaryHTML}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article id="resources" class="view">
|
|
||||||
${resources}
|
|
||||||
</article>
|
|
||||||
|
|
||||||
</article>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -1,6 +1,23 @@
|
||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { CLIError, MuseError } from "#lib";
|
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>
|
||||||
|
|
||||||
|
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 Renderers = ["json", "html"] as const;
|
||||||
const RendererSchema = z.enum(Renderers);
|
const RendererSchema = z.enum(Renderers);
|
||||||
|
|
|
@ -37,6 +37,23 @@ export async function loadYAMLFileOrFail(filePath: string): Promise<unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<unknown> {
|
||||||
|
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
|
* Returns the absolute path of the directory containing the given file
|
||||||
*
|
*
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue