Add basic support for markdown sources

This commit is contained in:
Endeavorance 2025-02-21 19:38:15 -05:00
parent 1848d3cfb6
commit 1c64a941cd
9 changed files with 118 additions and 50 deletions

View file

@ -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
}
}
}

View file

@ -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"
}
}

View file

@ -5,3 +5,4 @@ author: "Endeavorance <hello@endeavorance.camp>"
version: 0.0.1
files:
- ./spires/**/*.yaml
- ./spires/**/*.md

View file

@ -0,0 +1,7 @@
---
$define: lore
id: ancients
name: Ancients
---
Ancients are things and such, so, yknow.

View file

@ -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<typeof BindingSchema>;

View file

@ -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<boolean> {
// Parse command line arguments
@ -67,7 +67,7 @@ async function main(): Promise<boolean> {
* 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<boolean> {
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<boolean> {
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<boolean> {
}
// 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<boolean> {
}
// 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<boolean> {
return false;
}
v("Playbill validated");
verboseLog("Playbill validated");
// If --check is set, exit here
if (options.check) {

50
src/markdown.ts Normal file
View file

@ -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()}
`;
}

View file

@ -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<typeof AbilitySchema>;

View file

@ -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<AnyResource[]> {
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];
}