Improved HTML output
This commit is contained in:
parent
d8ddff01a6
commit
2032103412
16 changed files with 480 additions and 105 deletions
|
@ -10,6 +10,10 @@ import { loadYAMLFileOrFail } from "./files";
|
|||
import { loadResourceFile } from "./resource";
|
||||
import { FileNotFoundError } from "./errors";
|
||||
|
||||
const HTMLRenderOptionsSchema = z.object({
|
||||
stylesheet: z.string().optional(),
|
||||
});
|
||||
|
||||
const BindingFileSchema = z.object({
|
||||
$binding: z.literal("playbill"),
|
||||
extends: z.string().optional(),
|
||||
|
@ -19,6 +23,8 @@ const BindingFileSchema = z.object({
|
|||
description: z.string().optional(),
|
||||
version: z.string(),
|
||||
files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]),
|
||||
|
||||
html: HTMLRenderOptionsSchema.optional(),
|
||||
});
|
||||
|
||||
type BindingFileContent = z.infer<typeof BindingFileSchema>;
|
||||
|
@ -28,6 +34,7 @@ export interface PlaybillBinding {
|
|||
|
||||
// File information
|
||||
bindingFilePath: string;
|
||||
bindingFileDirname: string;
|
||||
includedFiles: string[];
|
||||
|
||||
// Binding properties
|
||||
|
@ -37,6 +44,8 @@ export interface PlaybillBinding {
|
|||
version: string;
|
||||
description: string;
|
||||
|
||||
html?: z.infer<typeof HTMLRenderOptionsSchema>;
|
||||
|
||||
playbill: Playbill;
|
||||
}
|
||||
|
||||
|
@ -126,6 +135,7 @@ export async function loadFromBinding(
|
|||
return {
|
||||
_raw: binding,
|
||||
bindingFilePath: bindingPath,
|
||||
bindingFileDirname,
|
||||
includedFiles: filePathsWithoutBindingFile,
|
||||
|
||||
id: binding.id,
|
||||
|
@ -134,6 +144,8 @@ export async function loadFromBinding(
|
|||
description: binding.description ?? "",
|
||||
version: binding.version,
|
||||
|
||||
html: binding.html,
|
||||
|
||||
playbill,
|
||||
};
|
||||
}
|
||||
|
|
34
src/index.ts
34
src/index.ts
|
@ -1,9 +1,13 @@
|
|||
import { parseArgs } from "node:util";
|
||||
import { ValidatedPlaybillSchema } from "@proscenium/playbill";
|
||||
import {
|
||||
serializePlaybill,
|
||||
ValidatedPlaybillSchema,
|
||||
} from "@proscenium/playbill";
|
||||
import chalk from "chalk";
|
||||
import { loadFromBinding, resolveBindingPath } from "./binding";
|
||||
import { usage } from "./usage";
|
||||
import { CLIError, MuseError } from "./errors";
|
||||
import { renderAsHTML } from "./renderers/html";
|
||||
|
||||
enum ExitCode {
|
||||
Success = 0,
|
||||
|
@ -45,6 +49,11 @@ async function main(): Promise<number> {
|
|||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
renderer: {
|
||||
short: "r",
|
||||
default: "json",
|
||||
type: "string",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
|
@ -103,20 +112,27 @@ async function main(): Promise<number> {
|
|||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
// -- SERIALIZE TO JSON --//
|
||||
const finalizedPlaybill = JSON.stringify(
|
||||
validatedPlaybill.data,
|
||||
null,
|
||||
options.minify ? undefined : 2,
|
||||
);
|
||||
// -- SERIALIZE USING RENDERER --//
|
||||
let serializedPlaybill = "";
|
||||
|
||||
switch (options.renderer) {
|
||||
case "json":
|
||||
serializedPlaybill = serializePlaybill(validatedPlaybill.data);
|
||||
break;
|
||||
case "html":
|
||||
serializedPlaybill = await renderAsHTML(validatedPlaybill.data, binding);
|
||||
break;
|
||||
default:
|
||||
throw new CLIError(`Unknown renderer: ${options.renderer}`);
|
||||
}
|
||||
|
||||
// -- WRITE TO DISK OR STDOUT --//
|
||||
if (options.write) {
|
||||
await Bun.write(options.outfile, finalizedPlaybill);
|
||||
await Bun.write(options.outfile, serializedPlaybill);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
console.log(finalizedPlaybill);
|
||||
console.log(serializedPlaybill);
|
||||
return ExitCode.Success;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
import rehypeStringify from "rehype-stringify";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkParse from "remark-parse";
|
||||
import remarkRehype from "remark-rehype";
|
||||
import { unified } from "unified";
|
||||
import YAML from "yaml";
|
||||
|
||||
const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm;
|
||||
|
@ -48,3 +53,14 @@ ${YAML.stringify(frontmatterToWrite)}
|
|||
${markdown.trim()}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function markdownToHtml(markdown: string): Promise<string> {
|
||||
const rendered = await unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype)
|
||||
.use(rehypeStringify)
|
||||
.process(markdown);
|
||||
|
||||
return String(rendered);
|
||||
}
|
||||
|
|
184
src/renderers/html.ts
Normal file
184
src/renderers/html.ts
Normal file
|
@ -0,0 +1,184 @@
|
|||
import path from "node:path";
|
||||
import type {
|
||||
Ability,
|
||||
Guide,
|
||||
Method,
|
||||
Rule,
|
||||
ValidatedPlaybill,
|
||||
} from "@proscenium/playbill";
|
||||
import { markdownToHtml } from "../markdown";
|
||||
import type { PlaybillBinding } from "../binding";
|
||||
import rehypeParse from "rehype-parse";
|
||||
import { unified } from "unified";
|
||||
import rehypeFormat from "rehype-format";
|
||||
import rehypeStringify from "rehype-stringify";
|
||||
import capitalize from "lodash-es/capitalize";
|
||||
import { FileNotFoundError } from "../errors";
|
||||
|
||||
async function renderGuide(guide: Guide): Promise<string> {
|
||||
const html = await markdownToHtml(guide.description);
|
||||
|
||||
return `<section class="guide flow">
|
||||
<h2>${guide.name}</h2>
|
||||
${html}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
async function renderRule(rule: Rule): Promise<string> {
|
||||
const html = await markdownToHtml(rule.description);
|
||||
|
||||
return `<section class="rule flow">
|
||||
<h2>${rule.name}</h2>
|
||||
${html}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
async function renderAbility(ability: Ability): Promise<string> {
|
||||
const html = await markdownToHtml(ability.description);
|
||||
|
||||
const costs: string[] = [];
|
||||
|
||||
if (ability.ap > 0) {
|
||||
costs.push(`<li>AP Cost: ${ability.ap}</li>`);
|
||||
}
|
||||
|
||||
if (ability.hp > 0) {
|
||||
costs.push(`<li>HP Cost: ${ability.hp}</li>`);
|
||||
}
|
||||
|
||||
if (ability.ep > 0) {
|
||||
costs.push(`<li>EP Cost: ${ability.ep}</li>`);
|
||||
}
|
||||
|
||||
const costList = costs.length > 0 ? `<ul>${costs.join("\n")}</ul>` : "";
|
||||
|
||||
return `
|
||||
<div class="ability flow">
|
||||
<h4>${ability.name}</h4>
|
||||
<small>${ability.type} / ${ability.xp} XP</small>
|
||||
${costList}
|
||||
${html}
|
||||
</div>`.trim();
|
||||
}
|
||||
|
||||
async function renderMethod(method: Method, playbill: ValidatedPlaybill) {
|
||||
const descriptionHTML = await markdownToHtml(method.description);
|
||||
let ranksHTML = "";
|
||||
let rankNumber = 1;
|
||||
|
||||
for (const rank of method.abilities) {
|
||||
ranksHTML += `<h3>Rank ${rankNumber}</h3><div class="method-rank flow">`;
|
||||
|
||||
for (const ability of rank) {
|
||||
const html = await renderAbility(playbill.ability[ability]);
|
||||
ranksHTML += html;
|
||||
}
|
||||
|
||||
ranksHTML += "</div>";
|
||||
|
||||
rankNumber++;
|
||||
}
|
||||
|
||||
return `<section class="method flow">
|
||||
<h2>${method.curator}'s Method of ${method.name}</h2>
|
||||
${descriptionHTML}
|
||||
${ranksHTML}
|
||||
</section>`;
|
||||
}
|
||||
|
||||
async function renderItemTags(playbill: ValidatedPlaybill) {
|
||||
let tagsTableHTML = `
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tag</th>
|
||||
<th>Item Types</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
`.trim();
|
||||
|
||||
for (const tag of Object.values(playbill.tag)) {
|
||||
tagsTableHTML += `
|
||||
<tr>
|
||||
<td>${tag.name}</td>
|
||||
<td>${tag.types.map(capitalize).join(", ")}</td>
|
||||
<td>${await markdownToHtml(tag.description)}</td>
|
||||
</tr>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
tagsTableHTML += "</tbody></table>";
|
||||
|
||||
return `<section class="tags flow"><h2>Item Tags</h2>${tagsTableHTML}</section>`;
|
||||
}
|
||||
|
||||
async function renderDocument(
|
||||
playbill: ValidatedPlaybill,
|
||||
binding: PlaybillBinding,
|
||||
): Promise<string> {
|
||||
const guides = await Promise.all(
|
||||
Object.values(playbill.guide).map(renderGuide),
|
||||
);
|
||||
|
||||
const methods = await Promise.all(
|
||||
Object.values(playbill.method).map((method) => {
|
||||
return renderMethod(method, playbill);
|
||||
}),
|
||||
);
|
||||
|
||||
const rules = await Promise.all(Object.values(playbill.rule).map(renderRule));
|
||||
|
||||
let css = "";
|
||||
|
||||
if (binding.html?.stylesheet) {
|
||||
const cssFilePath = path.resolve(
|
||||
binding.bindingFileDirname,
|
||||
binding.html.stylesheet,
|
||||
);
|
||||
const cssFile = Bun.file(cssFilePath);
|
||||
const cssFileExists = await cssFile.exists();
|
||||
|
||||
if (cssFileExists) {
|
||||
css = `<style>
|
||||
${await cssFile.text()}
|
||||
</style>`;
|
||||
} else {
|
||||
throw new FileNotFoundError(cssFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
return `
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
${css}
|
||||
</head>
|
||||
<body>
|
||||
<article>
|
||||
<h1>${playbill.name}</h1>
|
||||
<small>By ${playbill.author}</small>
|
||||
<blockquote>${playbill.description}</blockquote>
|
||||
${guides.join("\n")}
|
||||
${methods.join("\n")}
|
||||
${await renderItemTags(playbill)}
|
||||
${rules.join("\n")}
|
||||
</article>
|
||||
</body>
|
||||
</html>
|
||||
`.trim();
|
||||
}
|
||||
|
||||
export async function renderAsHTML(
|
||||
playbill: ValidatedPlaybill,
|
||||
binding: PlaybillBinding,
|
||||
): Promise<string> {
|
||||
const doc = await renderDocument(playbill, binding);
|
||||
const formatted = await unified()
|
||||
.use(rehypeParse)
|
||||
.use(rehypeFormat)
|
||||
.use(rehypeStringify)
|
||||
.process(doc);
|
||||
return String(formatted);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue