From c1166680a8639dff92db8899f7904964db4b0180 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Tue, 1 Apr 2025 15:43:48 -0400 Subject: [PATCH 01/14] Generic muse --- .gitignore | 1 + bun.lock | 3 + package.json | 1 + src/args.ts | 56 +++ src/binding.ts | 164 +++++++ src/css.d.ts | 2 - src/define/index.ts | 326 ------------- src/{lib => }/errors.ts | 0 src/index.ts | 95 +--- src/lib/binding.ts | 177 -------- src/lib/component-file.ts | 177 -------- src/lib/index.ts | 4 - src/lib/pfm.ts | 79 ---- src/lib/slug.ts | 5 - src/muse-file.ts | 168 +++++++ src/render/html/component/ability.tsx | 67 --- src/render/html/component/base/html.tsx | 10 - src/render/html/component/base/section.tsx | 41 -- src/render/html/component/glossary.tsx | 26 -- src/render/html/component/header.tsx | 18 - src/render/html/component/item.tsx | 55 --- src/render/html/component/method.tsx | 49 -- src/render/html/component/resource.tsx | 115 ----- src/render/html/component/rule.tsx | 15 - src/render/html/component/species.tsx | 120 ----- src/render/html/default.css | 505 --------------------- src/render/html/index.tsx | 160 ------- src/util/args.ts | 111 ----- src/util/files.ts | 65 --- src/util/index.ts | 2 - tsconfig.json | 16 +- 31 files changed, 412 insertions(+), 2221 deletions(-) create mode 100644 src/args.ts create mode 100644 src/binding.ts delete mode 100644 src/css.d.ts delete mode 100644 src/define/index.ts rename src/{lib => }/errors.ts (100%) delete mode 100644 src/lib/binding.ts delete mode 100644 src/lib/component-file.ts delete mode 100644 src/lib/index.ts delete mode 100644 src/lib/pfm.ts delete mode 100644 src/lib/slug.ts create mode 100644 src/muse-file.ts delete mode 100644 src/render/html/component/ability.tsx delete mode 100644 src/render/html/component/base/html.tsx delete mode 100644 src/render/html/component/base/section.tsx delete mode 100644 src/render/html/component/glossary.tsx delete mode 100644 src/render/html/component/header.tsx delete mode 100644 src/render/html/component/item.tsx delete mode 100644 src/render/html/component/method.tsx delete mode 100644 src/render/html/component/resource.tsx delete mode 100644 src/render/html/component/rule.tsx delete mode 100644 src/render/html/component/species.tsx delete mode 100644 src/render/html/default.css delete mode 100644 src/render/html/index.tsx delete mode 100644 src/util/args.ts delete mode 100644 src/util/files.ts delete mode 100644 src/util/index.ts diff --git a/.gitignore b/.gitignore index 4cda367..a5f06e1 100644 --- a/.gitignore +++ b/.gitignore @@ -176,3 +176,4 @@ dist demo-data demo.html +demo diff --git a/bun.lock b/bun.lock index b85882f..e428c07 100644 --- a/bun.lock +++ b/bun.lock @@ -20,6 +20,7 @@ "remark-rehype": "^11.1.1", "remark-wiki-link": "^2.0.1", "slugify": "^1.6.6", + "smol-toml": "^1.3.1", "unified": "^11.0.5", "yaml": "^2.7.0", "zod": "^3.24.1", @@ -283,6 +284,8 @@ "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], + "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], + "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], diff --git a/package.json b/package.json index a1bf80f..4ba9691 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "remark-rehype": "^11.1.1", "remark-wiki-link": "^2.0.1", "slugify": "^1.6.6", + "smol-toml": "^1.3.1", "unified": "^11.0.5", "yaml": "^2.7.0", "zod": "^3.24.1" diff --git a/src/args.ts b/src/args.ts new file mode 100644 index 0000000..9e9f8d1 --- /dev/null +++ b/src/args.ts @@ -0,0 +1,56 @@ +import { parseArgs } from "node:util"; +import chalk from "chalk"; +import { version } from "../package.json" with { type: "json" }; + +export const USAGE = ` +${chalk.bold("muse")} - Compile and process troves of data +${chalk.dim(`v${version}`)} + +Usage: + muse [/path/to/binding.yaml] + +Options: + --help, -h Show this help message +`.trim(); + +/** + * A shape representing the arguments passed to the CLI + */ +export interface CLIArguments { + inputFilePath: string; + + options: { + help: boolean; + }; +} + +/** + * Given an array of CLI arguments, parse them into a structured object + * + * @param argv The arguments to parse + * @returns The parsed CLI arguments + * + * @throws {CLIError} if the arguments are invalid + */ +export function parseCLIArguments(argv: string[]): CLIArguments { + const { values: options, positionals: args } = parseArgs({ + args: argv, + options: { + help: { + short: "h", + default: false, + type: "boolean", + }, + }, + strict: true, + allowPositionals: true, + }); + + return { + inputFilePath: args[0] ?? "./binding.yaml", + + options: { + help: options.help, + }, + }; +} diff --git a/src/binding.ts b/src/binding.ts new file mode 100644 index 0000000..6803251 --- /dev/null +++ b/src/binding.ts @@ -0,0 +1,164 @@ +import path from "node:path"; +import { Glob } from "bun"; +import z from "zod"; +import { FileNotFoundError, MuseError } from "./errors"; +import { MuseFile, type MuseFileContent } from "./muse-file"; + +// binding.yaml file schema +const BindingSchema = z.object({ + include: z.array(z.string()).default([]), + + name: z.string().default("Unnamed playbill"), + 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", "**/*.toml", "**/*.json"]), + + options: z.record(z.string(), z.any()).default({}), + processors: z.array(z.string()).default([]), +}); + +type ParsedBinding = z.infer; + +/** + * Given a path, find the binding.yaml file + * If the path is to a directory, check for a binding.yaml inside + * If the path is to a file, check if it exists and is a binding.yaml + * Otherwise throw an error + * + * @param bindingPath - The path to the binding file + * @returns The absolute resolved path to the binding file + */ +export async function resolveBindingPath(bindingPath: string): Promise { + // If the path does not specify a filename, use binding.yaml + const inputLocation = Bun.file(bindingPath); + + // If it is a directory, try again seeking a binding.yaml inside + const stat = await inputLocation.stat(); + if (stat.isDirectory()) { + return resolveBindingPath(path.resolve(bindingPath, "binding.yaml")); + } + + // If it doesnt exist, bail + if (!(await inputLocation.exists())) { + throw new FileNotFoundError(bindingPath); + } + + // Resolve the path + return path.resolve(bindingPath); +} + +export interface BindingMetadata { + name: string; + author: string; + description: string; + version: string; +} + +export interface MuseProcessor { + readonly filePath: string; + process(binding: Binding): Promise; +} + +export interface Binding { + readonly _raw: ParsedBinding; + readonly bindingPath: string; + readonly includedFiles: string[]; + readonly entries: MuseFileContent[]; + readonly metadata: BindingMetadata; + readonly options: Record; + readonly processors: MuseProcessor[]; + readonly imports: Binding[]; +} + +export async function loadBinding(initialPath: string): Promise { + // Resolve the file location if possible + const bindingPath = await resolveBindingPath(initialPath); + const bindingFile = new MuseFile(bindingPath); + + // Load and parse the Binding YAML content + const bindingFileContent = await bindingFile.read(); + const parsedBinding = BindingSchema.parse(bindingFileContent.data); + + // Prepare collections to load into + const extendsFrom: Binding[] = []; + const entries: MuseFileContent[] = []; + + // Process imported bindings and bring in their entries + for (const includePath of parsedBinding.include) { + const pathFromHere = path.resolve(bindingFile.dirname, includePath); + const extendBindingPath = await resolveBindingPath(pathFromHere); + const loadedBinding = await loadBinding(extendBindingPath); + extendsFrom.push(loadedBinding); + entries.push(...loadedBinding.entries); + } + + // Collect file manifest from globs + const allFilePaths: string[] = []; + for (const thisGlob of parsedBinding.files) { + const glob = new Glob(thisGlob); + + const results = glob.scanSync({ + cwd: bindingFile.dirname, + absolute: true, + followSymlinks: true, + onlyFiles: true, + }); + + allFilePaths.push(...results); + } + + // Exclude the binding.yaml file + const includedFilePaths = allFilePaths.filter((filePath) => { + return filePath !== bindingFile.path; + }); + + const fileLoadPromises: Promise[] = []; + for (const filePath of includedFilePaths) { + const file = new MuseFile(filePath); + fileLoadPromises.push(file.read()); + } + + const mainEntries = await Promise.all(fileLoadPromises); + entries.push(...mainEntries); + + // Load and check processors + const processors: MuseProcessor[] = []; + for (const processorPath of parsedBinding.processors) { + const resolvedProcessorPath = path.resolve( + bindingFile.dirname, + processorPath, + ); + const { process } = await import(resolvedProcessorPath); + + if (!process || typeof process !== "function") { + throw new MuseError( + `Processor at ${processorPath} does not export a process() function`, + ); + } + + processors.push({ + filePath: resolvedProcessorPath, + process: process as MuseProcessor["process"], + }); + } + + return { + _raw: parsedBinding, + bindingPath, + includedFiles: includedFilePaths, + entries, + options: parsedBinding.options, + processors: processors, + imports: extendsFrom, + metadata: { + name: parsedBinding.name, + author: parsedBinding.author, + description: parsedBinding.description, + version: parsedBinding.version, + }, + }; +} diff --git a/src/css.d.ts b/src/css.d.ts deleted file mode 100644 index cb02ac0..0000000 --- a/src/css.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -declare module '*.css' { -} diff --git a/src/define/index.ts b/src/define/index.ts deleted file mode 100644 index 68b0aa6..0000000 --- a/src/define/index.ts +++ /dev/null @@ -1,326 +0,0 @@ -import { - type Ability, - type AnyPlaybillComponent, - type Blueprint, - type Item, - type Method, - type Resource, - type Rule, - type Species, - parseAbility, - parseBlueprint, - parseItem, - parseMethod, - parseResource, - parseRule, - parseSpecies, -} from "@proscenium/playbill"; -import { z } from "zod"; - -const Base = z.object({ - $define: z.string(), - $hidden: z.boolean().default(false), - id: z.string(), // TODO: Validate ID shapes - name: z.string().default("Unnamed Component"), - description: z.string().default("No description provided"), - categories: z.array(z.string()).default([]), -}); - -const AbilitySchema = Base.extend({ - $define: z.literal("ability"), - type: z.string().default("action"), - ap: z.number().int().default(0), - hp: z.number().int().default(0), - ep: 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"), -}); - -const BlueprintSchema = Base.extend({ - $define: z.literal("blueprint"), - species: z.string(), - items: z.array(z.string()).default([]), - abilities: z.array(z.string()).default([]), - ap: z.number().int().default(4), - hp: z.number().int().default(5), - ep: z.number().int().default(1), - xp: z.number().int().default(0), - muscle: z.string().default("novice"), - focus: z.string().default("novice"), - knowledge: z.string().default("novice"), - charm: z.string().default("novice"), - cunning: z.string().default("novice"), - spark: z.string().default("novice"), -}); - -const GlossarySchema = Base.extend({ - $define: z.literal("glossary"), - terms: z.record(z.string(), z.string()), -}); - -const BaseItem = Base.extend({ - $define: z.literal("item"), - rarity: z.string().default("common"), - 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", [ - BaseItem.extend({ - type: z.literal("wielded"), - hands: z.number().int().default(1), - }), - BaseItem.extend({ - type: z.literal("worn"), - slot: z.string().default("unique"), - }), - BaseItem.extend({ - type: z.literal("trinket"), - }), -]); - -const MethodSchema = Base.extend({ - $define: z.literal("method"), - curator: z.string().default("Someone"), - abilities: z.array(z.array(z.string())).default([]), - rank1: z.array(z.string()).default([]), - rank2: z.array(z.string()).default([]), - rank3: z.array(z.string()).default([]), - rank4: z.array(z.string()).default([]), - rank5: z.array(z.string()).default([]), - rank6: z.array(z.string()).default([]), - rank7: z.array(z.string()).default([]), - rank8: z.array(z.string()).default([]), - rank9: z.array(z.string()).default([]), -}); - -const ResourceSchema = z.discriminatedUnion("type", [ - Base.extend({ - $define: z.literal("resource"), - type: z.literal("text"), - }), - Base.extend({ - $define: z.literal("resource"), - type: z.literal("image"), - url: z.string(), - }), - Base.extend({ - $define: z.literal("resource"), - type: z.literal("table"), - data: z.array(z.array(z.string())), - }), -]); - -const RuleSchema = Base.extend({ - $define: z.literal("rule"), - overrule: z.nullable(z.string()).default(null), - order: z.number().int().default(Number.POSITIVE_INFINITY), -}); - -const SpeciesSchema = Base.extend({ - $define: z.literal("species"), - hands: z.number().int().default(2), - abilities: z.array(z.string()).default([]), - ap: z.number().int().default(0), - hp: z.number().int().default(0), - ep: z.number().int().default(0), - xp: z.number().int().default(0), - muscle: z.string().default("novice"), - focus: z.string().default("novice"), - knowledge: z.string().default("novice"), - charm: z.string().default("novice"), - cunning: z.string().default("novice"), - spark: z.string().default("novice"), -}); - -const SchemaMapping = { - ability: AbilitySchema, - blueprint: BlueprintSchema, - glossary: GlossarySchema, - item: ItemSchema, - method: MethodSchema, - resource: ResourceSchema, - rule: RuleSchema, - species: SpeciesSchema, -} as const; - -type ComponentType = keyof typeof SchemaMapping; - -const parseComponentType = (val: unknown): ComponentType => { - if (typeof val !== "string") { - throw new Error("Component type must be a string"); - } - - if (!(val in SchemaMapping)) { - throw new Error(`Unknown component type: ${val}`); - } - - return val as ComponentType; -}; - -function parseAbilityDefinition(obj: unknown): Ability { - const parsed = AbilitySchema.parse(obj); - - const ability = parseAbility({ - id: parsed.id, - name: parsed.name, - description: parsed.description, - categories: parsed.categories, - type: parsed.type, - ap: parsed.ap, - ep: parsed.ep, - hp: parsed.hp, - xp: parsed.xp, - damage: - parsed.damage > 0 - ? { - amount: parsed.damage, - type: parsed.damageType, - } - : null, - roll: parsed.roll, - }); - - return ability; -} - -function parseBlueprintDefinition(obj: unknown): Blueprint { - const parsed = BlueprintSchema.parse(obj); - - const blueprint = parseBlueprint({ - id: parsed.id, - name: parsed.name, - description: parsed.description, - categories: parsed.categories, - species: parsed.species, - items: parsed.items, - abilities: parsed.abilities, - baggage: [], - ap: parsed.ap, - hp: parsed.hp, - ep: parsed.ep, - xp: parsed.xp, - muscle: parsed.muscle, - focus: parsed.focus, - knowledge: parsed.knowledge, - charm: parsed.charm, - cunning: parsed.cunning, - spark: parsed.spark, - }); - - return blueprint; -} - -function parseItemDefinition(obj: unknown): Item { - const parsed = ItemSchema.parse(obj); - - return parseItem({ - ...parsed, - damage: - parsed.damage > 0 - ? { - amount: parsed.damage, - type: parsed.damageType, - } - : null, - abilities: parsed.abilities, - tweak: "", - temper: "", - }); -} - -function parseMethodDefinition(obj: unknown): Method { - const parsed = MethodSchema.parse(obj); - - // Prefer `abilities` if defined, otherwise - // construct `abilities` using the rankX properties - const abilities = parsed.abilities; - - if (abilities.length === 0) { - const allRanks = [ - parsed.rank1, - parsed.rank2, - parsed.rank3, - parsed.rank4, - parsed.rank5, - parsed.rank6, - parsed.rank7, - parsed.rank8, - parsed.rank9, - ]; - - // Add all ranks until an empty rank is found - for (const rank of allRanks) { - if (rank.length > 0) { - abilities.push(rank); - } else { - break; - } - } - } - - const method = { - id: parsed.id, - name: parsed.name, - description: parsed.description, - categories: parsed.categories, - curator: parsed.curator, - abilities: abilities, - }; - - return parseMethod(method); -} - -function parseResourceDefinition(obj: unknown): Resource { - const parsed = ResourceSchema.parse(obj); - return parseResource(parsed); -} - -function parseRuleDefinition(obj: unknown): Rule { - const parsed = RuleSchema.parse(obj); - - return parseRule(parsed); -} - -function parseSpeciesDefinition(obj: unknown): Species { - const parsed = SpeciesSchema.parse(obj); - - return parseSpecies(parsed); -} - -export function parsePlaybillComponent( - obj: unknown, -): AnyPlaybillComponent | null { - const baseParse = Base.parse(obj); - - if (baseParse.$hidden) { - return null; - } - - const type = parseComponentType(baseParse.$define); - const schema = SchemaMapping[type]; - const component = schema.parse(obj); - - switch (type) { - case "ability": - return parseAbilityDefinition(component); - case "blueprint": - return parseBlueprintDefinition(component); - case "item": - return parseItemDefinition(component); - case "method": - return parseMethodDefinition(component); - case "resource": - return parseResourceDefinition(component); - case "rule": - return parseRuleDefinition(component); - case "species": - return parseSpeciesDefinition(component); - default: - throw new Error(`Unknown component type: ${type}`); - } -} diff --git a/src/lib/errors.ts b/src/errors.ts similarity index 100% rename from src/lib/errors.ts rename to src/errors.ts diff --git a/src/index.ts b/src/index.ts index 23d166c..32e0823 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,62 +1,29 @@ -import { watch } from "node:fs/promises"; import chalk from "chalk"; -import { - CLIError, - MuseError, - compileMarkdownInPlaybill, - loadFromBinding, - resolveBindingPath, -} from "#lib"; -import { renderPlaybillToHTML } from "#render/html"; -import { - type CLIArguments, - getAbsoluteDirname, - parseCLIArguments, - USAGE, -} from "#util"; +import { MuseError } from "./errors"; +import { loadBinding, type Binding } from "./binding"; +import { type CLIArguments, parseCLIArguments, USAGE } from "./args"; enum ExitCode { Success = 0, Error = 1, } -async function processBinding({ inputFilePath, options }: CLIArguments) { +async function processBinding({ inputFilePath }: CLIArguments) { // Load the binding - const bindingPath = await resolveBindingPath(inputFilePath); - const binding = await loadFromBinding(bindingPath); + const binding = await loadBinding(inputFilePath); - // If --check is specified, exit early - if (options.check) { - console.log(chalk.green("Playbill validated successfully")); - return ExitCode.Success; + // Run the data through all processors + const processedSteps: Binding[] = [binding]; + for (const processor of binding.processors) { + const processedStep = await processor.process(binding); + processedSteps.push(processedStep); } - if (options.markdown) { - await compileMarkdownInPlaybill(binding); - } + const finalState = processedSteps[processedSteps.length - 1]; + const serialized = JSON.stringify(finalState.entries, null, 2); - // Serialize (default: JSON) - let serializedPlaybill = ""; - - switch (options.renderer) { - case "json": - serializedPlaybill = binding.playbill.serialize(); - break; - case "html": - serializedPlaybill = await renderPlaybillToHTML(binding); - break; - default: - throw new CLIError(`Unknown renderer: ${options.renderer}`); - } - - // Write to disk if --outfile is specified - if (options.outfile !== "") { - await Bun.write(options.outfile, serializedPlaybill); - return ExitCode.Success; - } - - // Otherwise, write to stdout - console.log(serializedPlaybill); + // Otherwise + console.log(serialized); return ExitCode.Success; } @@ -70,39 +37,9 @@ async function main(): Promise { return ExitCode.Success; } - let lastProcessResult = await processBinding(cliArguments); + const lastProcessResult = await processBinding(cliArguments); - if (options.watch) { - const watchDir = getAbsoluteDirname(cliArguments.inputFilePath); - - console.log(`Watching ${watchDir} for changes...`); - - const watcher = watch(watchDir, { - recursive: true, - }); - - for await (const event of watcher) { - console.log( - `Detected ${event.eventType} on ${event.filename}. Reprocessing...`, - ); - - try { - lastProcessResult = await processBinding(cliArguments); - - if (lastProcessResult === ExitCode.Error) { - console.error(`Error processing ${event.filename}`); - } else { - console.log("Reprocessed changes"); - } - } catch (error) { - console.error(`Error processing ${event.filename}`); - console.error(error); - } - } - return ExitCode.Success; - } - - return ExitCode.Success; + return lastProcessResult; } try { diff --git a/src/lib/binding.ts b/src/lib/binding.ts deleted file mode 100644 index 9e75fc8..0000000 --- a/src/lib/binding.ts +++ /dev/null @@ -1,177 +0,0 @@ -import path from "node:path"; -import { type AnyPlaybillComponent, Playbill } from "@proscenium/playbill"; -import { Glob } from "bun"; -import z from "zod"; -import { loadYAMLFileOrFail } from "#util"; -import { ComponentFile } from "./component-file"; -import { FileNotFoundError } from "./errors"; - -// binding.yaml file schema -const BindingSchema = z.object({ - include: z.array(z.string()).default([]), - - name: z.string().default("Unnamed playbill"), - 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"]), - omitDefaultStyles: z.boolean().default(false), - 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({}), -}); - -export interface BoundPlaybill { - // File information - bindingFilePath: string; - bindingFileDirname: string; - files: string[]; - componentFiles: ComponentFile[]; - - // Binding properties - name: string; - author: string; - version: string; - description: string; - - omitDefaultStyles: boolean; - styles: string; - - playbill: Playbill; -} - -/** - * Given a path, find the binding.yaml file - * If the path is to a directory, check for a binding.yaml inside - * If the path is to a file, check if it exists and is a binding.yaml - * Otherwise throw an error - * - * @param bindingPath - The path to the binding file - * @returns The absolute resolved path to the binding file - */ -export async function resolveBindingPath(bindingPath: string): Promise { - // If the path does not specify a filename, use binding.yaml - const inputLocation = Bun.file(bindingPath); - - // If it is a directory, try again seeking a binding.yaml inside - const stat = await inputLocation.stat(); - if (stat.isDirectory()) { - return resolveBindingPath(path.resolve(bindingPath, "binding.yaml")); - } - - // If it doesnt exist, bail - if (!(await inputLocation.exists())) { - throw new FileNotFoundError(bindingPath); - } - - // Resolve the path - return path.resolve(bindingPath); -} - -export async function loadFromBinding( - bindingPath: string, -): Promise { - const resolvedBindingPath = await resolveBindingPath(bindingPath); - const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath); - const binding = BindingSchema.parse(yamlContent); - const fileGlobs = binding.files; - const bindingFileDirname = path.dirname(resolvedBindingPath); - - const playbill = new Playbill( - binding.name, - binding.author, - binding.description, - binding.version, - ); - - // If this is extending another binding, load that first - for (const includePath of binding.include) { - const pathFromHere = path.resolve(bindingFileDirname, includePath); - const extendBindingPath = await resolveBindingPath(pathFromHere); - const loadedBinding = await loadFromBinding(extendBindingPath); - playbill.extend(loadedBinding.playbill); - } - - // Load any specified stylesheets - 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) { - 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[] = []; - - for (const thisGlob of fileGlobs) { - const glob = new Glob(thisGlob); - - const results = glob.scanSync({ - cwd: bindingFileDirname, - absolute: true, - followSymlinks: true, - onlyFiles: true, - }); - - allFilePaths.push(...results); - } - - // Exclude the binding.yaml file - const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => { - return filePath !== resolvedBindingPath; - }); - - // Load discovered component files - const componentFiles: ComponentFile[] = []; - const fileLoadPromises: Promise[] = []; - - for (const filepath of filePathsWithoutBindingFile) { - const componentFile = new ComponentFile(filepath); - componentFiles.push(componentFile); - fileLoadPromises.push(componentFile.load()); - } - - await Promise.all(fileLoadPromises); - - // Aggregate all loaded components - const loadedComponents: AnyPlaybillComponent[] = []; - for (const file of componentFiles) { - loadedComponents.push(...file.components); - } - - // Add all components to the playbill - // (This method ensures proper addition order maintain correctness) - playbill.addManyComponents(loadedComponents); - - // Add all definitions - for (const [term, definition] of Object.entries(binding.terms)) { - playbill.addDefinition(term, definition); - } - - return { - bindingFilePath: bindingPath, - bindingFileDirname, - files: filePathsWithoutBindingFile, - componentFiles, - - name: binding.name, - author: binding.author ?? "Anonymous", - description: binding.description ?? "", - version: binding.version, - - omitDefaultStyles: binding.omitDefaultStyles, - styles, - - playbill, - }; -} diff --git a/src/lib/component-file.ts b/src/lib/component-file.ts deleted file mode 100644 index 0dd6ea4..0000000 --- a/src/lib/component-file.ts +++ /dev/null @@ -1,177 +0,0 @@ -import path from "node:path"; -import { parsePlaybillComponent } from "define"; -import YAML, { YAMLParseError } from "yaml"; -import { ZodError } from "zod"; -import { loadFileOrFail } from "#util"; -import { MalformedResourceFileError } from "./errors"; -import { toSlug } from "./slug"; -import type { AnyPlaybillComponent } from "@proscenium/playbill"; - -type FileFormat = "yaml" | "markdown"; - -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 - */ -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 {}; -} - -/** - * Given a string of a markdown document, extract the markdown content - * @param content The raw markdown content - * @returns The markdown content without frontmatter - */ -function extractMarkdown(content: string): string { - if (content.trim().indexOf("---") !== 0) { - return content; - } - return content.replace(FRONTMATTER_REGEX, "").trim(); -} - -function parseYAMLResourceFile( - filePath: string, - text: string, -): AnyPlaybillComponent[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new MalformedResourceFileError("Encountered NULL resource", filePath); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new MalformedResourceFileError( - "Error parsing YAML resource", - filePath, - errors.map((e) => e.message).join(", "), - ); - } - - const collection: AnyPlaybillComponent[] = []; - - for (const doc of parsedDocs) { - const raw = doc.toJS(); - const parsedComponent = parsePlaybillComponent(raw); - if (parsedComponent !== null) { - collection.push(parsedComponent); - } - } - - return collection; -} - -function parseMarkdownResourceFile( - filePath: string, - text: string, -): AnyPlaybillComponent[] { - try { - const defaultName = path.basename(filePath, ".md"); - const defaultId = toSlug(defaultName); - const frontmatter = extractFrontmatter(text); - const markdown = extractMarkdown(text); - - const together = parsePlaybillComponent({ - // Use the file name, allow it to be overridden by frontmatter - name: defaultName, - id: defaultId, - ...frontmatter, - description: markdown, - }); - - // Null means hidden - if (together === null) { - return []; - } - - return [together]; - } catch (e) { - if (e instanceof YAMLParseError) { - throw new MalformedResourceFileError( - "Error parsing Markdown frontmatter", - filePath, - e.message, - ); - } - - if (e instanceof ZodError) { - throw new MalformedResourceFileError( - "Error parsing resource file", - filePath, - e.message, - ); - } - - throw e; - } -} - -export class ComponentFile { - private _filePath: string; - private _raw = ""; - private _format: FileFormat; - private _components: AnyPlaybillComponent[] = []; - private _basename: string; - - constructor(filePath: string) { - this._filePath = path.resolve(filePath); - - const extension = path.extname(filePath).slice(1).toLowerCase(); - this._basename = path.basename(filePath, `.${extension}`); - - switch (extension) { - case "yaml": - this._format = "yaml"; - break; - case "md": - this._format = "markdown"; - break; - default: - throw new Error(`Unsupported file format: ${extension}`); - } - } - - async load() { - this._raw = await loadFileOrFail(this._filePath); - switch (this._format) { - case "yaml": - this._components = parseYAMLResourceFile(this._filePath, this._raw); - break; - case "markdown": - this._components = parseMarkdownResourceFile(this._filePath, this._raw); - } - } - - /** - * An array of well-formed components defined in this file - */ - get components() { - return this._components; - } - - /** - * The aboslute file path of this file - */ - get path() { - return this._filePath; - } - - get baseName() { - return this._basename; - } -} diff --git a/src/lib/index.ts b/src/lib/index.ts deleted file mode 100644 index e5a4496..0000000 --- a/src/lib/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./errors"; -export * from "./pfm"; -export * from "./component-file"; -export * from "./binding"; diff --git a/src/lib/pfm.ts b/src/lib/pfm.ts deleted file mode 100644 index 9584462..0000000 --- a/src/lib/pfm.ts +++ /dev/null @@ -1,79 +0,0 @@ -import rehypeStringify from "rehype-stringify"; -import remarkParse from "remark-parse"; -import remarkRehype from "remark-rehype"; -import { unified } from "unified"; -import type { BoundPlaybill } from "./binding"; -import remarkWikiLink from "remark-wiki-link"; -import { toSlug } from "./slug"; -import rehypeRaw from "rehype-raw"; -import remarkGfm from "remark-gfm"; -import rehypeShiftHeading from "rehype-shift-heading"; -import { - parseProsceniumScript, - type AnyPlaybillComponent, -} from "@proscenium/playbill"; - -export type MarkdownParserFunction = (input: string) => Promise; - -export function createMarkdownRenderer( - binding: BoundPlaybill, -): MarkdownParserFunction { - // TODO: Allow specifying links to other files via file paths - // In this scenario, assume its a markdown-defined file and find the ID, use that for the link - // to allow for more natural Obsidian-like linking - // For now, this will mostly work, but if you need to specify a unique ID, it wont work in Obsidian - // despite being valid for Muse - - const parser = unified() - .use(remarkParse) - .use(remarkGfm) - .use(remarkWikiLink, { - aliasDivider: "|", - permalinks: binding.playbill.allComponentIds, - pageResolver: (permalink: string) => { - return [toSlug(permalink)]; - }, - hrefTemplate: (permalink: string) => `#component/${permalink}`, - }) - .use(remarkRehype, { allowDangerousHtml: true }) - .use(rehypeRaw) - .use(rehypeShiftHeading, { shift: 1 }) - .use(rehypeStringify); - - // TODO: Add sanitization - - return async (input: string): Promise => { - const parsed = await parser.process(input); - return String(parsed); - }; -} - -export async function compileMarkdownInPlaybill( - boundPlaybill: BoundPlaybill, -): Promise { - const renderMarkdown = createMarkdownRenderer(boundPlaybill); - const playbill = boundPlaybill.playbill; - - // Define a processor function to iterate over all components and process their markdown - const processMarkdownInComponent = async (entry: AnyPlaybillComponent) => { - const preparsed = parseProsceniumScript(entry.description, playbill); - entry.description = await renderMarkdown(preparsed); - - if (entry._component === "resource" && entry.type === "table") { - const newData: string[][] = []; - for (const row of entry.data) { - const newRow: string[] = []; - for (const cell of row) { - newRow.push(await renderMarkdown(cell)); - } - newData.push(newRow); - } - entry.data = newData; - } - }; - - // Process all components - await playbill.processComponents(processMarkdownInComponent); - - return boundPlaybill; -} diff --git a/src/lib/slug.ts b/src/lib/slug.ts deleted file mode 100644 index b6a0de4..0000000 --- a/src/lib/slug.ts +++ /dev/null @@ -1,5 +0,0 @@ -import slugify from "slugify"; - -export function toSlug(str: string): string { - return slugify(str, { lower: true }); -} diff --git a/src/muse-file.ts b/src/muse-file.ts new file mode 100644 index 0000000..f8fd51e --- /dev/null +++ b/src/muse-file.ts @@ -0,0 +1,168 @@ +import type { BunFile } from "bun"; +import { normalize, basename, extname, dirname } from "node:path"; +import YAML from "yaml"; +import TOML from "smol-toml"; +import { FileError } from "./errors"; + +export type MuseFileType = "md" | "yaml" | "json" | "toml"; +export interface MuseFileContent { + type: MuseFileType; + text: string; + data: Record; +} + +function extensionToFiletype(ext: string): MuseFileType { + switch (ext) { + case "md": + return "md"; + case "yaml": + return "yaml"; + case "json": + return "json"; + case "toml": + return "toml"; + default: + throw new Error(`Unsupported file format: ${ext}`); + } +} + +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 + */ +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 {}; +} + +/** + * Given a string of a markdown document, extract the markdown content + * @param content The raw markdown content + * @returns The markdown content without frontmatter + */ +function extractMarkdown(content: string): string { + if (content.trim().indexOf("---") !== 0) { + return content; + } + return content.replace(FRONTMATTER_REGEX, "").trim(); +} + +export class MuseFile { + protected _path: string; + protected _file: BunFile; + protected _fileType: MuseFileType; + + constructor(filePath: string) { + this._path = normalize(filePath); + this._file = Bun.file(this._path); + this._fileType = extensionToFiletype(this.extension.slice(1)); + } + + get dirname(): string { + return dirname(this._path); + } + + get extension(): string { + return extname(this._path); + } + + get basename(): string { + return basename(this._path); + } + + get filename(): string { + return basename(this._path, this.extension); + } + + get path(): string { + return this._path; + } + + public async readJSON(): Promise { + try { + const text = await this._file.text(); + + return { + type: "json", + text: text, + data: JSON.parse(text), + }; + } catch (error) { + throw new FileError(`Failed to read JSON file: ${error}`, this._path); + } + } + + public async readYAML(): Promise { + try { + const text = await this._file.text(); + return { + type: "yaml", + text, + data: YAML.parse(text), + }; + } catch (error) { + throw new FileError(`Failed to read YAML file: ${error}`, this._path); + } + } + + public async readTOML(): Promise { + try { + const text = await this._file.text(); + return { + type: "toml", + text, + data: TOML.parse(text), + }; + } catch (error) { + throw new FileError(`Failed to read TOML file: ${error}`, this._path); + } + } + + public async readMarkdown(): Promise { + try { + const text = await this._file.text(); + const frontmatter = extractFrontmatter(text); + const markdown = extractMarkdown(text); + + return { + type: "md", + text: markdown, + data: { + ...frontmatter, + }, + }; + } catch (error) { + throw new FileError(`Failed to read Markdown file: ${error}`, this._path); + } + } + + public async read(): Promise { + switch (this._fileType) { + case "json": + return this.readJSON(); + case "yaml": + return this.readYAML(); + case "toml": + return this.readTOML(); + case "md": + return this.readMarkdown(); + default: + throw new FileError( + `No reader for file type ${this._fileType}`, + this._path, + ); + } + } +} diff --git a/src/render/html/component/ability.tsx b/src/render/html/component/ability.tsx deleted file mode 100644 index 09537da..0000000 --- a/src/render/html/component/ability.tsx +++ /dev/null @@ -1,67 +0,0 @@ -import type { Ability } from "@proscenium/playbill"; -import { HTML } from "./base/html"; - -interface AbilityCardProps { - ability: Ability; -} - -export function AbilityCard({ ability }: AbilityCardProps) { - const costs: React.ReactNode[] = []; - - if (ability.ap > 0) { - costs.push( - - {ability.ap} AP - , - ); - } - - if (ability.hp > 0) { - costs.push( - - {ability.hp} HP - , - ); - } - - if (ability.ep > 0) { - costs.push( - - {ability.ep} EP - , - ); - } - - const damage = - ability.damage !== null ? ( -
- {ability.damage.amount} {ability.damage.type === "phy" ? "Phy" : "Arc"} -
- ) : null; - - const roll = - ability.roll !== "none" ? ( -
{ability.roll}
- ) : null; - - const type = ( - {ability.type} - ); - - const classList = `ability ability-${ability.type}`; - - return ( -
-
-

{ability.name}

-
- {type} - {roll} - {damage} - {costs} -
-
- -
- ); -} diff --git a/src/render/html/component/base/html.tsx b/src/render/html/component/base/html.tsx deleted file mode 100644 index 761e2f1..0000000 --- a/src/render/html/component/base/html.tsx +++ /dev/null @@ -1,10 +0,0 @@ -interface HTMLWrapperProps { - html: string; - className?: string; -} - -export function HTML({ html, className }: HTMLWrapperProps) { - return ( -
- ); -} diff --git a/src/render/html/component/base/section.tsx b/src/render/html/component/base/section.tsx deleted file mode 100644 index 2c85f28..0000000 --- a/src/render/html/component/base/section.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import classNames from "classnames"; -import type React from "react"; - -interface SectionProps { - type: string; - title: string; - children: React.ReactNode; - preInfo?: React.ReactNode; - info?: React.ReactNode; - componentId?: string; - leftCornerTag?: React.ReactNode; - rightCornerTag?: React.ReactNode; -} - -export function Section({ - type, - title, - children, - preInfo, - info, - componentId, - leftCornerTag, - rightCornerTag, -}: SectionProps) { - const sectionClasses = classNames("section", type); - const headerClasses = classNames("section-header", `${type}-header`); - const contentClasses = classNames("section-content", `${type}-content`); - - return ( -
-
- {preInfo} -

{title}

- {info} -
-
{leftCornerTag}
-
{rightCornerTag}
-
{children}
-
- ); -} diff --git a/src/render/html/component/glossary.tsx b/src/render/html/component/glossary.tsx deleted file mode 100644 index a139dc4..0000000 --- a/src/render/html/component/glossary.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { Section } from "./base/section"; - -interface GlossaryTableProps { - terms: [string, string[]][]; -} - -export function GlossaryTable({ terms }: GlossaryTableProps) { - return ( -
-
-
- {terms.map(([term, defs]) => { - return ( -
-
{term}
- {defs.map((d, i) => ( -
{d}
- ))} -
- ); - })} -
-
-
- ); -} diff --git a/src/render/html/component/header.tsx b/src/render/html/component/header.tsx deleted file mode 100644 index 4c0535e..0000000 --- a/src/render/html/component/header.tsx +++ /dev/null @@ -1,18 +0,0 @@ -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.tsx b/src/render/html/component/item.tsx deleted file mode 100644 index ebe63b7..0000000 --- a/src/render/html/component/item.tsx +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index e39106b..0000000 --- a/src/render/html/component/method.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import type { Method, Playbill } from "@proscenium/playbill"; -import { AbilityCard } from "./ability"; -import { Section } from "./base/section"; - -interface MethodSectionProps { - method: Method; - playbill: Playbill; -} - -export function MethodSection({ method, playbill }: MethodSectionProps) { - const ranks = method.abilities.map((rank, i) => { - const gridTemplateColumns = new Array(Math.min(rank.length, 3)) - .fill("1fr") - .join(" "); - return ( -
-

Rank {i + 1}

-
- {rank.map((abilityId) => { - const ability = playbill.getAbility(abilityId); - - if (ability === null) { - throw new Error(`Ability not found: ${abilityId}`); - } - - return ; - })} -
-
- ); - }); - - return ( -
{method.curator}'s Method of

} - info={ -

- } - > -

{ranks}
-
- ); -} diff --git a/src/render/html/component/resource.tsx b/src/render/html/component/resource.tsx deleted file mode 100644 index cd07910..0000000 --- a/src/render/html/component/resource.tsx +++ /dev/null @@ -1,115 +0,0 @@ -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 deleted file mode 100644 index 7971be1..0000000 --- a/src/render/html/component/rule.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { Rule } from "@proscenium/playbill"; -import { Section } from "./base/section"; -import { HTML } from "./base/html"; - -interface RuleSectionProps { - rule: Rule; -} - -export function RuleSection({ rule }: RuleSectionProps) { - return ( -
    - -
    - ); -} diff --git a/src/render/html/component/species.tsx b/src/render/html/component/species.tsx deleted file mode 100644 index 6c11831..0000000 --- a/src/render/html/component/species.tsx +++ /dev/null @@ -1,120 +0,0 @@ -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) => { - const ability = playbill.getAbility(abilityId); - - if (ability === null) { - throw new Error(`Ability not found: ${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/default.css b/src/render/html/default.css deleted file mode 100644 index f7c5ab5..0000000 --- a/src/render/html/default.css +++ /dev/null @@ -1,505 +0,0 @@ -@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&family=Urbanist:ital,wght@0,100..900;1,100..900&display=swap"); - -:root { - /* Spacing and sizing */ - --max-width: 800px; - --padding: 0.5rem; - --margin: 1rem; - - /* Typography */ - --line-height: 1.5; - --font-size: 18px; - --font-body: "Open Sans", sans-serif; - --font-header: "Urbanist", sans-serif; - - /* Page Colors */ - --color-background: hsl(34deg 18 15); - --color-background--dark: hsl(34deg 18 10); - --color-background--light: hsl(34deg 18 20); - --color-text: hsl(35deg 37 73); - --color-text--dim: hsl(35deg 17 50); - --color-heading: hsl(32deg 48 85); - --color-emphasis: hsl(56deg 59 56); - --color-strong: hsl(4deg 88 61); - - /* Concept Colors */ - --color-muscle: var(--color-heading); - --color-focus: var(--color-heading); - --color-knowledge: var(--color-heading); - --color-charm: var(--color-heading); - --color-cunning: var(--color-heading); - --color-spark: var(--color-heading); - - --color-novice: gray; - --color-adept: pink; - --color-master: papayawhip; - --color-theatrical: red; - - --color-phy: red; - --color-arc: hsl(56deg 59 56); - - --color-ap: hsl(215deg 91 75); - --color-hp: hsl(15deg 91 75); - --color-ep: hsl(115deg 40 75); - --color-xp: hsl(0deg 0 45); -} - -/* Normalize box sizing */ -*, -*::before, -*::after { - box-sizing: border-box; -} - -/* Strip default margins */ -* { - margin: 0; -} - -/* Global style classes */ -.phy { - color: var(--color-phy); -} - -.arc { - color: var(--color-arc); -} - -.ap { - color: var(--color-ap); -} - -.hp { - color: var(--color-hp); -} - -.ep { - color: var(--color-ep); -} - -.muscle { - color: var(--color-muscle); -} - -.focus { - color: var(--color-focus); -} - -.knowledge { - color: var(--color-knowledge); -} - -.charm { - color: var(--color-charm); -} - -.cunning { - color: var(--color-cunning); -} - -.spark { - color: var(--color-spark); -} - -.novice { - color: var(--color-novice); -} - -.adept { - color: var(--color-adept); -} - -.master { - color: var(--color-master); -} - -.theatrical { - color: var(--color-theatrical); -} - -/* Sections */ - -section { - position: relative; - max-width: var(--max-width); - margin: 0 auto var(--margin); - border: 2px solid var(--color-background--dark); -} - -.section-header { - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - - min-height: 8rem; - - text-align: center; - background-color: var(--color-background--dark); -} - -.section-header p { - margin: 0; -} - -.section-content { - padding: var(--padding); -} - -.section-content :last-child { - margin-bottom: 0; -} - -.section-tag { - position: absolute; - top: var(--padding); - color: var(--color-text--dim); - font-weight: bold; - max-width: 45%; -} - -.section-tag--left { - left: var(--padding); -} - -.section-tag--right { - right: var(--padding); -} - -body { - background-color: var(--color-background); - color: var(--color-text); - font-family: var(--font-body); - font-size: var(--font-size); -} - -p { - margin-bottom: var(--margin); - line-height: var(--line-height); -} - -em { - color: var(--color-emphasis); -} - -strong { - color: var(--color-strong); -} - -/* Basic Typography */ -h1, -h2, -h3, -h4, -h5, -h6 { - font-family: var(--font-header); - color: var(--color-heading); -} - -h1 { - font-size: 4rem; - text-align: center; -} - -h2 { - font-size: 2rem; -} - -h3 { - font-size: 1.75rem; -} - -h4 { - font-size: 1.5rem; -} - -h5 { - font-size: 1.25rem; -} - -h6 { - font-weight: bold; - font-size: 1rem; -} - -table { - width: 100%; - margin-bottom: var(--margin); -} - -thead { - background-color: var(--color-background--light); -} - -td { - padding: var(--padding); - text-align: left; - vertical-align: top; -} - -tr:nth-child(even) { - background-color: var(--color-background--light); -} - -tr:hover { - background-color: var(--color-background--dark); -} - -article blockquote { - font-style: italic; - border-left: 3px solid var(--color-emphasis); - background-color: var(--color-background--light); - margin-bottom: var(--margin); - padding: 0.5rem 0.5rem 0.5rem 2rem; -} - -article blockquote p:last-child { - margin-bottom: 0; -} - -article img { - display: block; - margin: 0 auto; - max-width: 70%; - max-height: 35vh; -} - -@media screen and (min-width: 480px) { - article img { - float: right; - margin-left: var(--margin); - margin-right: 0; - max-width: 45%; - } - - article img.left { - float: left; - float: left; - margin-right: var(--margin); - } -} - -/* Main Header */ -header { - min-height: 20vh; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; -} - -.header-byline { - font-size: 1.2rem; -} - -/* Overview */ -.overview { - padding: 1rem; - background-color: var(--color-background--dark); - text-align: center; -} - -.overview p { - margin: 0 auto; -} - -/* Nav */ - -nav { - max-width: var(--max-width); - padding: var(--padding); - margin: 0 auto; - display: flex; - flex-direction: row; - justify-content: space-between; - flex-wrap: wrap; - gap: var(--padding); -} - -nav button { - padding: var(--padding); - font-size: inherit; - color: inherit; - background-color: var(--color-background--light); - border: 1px solid var(--color-background--dark); - cursor: pointer; - flex: 1; -} - -nav button:hover { - background-color: var(--color-background--dark); -} - -/* Ability */ - -.ability { - padding: var(--padding); - background-color: var(--color-background--light); -} - -.ability-details { - display: flex; - flex-direction: row; - gap: var(--padding); - flex-wrap: wrap; - text-transform: capitalize; - font-weight: 300; - font-style: italic; -} - -.ability-header { - position: relative; - margin-bottom: var(--margin); -} - -/* Method */ -.method-rank { - margin-bottom: var(--margin); - overflow-x: auto; -} - -.method-curator { - font-style: italic; -} - -.method-description { - margin-top: var(--margin); -} - -.method-rank-content { - display: grid; - gap: var(--padding); -} - -.method-rank-content .ability { - width: 100%; - height: 100%; -} - -/* Species */ - -.species { - margin-bottom: var(--margin); -} - -.species-stats { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - text-align: center; - gap: var(--padding); -} - -.species-talents .adept { - font-weight: 600; -} - -.species-talents .master { - font-weight: 700; -} - -.species-stats h4, -.species-talents h4 { - font-size: var(--font-size); -} - -.species-talents { - text-transform: capitalize; -} - -.species-talents { - display: grid; - grid-template-columns: 1fr 1fr 1fr; - gap: var(--padding); - text-align: center; - margin-bottom: var(--margin); -} - -.species-abilities .ability-cost-xp { - display: none; -} - -/* Items */ -.item { - position: relative; - background-color: var(--color-background--light); -} - -.item-info { - display: flex; - flex-direction: row; - gap: var(--padding); -} - -.item-affinity { - text-transform: capitalize; -} - -.item-rarity { - position: absolute; - top: var(--padding); - right: var(--padding); - text-transform: capitalize; -} - -/* Glossary */ - -.glossary-content > dl > div { - margin-bottom: var(--margin); -} - -dt { - color: var(--color-strong); - font-weight: bold; -} - -dd { - margin-left: 2ch; -} - -dd + dd { - margin-top: var(--padding); -} - -dd + dt { - margin-top: var(--margin); -} - -/* Resources */ -.resource-categories { - list-style: none; - padding: 0; - display: flex; - flex-direction: row; - text-transform: capitalize; - gap: var(--padding); - color: var(--color-text--dim); -} - -.resource-categories li { - font-style: italic; -} - -.resource-categories li:before { - content: "#"; - opacity: 0.5; -} - -.resource-image { - display: block; -} - -.resource-image img { - float: none; - margin: 0 auto; - - max-width: 90%; - max-height: 100vh; - - margin-bottom: var(--padding); -} - -.resource-image figcaption { - font-style: italic; - text-align: center; -} diff --git a/src/render/html/index.tsx b/src/render/html/index.tsx deleted file mode 100644 index bb26ddd..0000000 --- a/src/render/html/index.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import { Playbill } from "@proscenium/playbill"; -import { compileMarkdownInPlaybill, type BoundPlaybill } from "#lib"; -import { GlossaryTable } from "./component/glossary"; -import { PageHeader } from "./component/header"; -import { ItemCard } from "./component/item"; -import { MethodSection } from "./component/method"; -import { ResourceSection } from "./component/resource"; -import { RuleSection } from "./component/rule"; -import { SpeciesSection } from "./component/species"; -import { renderToString } from "react-dom/server"; -import defaultCSS from "./default.css" with { type: "text" }; - -export async function renderPlaybillToHTML( - boundPlaybill: BoundPlaybill, -): Promise { - // Make a copy of the playbill to avoid modifying the original - // and compile all description markdown - const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize()); - await compileMarkdownInPlaybill(boundPlaybill); - - // Prepare stylesheet - const cssParts: string[] = []; - - if (!boundPlaybill.omitDefaultStyles) { - cssParts.push(``); - } - - if (boundPlaybill.styles.length > 0) { - cssParts.push(``); - } - - const css = cssParts.join("\n"); - - const body = renderToString( - <> -
    - {playbill.allSpecies.map((species) => ( - - ))} -
    - -
    - {playbill.allMethods.map((method) => ( - - ))} -
    - -
    - {playbill.allItems.map((item) => ( - - ))} -
    - -
    - {playbill.allRules.map((rule) => ( - - ))} -
    - -
    - {} -
    - -
    - {playbill.allResources.map((resource) => ( - - ))} -
    - , - ); - - // Main document - const doc = ` - - - - - - Playbill: ${playbill.name} - ${css} - - - - - ${renderToString()} -

    ${playbill.description}

    - - ${body} - - - `; - - return doc; -} diff --git a/src/util/args.ts b/src/util/args.ts deleted file mode 100644 index 25d2d0f..0000000 --- a/src/util/args.ts +++ /dev/null @@ -1,111 +0,0 @@ -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 - --markdown Compile markdown in descriptions when rendering to JSON - --help, -h Show this help message -`.trim(); - -const Renderers = ["json", "html"] as const; -const RendererSchema = z.enum(Renderers); -type Renderer = z.infer; - -/** - * A shape representing the arguments passed to the CLI - */ -export interface CLIArguments { - inputFilePath: string; - - options: { - check: boolean; - outfile: string; - help: boolean; - renderer: Renderer; - watch: boolean; - markdown: boolean; - }; -} - -/** - * Given an array of CLI arguments, parse them into a structured object - * - * @param argv The arguments to parse - * @returns The parsed CLI arguments - * - * @throws {CLIError} if the arguments are invalid - */ -export function parseCLIArguments(argv: string[]): CLIArguments { - const { values: options, positionals: args } = parseArgs({ - args: argv, - options: { - check: { - short: "c", - type: "boolean", - default: false, - }, - outfile: { - short: "o", - type: "string", - default: "", - }, - help: { - short: "h", - default: false, - type: "boolean", - }, - renderer: { - short: "r", - default: "json", - type: "string", - }, - watch: { - default: false, - type: "boolean", - }, - markdown: { - default: false, - type: "boolean", - }, - }, - strict: true, - allowPositionals: true, - }); - - // -- ARG VALIDATION -- // - if (options.check && options.outfile !== "") { - throw new CLIError("Cannot use --check and --outfile together"); - } - - const parsedRenderer = RendererSchema.safeParse(options.renderer); - - if (!parsedRenderer.success) { - throw new MuseError(`Invalid renderer: ${parsedRenderer.data}`); - } - - return { - inputFilePath: args[0] ?? "./binding.yaml", - - options: { - check: options.check, - outfile: options.outfile, - help: options.help, - renderer: parsedRenderer.data, - watch: options.watch, - markdown: options.markdown, - }, - }; -} diff --git a/src/util/files.ts b/src/util/files.ts deleted file mode 100644 index cd6c865..0000000 --- a/src/util/files.ts +++ /dev/null @@ -1,65 +0,0 @@ -import path from "node:path"; -import YAML from "yaml"; -import { FileError, FileNotFoundError } from "../lib/errors"; - -/** - * Load a file from the filesystem or throw an error if it does not exist. - * @param filePath The path to the file to load - * @returns The contents of the file as a string - * - * @throws {FileNotFoundError} if the file does not exist - */ -export async function loadFileOrFail(filePath: string): Promise { - const fileToLoad = Bun.file(filePath); - const fileExists = await fileToLoad.exists(); - - if (!fileExists) { - throw new FileNotFoundError(`File not found: ${filePath}`); - } - - return await fileToLoad.text(); -} - -/** - * Load and parse a YAML file or throw an error if the file doesnt exist - * or the yaml 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 YAML - */ -export async function loadYAMLFileOrFail(filePath: string): Promise { - try { - return YAML.parse(await loadFileOrFail(filePath)); - } catch (error) { - throw new FileError("Failed to parse YAML file", filePath, `${error}`); - } -} - -/** - * 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 - * - * @param filePath The path to the file - * @returns The absolute path of the directory containing the file - */ -export function getAbsoluteDirname(filePath: string): string { - return path.resolve(path.dirname(filePath)); -} diff --git a/src/util/index.ts b/src/util/index.ts deleted file mode 100644 index 1d10193..0000000 --- a/src/util/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./args"; -export * from "./files"; diff --git a/tsconfig.json b/tsconfig.json index cc9ee32..1e3b4b2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,7 +14,8 @@ "moduleResolution": "bundler", "allowArbitraryExtensions": true, "verbatimModuleSyntax": true, - "noEmit": true, + "declaration": true, + "emitDeclarationOnly": true, // Best practices "strict": true, "skipLibCheck": true, @@ -24,21 +25,10 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", - "paths": { - "#lib": [ - "./lib/index.ts" - ], - "#render/*": [ - "./render/*" - ], - "#util": [ - "./util/index.ts" - ], - } + "paths": {} }, "include": [ "src/**/*.ts", "src/**/*.tsx", - "src/**/*.css", ] } From 6a3157762ac39c7794f5419879305bbedb40f114 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 11:45:08 -0400 Subject: [PATCH 02/14] Reworked support for processing --- .gitignore | 3 - bun.lock | 295 +++--------------------------- demo/main/binding.yaml | 3 + demo/main/processors/scream.js | 18 ++ demo/main/stats/statistics.toml | 1 + demo/main/stories/a-neat-story.md | 5 + package.json | 24 +-- src/args.ts | 16 ++ src/binding.ts | 82 +++++---- src/index.ts | 51 +++++- src/muse-file.ts | 224 ++++++++++------------- src/types.ts | 3 + tsconfig.json | 4 +- types/muse.d.ts | 62 +++++++ 14 files changed, 318 insertions(+), 473 deletions(-) create mode 100644 demo/main/binding.yaml create mode 100644 demo/main/processors/scream.js create mode 100644 demo/main/stats/statistics.toml create mode 100644 demo/main/stories/a-neat-story.md create mode 100644 src/types.ts create mode 100644 types/muse.d.ts diff --git a/.gitignore b/.gitignore index a5f06e1..8a4e461 100644 --- a/.gitignore +++ b/.gitignore @@ -174,6 +174,3 @@ dist # Finder (MacOS) folder config .DS_Store -demo-data -demo.html -demo diff --git a/bun.lock b/bun.lock index e428c07..8427e5e 100644 --- a/bun.lock +++ b/bun.lock @@ -4,33 +4,16 @@ "": { "name": "@proscenium/muse", "dependencies": { - "@proscenium/playbill": "link:@proscenium/playbill", + "@endeavorance/emdy": "1.0.0", "chalk": "^5.4.1", - "classnames": "^2.5.1", - "lodash-es": "^4.17.21", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "rehype-headings-normalize": "^0.0.2", - "rehype-raw": "^7.0.0", - "rehype-shift-heading": "^2.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-normalize-headings": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", - "remark-wiki-link": "^2.0.1", - "slugify": "^1.6.6", "smol-toml": "^1.3.1", - "unified": "^11.0.5", "yaml": "^2.7.0", "zod": "^3.24.1", }, "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/bun": "latest", - "@types/lodash-es": "^4.17.12", - "@types/react": "^19.0.11", - "@types/react-dom": "^19.0.4", + "dts-bundle-generator": "^9.5.1", }, "peerDependencies": { "typescript": "^5.0.0", @@ -38,8 +21,6 @@ }, }, "packages": { - "@babel/runtime": ["@babel/runtime@7.26.10", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="], - "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], @@ -58,286 +39,60 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], - "@proscenium/playbill": ["@proscenium/playbill@link:@proscenium/playbill", {}], + "@endeavorance/emdy": ["@endeavorance/emdy@1.0.0", "https://git.astral.camp/api/packages/endeavorance/npm/%40endeavorance%2Femdy/-/1.0.0/emdy-1.0.0.tgz", { "dependencies": { "yaml": "^2.7.1" }, "peerDependencies": { "typescript": "^5" } }, "sha512-W3yDK3Ya76mF/QqmcX/iyNdAswfF82uOtFP+XstG8qgsbwp4I0lnuVjESkyBfj0AKvKJ/s6Xpa/64RhDHtlt6g=="], - "@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="], + "@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="], - "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], + "@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="], - "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], - - "@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="], - - "@types/node": ["@types/node@22.13.10", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw=="], - - "@types/react": ["@types/react@19.0.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-vrdxRZfo9ALXth6yPfV16PYTLZwsUWhVjjC+DkfE5t1suNSbBrWC9YqSuuxJZ8Ps6z1o2ycRpIqzZJIgklq4Tw=="], - - "@types/react-dom": ["@types/react-dom@19.0.4", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4fSQ8vWFkg+TGhePfUzVmat3eC14TXYSsiiDSLI0dVLsrm9gZFABjPy/Qu6TKgl1tq1Bu1yDsuQgY3A3DOjCcg=="], - - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], - - "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], - - "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], - - "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - - "bun-types": ["bun-types@1.2.5", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg=="], - - "ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="], + "bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="], "chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], - "character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="], + "dts-bundle-generator": ["dts-bundle-generator@9.5.1", "", { "dependencies": { "typescript": ">=5.0.2", "yargs": "^17.6.0" }, "bin": { "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" } }, "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA=="], - "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="], - - "dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="], - - "devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], - - "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - - "hast-util-from-parse5": ["hast-util-from-parse5@8.0.3", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "hastscript": "^9.0.0", "property-information": "^7.0.0", "vfile": "^6.0.0", "vfile-location": "^5.0.0", "web-namespaces": "^2.0.0" } }, "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg=="], - - "hast-util-heading-rank": ["hast-util-heading-rank@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA=="], - - "hast-util-parse-selector": ["hast-util-parse-selector@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A=="], - - "hast-util-raw": ["hast-util-raw@9.1.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "@ungap/structured-clone": "^1.0.0", "hast-util-from-parse5": "^8.0.0", "hast-util-to-parse5": "^8.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "parse5": "^7.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw=="], - - "hast-util-shift-heading": ["hast-util-shift-heading@4.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-heading-rank": "^3.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-1WzYRfcydfUIAn/N/QBDLlaG/JlXnE3Tx2ybuQFZKP4ZfM9v8FPJ/8i6pEaSoEtEI+FzjYmALlOJROkgRg3epg=="], - - "hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="], - - "hast-util-to-parse5": ["hast-util-to-parse5@8.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "property-information": "^6.0.0", "space-separated-tokens": "^2.0.0", "web-namespaces": "^2.0.0", "zwitch": "^2.0.0" } }, "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw=="], - - "hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="], - - "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], - - "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], - - "is-alphabetical": ["is-alphabetical@1.0.4", "", {}, "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg=="], - - "is-alphanumerical": ["is-alphanumerical@1.0.4", "", { "dependencies": { "is-alphabetical": "^1.0.0", "is-decimal": "^1.0.0" } }, "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A=="], - - "is-decimal": ["is-decimal@1.0.4", "", {}, "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw=="], - - "is-hexadecimal": ["is-hexadecimal@1.0.4", "", {}, "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "lodash-es": ["lodash-es@4.17.21", "", {}, "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="], - - "longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="], - - "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], - - "mdast-normalize-headings": ["mdast-normalize-headings@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-visit": "^5.0.0" } }, "sha512-PlUAiR4x49XGYLoyEoQYbqJfO6MdcGt5Uq/0ivZvAUv55YABk8psT6177lhng6hAwTF64P6CjXrE8Aew8oqSAQ=="], - - "mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="], - - "mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="], - - "mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="], - - "mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="], - - "mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="], - - "mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="], - - "mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="], - - "mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="], - - "mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="], - - "mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="], - - "mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="], - - "mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="], - - "mdast-util-wiki-link": ["mdast-util-wiki-link@0.1.2", "", { "dependencies": { "@babel/runtime": "^7.12.1", "mdast-util-to-markdown": "^0.6.5" } }, "sha512-DTcDyOxKDo3pB3fc0zQlD8myfQjYkW4hazUKI9PUyhtoj9JBeHC2eIdlVXmaT22bZkFAVU2d47B6y2jVKGoUQg=="], - - "micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="], - - "micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="], - - "micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="], - - "micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="], - - "micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="], - - "micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="], - - "micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="], - - "micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="], - - "micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="], - - "micromark-extension-wiki-link": ["micromark-extension-wiki-link@0.0.4", "", { "dependencies": { "@babel/runtime": "^7.12.1" } }, "sha512-dJc8AfnoU8BHkN+7fWZvIS20SMsMS1ZlxQUn6We67MqeKbOiEDZV5eEvCpwqGBijbJbxX3Kxz879L4K9HIiOvw=="], - - "micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="], - - "micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="], - - "micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="], - - "micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="], - - "micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="], - - "micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="], - - "micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="], - - "micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="], - - "micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="], - - "micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="], - - "micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="], - - "micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="], - - "micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="], - - "micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="], - - "micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="], - - "micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="], - - "micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="], - - "micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="], - - "micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="], - - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "parse-entities": ["parse-entities@2.0.0", "", { "dependencies": { "character-entities": "^1.0.0", "character-entities-legacy": "^1.0.0", "character-reference-invalid": "^1.0.0", "is-alphanumerical": "^1.0.0", "is-decimal": "^1.0.0", "is-hexadecimal": "^1.0.0" } }, "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ=="], - - "parse5": ["parse5@7.2.1", "", { "dependencies": { "entities": "^4.5.0" } }, "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ=="], - - "property-information": ["property-information@7.0.0", "", {}, "sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg=="], - - "react": ["react@19.0.0", "", {}, "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ=="], - - "react-dom": ["react-dom@19.0.0", "", { "dependencies": { "scheduler": "^0.25.0" }, "peerDependencies": { "react": "^19.0.0" } }, "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ=="], - - "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - - "rehype-headings-normalize": ["rehype-headings-normalize@0.0.2", "", { "dependencies": { "hast-util-heading-rank": "^3.0.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "rehype-stringify": "^10.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.0", "unified": "^11.0.4" } }, "sha512-YIK76cnNu95g31etRITkKqxTJ2WAbfKUnzyKv5nSWoUlBM9YPR6n4MzyBke2gzy0WusejSBOGgiplw32rR296Q=="], - - "rehype-raw": ["rehype-raw@7.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-raw": "^9.0.0", "vfile": "^6.0.0" } }, "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww=="], - - "rehype-shift-heading": ["rehype-shift-heading@2.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-shift-heading": "^4.0.0" } }, "sha512-ykIgOpIop4Telm+JXv0yDaN8jeMSBE9jwcEOxQJrlSnC15OVFq4/jS/X3MC6zjhfGYAUyvMcYSY0eORne/gX5A=="], - - "rehype-stringify": ["rehype-stringify@10.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "hast-util-to-html": "^9.0.0", "unified": "^11.0.0" } }, "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA=="], - - "remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="], - - "remark-normalize-headings": ["remark-normalize-headings@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-normalize-headings": "^4.0.0" } }, "sha512-fSE2hJxQlqxEsiB/Oni3UcERQ0IbnUuO3RNmQd4LjgEaknek0I//fK05wgFrKP/wYhWEWA+hgm1MHkddpPDcJQ=="], - - "remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="], - - "remark-rehype": ["remark-rehype@11.1.1", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ=="], - - "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - - "remark-wiki-link": ["remark-wiki-link@2.0.1", "", { "dependencies": { "@babel/runtime": "^7.4.4", "mdast-util-wiki-link": "^0.1.2", "micromark-extension-wiki-link": "^0.0.4" } }, "sha512-F8Eut1E7GWfFm4ZDTI6/4ejeZEHZgnVk6E933Yqd/ssYsc4AyI32aGakxwsGcEzbbE7dkWi1EfLlGAdGgOZOsA=="], - - "repeat-string": ["repeat-string@1.6.1", "", {}, "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w=="], - - "scheduler": ["scheduler@0.25.0", "", {}, "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA=="], - - "slugify": ["slugify@1.6.6", "", {}, "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="], - "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], - - "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "typescript": ["typescript@5.8.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ=="], "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], - "unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="], + "yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], - "unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="], + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], - "unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="], - - "unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="], - - "vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="], - - "vfile-location": ["vfile-location@5.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg=="], - - "vfile-message": ["vfile-message@4.0.2", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw=="], - - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], - - "yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], "zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="], - - "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - - "hast-util-to-parse5/property-information": ["property-information@6.5.0", "", {}, "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig=="], - - "mdast-util-wiki-link/mdast-util-to-markdown": ["mdast-util-to-markdown@0.6.5", "", { "dependencies": { "@types/unist": "^2.0.0", "longest-streak": "^2.0.0", "mdast-util-to-string": "^2.0.0", "parse-entities": "^2.0.0", "repeat-string": "^1.0.0", "zwitch": "^1.0.0" } }, "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ=="], - - "parse-entities/character-entities": ["character-entities@1.2.4", "", {}, "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw=="], - - "parse-entities/character-entities-legacy": ["character-entities-legacy@1.1.4", "", {}, "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA=="], - - "mdast-util-wiki-link/mdast-util-to-markdown/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], - - "mdast-util-wiki-link/mdast-util-to-markdown/longest-streak": ["longest-streak@2.0.4", "", {}, "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg=="], - - "mdast-util-wiki-link/mdast-util-to-markdown/mdast-util-to-string": ["mdast-util-to-string@2.0.0", "", {}, "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w=="], - - "mdast-util-wiki-link/mdast-util-to-markdown/zwitch": ["zwitch@1.0.5", "", {}, "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw=="], } } diff --git a/demo/main/binding.yaml b/demo/main/binding.yaml new file mode 100644 index 0000000..9f9e12c --- /dev/null +++ b/demo/main/binding.yaml @@ -0,0 +1,3 @@ +markdownKey: description +processors: + - ./processors/scream.js diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js new file mode 100644 index 0000000..9b5132d --- /dev/null +++ b/demo/main/processors/scream.js @@ -0,0 +1,18 @@ +export const name = "Scream"; +export function process(binding, opts) { + const modifiedEntries = binding.entries.map(entry => { + if (binding.markdownKey in entry.data) { + entry.data = { + ...entry.data, + [binding.markdownKey]: entry.data[binding.markdownKey].toUpperCase() + } + } + + return entry; + }); + + return { + ...binding, + entries: modifiedEntries + }; +} diff --git a/demo/main/stats/statistics.toml b/demo/main/stats/statistics.toml new file mode 100644 index 0000000..8355b1e --- /dev/null +++ b/demo/main/stats/statistics.toml @@ -0,0 +1 @@ +slamDunks = 100 diff --git a/demo/main/stories/a-neat-story.md b/demo/main/stories/a-neat-story.md new file mode 100644 index 0000000..8b9e564 --- /dev/null +++ b/demo/main/stories/a-neat-story.md @@ -0,0 +1,5 @@ +--- +published: true +--- + +This is a neat story I wrote! diff --git a/package.json b/package.json index 4ba9691..0a9db1a 100644 --- a/package.json +++ b/package.json @@ -6,32 +6,15 @@ "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/bun": "latest", - "@types/lodash-es": "^4.17.12", - "@types/react": "^19.0.11", - "@types/react-dom": "^19.0.4" + "dts-bundle-generator": "^9.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "dependencies": { - "@proscenium/playbill": "link:@proscenium/playbill", + "@endeavorance/emdy": "1.0.0", "chalk": "^5.4.1", - "classnames": "^2.5.1", - "lodash-es": "^4.17.21", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "rehype-headings-normalize": "^0.0.2", - "rehype-raw": "^7.0.0", - "rehype-shift-heading": "^2.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-normalize-headings": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.1", - "remark-wiki-link": "^2.0.1", - "slugify": "^1.6.6", "smol-toml": "^1.3.1", - "unified": "^11.0.5", "yaml": "^2.7.0", "zod": "^3.24.1" }, @@ -39,6 +22,7 @@ "fmt": "bunx --bun biome check --fix", "build": "bun build ./src/index.ts --outfile muse --compile", "demo": "bun run ./src/index.ts -- ./demo-data/great-spires/binding.yaml", - "build:install": "bun run build && mv muse ~/.local/bin/muse" + "build:install": "bun run build && mv muse ~/.local/bin/muse", + "types": "dts-bundle-generator -o types/muse.d.ts --project ./tsconfig.json ./src/types.ts" } } diff --git a/src/args.ts b/src/args.ts index 9e9f8d1..09fe7a8 100644 --- a/src/args.ts +++ b/src/args.ts @@ -10,6 +10,8 @@ Usage: muse [/path/to/binding.yaml] Options: + --stdout -s Output final data to stdout + --verbose, -v Enable verbose logging --help, -h Show this help message `.trim(); @@ -21,6 +23,8 @@ export interface CLIArguments { options: { help: boolean; + verbose: boolean; + stdout: boolean; }; } @@ -41,6 +45,16 @@ export function parseCLIArguments(argv: string[]): CLIArguments { default: false, type: "boolean", }, + verbose: { + short: "v", + default: false, + type: "boolean", + }, + stdout: { + short: "s", + default: false, + type: "boolean", + }, }, strict: true, allowPositionals: true, @@ -51,6 +65,8 @@ export function parseCLIArguments(argv: string[]): CLIArguments { options: { help: options.help, + verbose: options.verbose, + stdout: options.stdout, }, }; } diff --git a/src/binding.ts b/src/binding.ts index 6803251..fd57739 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,22 +1,16 @@ -import path from "node:path"; +import path, { dirname } from "node:path"; import { Glob } from "bun"; import z from "zod"; import { FileNotFoundError, MuseError } from "./errors"; -import { MuseFile, type MuseFileContent } from "./muse-file"; +import { parseMuseFile, type MuseEntry } from "./muse-file"; // binding.yaml file schema const BindingSchema = z.object({ include: z.array(z.string()).default([]), - - name: z.string().default("Unnamed playbill"), - author: z.string().default("Someone"), - description: z.string().default("No description provided"), - version: z.string().default("0.0.0"), - + markdownKey: z.string().default("content"), files: z .array(z.string()) .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), - options: z.record(z.string(), z.any()).default({}), processors: z.array(z.string()).default([]), }); @@ -51,24 +45,25 @@ export async function resolveBindingPath(bindingPath: string): Promise { return path.resolve(bindingPath); } -export interface BindingMetadata { - name: string; - author: string; - description: string; - version: string; -} +export type MuseProcessorFn = ( + binding: Binding, + opts: Record, +) => Promise; export interface MuseProcessor { readonly filePath: string; - process(binding: Binding): Promise; + readonly name: string; + readonly description: string; + readonly process: MuseProcessorFn; } export interface Binding { readonly _raw: ParsedBinding; readonly bindingPath: string; + readonly markdownKey: string; readonly includedFiles: string[]; - readonly entries: MuseFileContent[]; - readonly metadata: BindingMetadata; + readonly entries: MuseEntry[]; + readonly meta: Record; readonly options: Record; readonly processors: MuseProcessor[]; readonly imports: Binding[]; @@ -77,19 +72,23 @@ export interface Binding { export async function loadBinding(initialPath: string): Promise { // Resolve the file location if possible const bindingPath = await resolveBindingPath(initialPath); - const bindingFile = new MuseFile(bindingPath); + const bindingDirname = dirname(bindingPath); + const loadedBindingData = await parseMuseFile(bindingPath); + + if (loadedBindingData.length === 0) { + throw new MuseError("No entries found in binding file"); + } // Load and parse the Binding YAML content - const bindingFileContent = await bindingFile.read(); - const parsedBinding = BindingSchema.parse(bindingFileContent.data); + const parsedBinding = BindingSchema.parse(loadedBindingData[0].data); // Prepare collections to load into const extendsFrom: Binding[] = []; - const entries: MuseFileContent[] = []; + const entries: MuseEntry[] = []; // Process imported bindings and bring in their entries for (const includePath of parsedBinding.include) { - const pathFromHere = path.resolve(bindingFile.dirname, includePath); + const pathFromHere = path.resolve(bindingDirname, includePath); const extendBindingPath = await resolveBindingPath(pathFromHere); const loadedBinding = await loadBinding(extendBindingPath); extendsFrom.push(loadedBinding); @@ -102,7 +101,7 @@ export async function loadBinding(initialPath: string): Promise { const glob = new Glob(thisGlob); const results = glob.scanSync({ - cwd: bindingFile.dirname, + cwd: bindingDirname, absolute: true, followSymlinks: true, onlyFiles: true, @@ -113,26 +112,26 @@ export async function loadBinding(initialPath: string): Promise { // Exclude the binding.yaml file const includedFilePaths = allFilePaths.filter((filePath) => { - return filePath !== bindingFile.path; + return filePath !== bindingPath; }); - const fileLoadPromises: Promise[] = []; + const fileLoadPromises: Promise[] = []; for (const filePath of includedFilePaths) { - const file = new MuseFile(filePath); - fileLoadPromises.push(file.read()); + fileLoadPromises.push( + parseMuseFile(filePath, { + markdownKey: parsedBinding.markdownKey, + }), + ); } const mainEntries = await Promise.all(fileLoadPromises); - entries.push(...mainEntries); + entries.push(...mainEntries.flat()); // Load and check processors const processors: MuseProcessor[] = []; for (const processorPath of parsedBinding.processors) { - const resolvedProcessorPath = path.resolve( - bindingFile.dirname, - processorPath, - ); - const { process } = await import(resolvedProcessorPath); + const resolvedProcessorPath = path.resolve(bindingDirname, processorPath); + const { process, name, description } = await import(resolvedProcessorPath); if (!process || typeof process !== "function") { throw new MuseError( @@ -140,25 +139,28 @@ export async function loadBinding(initialPath: string): Promise { ); } + const processorName = String(name ?? "Unnamed Processor"); + const processorDescription = String( + description ?? "No description provided", + ); + processors.push({ filePath: resolvedProcessorPath, process: process as MuseProcessor["process"], + name: processorName, + description: processorDescription, }); } return { _raw: parsedBinding, bindingPath, + markdownKey: parsedBinding.markdownKey, includedFiles: includedFilePaths, entries, options: parsedBinding.options, processors: processors, imports: extendsFrom, - metadata: { - name: parsedBinding.name, - author: parsedBinding.author, - description: parsedBinding.description, - version: parsedBinding.version, - }, + meta: {}, }; } diff --git a/src/index.ts b/src/index.ts index 32e0823..8b1c510 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,29 +1,56 @@ import chalk from "chalk"; -import { MuseError } from "./errors"; +import { MuseError } from "./errors.ts"; import { loadBinding, type Binding } from "./binding"; import { type CLIArguments, parseCLIArguments, USAGE } from "./args"; +interface Loggers { + log: (msg: string) => void; + verbose: (msg: string) => void; +} + enum ExitCode { Success = 0, Error = 1, } -async function processBinding({ inputFilePath }: CLIArguments) { +async function processBinding( + { inputFilePath, options }: CLIArguments, + { log, verbose }: Loggers, +) { // Load the binding const binding = await loadBinding(inputFilePath); + verbose(`Binding ${binding.bindingPath}`); + + const stepWord = binding.processors.length === 1 ? "step" : "steps"; + log( + `Processing ${binding.entries.length} entries with ${binding.processors.length} ${stepWord}`, + ); + + const processStart = performance.now(); + // Run the data through all processors const processedSteps: Binding[] = [binding]; for (const processor of binding.processors) { - const processedStep = await processor.process(binding); - processedSteps.push(processedStep); + const lastStep = processedSteps[processedSteps.length - 1]; + + const { process, name } = processor; + + log(chalk.bold(`↪ ${name}`) + chalk.dim(` (${processor.description})`)); + const thisStep = await process(lastStep, binding.options); + processedSteps.push(thisStep); } + const processEnd = performance.now(); + const processTime = ((processEnd - processStart) / 1000).toFixed(2); + verbose(`Processing completed in ${processTime}s`); + const finalState = processedSteps[processedSteps.length - 1]; const serialized = JSON.stringify(finalState.entries, null, 2); - // Otherwise - console.log(serialized); + if (options.stdout) { + console.log(serialized); + } return ExitCode.Success; } @@ -37,7 +64,17 @@ async function main(): Promise { return ExitCode.Success; } - const lastProcessResult = await processBinding(cliArguments); + const logFn = !options.stdout ? console.log : () => {}; + const verboseFn = options.verbose + ? (msg: string) => { + console.log(chalk.dim(msg)); + } + : () => {}; + + const lastProcessResult = await processBinding(cliArguments, { + log: logFn, + verbose: verboseFn, + }); return lastProcessResult; } diff --git a/src/muse-file.ts b/src/muse-file.ts index f8fd51e..3243c5d 100644 --- a/src/muse-file.ts +++ b/src/muse-file.ts @@ -1,17 +1,18 @@ -import type { BunFile } from "bun"; -import { normalize, basename, extname, dirname } from "node:path"; +import { normalize, extname } from "node:path"; import YAML from "yaml"; import TOML from "smol-toml"; -import { FileError } from "./errors"; +import EMDY from "@endeavorance/emdy"; +type UnknownRecord = Record; export type MuseFileType = "md" | "yaml" | "json" | "toml"; -export interface MuseFileContent { - type: MuseFileType; - text: string; +export interface MuseEntry { + _raw: string; + filePath: string; data: Record; + meta: Record; } -function extensionToFiletype(ext: string): MuseFileType { +function parseFileType(ext: string): MuseFileType { switch (ext) { case "md": return "md"; @@ -26,143 +27,102 @@ function extensionToFiletype(ext: string): MuseFileType { } } -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 - */ -function extractFrontmatter(content: string) { - // If it does not start with `---`, it is invalid for frontmatter - if (content.trim().indexOf("---") !== 0) { - return {}; +function parseYAMLEntries(text: string): UnknownRecord[] { + const parsedDocs = YAML.parseAllDocuments(text); + + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new Error("Encountered NULL resource"); } - if (FRONTMATTER_REGEX.test(content)) { - const frontmatterString = content.match(FRONTMATTER_REGEX)?.[0] ?? ""; - const cleanFrontmatter = frontmatterString.replaceAll("---", "").trim(); - return YAML.parse(cleanFrontmatter); + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new Error( + `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, + ); } - return {}; + const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); + return collection; } -/** - * Given a string of a markdown document, extract the markdown content - * @param content The raw markdown content - * @returns The markdown content without frontmatter - */ -function extractMarkdown(content: string): string { - if (content.trim().indexOf("---") !== 0) { - return content; +function parseJSONEntries(text: string): UnknownRecord[] { + const parsed = JSON.parse(text); + + if (Array.isArray(parsed)) { + return parsed; } - return content.replace(FRONTMATTER_REGEX, "").trim(); + + if (typeof parsed === "object") { + return [parsed]; + } + + throw new Error("JSON resource must be an object or an array of objects"); } -export class MuseFile { - protected _path: string; - protected _file: BunFile; - protected _fileType: MuseFileType; +function parseTOMLEntries(text: string): UnknownRecord[] { + const parsed = TOML.parse(text); - constructor(filePath: string) { - this._path = normalize(filePath); - this._file = Bun.file(this._path); - this._fileType = extensionToFiletype(this.extension.slice(1)); + if (Array.isArray(parsed)) { + return parsed; } - get dirname(): string { - return dirname(this._path); + if (typeof parsed === "object") { + return [parsed]; } - get extension(): string { - return extname(this._path); - } - - get basename(): string { - return basename(this._path); - } - - get filename(): string { - return basename(this._path, this.extension); - } - - get path(): string { - return this._path; - } - - public async readJSON(): Promise { - try { - const text = await this._file.text(); - - return { - type: "json", - text: text, - data: JSON.parse(text), - }; - } catch (error) { - throw new FileError(`Failed to read JSON file: ${error}`, this._path); - } - } - - public async readYAML(): Promise { - try { - const text = await this._file.text(); - return { - type: "yaml", - text, - data: YAML.parse(text), - }; - } catch (error) { - throw new FileError(`Failed to read YAML file: ${error}`, this._path); - } - } - - public async readTOML(): Promise { - try { - const text = await this._file.text(); - return { - type: "toml", - text, - data: TOML.parse(text), - }; - } catch (error) { - throw new FileError(`Failed to read TOML file: ${error}`, this._path); - } - } - - public async readMarkdown(): Promise { - try { - const text = await this._file.text(); - const frontmatter = extractFrontmatter(text); - const markdown = extractMarkdown(text); - - return { - type: "md", - text: markdown, - data: { - ...frontmatter, - }, - }; - } catch (error) { - throw new FileError(`Failed to read Markdown file: ${error}`, this._path); - } - } - - public async read(): Promise { - switch (this._fileType) { - case "json": - return this.readJSON(); - case "yaml": - return this.readYAML(); - case "toml": - return this.readTOML(); - case "md": - return this.readMarkdown(); - default: - throw new FileError( - `No reader for file type ${this._fileType}`, - this._path, - ); - } - } + throw new Error("TOML resource must be an object or an array of objects"); +} + +interface ParseMuseFileOptions { + markdownKey?: string; +} + +export async function parseMuseFile( + rawFilePath: string, + { markdownKey = "content" }: ParseMuseFileOptions = {}, +): Promise { + const filePath = normalize(rawFilePath); + const file = Bun.file(filePath); + const fileType = parseFileType(extname(filePath).slice(1)); + const rawFileContent = await file.text(); + + const partial = { + _raw: rawFileContent, + filePath, + meta: {}, + }; + + if (fileType === "md") { + const parsed = EMDY.parse(rawFileContent, markdownKey); + return [ + { + ...partial, + data: parsed, + }, + ]; + } + + if (fileType === "yaml") { + return parseYAMLEntries(rawFileContent).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "json") { + return parseJSONEntries(rawFileContent).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "toml") { + return parseTOMLEntries(rawFileContent).map((data) => ({ + ...partial, + data, + })); + } + + throw new Error(`Unsupported file type: ${fileType}`); } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..21dce91 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,3 @@ +import type { Binding, MuseProcessorFn } from "./binding"; +import type { MuseEntry } from "./muse-file"; +export type { Binding, MuseProcessorFn, MuseEntry }; diff --git a/tsconfig.json b/tsconfig.json index 1e3b4b2..0d2aab2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,7 @@ // Bundler mode "moduleResolution": "bundler", "allowArbitraryExtensions": true, + "allowImportingTsExtensions": true, "verbatimModuleSyntax": true, "declaration": true, "emitDeclarationOnly": true, @@ -25,7 +26,8 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", - "paths": {} + "paths": {}, + "outDir": "./types", }, "include": [ "src/**/*.ts", diff --git a/types/muse.d.ts b/types/muse.d.ts new file mode 100644 index 0000000..88fdc98 --- /dev/null +++ b/types/muse.d.ts @@ -0,0 +1,62 @@ +// Generated by dts-bundle-generator v9.5.1 + +import z from 'zod'; + +export type MuseFileType = "md" | "yaml" | "json" | "toml"; +export interface MuseFileContent { + type: MuseFileType; + text: string; + data: Record; +} +declare const BindingSchema: z.ZodObject<{ + include: z.ZodDefault>; + name: z.ZodDefault; + author: z.ZodDefault; + description: z.ZodDefault; + version: z.ZodDefault; + files: z.ZodDefault>; + options: z.ZodDefault>; + processors: z.ZodDefault>; +}, "strip", z.ZodTypeAny, { + options: Record; + include: string[]; + name: string; + author: string; + description: string; + version: string; + files: string[]; + processors: string[]; +}, { + options?: Record | undefined; + include?: string[] | undefined; + name?: string | undefined; + author?: string | undefined; + description?: string | undefined; + version?: string | undefined; + files?: string[] | undefined; + processors?: string[] | undefined; +}>; +export type ParsedBinding = z.infer; +export interface BindingMetadata { + name: string; + author: string; + description: string; + version: string; +} +export type MuseProcessorFn = (binding: Binding) => Promise; +export interface MuseProcessor { + readonly filePath: string; + readonly process: MuseProcessorFn; +} +export interface Binding { + readonly _raw: ParsedBinding; + readonly bindingPath: string; + readonly includedFiles: string[]; + readonly entries: MuseFileContent[]; + readonly metadata: BindingMetadata; + readonly options: Record; + readonly processors: MuseProcessor[]; + readonly imports: Binding[]; +} + +export {}; From 435d55539436382a6195b560a8deb48a94051737 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 15:41:20 -0400 Subject: [PATCH 03/14] Update readme and clean up --- README.md | 225 +++----------------- demo/main/binding.md | 11 + demo/main/binding.yaml | 3 - demo/main/processors/announce-slam-dunks.js | 13 ++ demo/main/processors/scream.js | 2 +- demo/main/stats/statistics.toml | 3 +- package.json | 21 +- src/args.ts | 72 ------- src/binding.ts | 194 ++++++++--------- src/{index.ts => cli.ts} | 76 ++++++- src/errors.ts | 2 - src/exports.ts | 2 + src/{muse-file.ts => parse.ts} | 33 +-- src/types.ts | 58 ++++- src/util.ts | 27 +++ tsconfig.json | 2 +- types/muse.d.ts | 62 ------ 17 files changed, 320 insertions(+), 486 deletions(-) create mode 100644 demo/main/binding.md delete mode 100644 demo/main/binding.yaml create mode 100644 demo/main/processors/announce-slam-dunks.js delete mode 100644 src/args.ts rename src/{index.ts => cli.ts} (54%) create mode 100644 src/exports.ts rename src/{muse-file.ts => parse.ts} (74%) create mode 100644 src/util.ts delete mode 100644 types/muse.d.ts diff --git a/README.md b/README.md index a1543bf..b985fbb 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,20 @@ -# Playbill Builder CLI +# Muse -This is a CLI tool for compiling Markdown and YAML files into a validated Playbill for [Proscenium](https://proscenium.game) +A CLI for wrangling directories of source files. + +Like a static generator, but for whatever. + +## Overview + +**Muse** is a CLI and toolchain for operating on directories of +json, yaml, toml, and markdown files. Each file can specify any shape +of data. Muse scans included files, parses them into data, and streams +the loaded data through processors and plugins. + +Each processor can modify the data at compile time, as well as enact +side effects such as writing files to disk. + +Muse does not edit source files, it only reads them in. ## Usage @@ -9,209 +23,24 @@ 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 + --stdout -s Output final data to stdout + --verbose, -v Enable verbose logging --help, -h Show this help message ``` ## Binding File -Each Muse project should specify a `binding.yaml` file with the following shape: +Each Muse project should specify a `binding` file with the following shape: ```yaml -name: My Playbill # Required -author: My Name # Required -version: 0.0.1 # Required -extend: ../another-playbill # Optional +include: + - ../another-project # Optional files: # Optional - "**/*.yaml" -styles: # Optional - - stylesheet.css -terms: # Optional - Some Term: Some Definition - Another Term: Another Definition +contentKey: "content" # Optional +options: # Optional + someOption: someValue +processors: + - first-processor + - second-processor ``` -The **Binding** file is used to specify the metadata for the Playbill, as well as the files to be compiled. It is also the entry point for a project. - -When using the `muse` CLI, you must provide either a path to a `binding.yaml` file or a directory which contains a `binding.yaml` file. - -## Common Types - -Many Playbill components share common properties that expect a value -from a predefined list. These are the common types used in the Playbill. - -### `Roll` Options - -- `none` -- `attack` -- `fate` -- `muscle` -- `focus` -- `knowledge` -- `charm` -- `cunning` -- `spark` - -### `Damage Type` Options - -- `phy` (denotes "physical damage") -- `arc` (denotes "arcane damage") - -### `Prowess` Options - -- `novice` -- `adept` -- `master` - -### `Challenge` Options - -- `novice` -- `adept` -- `master` -- `theatrical` - -### `Ability Type` Options - -- `action` -- `cue` -- `trait` - -## Definition Shapes - -These YAML representations of definitions show the properties available for each Playbill component. - -Note that every Playbill component definition can define the following properties: - -```yaml -id: an-id # defaults to a slugified version of the file name -name: Component Name # defaults to the file name -description: Markdown description # defaults to a placeholder -categories: # defaults to no categories - - list - - of - - categories -``` - -### Ability Definition - -```yaml -$define: ability -type: # Defaults to action -ap: 0 # AP cost to use this ability (default 0) -hp: 0 # HP cost to use this ability (default 0) -ep: 0 # EP cost to use this ability (default 0) -xp: 0 # XP cost to learn this ability (default 1) -damage: 0 # Damage dealt by this ability (default 0) -damageType: # Type of damage dealt by this ability (default phy) -roll: # Roll type for this ability (default none) -``` - -### Blueprint Definition - -```yaml -$define: blueprint -species: species-id # ID of the species this blueprint is for -items: # List of item IDs (default empty) - - item-id -abilities: # List of ability IDs (default empty) - - ability-id -ap: 0 # Starting AP (default 4) -hp: 0 # Starting HP (default 5) -ep: 0 # Starting EP (default 1) -xp: 0 # Starting XP (default 0) -muscle: # Starting muscle prowess (default novice) -focus: # Starting focus prowess (default novice) -knowledge: # Starting knowledge prowess (default novice) -charm: # Starting charm prowess (default novice) -cunning: # Starting cunning prowess (default novice) -spark: # Starting spark prowess (default novice) -``` - -### Item Definition - -```yaml -$define: item -type: # Defaults to wielded -rarity: # Defaults to common -affinity: # Defaults to none -damage: 0 # Defaults to 0 -damageType: # Defaults to phy -hands: 1 | 2 # Only for wielded items -slot: # Only for worn items -``` - -#### `Item Type` Options - -- `wielded` -- `worn` -- `trinket` - -#### `Rarity` Options - -- `common` -- `uncommon` -- `rare` -- `legendary` - -#### `Item Slot` Options - -- `headgear` -- `outfit` -- `gloves` -- `boots` -- `adornment` - -### Method Definition - -```yaml -$define: method -curator: Someone # The name of the curator -abilities: # A 2-d array of ability IDs - - [rank-1-ability-id-one, rank-1-ability-id-two] - - [rank-2-ability-id-one] -``` - -### Resource Definition - -```yaml -$define: resource -type: # Defaults to text -url: https://url.com/image.jpeg # Only for image resources -data: # Only for table resources -``` - -#### `Resource Type` Options - -- `text` -- `image` -- `table` - -### Rule Definition - -```yaml -$define: rule -overrule: another-rule-id # Optional -order: 0 # Optional -``` - -### Species Definition - -```yaml -$define: species -hands: 2 # Defaults to 2 -abilities: # A list of innate ability IDs - - some-ability-id - - another-ability-id -ap: 0 # Starting AP modifier -hp: 0 # Starting HP modifier -ep: 0 # Starting EP modifier -xp: 0 # Starting XP modifier -muscle: # Defaults to novice -focus: # Defaults to novice -knowledge: # Defaults to novice -charm: # Defaults to novice -cunning: # Defaults to novice -spark: # Defaults to novice -``` diff --git a/demo/main/binding.md b/demo/main/binding.md new file mode 100644 index 0000000..b660a51 --- /dev/null +++ b/demo/main/binding.md @@ -0,0 +1,11 @@ +--- +markdownKey: description +options: + dunks: + key: dunkaroos +processors: + - ./processors/scream.js + - ./processors/announce-slam-dunks.js +--- + +A markdown-powered binding file! diff --git a/demo/main/binding.yaml b/demo/main/binding.yaml deleted file mode 100644 index 9f9e12c..0000000 --- a/demo/main/binding.yaml +++ /dev/null @@ -1,3 +0,0 @@ -markdownKey: description -processors: - - ./processors/scream.js diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js new file mode 100644 index 0000000..24cdb96 --- /dev/null +++ b/demo/main/processors/announce-slam-dunks.js @@ -0,0 +1,13 @@ +export const name = "Announce Slam Dunks"; + +export const process = (binding, { dunks }) => { + const slamDunkKey = dunks?.key ?? "slamDunks"; + + for (const entry of binding.entries) { + if (slamDunkKey in entry.data) { + console.log(`Slam dunk!`); + } + } + + return binding; +}; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js index 9b5132d..11f177d 100644 --- a/demo/main/processors/scream.js +++ b/demo/main/processors/scream.js @@ -1,5 +1,5 @@ export const name = "Scream"; -export function process(binding, opts) { +export function process(binding) { const modifiedEntries = binding.entries.map(entry => { if (binding.markdownKey in entry.data) { entry.data = { diff --git a/demo/main/stats/statistics.toml b/demo/main/stats/statistics.toml index 8355b1e..d28b28b 100644 --- a/demo/main/stats/statistics.toml +++ b/demo/main/stats/statistics.toml @@ -1 +1,2 @@ -slamDunks = 100 +player = "Flynn" +dunkaroos = 100 diff --git a/package.json b/package.json index 0a9db1a..e9fae55 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,17 @@ { "name": "@proscenium/muse", - "version": "0.0.1", - "module": "index.ts", + "version": "0.1.0", + "module": "dist/index.js", + "exports": { + ".": { + "import": "./dist/muse.js", + "types": "./dist/muse.d.ts" + } + }, "type": "module", + "files": [ + "dist" + ], "devDependencies": { "@biomejs/biome": "^1.9.4", "@types/bun": "latest", @@ -20,9 +29,11 @@ }, "scripts": { "fmt": "bunx --bun biome check --fix", - "build": "bun build ./src/index.ts --outfile muse --compile", - "demo": "bun run ./src/index.ts -- ./demo-data/great-spires/binding.yaml", + "clean": "rm -rf dist", + "types": "dts-bundle-generator -o dist/muse.d.ts --project ./tsconfig.json ./src/exports.ts", + "compile": "bun build ./src/cli.ts --outfile ./dist/muse --compile", + "build": "bun run clean && bun run compile && bun run types", "build:install": "bun run build && mv muse ~/.local/bin/muse", - "types": "dts-bundle-generator -o types/muse.d.ts --project ./tsconfig.json ./src/types.ts" + "demo": "bun run ./src/cli.ts -- ./demo/main" } } diff --git a/src/args.ts b/src/args.ts deleted file mode 100644 index 09fe7a8..0000000 --- a/src/args.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { parseArgs } from "node:util"; -import chalk from "chalk"; -import { version } from "../package.json" with { type: "json" }; - -export const USAGE = ` -${chalk.bold("muse")} - Compile and process troves of data -${chalk.dim(`v${version}`)} - -Usage: - muse [/path/to/binding.yaml] - -Options: - --stdout -s Output final data to stdout - --verbose, -v Enable verbose logging - --help, -h Show this help message -`.trim(); - -/** - * A shape representing the arguments passed to the CLI - */ -export interface CLIArguments { - inputFilePath: string; - - options: { - help: boolean; - verbose: boolean; - stdout: boolean; - }; -} - -/** - * Given an array of CLI arguments, parse them into a structured object - * - * @param argv The arguments to parse - * @returns The parsed CLI arguments - * - * @throws {CLIError} if the arguments are invalid - */ -export function parseCLIArguments(argv: string[]): CLIArguments { - const { values: options, positionals: args } = parseArgs({ - args: argv, - options: { - help: { - short: "h", - default: false, - type: "boolean", - }, - verbose: { - short: "v", - default: false, - type: "boolean", - }, - stdout: { - short: "s", - default: false, - type: "boolean", - }, - }, - strict: true, - allowPositionals: true, - }); - - return { - inputFilePath: args[0] ?? "./binding.yaml", - - options: { - help: options.help, - verbose: options.verbose, - stdout: options.stdout, - }, - }; -} diff --git a/src/binding.ts b/src/binding.ts index fd57739..9499757 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,26 +1,42 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import z from "zod"; import { FileNotFoundError, MuseError } from "./errors"; -import { parseMuseFile, type MuseEntry } from "./muse-file"; +import { parseMuseFile } from "./parse"; +import { + type Binding, + BindingSchema, + type MuseEntry, + type MuseProcessor, +} from "./types"; +import { resolveFilePath } from "./util"; -// binding.yaml file schema -const BindingSchema = z.object({ - include: z.array(z.string()).default([]), - markdownKey: z.string().default("content"), - files: z - .array(z.string()) - .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), - options: z.record(z.string(), z.any()).default({}), - processors: z.array(z.string()).default([]), -}); +async function loadProcessor( + bindingDirname: string, + processorPath: string, +): Promise { + const resolvedProcessorPath = path.resolve(bindingDirname, processorPath); + const { process, name, description } = await import(resolvedProcessorPath); -type ParsedBinding = z.infer; + if (!process || typeof process !== "function") { + throw new MuseError( + `Processor at ${processorPath} does not export a process() function`, + ); + } + const processorName = String(name ?? "Unnamed Processor"); + const processorDescription = String(description ?? "No description provided"); + + return { + filePath: resolvedProcessorPath, + process: process as MuseProcessor["process"], + name: processorName, + description: processorDescription, + }; +} /** - * Given a path, find the binding.yaml file - * If the path is to a directory, check for a binding.yaml inside - * If the path is to a file, check if it exists and is a binding.yaml + * Given a path, find the binding file + * If the path is to a directory, check for a binding inside + * Check order: binding.json, binding.yaml, binding.toml, binding.md * Otherwise throw an error * * @param bindingPath - The path to the binding file @@ -30,45 +46,50 @@ export async function resolveBindingPath(bindingPath: string): Promise { // If the path does not specify a filename, use binding.yaml const inputLocation = Bun.file(bindingPath); - // If it is a directory, try again seeking a binding.yaml inside + // If it is a directory, try for the four main supported types const stat = await inputLocation.stat(); if (stat.isDirectory()) { - return resolveBindingPath(path.resolve(bindingPath, "binding.yaml")); - } + const jsonPath = await resolveFilePath( + path.resolve(bindingPath, "binding.json"), + ); + if (jsonPath) { + return jsonPath; + } + + const yamlPath = await resolveFilePath( + path.resolve(bindingPath, "binding.yaml"), + ); + if (yamlPath) { + return yamlPath; + } + + const tomlPath = await resolveFilePath( + path.resolve(bindingPath, "binding.toml"), + ); + if (tomlPath) { + return tomlPath; + } + + const mdPath = await resolveFilePath( + path.resolve(bindingPath, "binding.md"), + ); + if (mdPath) { + return mdPath; + } - // If it doesnt exist, bail - if (!(await inputLocation.exists())) { throw new FileNotFoundError(bindingPath); } - // Resolve the path + const exists = await inputLocation.exists(); + + if (!exists) { + throw new FileNotFoundError(bindingPath); + } + + // If it is a file, return the path return path.resolve(bindingPath); } -export type MuseProcessorFn = ( - binding: Binding, - opts: Record, -) => Promise; - -export interface MuseProcessor { - readonly filePath: string; - readonly name: string; - readonly description: string; - readonly process: MuseProcessorFn; -} - -export interface Binding { - readonly _raw: ParsedBinding; - readonly bindingPath: string; - readonly markdownKey: string; - readonly includedFiles: string[]; - readonly entries: MuseEntry[]; - readonly meta: Record; - readonly options: Record; - readonly processors: MuseProcessor[]; - readonly imports: Binding[]; -} - export async function loadBinding(initialPath: string): Promise { // Resolve the file location if possible const bindingPath = await resolveBindingPath(initialPath); @@ -95,68 +116,41 @@ export async function loadBinding(initialPath: string): Promise { entries.push(...loadedBinding.entries); } - // Collect file manifest from globs - const allFilePaths: string[] = []; - for (const thisGlob of parsedBinding.files) { - const glob = new Glob(thisGlob); + // Aggregate file paths to import + const includedFilePaths = parsedBinding.files + .flatMap((thisGlob) => { + return Array.from( + new Glob(thisGlob).scanSync({ + cwd: bindingDirname, + absolute: true, + followSymlinks: true, + onlyFiles: true, + }), + ); + }) + .filter((filePath) => filePath !== bindingPath); - const results = glob.scanSync({ - cwd: bindingDirname, - absolute: true, - followSymlinks: true, - onlyFiles: true, - }); + // Load files + const loadedEntries = await Promise.all( + includedFilePaths.map((filePath) => { + return parseMuseFile(filePath, { + contentKey: parsedBinding.contentKey, + }); + }), + ); - allFilePaths.push(...results); - } - - // Exclude the binding.yaml file - const includedFilePaths = allFilePaths.filter((filePath) => { - return filePath !== bindingPath; - }); - - const fileLoadPromises: Promise[] = []; - for (const filePath of includedFilePaths) { - fileLoadPromises.push( - parseMuseFile(filePath, { - markdownKey: parsedBinding.markdownKey, - }), - ); - } - - const mainEntries = await Promise.all(fileLoadPromises); - entries.push(...mainEntries.flat()); + // Add loaded entries to main list + entries.push(...loadedEntries.flat()); // Load and check processors - const processors: MuseProcessor[] = []; - for (const processorPath of parsedBinding.processors) { - const resolvedProcessorPath = path.resolve(bindingDirname, processorPath); - const { process, name, description } = await import(resolvedProcessorPath); - - if (!process || typeof process !== "function") { - throw new MuseError( - `Processor at ${processorPath} does not export a process() function`, - ); - } - - const processorName = String(name ?? "Unnamed Processor"); - const processorDescription = String( - description ?? "No description provided", - ); - - processors.push({ - filePath: resolvedProcessorPath, - process: process as MuseProcessor["process"], - name: processorName, - description: processorDescription, - }); - } + const processors: MuseProcessor[] = await Promise.all( + parsedBinding.processors.map(loadProcessor.bind(null, bindingDirname)), + ); return { _raw: parsedBinding, bindingPath, - markdownKey: parsedBinding.markdownKey, - includedFiles: includedFilePaths, + contentKey: parsedBinding.contentKey, entries, options: parsedBinding.options, processors: processors, diff --git a/src/index.ts b/src/cli.ts similarity index 54% rename from src/index.ts rename to src/cli.ts index 8b1c510..762d30e 100644 --- a/src/index.ts +++ b/src/cli.ts @@ -1,7 +1,9 @@ +import { parseArgs } from "node:util"; import chalk from "chalk"; -import { MuseError } from "./errors.ts"; -import { loadBinding, type Binding } from "./binding"; -import { type CLIArguments, parseCLIArguments, USAGE } from "./args"; +import { MuseError } from "./errors"; +import { loadBinding } from "./binding"; +import { version } from "../package.json"; +import type { Binding, CLIArguments } from "./types"; interface Loggers { log: (msg: string) => void; @@ -13,8 +15,62 @@ enum ExitCode { Error = 1, } +export const USAGE = ` +${chalk.bold("muse")} - Compile and process troves of data +${chalk.dim(`v${version}`)} + +Usage: + muse [/path/to/binding.yaml] + +Options: + --stdout -s Output final data to stdout + --verbose, -v Enable verbose logging + --help, -h Show this help message +`.trim(); + +/** + * Given an array of CLI arguments, parse them into a structured object + * + * @param argv The arguments to parse + * @returns The parsed CLI arguments + * + * @throws {CLIError} if the arguments are invalid + */ +export function parseCLIArguments(argv: string[]): CLIArguments { + const { values: options, positionals: args } = parseArgs({ + args: argv, + options: { + help: { + short: "h", + default: false, + type: "boolean", + }, + verbose: { + short: "v", + default: false, + type: "boolean", + }, + stdout: { + short: "s", + default: false, + type: "boolean", + }, + }, + strict: true, + allowPositionals: true, + }); + + return { + inputFilePath: args[0] ?? "./binding.yaml", + flags: { + help: options.help, + verbose: options.verbose, + stdout: options.stdout, + }, + }; +} async function processBinding( - { inputFilePath, options }: CLIArguments, + { inputFilePath, flags }: CLIArguments, { log, verbose }: Loggers, ) { // Load the binding @@ -37,7 +93,7 @@ async function processBinding( const { process, name } = processor; log(chalk.bold(`↪ ${name}`) + chalk.dim(` (${processor.description})`)); - const thisStep = await process(lastStep, binding.options); + const thisStep = await process(lastStep, binding.options, flags); processedSteps.push(thisStep); } @@ -48,7 +104,7 @@ async function processBinding( const finalState = processedSteps[processedSteps.length - 1]; const serialized = JSON.stringify(finalState.entries, null, 2); - if (options.stdout) { + if (flags.stdout) { console.log(serialized); } return ExitCode.Success; @@ -56,16 +112,16 @@ async function processBinding( async function main(): Promise { const cliArguments = parseCLIArguments(Bun.argv.slice(2)); - const { options } = cliArguments; + const { flags } = cliArguments; // If --help is specified, print usage and exit - if (options.help) { + if (flags.help) { console.log(USAGE); return ExitCode.Success; } - const logFn = !options.stdout ? console.log : () => {}; - const verboseFn = options.verbose + const logFn = !flags.stdout ? console.log : () => {}; + const verboseFn = flags.verbose ? (msg: string) => { console.log(chalk.dim(msg)); } diff --git a/src/errors.ts b/src/errors.ts index b92c44c..7c8c014 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -19,8 +19,6 @@ export class FileError extends MuseError { } } -export class MalformedResourceFileError extends FileError {} - export class FileNotFoundError extends FileError { constructor(filePath: string) { super("File not found", filePath); diff --git a/src/exports.ts b/src/exports.ts new file mode 100644 index 0000000..8a07b08 --- /dev/null +++ b/src/exports.ts @@ -0,0 +1,2 @@ +export type * from "./types"; +export const DEFAULT_CONTENT_KEY = "content"; diff --git a/src/muse-file.ts b/src/parse.ts similarity index 74% rename from src/muse-file.ts rename to src/parse.ts index 3243c5d..842f6d2 100644 --- a/src/muse-file.ts +++ b/src/parse.ts @@ -2,30 +2,7 @@ import { normalize, extname } from "node:path"; import YAML from "yaml"; import TOML from "smol-toml"; import EMDY from "@endeavorance/emdy"; - -type UnknownRecord = Record; -export type MuseFileType = "md" | "yaml" | "json" | "toml"; -export interface MuseEntry { - _raw: string; - filePath: string; - data: Record; - meta: Record; -} - -function parseFileType(ext: string): MuseFileType { - switch (ext) { - case "md": - return "md"; - case "yaml": - return "yaml"; - case "json": - return "json"; - case "toml": - return "toml"; - default: - throw new Error(`Unsupported file format: ${ext}`); - } -} +import type { MuseEntry, UnknownRecord } from "./types"; function parseYAMLEntries(text: string): UnknownRecord[] { const parsedDocs = YAML.parseAllDocuments(text); @@ -75,16 +52,16 @@ function parseTOMLEntries(text: string): UnknownRecord[] { } interface ParseMuseFileOptions { - markdownKey?: string; + contentKey?: string; } export async function parseMuseFile( rawFilePath: string, - { markdownKey = "content" }: ParseMuseFileOptions = {}, + { contentKey = "content" }: ParseMuseFileOptions = {}, ): Promise { const filePath = normalize(rawFilePath); const file = Bun.file(filePath); - const fileType = parseFileType(extname(filePath).slice(1)); + const fileType = extname(filePath).slice(1); const rawFileContent = await file.text(); const partial = { @@ -94,7 +71,7 @@ export async function parseMuseFile( }; if (fileType === "md") { - const parsed = EMDY.parse(rawFileContent, markdownKey); + const parsed = EMDY.parse(rawFileContent, contentKey); return [ { ...partial, diff --git a/src/types.ts b/src/types.ts index 21dce91..f9faa6c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,55 @@ -import type { Binding, MuseProcessorFn } from "./binding"; -import type { MuseEntry } from "./muse-file"; -export type { Binding, MuseProcessorFn, MuseEntry }; +import { z } from "zod"; + +export interface CLIFlags { + help: boolean; + verbose: boolean; + stdout: boolean; +} + +export interface CLIArguments { + inputFilePath: string; + flags: CLIFlags; +} + +export const BindingSchema = z.object({ + include: z.array(z.string()).default([]), + contentKey: z.string().default("content"), + files: z + .array(z.string()) + .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), + options: z.record(z.string(), z.any()).default({}), + processors: z.array(z.string()).default([]), +}); + +export interface Binding { + readonly _raw: z.infer; + readonly bindingPath: string; + readonly contentKey: string; + readonly entries: MuseEntry[]; + readonly meta: Record; + readonly options: Record; + readonly processors: MuseProcessor[]; + readonly imports: Binding[]; +} + +export type MuseProcessorFn = ( + binding: Binding, + opts: Record, + flags: CLIFlags, +) => Promise; + +export interface MuseProcessor { + readonly filePath: string; + readonly name: string; + readonly description: string; + readonly process: MuseProcessorFn; +} + +export interface MuseEntry { + _raw: string; + filePath: string; + data: Record; + meta: Record; +} + +export type UnknownRecord = Record; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 0000000..0c84a67 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,27 @@ +import { normalize } from "node:path"; + +/** + * Given a file path, ensure it is a file that exists + * Otherwise, return null + * + * @param filePath - The path to the file + * @returns A promise which resolves to the resolved file path or null + */ +export async function resolveFilePath( + filePath: string, +): Promise { + const normalized = normalize(filePath); + const file = Bun.file(normalized); + const exists = await file.exists(); + + if (!exists) { + return null; + } + + const stat = await file.stat(); + if (stat.isDirectory()) { + return null; + } + + return normalized; +} diff --git a/tsconfig.json b/tsconfig.json index 0d2aab2..f2b0046 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,7 @@ "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", "paths": {}, - "outDir": "./types", + "outDir": "./dist", }, "include": [ "src/**/*.ts", diff --git a/types/muse.d.ts b/types/muse.d.ts deleted file mode 100644 index 88fdc98..0000000 --- a/types/muse.d.ts +++ /dev/null @@ -1,62 +0,0 @@ -// Generated by dts-bundle-generator v9.5.1 - -import z from 'zod'; - -export type MuseFileType = "md" | "yaml" | "json" | "toml"; -export interface MuseFileContent { - type: MuseFileType; - text: string; - data: Record; -} -declare const BindingSchema: z.ZodObject<{ - include: z.ZodDefault>; - name: z.ZodDefault; - author: z.ZodDefault; - description: z.ZodDefault; - version: z.ZodDefault; - files: z.ZodDefault>; - options: z.ZodDefault>; - processors: z.ZodDefault>; -}, "strip", z.ZodTypeAny, { - options: Record; - include: string[]; - name: string; - author: string; - description: string; - version: string; - files: string[]; - processors: string[]; -}, { - options?: Record | undefined; - include?: string[] | undefined; - name?: string | undefined; - author?: string | undefined; - description?: string | undefined; - version?: string | undefined; - files?: string[] | undefined; - processors?: string[] | undefined; -}>; -export type ParsedBinding = z.infer; -export interface BindingMetadata { - name: string; - author: string; - description: string; - version: string; -} -export type MuseProcessorFn = (binding: Binding) => Promise; -export interface MuseProcessor { - readonly filePath: string; - readonly process: MuseProcessorFn; -} -export interface Binding { - readonly _raw: ParsedBinding; - readonly bindingPath: string; - readonly includedFiles: string[]; - readonly entries: MuseFileContent[]; - readonly metadata: BindingMetadata; - readonly options: Record; - readonly processors: MuseProcessor[]; - readonly imports: Binding[]; -} - -export {}; From f834accb8df6f391f54293232a0a58ac10ee3059 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 15:44:10 -0400 Subject: [PATCH 04/14] Update readme --- README.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/README.md b/README.md index b985fbb..df86874 100644 --- a/README.md +++ b/README.md @@ -44,3 +44,27 @@ processors: - second-processor ``` +The `binding` file can be any of the supported file types for Muse. If +the CLI is invoked with a directory instead of a file, Muse will look +for a binding file in the directory with the following order: + +1. `binding.json` +2. `binding.yaml` +3. `binding.toml` +4. `binding.md` + +## Markdown Files + +Muse supports loading and parsing Markdown files with optional YAML frontmatter. + +``` +--- +someKey: someValue +--- + +Your markdown content here +``` + +When loading markdown files, Muse will load the content of the file +into a key called `content` by default. This can be changed in the binding +configuration by setting a `contentKey` value as an override. From afbb0cbaa587060b6b2fabf009df8712abd678be Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Wed, 2 Apr 2025 20:43:46 -0400 Subject: [PATCH 05/14] Update package.json --- package.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index e9fae55..13a34be 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@proscenium/muse", + "name": "@endeavorance/muse", "version": "0.1.0", "module": "dist/index.js", "exports": { @@ -31,9 +31,10 @@ "fmt": "bunx --bun biome check --fix", "clean": "rm -rf dist", "types": "dts-bundle-generator -o dist/muse.d.ts --project ./tsconfig.json ./src/exports.ts", + "transpile": "bun build ./src/exports.ts --outfile ./dist/muse.js", "compile": "bun build ./src/cli.ts --outfile ./dist/muse --compile", - "build": "bun run clean && bun run compile && bun run types", - "build:install": "bun run build && mv muse ~/.local/bin/muse", + "compile:install": "bun run compile && mv ./dist/muse ~/.local/bin/muse", + "build": "bun run clean && bun run transpile && bun run types", "demo": "bun run ./src/cli.ts -- ./demo/main" } } From b5136c5d02bfa4fd5597811589ce77f5c39b34c3 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 09:36:56 -0400 Subject: [PATCH 06/14] Clean up exports --- src/binding.ts | 44 +++++++++++++++----------------------------- src/errors.ts | 6 +++--- src/exports.ts | 1 + src/types.ts | 37 +++++++++++++++++++++---------------- 4 files changed, 40 insertions(+), 48 deletions(-) diff --git a/src/binding.ts b/src/binding.ts index 9499757..c1f9e96 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,6 +1,6 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { FileNotFoundError, MuseError } from "./errors"; +import { MuseFileNotFoundError, MuseError } from "./errors"; import { parseMuseFile } from "./parse"; import { type Binding, @@ -33,6 +33,7 @@ async function loadProcessor( description: processorDescription, }; } + /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside @@ -42,7 +43,7 @@ async function loadProcessor( * @param bindingPath - The path to the binding file * @returns The absolute resolved path to the binding file */ -export async function resolveBindingPath(bindingPath: string): Promise { +async function resolveBindingPath(bindingPath: string): Promise { // If the path does not specify a filename, use binding.yaml const inputLocation = Bun.file(bindingPath); @@ -77,13 +78,13 @@ export async function resolveBindingPath(bindingPath: string): Promise { return mdPath; } - throw new FileNotFoundError(bindingPath); + throw new MuseFileNotFoundError(bindingPath); } const exists = await inputLocation.exists(); if (!exists) { - throw new FileNotFoundError(bindingPath); + throw new MuseFileNotFoundError(bindingPath); } // If it is a file, return the path @@ -103,21 +104,8 @@ export async function loadBinding(initialPath: string): Promise { // Load and parse the Binding YAML content const parsedBinding = BindingSchema.parse(loadedBindingData[0].data); - // Prepare collections to load into - const extendsFrom: Binding[] = []; - const entries: MuseEntry[] = []; - - // Process imported bindings and bring in their entries - for (const includePath of parsedBinding.include) { - const pathFromHere = path.resolve(bindingDirname, includePath); - const extendBindingPath = await resolveBindingPath(pathFromHere); - const loadedBinding = await loadBinding(extendBindingPath); - extendsFrom.push(loadedBinding); - entries.push(...loadedBinding.entries); - } - // Aggregate file paths to import - const includedFilePaths = parsedBinding.files + const includedFilePaths = parsedBinding.sources .flatMap((thisGlob) => { return Array.from( new Glob(thisGlob).scanSync({ @@ -131,16 +119,15 @@ export async function loadBinding(initialPath: string): Promise { .filter((filePath) => filePath !== bindingPath); // Load files - const loadedEntries = await Promise.all( - includedFilePaths.map((filePath) => { - return parseMuseFile(filePath, { - contentKey: parsedBinding.contentKey, - }); - }), - ); - - // Add loaded entries to main list - entries.push(...loadedEntries.flat()); + const entries: MuseEntry[] = ( + await Promise.all( + includedFilePaths.map((filePath) => { + return parseMuseFile(filePath, { + contentKey: parsedBinding.contentKey, + }); + }), + ) + ).flat(); // Load and check processors const processors: MuseProcessor[] = await Promise.all( @@ -154,7 +141,6 @@ export async function loadBinding(initialPath: string): Promise { entries, options: parsedBinding.options, processors: processors, - imports: extendsFrom, meta: {}, }; } diff --git a/src/errors.ts b/src/errors.ts index 7c8c014..d5184d7 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -4,7 +4,7 @@ export class MuseError extends Error { } } -export class FileError extends MuseError { +export class MuseFileError extends MuseError { filePath: string; details?: string; @@ -19,7 +19,7 @@ export class FileError extends MuseError { } } -export class FileNotFoundError extends FileError { +export class MuseFileNotFoundError extends MuseFileError { constructor(filePath: string) { super("File not found", filePath); this.message = "File not found"; @@ -27,4 +27,4 @@ export class FileNotFoundError extends FileError { } } -export class CLIError extends MuseError {} +export class CLIError extends MuseError { } diff --git a/src/exports.ts b/src/exports.ts index 8a07b08..a05b13f 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,2 +1,3 @@ export type * from "./types"; +export * from "./errors"; export const DEFAULT_CONTENT_KEY = "content"; diff --git a/src/types.ts b/src/types.ts index f9faa6c..7aabb16 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,7 @@ import { z } from "zod"; +export type UnknownRecord = Record; + export interface CLIFlags { help: boolean; verbose: boolean; @@ -12,32 +14,27 @@ export interface CLIArguments { } export const BindingSchema = z.object({ - include: z.array(z.string()).default([]), contentKey: z.string().default("content"), - files: z + sources: z .array(z.string()) .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), options: z.record(z.string(), z.any()).default({}), processors: z.array(z.string()).default([]), + renderer: z.string().optional(), }); -export interface Binding { - readonly _raw: z.infer; - readonly bindingPath: string; - readonly contentKey: string; - readonly entries: MuseEntry[]; - readonly meta: Record; - readonly options: Record; - readonly processors: MuseProcessor[]; - readonly imports: Binding[]; -} - export type MuseProcessorFn = ( binding: Binding, opts: Record, flags: CLIFlags, ) => Promise; +export type MuseRenderFn = ( + binding: Binding, + opts: UnknownRecord, + flags: CLIFlags, +) => Promise; + export interface MuseProcessor { readonly filePath: string; readonly name: string; @@ -45,11 +42,19 @@ export interface MuseProcessor { readonly process: MuseProcessorFn; } -export interface MuseEntry { +export interface MuseEntry { _raw: string; filePath: string; data: Record; - meta: Record; + meta: MetaShape; } -export type UnknownRecord = Record; +export interface Binding { + readonly _raw: z.infer; + readonly bindingPath: string; + readonly contentKey: string; + readonly entries: MuseEntry[]; + readonly processors: MuseProcessor[]; + readonly meta: MetaShape; + readonly options: UnknownRecord; +} From 3682a7a7633c15b18874d100b8327ba46c794153 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 09:47:19 -0400 Subject: [PATCH 07/14] Add support for processors over http --- biome.json | 5 +---- bunfig.toml | 1 + src/binding.ts | 6 ++++-- src/cli.ts | 4 ++-- src/errors.ts | 2 +- src/parse.ts | 6 +++--- src/preload/http-plugin.js | 40 ++++++++++++++++++++++++++++++++++++++ 7 files changed, 52 insertions(+), 12 deletions(-) create mode 100644 bunfig.toml create mode 100644 src/preload/http-plugin.js diff --git a/biome.json b/biome.json index 1814905..7988bf6 100644 --- a/biome.json +++ b/biome.json @@ -7,10 +7,7 @@ }, "files": { "ignoreUnknown": false, - "ignore": [ - "*.d.ts", - "*.json" - ], + "ignore": ["*.d.ts", "*.json"], "include": [ "./src/**/*.ts", "./src/**/*.tsx", diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..a7461c6 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1 @@ +preload = ["./src/preload/http-plugin.js"] diff --git a/src/binding.ts b/src/binding.ts index c1f9e96..ae2bcc2 100644 --- a/src/binding.ts +++ b/src/binding.ts @@ -1,6 +1,6 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { MuseFileNotFoundError, MuseError } from "./errors"; +import { MuseError, MuseFileNotFoundError } from "./errors"; import { parseMuseFile } from "./parse"; import { type Binding, @@ -14,7 +14,9 @@ async function loadProcessor( bindingDirname: string, processorPath: string, ): Promise { - const resolvedProcessorPath = path.resolve(bindingDirname, processorPath); + const resolvedProcessorPath = processorPath.startsWith("http") + ? processorPath + : path.resolve(bindingDirname, processorPath); const { process, name, description } = await import(resolvedProcessorPath); if (!process || typeof process !== "function") { diff --git a/src/cli.ts b/src/cli.ts index 762d30e..c8380ab 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,8 +1,8 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { MuseError } from "./errors"; -import { loadBinding } from "./binding"; import { version } from "../package.json"; +import { loadBinding } from "./binding"; +import { MuseError } from "./errors"; import type { Binding, CLIArguments } from "./types"; interface Loggers { diff --git a/src/errors.ts b/src/errors.ts index d5184d7..47d98d5 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -27,4 +27,4 @@ export class MuseFileNotFoundError extends MuseFileError { } } -export class CLIError extends MuseError { } +export class CLIError extends MuseError {} diff --git a/src/parse.ts b/src/parse.ts index 842f6d2..509bf63 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -1,7 +1,7 @@ -import { normalize, extname } from "node:path"; -import YAML from "yaml"; -import TOML from "smol-toml"; +import { extname, normalize } from "node:path"; import EMDY from "@endeavorance/emdy"; +import TOML from "smol-toml"; +import YAML from "yaml"; import type { MuseEntry, UnknownRecord } from "./types"; function parseYAMLEntries(text: string): UnknownRecord[] { diff --git a/src/preload/http-plugin.js b/src/preload/http-plugin.js new file mode 100644 index 0000000..bb7f732 --- /dev/null +++ b/src/preload/http-plugin.js @@ -0,0 +1,40 @@ +const rx_any = /./; +const rx_http = /^https?:\/\//; +const rx_relative_path = /^\.\.?\//; +const rx_absolute_path = /^\//; + +async function load_http_module(href) { + return fetch(href).then((response) => + response + .text() + .then((text) => + response.ok + ? { contents: text, loader: "js" } + : Promise.reject( + new Error(`Failed to load module '${href}'': ${text}`), + ), + ), + ); +} + +Bun.plugin({ + name: "http_imports", + setup(build) { + build.onResolve({ filter: rx_relative_path }, (args) => { + if (rx_http.test(args.importer)) { + return { path: new URL(args.path, args.importer).href }; + } + }); + build.onResolve({ filter: rx_absolute_path }, (args) => { + if (rx_http.test(args.importer)) { + return { path: new URL(args.path, args.importer).href }; + } + }); + build.onLoad({ filter: rx_any, namespace: "http" }, (args) => + load_http_module(`http:${args.path}`), + ); + build.onLoad({ filter: rx_any, namespace: "https" }, (args) => + load_http_module(`https:${args.path}`), + ); + }, +}); From 16660823ea040bf9f419b73f96614ab209eda9fa Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 10:16:08 -0400 Subject: [PATCH 08/14] Organize new setup --- demo/main/processors/announce-slam-dunks.js | 1 + demo/main/processors/scream.js | 1 + package.json | 4 +- src/{cli.ts => cli/index.ts} | 45 ++++---- src/{ => core}/binding.ts | 118 ++++++++++---------- src/{ => core}/parse.ts | 16 ++- src/core/processor.ts | 42 +++++++ src/errors.ts | 3 +- src/exports.ts | 4 +- src/types.ts | 57 ---------- tsconfig.json | 16 ++- 11 files changed, 165 insertions(+), 142 deletions(-) rename src/{cli.ts => cli/index.ts} (77%) rename src/{ => core}/binding.ts (51%) rename src/{ => core}/parse.ts (85%) create mode 100644 src/core/processor.ts diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js index 24cdb96..56824e5 100644 --- a/demo/main/processors/announce-slam-dunks.js +++ b/demo/main/processors/announce-slam-dunks.js @@ -1,4 +1,5 @@ export const name = "Announce Slam Dunks"; +export const description = "Get hype when a file has slam dunks"; export const process = (binding, { dunks }) => { const slamDunkKey = dunks?.key ?? "slamDunks"; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js index 11f177d..17e0bac 100644 --- a/demo/main/processors/scream.js +++ b/demo/main/processors/scream.js @@ -1,4 +1,5 @@ export const name = "Scream"; + export function process(binding) { const modifiedEntries = binding.entries.map(entry => { if (binding.markdownKey in entry.data) { diff --git a/package.json b/package.json index 13a34be..ba1d46c 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,9 @@ "clean": "rm -rf dist", "types": "dts-bundle-generator -o dist/muse.d.ts --project ./tsconfig.json ./src/exports.ts", "transpile": "bun build ./src/exports.ts --outfile ./dist/muse.js", - "compile": "bun build ./src/cli.ts --outfile ./dist/muse --compile", + "compile": "bun run clean && bun build ./src/cli/index.ts --outfile ./dist/muse --compile", "compile:install": "bun run compile && mv ./dist/muse ~/.local/bin/muse", "build": "bun run clean && bun run transpile && bun run types", - "demo": "bun run ./src/cli.ts -- ./demo/main" + "demo": "bun run ./src/cli/index.ts -- ./demo/main" } } diff --git a/src/cli.ts b/src/cli/index.ts similarity index 77% rename from src/cli.ts rename to src/cli/index.ts index c8380ab..6e631a0 100644 --- a/src/cli.ts +++ b/src/cli/index.ts @@ -1,9 +1,14 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { version } from "../package.json"; -import { loadBinding } from "./binding"; -import { MuseError } from "./errors"; -import type { Binding, CLIArguments } from "./types"; +import { type Binding, loadBinding } from "#core/binding"; +import { MuseError } from "#errors"; +import { version } from "../../package.json"; + +interface CLIFlags { + help: boolean; + verbose: boolean; + stdout: boolean; +} interface Loggers { log: (msg: string) => void; @@ -36,7 +41,7 @@ Options: * * @throws {CLIError} if the arguments are invalid */ -export function parseCLIArguments(argv: string[]): CLIArguments { +function parseCLIArguments(argv: string[]): [string, CLIFlags] { const { values: options, positionals: args } = parseArgs({ args: argv, options: { @@ -60,17 +65,12 @@ export function parseCLIArguments(argv: string[]): CLIArguments { allowPositionals: true, }); - return { - inputFilePath: args[0] ?? "./binding.yaml", - flags: { - help: options.help, - verbose: options.verbose, - stdout: options.stdout, - }, - }; + return [args[0] ?? "./", options]; } + async function processBinding( - { inputFilePath, flags }: CLIArguments, + inputFilePath: string, + flags: CLIFlags, { log, verbose }: Loggers, ) { // Load the binding @@ -90,10 +90,16 @@ async function processBinding( for (const processor of binding.processors) { const lastStep = processedSteps[processedSteps.length - 1]; - const { process, name } = processor; + const { process, name, description } = processor; - log(chalk.bold(`↪ ${name}`) + chalk.dim(` (${processor.description})`)); - const thisStep = await process(lastStep, binding.options, flags); + let processorLogLine = chalk.bold(`↪ ${name}`); + + if (description && description.length > 0) { + processorLogLine += chalk.dim(` (${description})`); + } + + log(processorLogLine); + const thisStep = await process(lastStep, binding.options); processedSteps.push(thisStep); } @@ -111,8 +117,7 @@ async function processBinding( } async function main(): Promise { - const cliArguments = parseCLIArguments(Bun.argv.slice(2)); - const { flags } = cliArguments; + const [inputFilePath, flags] = parseCLIArguments(Bun.argv.slice(2)); // If --help is specified, print usage and exit if (flags.help) { @@ -127,7 +132,7 @@ async function main(): Promise { } : () => {}; - const lastProcessResult = await processBinding(cliArguments, { + const lastProcessResult = await processBinding(inputFilePath, flags, { log: logFn, verbose: verboseFn, }); diff --git a/src/binding.ts b/src/core/binding.ts similarity index 51% rename from src/binding.ts rename to src/core/binding.ts index ae2bcc2..eee86b1 100644 --- a/src/binding.ts +++ b/src/core/binding.ts @@ -1,39 +1,49 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; -import { MuseError, MuseFileNotFoundError } from "./errors"; -import { parseMuseFile } from "./parse"; -import { - type Binding, - BindingSchema, - type MuseEntry, - type MuseProcessor, -} from "./types"; -import { resolveFilePath } from "./util"; +import { z } from "zod"; +import { MuseError, MuseFileNotFoundError } from "#errors"; +import type { UnknownRecord } from "#types"; +import { resolveFilePath } from "#util"; +import { type MuseEntry, parseMuseFile } from "./parse"; +import { type MuseProcessor, loadProcessor } from "./processor"; -async function loadProcessor( - bindingDirname: string, - processorPath: string, -): Promise { - const resolvedProcessorPath = processorPath.startsWith("http") - ? processorPath - : path.resolve(bindingDirname, processorPath); - const { process, name, description } = await import(resolvedProcessorPath); +// Function to parse an unknown object into parts of a binding +export const BindingSchema = z.object({ + contentKey: z.string().default("content"), + sources: z + .array(z.string()) + .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), + options: z.record(z.string(), z.any()).default({}), + processors: z.array(z.string()).default([]), + renderer: z.string().optional(), +}); - if (!process || typeof process !== "function") { - throw new MuseError( - `Processor at ${processorPath} does not export a process() function`, - ); - } +/** + * The core object of a Muse binding + * Contains information about the binding file, the configuration of + * the project, and a list of entries to be processed + */ +export interface Binding { + /** The raw data read from the binding file */ + readonly _raw: z.infer; - const processorName = String(name ?? "Unnamed Processor"); - const processorDescription = String(description ?? "No description provided"); + /** The full file path to the binding file */ + readonly bindingPath: string; - return { - filePath: resolvedProcessorPath, - process: process as MuseProcessor["process"], - name: processorName, - description: processorDescription, - }; + /** The key used to for default content in unstructured files */ + readonly contentKey: string; + + /** All entries loaded from the binding file */ + readonly entries: MuseEntry[]; + + /** A list of processors to be applied to the entries */ + readonly processors: MuseProcessor[]; + + /** Arbitrary metadata for processors to use to cache project data */ + readonly meta: MetaShape; + + /** Configuration options for Muse and all processors */ + readonly options: UnknownRecord; } /** @@ -46,47 +56,39 @@ async function loadProcessor( * @returns The absolute resolved path to the binding file */ async function resolveBindingPath(bindingPath: string): Promise { - // If the path does not specify a filename, use binding.yaml + const DEFAULT_BINDING_FILES = [ + "binding.json", + "binding.yaml", + "binding.toml", + "binding.md", + ] as const; const inputLocation = Bun.file(bindingPath); // If it is a directory, try for the four main supported types const stat = await inputLocation.stat(); if (stat.isDirectory()) { - const jsonPath = await resolveFilePath( - path.resolve(bindingPath, "binding.json"), - ); - if (jsonPath) { - return jsonPath; + for (const bindingFile of DEFAULT_BINDING_FILES) { + const filePath = path.resolve(bindingPath, bindingFile); + const resolvedPath = await resolveFilePath(filePath); + if (resolvedPath) { + return resolvedPath; + } } - const yamlPath = await resolveFilePath( - path.resolve(bindingPath, "binding.yaml"), + throw new MuseFileNotFoundError( + bindingPath, + "Failed to find a binding file at the provided directory.", ); - if (yamlPath) { - return yamlPath; - } - - const tomlPath = await resolveFilePath( - path.resolve(bindingPath, "binding.toml"), - ); - if (tomlPath) { - return tomlPath; - } - - const mdPath = await resolveFilePath( - path.resolve(bindingPath, "binding.md"), - ); - if (mdPath) { - return mdPath; - } - - throw new MuseFileNotFoundError(bindingPath); } + // If not a directory, check if its a file that exists const exists = await inputLocation.exists(); if (!exists) { - throw new MuseFileNotFoundError(bindingPath); + throw new MuseFileNotFoundError( + bindingPath, + "Provided binding path does not exist.", + ); } // If it is a file, return the path diff --git a/src/parse.ts b/src/core/parse.ts similarity index 85% rename from src/parse.ts rename to src/core/parse.ts index 509bf63..128de7f 100644 --- a/src/parse.ts +++ b/src/core/parse.ts @@ -2,7 +2,15 @@ import { extname, normalize } from "node:path"; import EMDY from "@endeavorance/emdy"; import TOML from "smol-toml"; import YAML from "yaml"; -import type { MuseEntry, UnknownRecord } from "./types"; +import type { UnknownRecord } from "#types"; +import { MuseFileNotFoundError } from "#errors"; + +export interface MuseEntry { + _raw: string; + filePath: string; + data: Record; + meta: MetaShape; +} function parseYAMLEntries(text: string): UnknownRecord[] { const parsedDocs = YAML.parseAllDocuments(text); @@ -62,6 +70,12 @@ export async function parseMuseFile( const filePath = normalize(rawFilePath); const file = Bun.file(filePath); const fileType = extname(filePath).slice(1); + + const fileExists = await file.exists(); + if (!fileExists) { + throw new MuseFileNotFoundError(filePath, "Failed to load source file."); + } + const rawFileContent = await file.text(); const partial = { diff --git a/src/core/processor.ts b/src/core/processor.ts new file mode 100644 index 0000000..1df9753 --- /dev/null +++ b/src/core/processor.ts @@ -0,0 +1,42 @@ +import path from "node:path"; +import { MuseError } from "#errors"; +import type { Binding } from "./binding"; + +export type MuseProcessorFn = ( + binding: Binding, + opts: Record, +) => Promise; + +export interface MuseProcessor { + readonly filePath: string; + readonly name: string; + readonly description: string; + readonly process: MuseProcessorFn; +} + +export async function loadProcessor( + bindingDirname: string, + processorPath: string, +): Promise { + // Resolve local paths, use URLs as-is + const resolvedProcessorPath = processorPath.startsWith("http") + ? processorPath + : path.resolve(bindingDirname, processorPath); + const { process, name, description } = await import(resolvedProcessorPath); + + if (!process || typeof process !== "function") { + throw new MuseError( + `Processor at ${processorPath} does not export a process() function`, + ); + } + + const processorName = String(name ?? "Unnamed Processor"); + const processorDescription = String(description ?? ""); + + return { + filePath: resolvedProcessorPath, + process: process as MuseProcessor["process"], + name: processorName, + description: processorDescription, + }; +} diff --git a/src/errors.ts b/src/errors.ts index 47d98d5..9ad54ac 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -20,10 +20,11 @@ export class MuseFileError extends MuseError { } export class MuseFileNotFoundError extends MuseFileError { - constructor(filePath: string) { + constructor(filePath: string, details?: string) { super("File not found", filePath); this.message = "File not found"; this.filePath = filePath; + this.details = details ?? "No additional information."; } } diff --git a/src/exports.ts b/src/exports.ts index a05b13f..65319af 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,3 +1,3 @@ -export type * from "./types"; -export * from "./errors"; +export type * from "#types"; +export * from "#errors"; export const DEFAULT_CONTENT_KEY = "content"; diff --git a/src/types.ts b/src/types.ts index 7aabb16..b57ec2f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,60 +1,3 @@ import { z } from "zod"; export type UnknownRecord = Record; - -export interface CLIFlags { - help: boolean; - verbose: boolean; - stdout: boolean; -} - -export interface CLIArguments { - inputFilePath: string; - flags: CLIFlags; -} - -export const BindingSchema = z.object({ - contentKey: z.string().default("content"), - sources: z - .array(z.string()) - .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), - options: z.record(z.string(), z.any()).default({}), - processors: z.array(z.string()).default([]), - renderer: z.string().optional(), -}); - -export type MuseProcessorFn = ( - binding: Binding, - opts: Record, - flags: CLIFlags, -) => Promise; - -export type MuseRenderFn = ( - binding: Binding, - opts: UnknownRecord, - flags: CLIFlags, -) => Promise; - -export interface MuseProcessor { - readonly filePath: string; - readonly name: string; - readonly description: string; - readonly process: MuseProcessorFn; -} - -export interface MuseEntry { - _raw: string; - filePath: string; - data: Record; - meta: MetaShape; -} - -export interface Binding { - readonly _raw: z.infer; - readonly bindingPath: string; - readonly contentKey: string; - readonly entries: MuseEntry[]; - readonly processors: MuseProcessor[]; - readonly meta: MetaShape; - readonly options: UnknownRecord; -} diff --git a/tsconfig.json b/tsconfig.json index f2b0046..0f25433 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,11 +26,25 @@ "noUnusedParameters": false, "noPropertyAccessFromIndexSignature": false, "baseUrl": "./src", - "paths": {}, + "paths": { + "#core/*": [ + "./core/*.ts" + ], + "#util": [ + "./util.ts" + ], + "#types": [ + "./types.ts" + ], + "#errors": [ + "./errors.ts" + ] + }, "outDir": "./dist", }, "include": [ "src/**/*.ts", "src/**/*.tsx", + "src/preload/http-plugin.js", ] } From 504c1e6f3c340235d2ca4e38a17c683555a294ba Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 10:16:48 -0400 Subject: [PATCH 09/14] 0.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ba1d46c..36bf54a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endeavorance/muse", - "version": "0.1.0", + "version": "0.2.0", "module": "dist/index.js", "exports": { ".": { From ae1b9e262e2967516bb6927f0544a25bf6f249f6 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Thu, 3 Apr 2025 14:55:47 -0400 Subject: [PATCH 10/14] Update plugin shape --- demo/main/processors/announce-slam-dunks.js | 11 ++- demo/main/processors/scream.js | 4 +- src/cli/index.ts | 51 +++++++------- src/core/binding.ts | 24 +++---- src/core/files.ts | 32 +++++++++ src/core/parse.ts | 75 +++++++++------------ src/core/plugins.ts | 52 ++++++++++++++ src/core/processor.ts | 42 ------------ src/{ => core}/types.ts | 2 - src/errors.ts | 26 ++++--- src/exports.ts | 8 ++- tsconfig.json | 3 - 12 files changed, 181 insertions(+), 149 deletions(-) create mode 100644 src/core/files.ts create mode 100644 src/core/plugins.ts delete mode 100644 src/core/processor.ts rename src/{ => core}/types.ts (67%) diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js index 56824e5..e1889a3 100644 --- a/demo/main/processors/announce-slam-dunks.js +++ b/demo/main/processors/announce-slam-dunks.js @@ -1,14 +1,19 @@ export const name = "Announce Slam Dunks"; export const description = "Get hype when a file has slam dunks"; -export const process = (binding, { dunks }) => { +export const step = ({ entries, options, ...rest }) => { + const { dunks } = options; const slamDunkKey = dunks?.key ?? "slamDunks"; - for (const entry of binding.entries) { + for (const entry of entries) { if (slamDunkKey in entry.data) { console.log(`Slam dunk!`); } } - return binding; + return { + entries, + options, + ...rest, + }; }; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js index 17e0bac..4a8a629 100644 --- a/demo/main/processors/scream.js +++ b/demo/main/processors/scream.js @@ -1,6 +1,4 @@ -export const name = "Scream"; - -export function process(binding) { +export function step(binding) { const modifiedEntries = binding.entries.map(entry => { if (binding.markdownKey in entry.data) { entry.data = { diff --git a/src/cli/index.ts b/src/cli/index.ts index 6e631a0..edb3d9d 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -1,6 +1,7 @@ import { parseArgs } from "node:util"; import chalk from "chalk"; -import { type Binding, loadBinding } from "#core/binding"; +import { loadBinding } from "#core/binding"; +import type { MusePlugin } from "#core/plugins"; import { MuseError } from "#errors"; import { version } from "../../package.json"; @@ -68,50 +69,46 @@ function parseCLIArguments(argv: string[]): [string, CLIFlags] { return [args[0] ?? "./", options]; } +function getStepLogLine(plugin: MusePlugin): string { + const { name, description } = plugin; + let processorLogLine = chalk.bold(`↪ ${name}`); + + if (description && description.length > 0) { + processorLogLine += chalk.dim(` (${description})`); + } + + return processorLogLine; +} + async function processBinding( inputFilePath: string, flags: CLIFlags, { log, verbose }: Loggers, ) { // Load the binding - const binding = await loadBinding(inputFilePath); - + let binding = await loadBinding(inputFilePath); verbose(`Binding ${binding.bindingPath}`); - const stepWord = binding.processors.length === 1 ? "step" : "steps"; - log( - `Processing ${binding.entries.length} entries with ${binding.processors.length} ${stepWord}`, - ); + const entryCount = binding.entries.length; + const stepCount = binding.plugins.length; + const stepWord = stepCount === 1 ? "step" : "steps"; + log(`Processing ${entryCount} entries with ${stepCount} ${stepWord}`); const processStart = performance.now(); - // Run the data through all processors - const processedSteps: Binding[] = [binding]; - for (const processor of binding.processors) { - const lastStep = processedSteps[processedSteps.length - 1]; - - const { process, name, description } = processor; - - let processorLogLine = chalk.bold(`↪ ${name}`); - - if (description && description.length > 0) { - processorLogLine += chalk.dim(` (${description})`); - } - - log(processorLogLine); - const thisStep = await process(lastStep, binding.options); - processedSteps.push(thisStep); + // Run the data through relevant plugins + for (const plugin of binding.plugins) { + log(getStepLogLine(plugin)); + const { step } = plugin; + binding = await step(binding); } const processEnd = performance.now(); const processTime = ((processEnd - processStart) / 1000).toFixed(2); verbose(`Processing completed in ${processTime}s`); - const finalState = processedSteps[processedSteps.length - 1]; - const serialized = JSON.stringify(finalState.entries, null, 2); - if (flags.stdout) { - console.log(serialized); + console.log(JSON.stringify(binding, null, 2)); } return ExitCode.Success; } diff --git a/src/core/binding.ts b/src/core/binding.ts index eee86b1..b78292d 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -2,10 +2,10 @@ import path, { dirname } from "node:path"; import { Glob } from "bun"; import { z } from "zod"; import { MuseError, MuseFileNotFoundError } from "#errors"; -import type { UnknownRecord } from "#types"; import { resolveFilePath } from "#util"; import { type MuseEntry, parseMuseFile } from "./parse"; -import { type MuseProcessor, loadProcessor } from "./processor"; +import { type MusePlugin, loadPlugin } from "./plugins"; +import type { UnknownRecord } from "./types"; // Function to parse an unknown object into parts of a binding export const BindingSchema = z.object({ @@ -37,7 +37,7 @@ export interface Binding { readonly entries: MuseEntry[]; /** A list of processors to be applied to the entries */ - readonly processors: MuseProcessor[]; + readonly plugins: MusePlugin[]; /** Arbitrary metadata for processors to use to cache project data */ readonly meta: MetaShape; @@ -75,20 +75,14 @@ async function resolveBindingPath(bindingPath: string): Promise { } } - throw new MuseFileNotFoundError( - bindingPath, - "Failed to find a binding file at the provided directory.", - ); + throw new MuseFileNotFoundError(bindingPath); } // If not a directory, check if its a file that exists const exists = await inputLocation.exists(); if (!exists) { - throw new MuseFileNotFoundError( - bindingPath, - "Provided binding path does not exist.", - ); + throw new MuseFileNotFoundError(bindingPath); } // If it is a file, return the path @@ -133,9 +127,9 @@ export async function loadBinding(initialPath: string): Promise { ) ).flat(); - // Load and check processors - const processors: MuseProcessor[] = await Promise.all( - parsedBinding.processors.map(loadProcessor.bind(null, bindingDirname)), + // Load and check plugins + const plugins: MusePlugin[] = await Promise.all( + parsedBinding.processors.map(loadPlugin.bind(null, bindingDirname)), ); return { @@ -144,7 +138,7 @@ export async function loadBinding(initialPath: string): Promise { contentKey: parsedBinding.contentKey, entries, options: parsedBinding.options, - processors: processors, + plugins, meta: {}, }; } diff --git a/src/core/files.ts b/src/core/files.ts new file mode 100644 index 0000000..a32b877 --- /dev/null +++ b/src/core/files.ts @@ -0,0 +1,32 @@ +import { basename, dirname, extname, normalize } from "node:path"; +import { MuseFileNotFoundError } from "#errors"; + +export interface LoadedFile { + content: string; + filePath: string; + fileType: string; + dirname: string; + basename: string; +} + +export async function loadFileContent(filePath: string): Promise { + const normalizedPath = normalize(filePath); + const file = Bun.file(normalizedPath); + + const exists = await file.exists(); + if (!exists) { + throw new MuseFileNotFoundError(normalizedPath); + } + + const filecontent = await file.text(); + + const extension = extname(normalizedPath); + + return { + content: filecontent, + filePath: normalizedPath, + fileType: extension.slice(1), + dirname: dirname(normalizedPath), + basename: basename(normalizedPath, extension), + }; +} diff --git a/src/core/parse.ts b/src/core/parse.ts index 128de7f..6617db2 100644 --- a/src/core/parse.ts +++ b/src/core/parse.ts @@ -1,9 +1,8 @@ -import { extname, normalize } from "node:path"; import EMDY from "@endeavorance/emdy"; import TOML from "smol-toml"; import YAML from "yaml"; -import type { UnknownRecord } from "#types"; -import { MuseFileNotFoundError } from "#errors"; +import { loadFileContent } from "./files"; +import type { UnknownRecord } from "./types"; export interface MuseEntry { _raw: string; @@ -12,6 +11,20 @@ export interface MuseEntry { meta: MetaShape; } +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + function parseYAMLEntries(text: string): UnknownRecord[] { const parsedDocs = YAML.parseAllDocuments(text); @@ -32,31 +45,15 @@ function parseYAMLEntries(text: string): UnknownRecord[] { } function parseJSONEntries(text: string): UnknownRecord[] { - const parsed = JSON.parse(text); - - if (Array.isArray(parsed)) { - return parsed; - } - - if (typeof parsed === "object") { - return [parsed]; - } - - throw new Error("JSON resource must be an object or an array of objects"); + return formatEntries(JSON.parse(text)); } function parseTOMLEntries(text: string): UnknownRecord[] { - const parsed = TOML.parse(text); + return formatEntries(TOML.parse(text)); +} - if (Array.isArray(parsed)) { - return parsed; - } - - if (typeof parsed === "object") { - return [parsed]; - } - - throw new Error("TOML resource must be an object or an array of objects"); +function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); } interface ParseMuseFileOptions { @@ -67,49 +64,37 @@ export async function parseMuseFile( rawFilePath: string, { contentKey = "content" }: ParseMuseFileOptions = {}, ): Promise { - const filePath = normalize(rawFilePath); - const file = Bun.file(filePath); - const fileType = extname(filePath).slice(1); - - const fileExists = await file.exists(); - if (!fileExists) { - throw new MuseFileNotFoundError(filePath, "Failed to load source file."); - } - - const rawFileContent = await file.text(); + const { content, filePath, fileType } = await loadFileContent(rawFilePath); const partial = { - _raw: rawFileContent, + _raw: content, filePath, meta: {}, }; if (fileType === "md") { - const parsed = EMDY.parse(rawFileContent, contentKey); - return [ - { - ...partial, - data: parsed, - }, - ]; + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); } if (fileType === "yaml") { - return parseYAMLEntries(rawFileContent).map((data) => ({ + return parseYAMLEntries(content).map((data) => ({ ...partial, data, })); } if (fileType === "json") { - return parseJSONEntries(rawFileContent).map((data) => ({ + return parseJSONEntries(content).map((data) => ({ ...partial, data, })); } if (fileType === "toml") { - return parseTOMLEntries(rawFileContent).map((data) => ({ + return parseTOMLEntries(content).map((data) => ({ ...partial, data, })); diff --git a/src/core/plugins.ts b/src/core/plugins.ts new file mode 100644 index 0000000..3f2ffed --- /dev/null +++ b/src/core/plugins.ts @@ -0,0 +1,52 @@ +import path from "node:path"; +import { MuseError, MusePluginError } from "#errors"; +import type { Binding } from "./binding"; + +export type MuseStepFn = (binding: Binding) => Promise; + +export interface MusePlugin { + readonly filePath: string; + readonly name: string; + readonly description: string; + readonly version: string; + readonly step: MuseStepFn; +} + +export async function loadPlugin( + bindingDirname: string, + pluginPath: string, +): Promise { + // Resolve local paths, use URLs as-is + const resolvedProcessorPath = pluginPath.startsWith("http") + ? pluginPath + : path.resolve(bindingDirname, pluginPath); + const { step, name, description, version } = await import( + resolvedProcessorPath + ); + + if (!step || typeof step !== "function") { + throw new MuseError( + `Processor at ${pluginPath} does not export a step() function`, + ); + } + + if (name && typeof name !== "string") { + throw new MusePluginError(pluginPath, "Plugin name is not a string."); + } + + if (description && typeof description !== "string") { + throw new MusePluginError(pluginPath, "Plugin description is not a string"); + } + + if (version && typeof version !== "string") { + throw new MusePluginError(pluginPath, "Plugin version is not a string"); + } + + return { + filePath: resolvedProcessorPath, + step: step as MuseStepFn, + name: String(name ?? pluginPath), + description: String(description ?? ""), + version: String(version ?? "0.0.0"), + }; +} diff --git a/src/core/processor.ts b/src/core/processor.ts deleted file mode 100644 index 1df9753..0000000 --- a/src/core/processor.ts +++ /dev/null @@ -1,42 +0,0 @@ -import path from "node:path"; -import { MuseError } from "#errors"; -import type { Binding } from "./binding"; - -export type MuseProcessorFn = ( - binding: Binding, - opts: Record, -) => Promise; - -export interface MuseProcessor { - readonly filePath: string; - readonly name: string; - readonly description: string; - readonly process: MuseProcessorFn; -} - -export async function loadProcessor( - bindingDirname: string, - processorPath: string, -): Promise { - // Resolve local paths, use URLs as-is - const resolvedProcessorPath = processorPath.startsWith("http") - ? processorPath - : path.resolve(bindingDirname, processorPath); - const { process, name, description } = await import(resolvedProcessorPath); - - if (!process || typeof process !== "function") { - throw new MuseError( - `Processor at ${processorPath} does not export a process() function`, - ); - } - - const processorName = String(name ?? "Unnamed Processor"); - const processorDescription = String(description ?? ""); - - return { - filePath: resolvedProcessorPath, - process: process as MuseProcessor["process"], - name: processorName, - description: processorDescription, - }; -} diff --git a/src/types.ts b/src/core/types.ts similarity index 67% rename from src/types.ts rename to src/core/types.ts index b57ec2f..35ba466 100644 --- a/src/types.ts +++ b/src/core/types.ts @@ -1,3 +1 @@ -import { z } from "zod"; - export type UnknownRecord = Record; diff --git a/src/errors.ts b/src/errors.ts index 9ad54ac..5f43323 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,30 +1,40 @@ export class MuseError extends Error { fmt(): string { - return this.message; + return `-- ERROR -- \nDetails: ${this.message}`; + } +} + +export class MusePluginError extends MuseError { + plugin: string; + + constructor(message: string, plugin: string) { + super(message); + this.plugin = plugin; + } + + fmt(): string { + return `-- PLUGIN ERROR --\Plugin: ${this.plugin}\nDetails: ${this.message}`; } } export class MuseFileError extends MuseError { filePath: string; - details?: string; - constructor(message: string, filePath: string, details?: string) { + constructor(message: string, filePath: string) { super(message); this.filePath = filePath; - this.details = details; } fmt(): string { - return `-- ${this.message} --\nFile Path: ${this.filePath}\nDetails: ${this.details}`; + return `-- FILE ERROR --\nFile Path: ${this.filePath}\nDetails: ${this.message}`; } } export class MuseFileNotFoundError extends MuseFileError { - constructor(filePath: string, details?: string) { + constructor(filePath: string) { super("File not found", filePath); - this.message = "File not found"; + this.message = "File does not exist"; this.filePath = filePath; - this.details = details ?? "No additional information."; } } diff --git a/src/exports.ts b/src/exports.ts index 65319af..f520a46 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -1,3 +1,9 @@ -export type * from "#types"; +// Content exports export * from "#errors"; + +// Constants export const DEFAULT_CONTENT_KEY = "content"; + +// Types +export type * from "#types"; +export type { MusePlugin } from "#core/plugins"; diff --git a/tsconfig.json b/tsconfig.json index 0f25433..a964c9c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -33,9 +33,6 @@ "#util": [ "./util.ts" ], - "#types": [ - "./types.ts" - ], "#errors": [ "./errors.ts" ] From 5b90a8e1b32e8f7e11644e7cca36350e80a51dd5 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 13:50:43 -0400 Subject: [PATCH 11/14] Support for url based resources --- demo/main/binding.md | 3 + src/core/binding.ts | 94 ++++++++++------ src/core/files.ts | 2 + src/core/parse.ts | 104 ------------------ src/core/sources.ts | 254 +++++++++++++++++++++++++++++++++++++++++++ src/muse.ts | 20 ++++ 6 files changed, 342 insertions(+), 135 deletions(-) delete mode 100644 src/core/parse.ts create mode 100644 src/core/sources.ts create mode 100644 src/muse.ts diff --git a/demo/main/binding.md b/demo/main/binding.md index b660a51..cb097e7 100644 --- a/demo/main/binding.md +++ b/demo/main/binding.md @@ -3,6 +3,9 @@ markdownKey: description options: dunks: key: dunkaroos +sources: + - file: ./stats/*.toml + - url: https://git.astral.camp/endeavorance/emdy/raw/branch/main/package.json processors: - ./processors/scream.js - ./processors/announce-slam-dunks.js diff --git a/src/core/binding.ts b/src/core/binding.ts index b78292d..7db7c5e 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -1,21 +1,37 @@ import path, { dirname } from "node:path"; -import { Glob } from "bun"; import { z } from "zod"; import { MuseError, MuseFileNotFoundError } from "#errors"; import { resolveFilePath } from "#util"; -import { type MuseEntry, parseMuseFile } from "./parse"; +import { + type MuseEntry, + type MuseSource, + loadFromSource, + parseMuseFile, +} from "./sources"; import { type MusePlugin, loadPlugin } from "./plugins"; import type { UnknownRecord } from "./types"; +const SourceSchema = z.union([ + z.object({ + file: z.string(), + }), + z.object({ + url: z.string().url(), + }), +]); + +type SourceShorthand = z.infer; + // Function to parse an unknown object into parts of a binding -export const BindingSchema = z.object({ +const BindingSchema = z.object({ contentKey: z.string().default("content"), - sources: z - .array(z.string()) - .default(["**/*.yaml", "**/*.md", "**/*.toml", "**/*.json"]), + sources: z.array(SourceSchema).default([ + { + file: "./**/*.{json,yaml,toml,md}", + }, + ]), options: z.record(z.string(), z.any()).default({}), processors: z.array(z.string()).default([]), - renderer: z.string().optional(), }); /** @@ -27,6 +43,9 @@ export interface Binding { /** The raw data read from the binding file */ readonly _raw: z.infer; + /** Information about the sources used to load entries */ + readonly sources: MuseSource[]; + /** The full file path to the binding file */ readonly bindingPath: string; @@ -46,6 +65,24 @@ export interface Binding { readonly options: UnknownRecord; } +function sourceShorthandToSource(shorthand: SourceShorthand): MuseSource { + if ("file" in shorthand) { + return { + location: shorthand.file, + type: "file", + }; + } + + if ("url" in shorthand) { + return { + location: shorthand.url, + type: "url", + }; + } + + throw new MuseError("Invalid source shorthand"); +} + /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside @@ -93,7 +130,11 @@ export async function loadBinding(initialPath: string): Promise { // Resolve the file location if possible const bindingPath = await resolveBindingPath(initialPath); const bindingDirname = dirname(bindingPath); - const loadedBindingData = await parseMuseFile(bindingPath); + const loadedBindingData = await parseMuseFile(bindingPath, { + contentKey: "content", + cwd: bindingDirname, + ignore: [], + }); if (loadedBindingData.length === 0) { throw new MuseError("No entries found in binding file"); @@ -102,30 +143,20 @@ export async function loadBinding(initialPath: string): Promise { // Load and parse the Binding YAML content const parsedBinding = BindingSchema.parse(loadedBindingData[0].data); - // Aggregate file paths to import - const includedFilePaths = parsedBinding.sources - .flatMap((thisGlob) => { - return Array.from( - new Glob(thisGlob).scanSync({ - cwd: bindingDirname, - absolute: true, - followSymlinks: true, - onlyFiles: true, - }), - ); - }) - .filter((filePath) => filePath !== bindingPath); + const sourceOptions = { + cwd: bindingDirname, + contentKey: parsedBinding.contentKey, + ignore: [bindingPath], + }; - // Load files - const entries: MuseEntry[] = ( - await Promise.all( - includedFilePaths.map((filePath) => { - return parseMuseFile(filePath, { - contentKey: parsedBinding.contentKey, - }); - }), - ) - ).flat(); + const sources = parsedBinding.sources.map(sourceShorthandToSource); + + const entries: MuseEntry[] = []; + + for (const source of sources) { + const entriesFromSource = await loadFromSource(source, sourceOptions); + entries.push(...entriesFromSource); + } // Load and check plugins const plugins: MusePlugin[] = await Promise.all( @@ -134,6 +165,7 @@ export async function loadBinding(initialPath: string): Promise { return { _raw: parsedBinding, + sources, bindingPath, contentKey: parsedBinding.contentKey, entries, diff --git a/src/core/files.ts b/src/core/files.ts index a32b877..89b2817 100644 --- a/src/core/files.ts +++ b/src/core/files.ts @@ -7,6 +7,7 @@ export interface LoadedFile { fileType: string; dirname: string; basename: string; + mimeType: string; } export async function loadFileContent(filePath: string): Promise { @@ -28,5 +29,6 @@ export async function loadFileContent(filePath: string): Promise { fileType: extension.slice(1), dirname: dirname(normalizedPath), basename: basename(normalizedPath, extension), + mimeType: file.type, }; } diff --git a/src/core/parse.ts b/src/core/parse.ts deleted file mode 100644 index 6617db2..0000000 --- a/src/core/parse.ts +++ /dev/null @@ -1,104 +0,0 @@ -import EMDY from "@endeavorance/emdy"; -import TOML from "smol-toml"; -import YAML from "yaml"; -import { loadFileContent } from "./files"; -import type { UnknownRecord } from "./types"; - -export interface MuseEntry { - _raw: string; - filePath: string; - data: Record; - meta: MetaShape; -} - -function formatEntries(val: unknown): UnknownRecord[] { - if (Array.isArray(val) && val.every((el) => typeof el === "object")) { - return val; - } - - if (typeof val === "object" && val !== null) { - return [val as UnknownRecord]; - } - - throw new Error( - `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, - ); -} - -function parseYAMLEntries(text: string): UnknownRecord[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new Error("Encountered NULL resource"); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new Error( - `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, - ); - } - - const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); - return collection; -} - -function parseJSONEntries(text: string): UnknownRecord[] { - return formatEntries(JSON.parse(text)); -} - -function parseTOMLEntries(text: string): UnknownRecord[] { - return formatEntries(TOML.parse(text)); -} - -function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { - return formatEntries(EMDY.parse(text, contentKey)); -} - -interface ParseMuseFileOptions { - contentKey?: string; -} - -export async function parseMuseFile( - rawFilePath: string, - { contentKey = "content" }: ParseMuseFileOptions = {}, -): Promise { - const { content, filePath, fileType } = await loadFileContent(rawFilePath); - - const partial = { - _raw: content, - filePath, - meta: {}, - }; - - if (fileType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - throw new Error(`Unsupported file type: ${fileType}`); -} diff --git a/src/core/sources.ts b/src/core/sources.ts new file mode 100644 index 0000000..8b57103 --- /dev/null +++ b/src/core/sources.ts @@ -0,0 +1,254 @@ +import EMDY from "@endeavorance/emdy"; +import TOML from "smol-toml"; +import YAML from "yaml"; +import { loadFileContent } from "./files"; +import type { UnknownRecord } from "./types"; +import { Glob } from "bun"; + +export type SourceType = "file" | "url"; + +export interface MuseSource { + location: string; + type: SourceType; +} + +interface SourceOptions { + cwd: string; + contentKey: string; + ignore: string[]; +} + +export interface MuseEntry { + _raw: string; + location: string; + data: Record; + meta: MetaShape; + source: MuseSource; +} + +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + +function parseYAMLEntries(text: string): UnknownRecord[] { + const parsedDocs = YAML.parseAllDocuments(text); + + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new Error("Encountered NULL resource"); + } + + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new Error( + `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, + ); + } + + const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); + return collection; +} + +function parseJSONEntries(text: string): UnknownRecord[] { + return formatEntries(JSON.parse(text)); +} + +function parseTOMLEntries(text: string): UnknownRecord[] { + return formatEntries(TOML.parse(text)); +} + +function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); +} + +export async function parseMuseFile( + rawFilePath: string, + { contentKey = "content" }: SourceOptions, +): Promise { + const { content, filePath, fileType } = await loadFileContent(rawFilePath); + + const partial = { + _raw: content, + source: { + location: filePath, + type: "file" as SourceType, + }, + location: filePath, + meta: {}, + }; + + if (fileType === "md") { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "yaml") { + return parseYAMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "json") { + return parseJSONEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (fileType === "toml") { + return parseTOMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + throw new Error(`Unsupported file type: ${fileType}`); +} + +async function loadFromFileSource( + source: MuseSource, + options: SourceOptions, +): Promise { + const paths = Array.from( + new Glob(source.location).scanSync({ + cwd: options.cwd, + absolute: true, + followSymlinks: true, + onlyFiles: true, + }), + ); + + const filteredPaths = paths.filter((path) => !options.ignore.includes(path)); + + const entries: MuseEntry[] = []; + for (const filePath of filteredPaths) { + const fileEntries = await parseMuseFile(filePath, options); + entries.push(...fileEntries); + } + + return entries; +} + +function getFileExtensionFromURL(url: string): string | null { + const parsedUrl = new URL(url); + + const pathname = parsedUrl.pathname; + const filename = pathname.substring(pathname.lastIndexOf("/") + 1); + const extension = filename.substring(filename.lastIndexOf(".") + 1); + + return extension === filename ? null : extension; +} + +function mimeTypeToFileType(mimeType: string): string | null { + switch (mimeType) { + case "text/markdown": + return "md"; + case "text/yaml": + return "yaml"; + case "application/x-yaml": + return "yaml"; + case "application/json": + return "json"; + case "application/toml": + return "toml"; + default: + return null; + } +} + +async function loadFromURLSource( + source: MuseSource, + options: SourceOptions, +): Promise { + const { contentKey = "content" } = options; + const response = await fetch(source.location); + + if (!response.ok) { + throw new Error(`Failed to fetch URL: ${source.location}`); + } + const content = await response.text(); + const mimeType = response.headers.get("Content-Type") || "unknown"; + + const parseType = + mimeTypeToFileType(mimeType) ?? + getFileExtensionFromURL(source.location) ?? + "unknown"; + + const partial = { + _raw: content, + source: { + location: source.location, + type: "url" as SourceType, + }, + location: source.location, + meta: {}, + }; + + if (parseType === "md") { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "yaml") { + return parseYAMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "json") { + return parseJSONEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + if (parseType === "toml") { + return parseTOMLEntries(content).map((data) => ({ + ...partial, + data, + })); + } + + // If it doesnt match one of these, try brute force parsing + // and return the result if any of them work + try { + return parseMarkdownEntry(content, contentKey).map((data) => ({ + ...partial, + data, + })); + } catch { + // noop + } + + throw new Error(`Unsupported MIME type from URL source: ${mimeType}`); +} + +export async function loadFromSource( + source: MuseSource, + options: SourceOptions, +): Promise { + switch (source.type) { + case "file": + return loadFromFileSource(source, options); + case "url": + return loadFromURLSource(source, options); + default: + throw new Error(`Unsupported source type: ${source.type}`); + } +} diff --git a/src/muse.ts b/src/muse.ts new file mode 100644 index 0000000..d2b61f9 --- /dev/null +++ b/src/muse.ts @@ -0,0 +1,20 @@ +import { loadBinding, type Binding } from "#core/binding"; + +export class Muse { + bindingLoaded = false; + preliminaryBinding: Partial = {}; + additionalSources: string[] = []; + additionalPluginPaths: string[] = []; + + constructor(bindingConfig: Partial) { + this.preliminaryBinding = bindingConfig; + } + + source(newSource: string) { + this.additionalSources.push(newSource); + } + + plugin(pluginPath: string) { + this.additionalPluginPaths.push(pluginPath); + } +} From ffaeb4841eb133e124dfeb00a79c7fc83fe44687 Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 14:05:15 -0400 Subject: [PATCH 12/14] Clean up record management --- src/core/binding.ts | 4 +- src/core/records.ts | 107 +++++++++++++++++++++++++++ src/core/sources.ts | 173 ++++++++------------------------------------ src/core/types.ts | 1 - src/muse.ts | 2 +- 5 files changed, 139 insertions(+), 148 deletions(-) create mode 100644 src/core/records.ts delete mode 100644 src/core/types.ts diff --git a/src/core/binding.ts b/src/core/binding.ts index 7db7c5e..d0528b5 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -1,15 +1,15 @@ import path, { dirname } from "node:path"; import { z } from "zod"; +import type { UnknownRecord } from "#core/records"; import { MuseError, MuseFileNotFoundError } from "#errors"; import { resolveFilePath } from "#util"; +import { type MusePlugin, loadPlugin } from "./plugins"; import { type MuseEntry, type MuseSource, loadFromSource, parseMuseFile, } from "./sources"; -import { type MusePlugin, loadPlugin } from "./plugins"; -import type { UnknownRecord } from "./types"; const SourceSchema = z.union([ z.object({ diff --git a/src/core/records.ts b/src/core/records.ts new file mode 100644 index 0000000..891b18e --- /dev/null +++ b/src/core/records.ts @@ -0,0 +1,107 @@ +import EMDY from "@endeavorance/emdy"; +import TOML from "smol-toml"; +import YAML from "yaml"; +import type { LoadedFile } from "./files"; + +export type UnknownRecord = Record; + +/** + * Given an unknown shape, ensure it is an object or array of objects, + * then return it as an array of objects. + * + * @param val - The unknown value to format + * @returns - An array of objects + */ +function formatEntries(val: unknown): UnknownRecord[] { + if (Array.isArray(val) && val.every((el) => typeof el === "object")) { + return val; + } + + if (typeof val === "object" && val !== null) { + return [val as UnknownRecord]; + } + + throw new Error( + `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, + ); +} + +/** + * Parse one or more YAML documents from a string + * + * @param text - The YAML string to parse + * @returns - An array of parsed objects + */ +export function parseYAMLEntries(text: string): UnknownRecord[] { + const parsedDocs = YAML.parseAllDocuments(text); + + if (parsedDocs.some((doc) => doc.toJS() === null)) { + throw new Error("Encountered NULL resource"); + } + + const errors = parsedDocs.flatMap((doc) => doc.errors); + + if (errors.length > 0) { + throw new Error( + `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, + ); + } + + const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); + return collection; +} + +/** + * Parse one or more JSON documents from a string + * + * @param text - The JSON string to parse + * @returns - An array of parsed objects + */ +export function parseJSONEntries(text: string): UnknownRecord[] { + return formatEntries(JSON.parse(text)); +} + +/** + * Parse one or more TOML documents from a string + * + * @param text - The TOML string to parse + * @returns - An array of parsed objects + */ +export function parseTOMLEntries(text: string): UnknownRecord[] { + return formatEntries(TOML.parse(text)); +} + +/** + * Parse one or more Markdown documents from a string + * + * @param text - The Markdown string to parse + * @returns - An array of parsed objects + */ +export function parseMarkdownEntry( + text: string, + contentKey: string, +): UnknownRecord[] { + return formatEntries(EMDY.parse(text, contentKey)); +} + +export function autoParseEntry(loadedFile: LoadedFile, contentKey: string) { + const { fileType, content } = loadedFile; + + if (fileType === "md") { + return parseMarkdownEntry(content, contentKey); + } + + if (fileType === "yaml") { + return parseYAMLEntries(content); + } + + if (fileType === "json") { + return parseJSONEntries(content); + } + + if (fileType === "toml") { + return parseTOMLEntries(content); + } + + throw new Error(`Unsupported file type: ${fileType}`); +} diff --git a/src/core/sources.ts b/src/core/sources.ts index 8b57103..0ab1495 100644 --- a/src/core/sources.ts +++ b/src/core/sources.ts @@ -1,12 +1,10 @@ -import EMDY from "@endeavorance/emdy"; -import TOML from "smol-toml"; -import YAML from "yaml"; -import { loadFileContent } from "./files"; -import type { UnknownRecord } from "./types"; import { Glob } from "bun"; +import { type LoadedFile, loadFileContent } from "./files"; +import { type UnknownRecord, autoParseEntry } from "./records"; export type SourceType = "file" | "url"; +/** A descriptor of a location to load into a Muse binding */ export interface MuseSource { location: string; type: SourceType; @@ -18,6 +16,7 @@ interface SourceOptions { ignore: string[]; } +/** A single loaded entry from a Muse source */ export interface MuseEntry { _raw: string; location: string; @@ -26,96 +25,27 @@ export interface MuseEntry { source: MuseSource; } -function formatEntries(val: unknown): UnknownRecord[] { - if (Array.isArray(val) && val.every((el) => typeof el === "object")) { - return val; - } - - if (typeof val === "object" && val !== null) { - return [val as UnknownRecord]; - } - - throw new Error( - `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, - ); -} - -function parseYAMLEntries(text: string): UnknownRecord[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new Error("Encountered NULL resource"); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new Error( - `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, - ); - } - - const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); - return collection; -} - -function parseJSONEntries(text: string): UnknownRecord[] { - return formatEntries(JSON.parse(text)); -} - -function parseTOMLEntries(text: string): UnknownRecord[] { - return formatEntries(TOML.parse(text)); -} - -function parseMarkdownEntry(text: string, contentKey: string): UnknownRecord[] { - return formatEntries(EMDY.parse(text, contentKey)); -} - export async function parseMuseFile( rawFilePath: string, { contentKey = "content" }: SourceOptions, ): Promise { - const { content, filePath, fileType } = await loadFileContent(rawFilePath); + const file = await loadFileContent(rawFilePath); + const entries = autoParseEntry(file, contentKey); const partial = { - _raw: content, + _raw: file.content, source: { - location: filePath, + location: file.filePath, type: "file" as SourceType, }, - location: filePath, + location: file.filePath, meta: {}, }; - if (fileType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (fileType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - throw new Error(`Unsupported file type: ${fileType}`); + return entries.map((data) => ({ + ...partial, + data, + })); } async function loadFromFileSource( @@ -152,23 +82,6 @@ function getFileExtensionFromURL(url: string): string | null { return extension === filename ? null : extension; } -function mimeTypeToFileType(mimeType: string): string | null { - switch (mimeType) { - case "text/markdown": - return "md"; - case "text/yaml": - return "yaml"; - case "application/x-yaml": - return "yaml"; - case "application/json": - return "json"; - case "application/toml": - return "toml"; - default: - return null; - } -} - async function loadFromURLSource( source: MuseSource, options: SourceOptions, @@ -182,10 +95,18 @@ async function loadFromURLSource( const content = await response.text(); const mimeType = response.headers.get("Content-Type") || "unknown"; - const parseType = - mimeTypeToFileType(mimeType) ?? - getFileExtensionFromURL(source.location) ?? - "unknown"; + const parseType = getFileExtensionFromURL(source.location) ?? "unknown"; + + const loadedFile: LoadedFile = { + content, + filePath: source.location, + fileType: parseType, + dirname: "", + basename: "", + mimeType, + }; + + const entries = autoParseEntry(loadedFile, contentKey); const partial = { _raw: content, @@ -197,46 +118,10 @@ async function loadFromURLSource( meta: {}, }; - if (parseType === "md") { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "yaml") { - return parseYAMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "json") { - return parseJSONEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - if (parseType === "toml") { - return parseTOMLEntries(content).map((data) => ({ - ...partial, - data, - })); - } - - // If it doesnt match one of these, try brute force parsing - // and return the result if any of them work - try { - return parseMarkdownEntry(content, contentKey).map((data) => ({ - ...partial, - data, - })); - } catch { - // noop - } - - throw new Error(`Unsupported MIME type from URL source: ${mimeType}`); + return entries.map((data) => ({ + ...partial, + data, + })); } export async function loadFromSource( diff --git a/src/core/types.ts b/src/core/types.ts deleted file mode 100644 index 35ba466..0000000 --- a/src/core/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type UnknownRecord = Record; diff --git a/src/muse.ts b/src/muse.ts index d2b61f9..814dc78 100644 --- a/src/muse.ts +++ b/src/muse.ts @@ -1,4 +1,4 @@ -import { loadBinding, type Binding } from "#core/binding"; +import type { Binding } from "#core/binding"; export class Muse { bindingLoaded = false; From dfc65dacfaa4b148f90b247bfa62c12d758cbf8c Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 14:07:59 -0400 Subject: [PATCH 13/14] Further cleanup --- src/core/binding.ts | 31 ++----------------------------- src/core/sources.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/core/binding.ts b/src/core/binding.ts index d0528b5..1a768e5 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -7,21 +7,12 @@ import { type MusePlugin, loadPlugin } from "./plugins"; import { type MuseEntry, type MuseSource, + SourceSchema, loadFromSource, parseMuseFile, + sourceShorthandToSource, } from "./sources"; -const SourceSchema = z.union([ - z.object({ - file: z.string(), - }), - z.object({ - url: z.string().url(), - }), -]); - -type SourceShorthand = z.infer; - // Function to parse an unknown object into parts of a binding const BindingSchema = z.object({ contentKey: z.string().default("content"), @@ -65,24 +56,6 @@ export interface Binding { readonly options: UnknownRecord; } -function sourceShorthandToSource(shorthand: SourceShorthand): MuseSource { - if ("file" in shorthand) { - return { - location: shorthand.file, - type: "file", - }; - } - - if ("url" in shorthand) { - return { - location: shorthand.url, - type: "url", - }; - } - - throw new MuseError("Invalid source shorthand"); -} - /** * Given a path, find the binding file * If the path is to a directory, check for a binding inside diff --git a/src/core/sources.ts b/src/core/sources.ts index 0ab1495..5c38e4b 100644 --- a/src/core/sources.ts +++ b/src/core/sources.ts @@ -1,6 +1,39 @@ import { Glob } from "bun"; import { type LoadedFile, loadFileContent } from "./files"; import { type UnknownRecord, autoParseEntry } from "./records"; +import { z } from "zod"; +import { MuseError } from "#errors"; + +export const SourceSchema = z.union([ + z.object({ + file: z.string(), + }), + z.object({ + url: z.string().url(), + }), +]); + +export type SourceShorthand = z.infer; + +export function sourceShorthandToSource( + shorthand: SourceShorthand, +): MuseSource { + if ("file" in shorthand) { + return { + location: shorthand.file, + type: "file", + }; + } + + if ("url" in shorthand) { + return { + location: shorthand.url, + type: "url", + }; + } + + throw new MuseError("Invalid source shorthand"); +} export type SourceType = "file" | "url"; From 2d3ba356b5052eb7df0d766fd7b534f3f283113c Mon Sep 17 00:00:00 2001 From: Endeavorance Date: Fri, 4 Apr 2025 16:14:17 -0400 Subject: [PATCH 14/14] Update plugin shape again --- README.md | 61 ++++++++++++++++++++++++++++++++------------- package.json | 2 +- src/core/binding.ts | 8 ++---- src/exports.ts | 7 ++++-- src/muse.ts | 20 --------------- 5 files changed, 52 insertions(+), 46 deletions(-) delete mode 100644 src/muse.ts diff --git a/README.md b/README.md index df86874..d122169 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,17 @@ # Muse -A CLI for wrangling directories of source files. - -Like a static generator, but for whatever. +_Bind data into anything_ ## Overview -**Muse** is a CLI and toolchain for operating on directories of -json, yaml, toml, and markdown files. Each file can specify any shape -of data. Muse scans included files, parses them into data, and streams -the loaded data through processors and plugins. +**Muse** is a CLI and toolchain for operating on data collected from +json, yaml, toml, and markdown files. -Each processor can modify the data at compile time, as well as enact -side effects such as writing files to disk. +Muse scans included files, parses them into data, and streams +the loaded data through plugins. -Muse does not edit source files, it only reads them in. +Each plugin can modify the data as well as enact side effects +such as writing files to disk. ## Usage @@ -32,10 +29,9 @@ Options: Each Muse project should specify a `binding` file with the following shape: ```yaml -include: - - ../another-project # Optional -files: # Optional - - "**/*.yaml" +sources: # Optional + - file: "**/*.yaml" + - web: "https://example.com/data.json" contentKey: "content" # Optional options: # Optional someOption: someValue @@ -44,9 +40,9 @@ processors: - second-processor ``` -The `binding` file can be any of the supported file types for Muse. If -the CLI is invoked with a directory instead of a file, Muse will look -for a binding file in the directory with the following order: +The `binding` file can be any of the supported file types for Muse and can +be named anything. If the CLI is invoked with a directory instead of a file, +Muse will look for a binding file in the directory with the following order: 1. `binding.json` 2. `binding.yaml` @@ -68,3 +64,34 @@ Your markdown content here When loading markdown files, Muse will load the content of the file into a key called `content` by default. This can be changed in the binding configuration by setting a `contentKey` value as an override. + +## Plugin API + +Muse plugins are written in TypeScript/JavaScript and expose information +describing the plugin, as well as the functions to operate with: + +```typescript +export const name = "Plugin Name"; +export const description = "Plugin Description"; +export async function step(binding: Binding): Promise {} +``` + +The main function of a plugin is `step`, which takes a `Binding` object +and returns a modified `Binding` object. + +When returning a new Binding object, it is best practice to make immutable +changes: + +```typescript +export async function step(binding: Binding): Promise { + const newBinding = { ...binding }; + newBinding.options.someOption = "newValue"; + return { + ...binding, + meta: { + ...binding.meta, + customValue: true, + } + }; +} +``` diff --git a/package.json b/package.json index 36bf54a..e7e1428 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@endeavorance/muse", - "version": "0.2.0", + "version": "0.3.0", "module": "dist/index.js", "exports": { ".": { diff --git a/src/core/binding.ts b/src/core/binding.ts index 1a768e5..7652c0a 100644 --- a/src/core/binding.ts +++ b/src/core/binding.ts @@ -22,7 +22,7 @@ const BindingSchema = z.object({ }, ]), options: z.record(z.string(), z.any()).default({}), - processors: z.array(z.string()).default([]), + plugins: z.array(z.string()).default([]), }); /** @@ -31,9 +31,6 @@ const BindingSchema = z.object({ * the project, and a list of entries to be processed */ export interface Binding { - /** The raw data read from the binding file */ - readonly _raw: z.infer; - /** Information about the sources used to load entries */ readonly sources: MuseSource[]; @@ -133,11 +130,10 @@ export async function loadBinding(initialPath: string): Promise { // Load and check plugins const plugins: MusePlugin[] = await Promise.all( - parsedBinding.processors.map(loadPlugin.bind(null, bindingDirname)), + parsedBinding.plugins.map(loadPlugin.bind(null, bindingDirname)), ); return { - _raw: parsedBinding, sources, bindingPath, contentKey: parsedBinding.contentKey, diff --git a/src/exports.ts b/src/exports.ts index f520a46..451c4f8 100644 --- a/src/exports.ts +++ b/src/exports.ts @@ -5,5 +5,8 @@ export * from "#errors"; export const DEFAULT_CONTENT_KEY = "content"; // Types -export type * from "#types"; -export type { MusePlugin } from "#core/plugins"; +export type * from "#core/binding"; +export type * from "#core/plugins"; +export type * from "#core/sources"; +export type * from "#core/records"; +export type * from "#core/files"; diff --git a/src/muse.ts b/src/muse.ts deleted file mode 100644 index 814dc78..0000000 --- a/src/muse.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Binding } from "#core/binding"; - -export class Muse { - bindingLoaded = false; - preliminaryBinding: Partial = {}; - additionalSources: string[] = []; - additionalPluginPaths: string[] = []; - - constructor(bindingConfig: Partial) { - this.preliminaryBinding = bindingConfig; - } - - source(newSource: string) { - this.additionalSources.push(newSource); - } - - plugin(pluginPath: string) { - this.additionalPluginPaths.push(pluginPath); - } -}