From 1c64a941cd68901229c931941b0dc04249aa3096 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 21 Feb 2025 19:38:15 -0500 Subject: [PATCH] Add basic support for markdown sources --- biome.json | 13 +------ package.json | 3 +- playbill/binding.yaml | 1 + playbill/spires/lore/ancients.md | 7 ++++ src/binding.ts | 2 +- src/index.ts | 24 ++++++------ src/markdown.ts | 50 ++++++++++++++++++++++++ src/playbill-schema.ts | 1 + src/resource.ts | 67 ++++++++++++++++++++------------ 9 files changed, 118 insertions(+), 50 deletions(-) create mode 100644 playbill/spires/lore/ancients.md create mode 100644 src/markdown.ts diff --git a/biome.json b/biome.json index bcfca3b..5335960 100644 --- a/biome.json +++ b/biome.json @@ -8,14 +8,8 @@ "files": { "ignoreUnknown": false, "ignore": [ - "dist", - ".next", - "public", "*.d.ts", - "*.json", - "build", - "*.svelte", - ".svelte-kit" + "*.json" ] }, "formatter": { @@ -39,10 +33,5 @@ "formatter": { "quoteStyle": "double" } - }, - "css": { - "parser": { - "cssModules": true - } } } diff --git a/package.json b/package.json index 85dcc52..5670f65 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "scripts": { "fmt": "bunx --bun biome check --fix", "build": "bun build ./src/index.ts --outfile muse --compile", - "demo": "bun run ./src/index.ts -- ./playbill/binding.yaml" + "demo": "bun run ./src/index.ts -- ./playbill/binding.yaml", + "build:install": "bun run build && mv muse ~/.local/bin/muse" } } diff --git a/playbill/binding.yaml b/playbill/binding.yaml index a7d46bf..8cd523d 100644 --- a/playbill/binding.yaml +++ b/playbill/binding.yaml @@ -5,3 +5,4 @@ author: "Endeavorance " version: 0.0.1 files: - ./spires/**/*.yaml + - ./spires/**/*.md diff --git a/playbill/spires/lore/ancients.md b/playbill/spires/lore/ancients.md new file mode 100644 index 0000000..24b6e71 --- /dev/null +++ b/playbill/spires/lore/ancients.md @@ -0,0 +1,7 @@ +--- +$define: lore +id: ancients +name: Ancients +--- + +Ancients are things and such, so, yknow. diff --git a/src/binding.ts b/src/binding.ts index a85aad4..957c37f 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -9,7 +9,7 @@ const BindingSchema = z.object({ name: z.string().default("Unnamed playbill"), author: z.string().optional(), version: z.string(), - files: z.array(z.string()).default(["**/*.yaml"]), + files: z.array(z.string()).default(["**/*.yaml", "**/*.md"]), }); type Binding = z.infer; diff --git a/src/index.ts b/src/index.ts index dfeff05..b0c17ef 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,7 +10,7 @@ import { import { loadResourceFile } from "./resource"; import { usage } from "./usage"; -class CLIError extends Error { } +class CLIError extends Error {} async function main(): Promise { // Parse command line arguments @@ -67,7 +67,7 @@ async function main(): Promise { * Log a message if the vervose flag has been set * @param msg - The message to log */ - function v(msg: string) { + function verboseLog(msg: string) { if (VERBOSE) { console.log(chalk.dim(msg)); } @@ -75,7 +75,7 @@ async function main(): Promise { const bindingPath = args[0] ?? "./binding.yaml"; - v(`Building Playbill with binding: ${bindingPath}`); + verboseLog(`Building Playbill with binding: ${bindingPath}`); // Check if the binding file exists const bindingFileCheck = Bun.file(bindingPath); @@ -87,21 +87,23 @@ async function main(): Promise { const binding = await loadBindingFile(bindingPath); - v(`↳ Binding loaded with ${binding.files.length} associated resources`); + verboseLog( + `↳ Binding loaded with ${binding.files.length} associated resources`, + ); const loadedResources: AnyResource[] = []; - v("Loading resources"); + verboseLog("Loading resources"); // Load resources listed in the binding file for (const filepath of binding.files) { - v(`↳ Loading ${filepath}`); + verboseLog(`↳ Loading ${filepath}`); const loaded = await loadResourceFile(filepath); - v(` ↳ Loaded ${loaded.length} resources`); + verboseLog(` ↳ Loaded ${loaded.length} resources`); loadedResources.push(...loaded); } - v("Constructing playbill"); + verboseLog("Constructing playbill"); // Consjtruct the playbill object const playbill = getEmptyPlaybill(); @@ -143,7 +145,7 @@ async function main(): Promise { } // Evaluate directives in descriptions for all resources - v("Processing descriptions"); + verboseLog("Processing descriptions"); for (const resource of loadedResources) { try { resource.description = await processLang(resource.description, playbill); @@ -161,7 +163,7 @@ async function main(): Promise { } // Validate the playbill object - v("Validating playbill"); + verboseLog("Validating playbill"); const validatedPlaybill = ValidatedPlaybillSchema.safeParse(playbill); if (!validatedPlaybill.success) { @@ -170,7 +172,7 @@ async function main(): Promise { return false; } - v("Playbill validated"); + verboseLog("Playbill validated"); // If --check is set, exit here if (options.check) { diff --git a/src/markdown.ts b/src/markdown.ts new file mode 100644 index 0000000..df75196 --- /dev/null +++ b/src/markdown.ts @@ -0,0 +1,50 @@ +import YAML from "yaml"; + +const FRONTMATTER_REGEX = /^---[\s\S]*?---/gm; + +/** + * Attempt to parse YAML frontmatter from a mixed yaml/md doc + * @param content The raw markdown content + * @returns Any successfully parsed frontmatter + */ +export function extractFrontmatter(content: string) { + // If it does not start with `---`, it is invalid for frontmatter + if (content.trim().indexOf("---") !== 0) { + return {}; + } + + if (FRONTMATTER_REGEX.test(content)) { + const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? ""; + const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim(); + return YAML.parse(cleanFrontmatter); + } + + return {}; +} + +export function extractMarkdown(content: string): string { + if (content.trim().indexOf("---") !== 0) { + return content; + } + 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/playbill-schema.ts b/src/playbill-schema.ts index 6219fbf..589d193 100644 --- a/src/playbill-schema.ts +++ b/src/playbill-schema.ts @@ -108,6 +108,7 @@ export const AbilitySchema = PlaybillResourceSchema.extend({ roll: RollTypeSchema.or(TalentSchema).default("none"), banes: z.array(z.string()).default([]), boons: z.array(z.string()).default([]), + effects: AbilityEffectSchema.default({}), }); export type PlaybillAbility = z.infer; diff --git a/src/resource.ts b/src/resource.ts index 8b7e6f2..a33a730 100644 --- a/src/resource.ts +++ b/src/resource.ts @@ -4,9 +4,11 @@ import { AnyResourceSchema, ResourceMap, } from "./playbill-schema"; +import { extractFrontmatter, extractMarkdown } from "./markdown"; -export class ResourceFileError extends Error { } -export class NullResourceError extends Error { } +type FileFormat = "yaml" | "markdown"; +export class ResourceFileError extends Error {} +export class NullResourceError extends Error {} /** * Load and process all documents in a yaml file at the given path @@ -20,34 +22,49 @@ export async function loadResourceFile( filePath: string, ): Promise { const file = Bun.file(filePath); + const format: FileFormat = + filePath.split(".").pop() === "md" ? "markdown" : "yaml"; const text = await file.text(); - const parsedDocs = YAML.parseAllDocuments(text); + if (format === "yaml") { + const parsedDocs = YAML.parseAllDocuments(text); - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new NullResourceError(`Null resource defined in ${filePath}`); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`); - } - - const collection: AnyResource[] = []; - - for (const doc of parsedDocs) { - if (doc.errors.length > 0) { - throw new ResourceFileError(`Error parsing ${filePath}: ${doc.errors}`); + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new NullResourceError(`Null resource defined in ${filePath}`); } - const raw = doc.toJS(); - const parsed = AnyResourceSchema.parse(raw); - const type = parsed.$define; - const schemaToUse = ResourceMap[type]; - const validated = schemaToUse.parse(parsed); - collection.push(validated); + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`); + } + + const collection: AnyResource[] = []; + + for (const doc of parsedDocs) { + if (doc.errors.length > 0) { + throw new ResourceFileError(`Error parsing ${filePath}: ${doc.errors}`); + } + + const raw = doc.toJS(); + const parsed = AnyResourceSchema.parse(raw); + const type = parsed.$define; + const schemaToUse = ResourceMap[type]; + const validated = schemaToUse.parse(parsed); + collection.push(validated); + } + + return collection; } - return collection; + const frontmatter = extractFrontmatter(text); + const markdown = extractMarkdown(text); + + const together = { + ...frontmatter, + description: markdown, + }; + + const parsed = AnyResourceSchema.parse(together); + return [parsed]; }