Compare commits

..

No commits in common. "main" and "alpha" have entirely different histories.
main ... alpha

46 changed files with 2778 additions and 970 deletions

2
.gitignore vendored
View file

@ -174,3 +174,5 @@ dist
# Finder (MacOS) folder config
.DS_Store
demo-data
demo.html

256
README.md
View file

@ -1,17 +1,6 @@
# Muse
# Playbill Builder CLI
_Bind data into anything_
## Overview
**Muse** is a CLI and toolchain for operating on data collected from
json, yaml, toml, and markdown files.
Muse scans included files, parses them into data, and streams
the loaded data through plugins.
Each plugin can modify the data as well as enact side effects
such as writing files to disk.
This is a CLI tool for compiling Markdown and YAML files into a validated Playbill for [Proscenium](https://proscenium.game)
## Usage
@ -20,78 +9,209 @@ Usage:
muse [/path/to/binding.yaml] <options>
Options:
--stdout -s Output final data to stdout
--verbose, -v Enable verbose logging
--check Only load and check the current binding and resources, but do not compile
--outfile, -o Specify the output file path. If not specified, output to stdout
--watch, -w Watch the directory for changes and recompile
--renderer, -r Specify the output renderer. Options: json, html
--help, -h Show this help message
```
## Binding File
Each Muse project should specify a `binding` file with the following shape:
Each Muse project should specify a `binding.yaml` file with the following shape:
```yaml
sources: # Optional
- file: "**/*.yaml"
- web: "https://example.com/data.json"
contentKey: "content" # Optional
options: # Optional
someOption: someValue
processors:
- first-processor
- second-processor
name: My Playbill # Required
author: My Name # Required
version: 0.0.1 # Required
extend: ../another-playbill # Optional
files: # Optional
- "**/*.yaml"
styles: # Optional
- stylesheet.css
terms: # Optional
Some Term: Some Definition
Another Term: Another Definition
```
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:
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.
1. `binding.json`
2. `binding.yaml`
3. `binding.toml`
4. `binding.md`
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.
## Markdown Files
## Common Types
Muse supports loading and parsing Markdown files with optional YAML frontmatter.
Many Playbill components share common properties that expect a value
from a predefined list. These are the common types used in the Playbill.
```
---
someKey: someValue
---
### `Roll` Options
Your markdown content here
- `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
```
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.
### Ability Definition
## 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<Binding> {}
```yaml
$define: ability
type: <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: <damage type> # Type of damage dealt by this ability (default phy)
roll: <roll type> # Roll type for this ability (default none)
```
The main function of a plugin is `step`, which takes a `Binding` object
and returns a modified `Binding` object.
### Blueprint Definition
When returning a new Binding object, it is best practice to make immutable
changes:
```typescript
export async function step(binding: Binding): Promise<Binding> {
const newBinding = { ...binding };
newBinding.options.someOption = "newValue";
return {
...binding,
meta: {
...binding.meta,
customValue: true,
}
};
}
```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: <prowess> # Starting muscle prowess (default novice)
focus: <prowess> # Starting focus prowess (default novice)
knowledge: <prowess> # Starting knowledge prowess (default novice)
charm: <prowess> # Starting charm prowess (default novice)
cunning: <prowess> # Starting cunning prowess (default novice)
spark: <prowess> # Starting spark prowess (default novice)
```
### Item Definition
```yaml
$define: item
type: <item type> # Defaults to wielded
rarity: <rarity> # Defaults to common
affinity: <roll type> # Defaults to none
damage: 0 # Defaults to 0
damageType: <damage type> # Defaults to phy
hands: 1 | 2 # Only for wielded items
slot: <item 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: <resource type> # Defaults to text
url: https://url.com/image.jpeg # Only for image resources
data: <a 2-d array of strings> # 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: <prowess> # Defaults to novice
focus: <prowess> # Defaults to novice
knowledge: <prowess> # Defaults to novice
charm: <prowess> # Defaults to novice
cunning: <prowess> # Defaults to novice
spark: <prowess> # Defaults to novice
```

View file

@ -7,7 +7,10 @@
},
"files": {
"ignoreUnknown": false,
"ignore": ["*.d.ts", "*.json"],
"ignore": [
"*.d.ts",
"*.json"
],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx",

296
bun.lock
View file

@ -4,16 +4,32 @@
"": {
"name": "@proscenium/muse",
"dependencies": {
"@endeavorance/emdy": "1.0.0",
"@proscenium/playbill": "link:@proscenium/playbill",
"chalk": "^5.4.1",
"smol-toml": "^1.3.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",
"unified": "^11.0.5",
"yaml": "^2.7.0",
"zod": "^3.24.1",
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"dts-bundle-generator": "^9.5.1",
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4",
},
"peerDependencies": {
"typescript": "^5.0.0",
@ -21,6 +37,8 @@
},
},
"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=="],
@ -39,60 +57,284 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
"@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=="],
"@proscenium/playbill": ["@proscenium/playbill@link:@proscenium/playbill", {}],
"@types/bun": ["@types/bun@1.2.8", "", { "dependencies": { "bun-types": "1.2.7" } }, "sha512-t8L1RvJVUghW5V+M/fL3Thbxcs0HwNsXsnTEBEfEVqGteiJToOlZ/fyOEaR1kZsNqnu+3XA4RI/qmnX4w6+S+w=="],
"@types/bun": ["@types/bun@1.2.5", "", { "dependencies": { "bun-types": "1.2.5" } }, "sha512-w2OZTzrZTVtbnJew1pdFmgV99H0/L+Pvw+z1P67HaR18MHOzYnTYOi6qzErhK8HyT+DB782ADVPPE92Xu2/Opg=="],
"@types/node": ["@types/node@22.13.17", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-nAJuQXoyPj04uLgu+obZcSmsfOenUg6DxPKogeUy6yNCFwWaj5sBF8/G/pNo8EtBJjAfSVgfIlugR/BCOleO+g=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"@types/lodash": ["@types/lodash@4.17.16", "", {}, "sha512-HX7Em5NYQAXKW+1T+FiuG27NGwzJfCX3s1GjOa7ujxZa52kjJLOr4FUxT+giF6Tgxv1e+/czV/iTtBw27WTU9g=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"@types/lodash-es": ["@types/lodash-es@4.17.12", "", { "dependencies": { "@types/lodash": "*" } }, "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ=="],
"bun-types": ["bun-types@1.2.7", "", { "dependencies": { "@types/node": "*", "@types/ws": "*" } }, "sha512-P4hHhk7kjF99acXqKvltyuMQ2kf/rzIw3ylEDpCxDS9Xa0X0Yp/gJu/vDCucmWpiur5qJ0lwB2bWzOXa2GlHqA=="],
"@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=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
"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": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"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=="],
"character-reference-invalid": ["character-reference-invalid@1.1.4", "", {}, "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"decode-named-character-reference": ["decode-named-character-reference@1.1.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w=="],
"smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"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=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"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=="],
"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=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"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=="],
"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=="],
"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=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
"yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"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-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"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=="],
"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=="],
}
}

View file

@ -1 +0,0 @@
preload = ["./src/preload/http-plugin.js"]

View file

@ -1,14 +0,0 @@
---
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
---
A markdown-powered binding file!

View file

@ -1,19 +0,0 @@
export const name = "Announce Slam Dunks";
export const description = "Get hype when a file has slam dunks";
export const step = ({ entries, options, ...rest }) => {
const { dunks } = options;
const slamDunkKey = dunks?.key ?? "slamDunks";
for (const entry of entries) {
if (slamDunkKey in entry.data) {
console.log(`Slam dunk!`);
}
}
return {
entries,
options,
...rest,
};
};

View file

@ -1,17 +0,0 @@
export function step(binding) {
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
};
}

View file

@ -1,2 +0,0 @@
player = "Flynn"
dunkaroos = 100

View file

@ -1,5 +0,0 @@
---
published: true
---
This is a neat story I wrote!

View file

@ -1,40 +1,43 @@
{
"name": "@endeavorance/muse",
"version": "0.3.0",
"module": "dist/index.js",
"exports": {
".": {
"import": "./dist/muse.js",
"types": "./dist/muse.d.ts"
}
},
"name": "@proscenium/muse",
"version": "0.0.1",
"module": "index.ts",
"type": "module",
"files": [
"dist"
],
"devDependencies": {
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"dts-bundle-generator": "^9.5.1"
"@types/lodash-es": "^4.17.12",
"@types/react": "^19.0.11",
"@types/react-dom": "^19.0.4"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
"dependencies": {
"@endeavorance/emdy": "1.0.0",
"@proscenium/playbill": "link:@proscenium/playbill",
"chalk": "^5.4.1",
"smol-toml": "^1.3.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",
"unified": "^11.0.5",
"yaml": "^2.7.0",
"zod": "^3.24.1"
},
"scripts": {
"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 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/index.ts -- ./demo/main"
"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"
}
}

View file

@ -1,152 +0,0 @@
import { parseArgs } from "node:util";
import chalk from "chalk";
import { loadBinding } from "#core/binding";
import type { MusePlugin } from "#core/plugins";
import { MuseError } from "#errors";
import { version } from "../../package.json";
interface CLIFlags {
help: boolean;
verbose: boolean;
stdout: boolean;
}
interface Loggers {
log: (msg: string) => void;
verbose: (msg: string) => void;
}
enum ExitCode {
Success = 0,
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>
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
*/
function parseCLIArguments(argv: string[]): [string, CLIFlags] {
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 [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
let binding = await loadBinding(inputFilePath);
verbose(`Binding ${binding.bindingPath}`);
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 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`);
if (flags.stdout) {
console.log(JSON.stringify(binding, null, 2));
}
return ExitCode.Success;
}
async function main(): Promise<number> {
const [inputFilePath, flags] = parseCLIArguments(Bun.argv.slice(2));
// If --help is specified, print usage and exit
if (flags.help) {
console.log(USAGE);
return ExitCode.Success;
}
const logFn = !flags.stdout ? console.log : () => {};
const verboseFn = flags.verbose
? (msg: string) => {
console.log(chalk.dim(msg));
}
: () => {};
const lastProcessResult = await processBinding(inputFilePath, flags, {
log: logFn,
verbose: verboseFn,
});
return lastProcessResult;
}
try {
const exitCode = await main();
process.exit(exitCode);
} catch (error) {
if (error instanceof MuseError) {
console.error(chalk.red(error.fmt()));
} else {
console.error("An unexpected error occurred");
console.error(error);
}
process.exit(1);
}

View file

@ -1,145 +0,0 @@
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,
SourceSchema,
loadFromSource,
parseMuseFile,
sourceShorthandToSource,
} from "./sources";
// Function to parse an unknown object into parts of a binding
const BindingSchema = z.object({
contentKey: z.string().default("content"),
sources: z.array(SourceSchema).default([
{
file: "./**/*.{json,yaml,toml,md}",
},
]),
options: z.record(z.string(), z.any()).default({}),
plugins: z.array(z.string()).default([]),
});
/**
* 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<MetaShape = UnknownRecord> {
/** Information about the sources used to load entries */
readonly sources: MuseSource[];
/** The full file path to the binding file */
readonly bindingPath: string;
/** 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 plugins: MusePlugin[];
/** Arbitrary metadata for processors to use to cache project data */
readonly meta: MetaShape;
/** Configuration options for Muse and all processors */
readonly options: UnknownRecord;
}
/**
* 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
* @returns The absolute resolved path to the binding file
*/
async function resolveBindingPath(bindingPath: string): Promise<string> {
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()) {
for (const bindingFile of DEFAULT_BINDING_FILES) {
const filePath = path.resolve(bindingPath, bindingFile);
const resolvedPath = await resolveFilePath(filePath);
if (resolvedPath) {
return resolvedPath;
}
}
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);
}
// If it is a file, return the path
return path.resolve(bindingPath);
}
export async function loadBinding(initialPath: string): Promise<Binding> {
// Resolve the file location if possible
const bindingPath = await resolveBindingPath(initialPath);
const bindingDirname = dirname(bindingPath);
const loadedBindingData = await parseMuseFile(bindingPath, {
contentKey: "content",
cwd: bindingDirname,
ignore: [],
});
if (loadedBindingData.length === 0) {
throw new MuseError("No entries found in binding file");
}
// Load and parse the Binding YAML content
const parsedBinding = BindingSchema.parse(loadedBindingData[0].data);
const sourceOptions = {
cwd: bindingDirname,
contentKey: parsedBinding.contentKey,
ignore: [bindingPath],
};
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(
parsedBinding.plugins.map(loadPlugin.bind(null, bindingDirname)),
);
return {
sources,
bindingPath,
contentKey: parsedBinding.contentKey,
entries,
options: parsedBinding.options,
plugins,
meta: {},
};
}

View file

@ -1,34 +0,0 @@
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;
mimeType: string;
}
export async function loadFileContent(filePath: string): Promise<LoadedFile> {
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),
mimeType: file.type,
};
}

View file

@ -1,52 +0,0 @@
import path from "node:path";
import { MuseError, MusePluginError } from "#errors";
import type { Binding } from "./binding";
export type MuseStepFn = (binding: Binding) => Promise<Binding>;
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<MusePlugin> {
// 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"),
};
}

View file

@ -1,107 +0,0 @@
import EMDY from "@endeavorance/emdy";
import TOML from "smol-toml";
import YAML from "yaml";
import type { LoadedFile } from "./files";
export type UnknownRecord = Record<string, unknown>;
/**
* 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}`);
}

View file

@ -1,172 +0,0 @@
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<typeof SourceSchema>;
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";
/** A descriptor of a location to load into a Muse binding */
export interface MuseSource {
location: string;
type: SourceType;
}
interface SourceOptions {
cwd: string;
contentKey: string;
ignore: string[];
}
/** A single loaded entry from a Muse source */
export interface MuseEntry<MetaShape = UnknownRecord> {
_raw: string;
location: string;
data: Record<string, unknown>;
meta: MetaShape;
source: MuseSource;
}
export async function parseMuseFile(
rawFilePath: string,
{ contentKey = "content" }: SourceOptions,
): Promise<MuseEntry[]> {
const file = await loadFileContent(rawFilePath);
const entries = autoParseEntry(file, contentKey);
const partial = {
_raw: file.content,
source: {
location: file.filePath,
type: "file" as SourceType,
},
location: file.filePath,
meta: {},
};
return entries.map((data) => ({
...partial,
data,
}));
}
async function loadFromFileSource(
source: MuseSource,
options: SourceOptions,
): Promise<MuseEntry[]> {
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;
}
async function loadFromURLSource(
source: MuseSource,
options: SourceOptions,
): Promise<MuseEntry[]> {
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 = 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,
source: {
location: source.location,
type: "url" as SourceType,
},
location: source.location,
meta: {},
};
return entries.map((data) => ({
...partial,
data,
}));
}
export async function loadFromSource(
source: MuseSource,
options: SourceOptions,
): Promise<MuseEntry[]> {
switch (source.type) {
case "file":
return loadFromFileSource(source, options);
case "url":
return loadFromURLSource(source, options);
default:
throw new Error(`Unsupported source type: ${source.type}`);
}
}

2
src/css.d.ts vendored Normal file
View file

@ -0,0 +1,2 @@
declare module '*.css' {
}

326
src/define/index.ts Normal file
View file

@ -0,0 +1,326 @@
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}`);
}
}

View file

@ -1,41 +0,0 @@
export class MuseError extends Error {
fmt(): string {
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;
constructor(message: string, filePath: string) {
super(message);
this.filePath = filePath;
}
fmt(): string {
return `-- FILE ERROR --\nFile Path: ${this.filePath}\nDetails: ${this.message}`;
}
}
export class MuseFileNotFoundError extends MuseFileError {
constructor(filePath: string) {
super("File not found", filePath);
this.message = "File does not exist";
this.filePath = filePath;
}
}
export class CLIError extends MuseError {}

View file

@ -1,12 +0,0 @@
// Content exports
export * from "#errors";
// Constants
export const DEFAULT_CONTENT_KEY = "content";
// Types
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";

120
src/index.ts Normal file
View file

@ -0,0 +1,120 @@
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";
enum ExitCode {
Success = 0,
Error = 1,
}
async function processBinding({ inputFilePath, options }: CLIArguments) {
// Load the binding
const bindingPath = await resolveBindingPath(inputFilePath);
const binding = await loadFromBinding(bindingPath);
// If --check is specified, exit early
if (options.check) {
console.log(chalk.green("Playbill validated successfully"));
return ExitCode.Success;
}
if (options.markdown) {
await compileMarkdownInPlaybill(binding);
}
// 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);
return ExitCode.Success;
}
async function main(): Promise<number> {
const cliArguments = parseCLIArguments(Bun.argv.slice(2));
const { options } = cliArguments;
// If --help is specified, print usage and exit
if (options.help) {
console.log(USAGE);
return ExitCode.Success;
}
let 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;
}
try {
const exitCode = await main();
process.exit(exitCode);
} catch (error) {
if (error instanceof MuseError) {
console.error(chalk.red(error.fmt()));
} else {
console.error("An unexpected error occurred");
console.error(error);
}
process.exit(1);
}

177
src/lib/binding.ts Normal file
View file

@ -0,0 +1,177 @@
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<string> {
// 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<BoundPlaybill> {
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<void>[] = [];
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,
};
}

177
src/lib/component-file.ts Normal file
View file

@ -0,0 +1,177 @@
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;
}
}

32
src/lib/errors.ts Normal file
View file

@ -0,0 +1,32 @@
export class MuseError extends Error {
fmt(): string {
return this.message;
}
}
export class FileError extends MuseError {
filePath: string;
details?: string;
constructor(message: string, filePath: string, details?: string) {
super(message);
this.filePath = filePath;
this.details = details;
}
fmt(): string {
return `-- ${this.message} --\nFile Path: ${this.filePath}\nDetails: ${this.details}`;
}
}
export class MalformedResourceFileError extends FileError {}
export class FileNotFoundError extends FileError {
constructor(filePath: string) {
super("File not found", filePath);
this.message = "File not found";
this.filePath = filePath;
}
}
export class CLIError extends MuseError {}

4
src/lib/index.ts Normal file
View file

@ -0,0 +1,4 @@
export * from "./errors";
export * from "./pfm";
export * from "./component-file";
export * from "./binding";

79
src/lib/pfm.ts Normal file
View file

@ -0,0 +1,79 @@
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<string>;
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<string> => {
const parsed = await parser.process(input);
return String(parsed);
};
}
export async function compileMarkdownInPlaybill(
boundPlaybill: BoundPlaybill,
): Promise<BoundPlaybill> {
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;
}

5
src/lib/slug.ts Normal file
View file

@ -0,0 +1,5 @@
import slugify from "slugify";
export function toSlug(str: string): string {
return slugify(str, { lower: true });
}

View file

@ -1,40 +0,0 @@
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}`),
);
},
});

View file

@ -0,0 +1,67 @@
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(
<span key="ap" className="ability-cost ap">
{ability.ap} AP
</span>,
);
}
if (ability.hp > 0) {
costs.push(
<span key="hp" className="ability-cost hp">
{ability.hp} HP
</span>,
);
}
if (ability.ep > 0) {
costs.push(
<span key="ep" className="ability-cost ep">
{ability.ep} EP
</span>,
);
}
const damage =
ability.damage !== null ? (
<div className={`ability-damage ${ability.damage.type}`}>
{ability.damage.amount} {ability.damage.type === "phy" ? "Phy" : "Arc"}
</div>
) : null;
const roll =
ability.roll !== "none" ? (
<div className={`ability-roll ${ability.roll}`}>{ability.roll}</div>
) : null;
const type = (
<span className={`ability-type ${ability.type}`}>{ability.type}</span>
);
const classList = `ability ability-${ability.type}`;
return (
<div className={classList} data-component-id={ability.id}>
<div className="ability-header">
<h4>{ability.name}</h4>
<div className="ability-details">
{type}
{roll}
{damage}
{costs}
</div>
</div>
<HTML className="ability-content" html={ability.description} />
</div>
);
}

View file

@ -0,0 +1,10 @@
interface HTMLWrapperProps {
html: string;
className?: string;
}
export function HTML({ html, className }: HTMLWrapperProps) {
return (
<div className={className} dangerouslySetInnerHTML={{ __html: html }} />
);
}

View file

@ -0,0 +1,41 @@
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 (
<section className={sectionClasses} data-component-id={componentId}>
<div className={headerClasses}>
{preInfo}
<h2>{title}</h2>
{info}
</div>
<div className="section-tag section-tag--left">{leftCornerTag}</div>
<div className="section-tag section-tag--right">{rightCornerTag}</div>
<div className={contentClasses}>{children}</div>
</section>
);
}

View file

@ -0,0 +1,26 @@
import { Section } from "./base/section";
interface GlossaryTableProps {
terms: [string, string[]][];
}
export function GlossaryTable({ terms }: GlossaryTableProps) {
return (
<Section type="glossary" componentId="glossary" title="Glossary">
<div className="glossary-content">
<dl>
{terms.map(([term, defs]) => {
return (
<div key={term}>
<dt>{term}</dt>
{defs.map((d, i) => (
<dd key={i}>{d}</dd>
))}
</div>
);
})}
</dl>
</div>
</Section>
);
}

View file

@ -0,0 +1,18 @@
import type { Playbill } from "@proscenium/playbill";
interface PlaybillHeaderProps {
playbill: Playbill;
}
export function PageHeader({ playbill }: PlaybillHeaderProps) {
return (
<header>
<h1 className="header-title">{playbill.name}</h1>
<p className="header-byline">A Playbill by {playbill.author} </p>
<p>
{" "}
<small className="header-info"> Version {playbill.version}</small>
</p>
</header>
);
}

View file

@ -0,0 +1,55 @@
import type { Item } from "@proscenium/playbill";
import { capitalize } from "lodash-es";
import { Section } from "./base/section";
import { HTML } from "./base/html";
interface ItemSectionProps {
item: Item;
}
export function ItemCard({ item }: ItemSectionProps) {
let itemTypeDescriptor = "";
switch (item.type) {
case "wielded":
itemTypeDescriptor = `Wielded (${item.hands} ${item.hands === 1 ? "Hand" : "Hands"})`;
break;
case "worn":
itemTypeDescriptor = `Worn (${item.slot})`;
break;
case "trinket":
itemTypeDescriptor = "Trinket";
break;
}
const infoParts: React.ReactNode[] = [];
if (item.affinity !== "none") {
infoParts.push(
<p key="affinity" className={`item-affinity ${item.affinity}`}>
{item.affinity} Affinity
</p>,
);
}
if (item.damage !== null && item.damage.amount > 0) {
infoParts.push(
<p key="damage" className={`item-damage ${item.damage.type}`}>
{item.damage.amount} {capitalize(item.damage.type)} Damage
</p>,
);
}
return (
<Section
type="item"
componentId={item.id}
title={item.name}
info={<div className="item-info">{infoParts}</div>}
rightCornerTag={capitalize(item.rarity)}
leftCornerTag={itemTypeDescriptor}
>
<HTML html={item.description} />
</Section>
);
}

View file

@ -0,0 +1,49 @@
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 (
<div className="method-rank" key={i}>
<h3>Rank {i + 1}</h3>
<div className="method-rank-content" style={{ gridTemplateColumns }}>
{rank.map((abilityId) => {
const ability = playbill.getAbility(abilityId);
if (ability === null) {
throw new Error(`Ability not found: ${abilityId}`);
}
return <AbilityCard ability={ability} key={abilityId} />;
})}
</div>
</div>
);
});
return (
<Section
type="method"
componentId={method.id}
title={method.name}
preInfo={<p className="method-curator">{method.curator}'s Method of</p>}
info={
<p
className="method-description"
dangerouslySetInnerHTML={{ __html: method.description }}
/>
}
>
<div className="method-ranks">{ranks}</div>
</Section>
);
}

View file

@ -0,0 +1,115 @@
import type {
ImageResource,
Resource,
TableResource,
TextResource,
} from "@proscenium/playbill";
import { Section } from "./base/section";
import { HTML } from "./base/html";
interface TextResourceSectionProps {
resource: TextResource;
}
function TextResourceSection({ resource }: TextResourceSectionProps) {
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<HTML html={resource.description} />
</Section>
);
}
interface ImageResourceSectionProps {
resource: ImageResource;
}
function ImageResourceSection({ resource }: ImageResourceSectionProps) {
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<figure className="resource-image">
<img src={resource.url} alt={resource.name} />
<figcaption
dangerouslySetInnerHTML={{ __html: resource.description }}
/>
</figure>
</Section>
);
}
interface TableResourceSectionProps {
resource: TableResource;
}
function TableResourceSection({ resource }: TableResourceSectionProps) {
const [headers, ...rows] = resource.data;
return (
<Section
type="resource"
componentId={resource.id}
title={resource.name}
info={
<ul className="resource-categories">
{resource.categories.map((category) => (
<li key={category}>{category}</li>
))}
</ul>
}
>
<table className="resource-table">
<thead>
<tr>
{headers.map((header) => (
<th key={header} dangerouslySetInnerHTML={{ __html: header }} />
))}
</tr>
</thead>
<tbody>
{rows.map((row, i) => (
<tr key={i}>
{row.map((cell, j) => (
<td key={j} dangerouslySetInnerHTML={{ __html: cell }} />
))}
</tr>
))}
</tbody>
</table>
</Section>
);
}
interface ResourceSectionProps {
resource: Resource;
}
export function ResourceSection({ resource }: ResourceSectionProps) {
switch (resource.type) {
case "text":
return <TextResourceSection resource={resource} />;
case "image":
return <ImageResourceSection resource={resource} />;
case "table":
return <TableResourceSection resource={resource} />;
}
}

View file

@ -0,0 +1,15 @@
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 (
<Section title={rule.name} componentId={rule.id} type="rule">
<HTML html={rule.description} />
</Section>
);
}

View file

@ -0,0 +1,120 @@
import type { Playbill, Species } from "@proscenium/playbill";
import { Section } from "./base/section";
import { AbilityCard } from "./ability";
import { HTML } from "./base/html";
interface SpeciesSectionProps {
species: Species;
playbill: Playbill;
}
export function SpeciesSection({ species, playbill }: SpeciesSectionProps) {
const hpString = species.hp >= 0 ? `+${species.hp}` : `-${species.hp}`;
const apString = species.ap >= 0 ? `+${species.ap}` : `-${species.ap}`;
const epString = species.ep >= 0 ? `+${species.ep}` : `-${species.ep}`;
const hasAbilities = species.abilities.length > 0;
const abilitySection = hasAbilities ? (
<div className="species-abilities">
<h3>Innate Abilities</h3>
{species.abilities.map((abilityId) => {
const ability = playbill.getAbility(abilityId);
if (ability === null) {
throw new Error(`Ability not found: ${abilityId}`);
}
return <AbilityCard ability={ability} key={abilityId} />;
})}
</div>
) : (
""
);
const sectionContent = (
<>
<div className="species-stats">
<div className="species-stat-hp">
<h4 className="hp species-stat-hp-title" title="Health Points">
HP
</h4>
<p className="species-stat-hp-value">{hpString}</p>
</div>
<div className="species-stat-ap">
<h4 className="ap species-stat-ap-title" title="Action Points">
AP
</h4>
<p className="species-stat-ap-value">{apString}</p>
</div>
<div className="species-stat-ep">
<h4 className="ep species-stat-ep-title" title="Exhaustion Points">
EP
</h4>
<p className="species-stat-ep-value">{epString}</p>
</div>
</div>
<div className="species-talents">
<div className="species-talent-muscle">
<h4 className="species-talent-muscle-title muscle">Muscle</h4>
<p className={`species-talent-muscle-value ${species.muscle}`}>
{species.muscle}
</p>
</div>
<div className="species-talent-focus">
<h4 className="species-talent-focus-title focus">Focus</h4>
<p className={`species-talent-focus-value ${species.focus}`}>
{species.focus}
</p>
</div>
<div className="species-talent-knowledge">
<h4 className="species-talent-knowledge-title knowledge">
Knowledge
</h4>
<p className={`species-talent-knowledge-value ${species.knowledge}`}>
{species.knowledge}
</p>
</div>
<div className="species-talent-charm">
<h4 className="species-talent-charm-title charm">Charm</h4>
<p className={`species-talent-charm-value ${species.charm}`}>
{species.charm}
</p>
</div>
<div className="species-talent-cunning">
<h4 className="species-talent-cunning-title cunning">Cunning</h4>
<p className={`species-talent-cunning-value ${species.cunning}`}>
{species.cunning}
</p>
</div>
<div className="species-talent-spark">
<h4 className="species-talent-spark-title spark">Spark</h4>
<p className={`species-talent-spark-value ${species.spark}`}>
{species.spark}
</p>
</div>
</div>
<HTML className="species-content" html={species.description} />
{abilitySection}
</>
);
return (
<Section
type="species"
componentId={species.id}
title={species.name}
info={<p>Species</p>}
>
{sectionContent}
</Section>
);
}

505
src/render/html/default.css Normal file
View file

@ -0,0 +1,505 @@
@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;
}

160
src/render/html/index.tsx Normal file
View file

@ -0,0 +1,160 @@
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<string> {
// 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(`<style>${defaultCSS}</style>`);
}
if (boundPlaybill.styles.length > 0) {
cssParts.push(`<style>${boundPlaybill.styles}</style>`);
}
const css = cssParts.join("\n");
const body = renderToString(
<>
<article id="species" className="view">
{playbill.allSpecies.map((species) => (
<SpeciesSection
key={species.id}
species={species}
playbill={playbill}
/>
))}
</article>
<article id="methods" className="view" style={{ display: "none" }}>
{playbill.allMethods.map((method) => (
<MethodSection key={method.id} method={method} playbill={playbill} />
))}
</article>
<article id="items" className="view" style={{ display: "none" }}>
{playbill.allItems.map((item) => (
<ItemCard key={item.id} item={item} />
))}
</article>
<article id="rules" className="view" style={{ display: "none" }}>
{playbill.allRules.map((rule) => (
<RuleSection key={rule.id} rule={rule} />
))}
</article>
<article id="glossary" className="view" style={{ display: "none" }}>
{<GlossaryTable terms={playbill.allDefinitions} />}
</article>
<article id="resources" className="view" style={{ display: "none" }}>
{playbill.allResources.map((resource) => (
<ResourceSection key={resource.id} resource={resource} />
))}
</article>
</>,
);
// Main document
const doc = `
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Playbill: ${playbill.name}</title>
${css}
<script>
function removeHash() {
history.pushState("", document.title, window.location.pathname);
}
function setHash(hash) {
if (hash.length > 0) {
history.pushState(null, null, "#" + hash);
} else {
removeHash();
}
}
function hideAllViews() {
document.querySelectorAll(".view").forEach((view) => {
view.style.display = "none";
});
}
function showAllViews() {
document.querySelectorAll(".view").forEach((view) => {
view.style.display = "block";
});
setHash("");
}
function showView(viewId) {
document.getElementById(viewId).style.display = "block";
setHash(viewId);
}
function hideAllAndShow(viewId) {
hideAllViews();
showView(viewId);
}
function navigateFromHash() {
const hash = window.location.hash.slice(1);
if (hash.length > 0) {
const view = document.getElementById(hash);
if (view !== null) {
hideAllAndShow(hash);
} else {
showAllViews();
}
} else {
showAllViews();
}
}
document.addEventListener("DOMContentLoaded", navigateFromHash());
window.addEventListener("hashchange", navigateFromHash())
</script>
</head>
<body>
${renderToString(<PageHeader playbill={playbill} />)}
<blockquote class="overview"><p>${playbill.description}</p></blockquote>
<nav>
<button onclick="hideAllAndShow('species')" id="species-nav">Species</button>
<button onclick="hideAllAndShow('methods')" id="methods-nav">Methods</button>
<button onclick="hideAllAndShow('items')" id="items-nav">Items</button>
<button onclick="hideAllAndShow('rules')" id="rules-nav">Rulebook</button>
<button onclick="hideAllAndShow('glossary')" id="glossary-nav">Glossary</button>
<button onclick="hideAllAndShow('resources')" id="resources-nav">Resources</button>
<button onclick="showAllViews()" id="all-nav">Show All</button>
</nav>
${body}
</body>
</html>
`;
return doc;
}

View file

@ -1,27 +0,0 @@
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<string | null> {
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;
}

111
src/util/args.ts Normal file
View file

@ -0,0 +1,111 @@
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>
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<typeof RendererSchema>;
/**
* 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,
},
};
}

65
src/util/files.ts Normal file
View file

@ -0,0 +1,65 @@
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<string> {
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<unknown> {
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<unknown> {
try {
return JSON.parse(await loadFileOrFail(filePath));
} catch (error) {
throw new FileError("Failed to parse JSON file", filePath, `${error}`);
}
}
/**
* Returns the absolute path of the directory containing the given file
*
* @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));
}

2
src/util/index.ts Normal file
View file

@ -0,0 +1,2 @@
export * from "./args";
export * from "./files";

View file

@ -13,10 +13,8 @@
// Bundler mode
"moduleResolution": "bundler",
"allowArbitraryExtensions": true,
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"declaration": true,
"emitDeclarationOnly": true,
"noEmit": true,
// Best practices
"strict": true,
"skipLibCheck": true,
@ -27,21 +25,20 @@
"noPropertyAccessFromIndexSignature": false,
"baseUrl": "./src",
"paths": {
"#core/*": [
"./core/*.ts"
"#lib": [
"./lib/index.ts"
],
"#render/*": [
"./render/*"
],
"#util": [
"./util.ts"
"./util/index.ts"
],
"#errors": [
"./errors.ts"
]
},
"outDir": "./dist",
}
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/preload/http-plugin.js",
"src/**/*.css",
]
}