diff --git a/.gitignore b/.gitignore index 8a4e461..4cda367 100644 --- a/.gitignore +++ b/.gitignore @@ -174,3 +174,5 @@ dist # Finder (MacOS) folder config .DS_Store +demo-data +demo.html diff --git a/README.md b/README.md index d122169..a1543bf 100644 --- a/README.md +++ b/README.md @@ -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: - --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 {} +```yaml +$define: ability +type: # Defaults to action +ap: 0 # AP cost to use this ability (default 0) +hp: 0 # HP cost to use this ability (default 0) +ep: 0 # EP cost to use this ability (default 0) +xp: 0 # XP cost to learn this ability (default 1) +damage: 0 # Damage dealt by this ability (default 0) +damageType: # Type of damage dealt by this ability (default phy) +roll: # Roll type for this ability (default none) ``` -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 { - 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: # Starting muscle prowess (default novice) +focus: # Starting focus prowess (default novice) +knowledge: # Starting knowledge prowess (default novice) +charm: # Starting charm prowess (default novice) +cunning: # Starting cunning prowess (default novice) +spark: # Starting spark prowess (default novice) +``` + +### Item Definition + +```yaml +$define: item +type: # Defaults to wielded +rarity: # Defaults to common +affinity: # Defaults to none +damage: 0 # Defaults to 0 +damageType: # Defaults to phy +hands: 1 | 2 # Only for wielded items +slot: # Only for worn items +``` + +#### `Item Type` Options + +- `wielded` +- `worn` +- `trinket` + +#### `Rarity` Options + +- `common` +- `uncommon` +- `rare` +- `legendary` + +#### `Item Slot` Options + +- `headgear` +- `outfit` +- `gloves` +- `boots` +- `adornment` + +### Method Definition + +```yaml +$define: method +curator: Someone # The name of the curator +abilities: # A 2-d array of ability IDs + - [rank-1-ability-id-one, rank-1-ability-id-two] + - [rank-2-ability-id-one] +``` + +### Resource Definition + +```yaml +$define: resource +type: # Defaults to text +url: https://url.com/image.jpeg # Only for image resources +data: # Only for table resources +``` + +#### `Resource Type` Options + +- `text` +- `image` +- `table` + +### Rule Definition + +```yaml +$define: rule +overrule: another-rule-id # Optional +order: 0 # Optional +``` + +### Species Definition + +```yaml +$define: species +hands: 2 # Defaults to 2 +abilities: # A list of innate ability IDs + - some-ability-id + - another-ability-id +ap: 0 # Starting AP modifier +hp: 0 # Starting HP modifier +ep: 0 # Starting EP modifier +xp: 0 # Starting XP modifier +muscle: # Defaults to novice +focus: # Defaults to novice +knowledge: # Defaults to novice +charm: # Defaults to novice +cunning: # Defaults to novice +spark: # Defaults to novice ``` diff --git a/biome.json b/biome.json index 7988bf6..1814905 100644 --- a/biome.json +++ b/biome.json @@ -7,7 +7,10 @@ }, "files": { "ignoreUnknown": false, - "ignore": ["*.d.ts", "*.json"], + "ignore": [ + "*.d.ts", + "*.json" + ], "include": [ "./src/**/*.ts", "./src/**/*.tsx", diff --git a/bun.lock b/bun.lock index 8427e5e..b85882f 100644 --- a/bun.lock +++ b/bun.lock @@ -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=="], } } diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index a7461c6..0000000 --- a/bunfig.toml +++ /dev/null @@ -1 +0,0 @@ -preload = ["./src/preload/http-plugin.js"] diff --git a/demo/main/binding.md b/demo/main/binding.md deleted file mode 100644 index cb097e7..0000000 --- a/demo/main/binding.md +++ /dev/null @@ -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! diff --git a/demo/main/processors/announce-slam-dunks.js b/demo/main/processors/announce-slam-dunks.js deleted file mode 100644 index e1889a3..0000000 --- a/demo/main/processors/announce-slam-dunks.js +++ /dev/null @@ -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, - }; -}; diff --git a/demo/main/processors/scream.js b/demo/main/processors/scream.js deleted file mode 100644 index 4a8a629..0000000 --- a/demo/main/processors/scream.js +++ /dev/null @@ -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 - }; -} diff --git a/demo/main/stats/statistics.toml b/demo/main/stats/statistics.toml deleted file mode 100644 index d28b28b..0000000 --- a/demo/main/stats/statistics.toml +++ /dev/null @@ -1,2 +0,0 @@ -player = "Flynn" -dunkaroos = 100 diff --git a/demo/main/stories/a-neat-story.md b/demo/main/stories/a-neat-story.md deleted file mode 100644 index 8b9e564..0000000 --- a/demo/main/stories/a-neat-story.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -published: true ---- - -This is a neat story I wrote! diff --git a/package.json b/package.json index e7e1428..a1bf80f 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/cli/index.ts b/src/cli/index.ts deleted file mode 100644 index edb3d9d..0000000 --- a/src/cli/index.ts +++ /dev/null @@ -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: - --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 { - 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); -} diff --git a/src/core/binding.ts b/src/core/binding.ts deleted file mode 100644 index 7652c0a..0000000 --- a/src/core/binding.ts +++ /dev/null @@ -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 { - /** 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 { - 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 { - // 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: {}, - }; -} diff --git a/src/core/files.ts b/src/core/files.ts deleted file mode 100644 index 89b2817..0000000 --- a/src/core/files.ts +++ /dev/null @@ -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 { - 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, - }; -} diff --git a/src/core/plugins.ts b/src/core/plugins.ts deleted file mode 100644 index 3f2ffed..0000000 --- a/src/core/plugins.ts +++ /dev/null @@ -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; - -export interface MusePlugin { - readonly filePath: string; - readonly name: string; - readonly description: string; - readonly version: string; - readonly step: MuseStepFn; -} - -export async function loadPlugin( - bindingDirname: string, - pluginPath: string, -): Promise { - // Resolve local paths, use URLs as-is - const resolvedProcessorPath = pluginPath.startsWith("http") - ? pluginPath - : path.resolve(bindingDirname, pluginPath); - const { step, name, description, version } = await import( - resolvedProcessorPath - ); - - if (!step || typeof step !== "function") { - throw new MuseError( - `Processor at ${pluginPath} does not export a step() function`, - ); - } - - if (name && typeof name !== "string") { - throw new MusePluginError(pluginPath, "Plugin name is not a string."); - } - - if (description && typeof description !== "string") { - throw new MusePluginError(pluginPath, "Plugin description is not a string"); - } - - if (version && typeof version !== "string") { - throw new MusePluginError(pluginPath, "Plugin version is not a string"); - } - - return { - filePath: resolvedProcessorPath, - step: step as MuseStepFn, - name: String(name ?? pluginPath), - description: String(description ?? ""), - version: String(version ?? "0.0.0"), - }; -} diff --git a/src/core/records.ts b/src/core/records.ts deleted file mode 100644 index 891b18e..0000000 --- a/src/core/records.ts +++ /dev/null @@ -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; - -/** - * Given an unknown shape, ensure it is an object or array of objects, - * then return it as an array of objects. - * - * @param val - The unknown value to format - * @returns - An array of objects - */ -function formatEntries(val: unknown): UnknownRecord[] { - if (Array.isArray(val) && val.every((el) => typeof el === "object")) { - return val; - } - - if (typeof val === "object" && val !== null) { - return [val as UnknownRecord]; - } - - throw new Error( - `Invalid data format. Entry files must define an object or array of objects, but found "${val}"`, - ); -} - -/** - * Parse one or more YAML documents from a string - * - * @param text - The YAML string to parse - * @returns - An array of parsed objects - */ -export function parseYAMLEntries(text: string): UnknownRecord[] { - const parsedDocs = YAML.parseAllDocuments(text); - - if (parsedDocs.some((doc) => doc.toJS() === null)) { - throw new Error("Encountered NULL resource"); - } - - const errors = parsedDocs.flatMap((doc) => doc.errors); - - if (errors.length > 0) { - throw new Error( - `Error parsing YAML resource: ${errors.map((e) => e.message).join(", ")}`, - ); - } - - const collection: UnknownRecord[] = parsedDocs.map((doc) => doc.toJS()); - return collection; -} - -/** - * Parse one or more JSON documents from a string - * - * @param text - The JSON string to parse - * @returns - An array of parsed objects - */ -export function parseJSONEntries(text: string): UnknownRecord[] { - return formatEntries(JSON.parse(text)); -} - -/** - * Parse one or more TOML documents from a string - * - * @param text - The TOML string to parse - * @returns - An array of parsed objects - */ -export function parseTOMLEntries(text: string): UnknownRecord[] { - return formatEntries(TOML.parse(text)); -} - -/** - * Parse one or more Markdown documents from a string - * - * @param text - The Markdown string to parse - * @returns - An array of parsed objects - */ -export function parseMarkdownEntry( - text: string, - contentKey: string, -): UnknownRecord[] { - return formatEntries(EMDY.parse(text, contentKey)); -} - -export function autoParseEntry(loadedFile: LoadedFile, contentKey: string) { - const { fileType, content } = loadedFile; - - if (fileType === "md") { - return parseMarkdownEntry(content, contentKey); - } - - if (fileType === "yaml") { - return parseYAMLEntries(content); - } - - if (fileType === "json") { - return parseJSONEntries(content); - } - - if (fileType === "toml") { - return parseTOMLEntries(content); - } - - throw new Error(`Unsupported file type: ${fileType}`); -} diff --git a/src/core/sources.ts b/src/core/sources.ts deleted file mode 100644 index 5c38e4b..0000000 --- a/src/core/sources.ts +++ /dev/null @@ -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; - -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 { - _raw: string; - location: string; - data: Record; - meta: MetaShape; - source: MuseSource; -} - -export async function parseMuseFile( - rawFilePath: string, - { contentKey = "content" }: SourceOptions, -): Promise { - 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 { - 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 { - 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 { - switch (source.type) { - case "file": - return loadFromFileSource(source, options); - case "url": - return loadFromURLSource(source, options); - default: - throw new Error(`Unsupported source type: ${source.type}`); - } -} diff --git a/src/css.d.ts b/src/css.d.ts new file mode 100644 index 0000000..cb02ac0 --- /dev/null +++ b/src/css.d.ts @@ -0,0 +1,2 @@ +declare module '*.css' { +} diff --git a/src/define/index.ts b/src/define/index.ts new file mode 100644 index 0000000..68b0aa6 --- /dev/null +++ b/src/define/index.ts @@ -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}`); + } +} diff --git a/src/errors.ts b/src/errors.ts deleted file mode 100644 index 5f43323..0000000 --- a/src/errors.ts +++ /dev/null @@ -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 {} diff --git a/src/exports.ts b/src/exports.ts deleted file mode 100644 index 451c4f8..0000000 --- a/src/exports.ts +++ /dev/null @@ -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"; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..23d166c --- /dev/null +++ b/src/index.ts @@ -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 { + 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); +} diff --git a/src/lib/binding.ts b/src/lib/binding.ts new file mode 100644 index 0000000..9e75fc8 --- /dev/null +++ b/src/lib/binding.ts @@ -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 { + // If the path does not specify a filename, use binding.yaml + const inputLocation = Bun.file(bindingPath); + + // If it is a directory, try again seeking a binding.yaml inside + const stat = await inputLocation.stat(); + if (stat.isDirectory()) { + return resolveBindingPath(path.resolve(bindingPath, "binding.yaml")); + } + + // If it doesnt exist, bail + if (!(await inputLocation.exists())) { + throw new FileNotFoundError(bindingPath); + } + + // Resolve the path + return path.resolve(bindingPath); +} + +export async function loadFromBinding( + bindingPath: string, +): Promise { + const resolvedBindingPath = await resolveBindingPath(bindingPath); + const yamlContent = await loadYAMLFileOrFail(resolvedBindingPath); + const binding = BindingSchema.parse(yamlContent); + const fileGlobs = binding.files; + const bindingFileDirname = path.dirname(resolvedBindingPath); + + const playbill = new Playbill( + binding.name, + binding.author, + binding.description, + binding.version, + ); + + // If this is extending another binding, load that first + for (const includePath of binding.include) { + const pathFromHere = path.resolve(bindingFileDirname, includePath); + const extendBindingPath = await resolveBindingPath(pathFromHere); + const loadedBinding = await loadFromBinding(extendBindingPath); + playbill.extend(loadedBinding.playbill); + } + + // Load any specified stylesheets + const loadedStyles: string[] = []; + for (const style of binding.styles) { + const cssFilePath = path.resolve(bindingFileDirname, style); + const cssFile = Bun.file(cssFilePath); + const cssFileExists = await cssFile.exists(); + + if (cssFileExists) { + loadedStyles.push(await cssFile.text()); + } else { + throw new FileNotFoundError(cssFilePath); + } + } + const styles = loadedStyles.join("\n"); + + // Scan for all files matching the globs + const allFilePaths: string[] = []; + + for (const thisGlob of fileGlobs) { + const glob = new Glob(thisGlob); + + const results = glob.scanSync({ + cwd: bindingFileDirname, + absolute: true, + followSymlinks: true, + onlyFiles: true, + }); + + allFilePaths.push(...results); + } + + // Exclude the binding.yaml file + const filePathsWithoutBindingFile = allFilePaths.filter((filePath) => { + return filePath !== resolvedBindingPath; + }); + + // Load discovered component files + const componentFiles: ComponentFile[] = []; + const fileLoadPromises: Promise[] = []; + + for (const filepath of filePathsWithoutBindingFile) { + const componentFile = new ComponentFile(filepath); + componentFiles.push(componentFile); + fileLoadPromises.push(componentFile.load()); + } + + await Promise.all(fileLoadPromises); + + // Aggregate all loaded components + const loadedComponents: AnyPlaybillComponent[] = []; + for (const file of componentFiles) { + loadedComponents.push(...file.components); + } + + // Add all components to the playbill + // (This method ensures proper addition order maintain correctness) + playbill.addManyComponents(loadedComponents); + + // Add all definitions + for (const [term, definition] of Object.entries(binding.terms)) { + playbill.addDefinition(term, definition); + } + + return { + bindingFilePath: bindingPath, + bindingFileDirname, + files: filePathsWithoutBindingFile, + componentFiles, + + name: binding.name, + author: binding.author ?? "Anonymous", + description: binding.description ?? "", + version: binding.version, + + omitDefaultStyles: binding.omitDefaultStyles, + styles, + + playbill, + }; +} diff --git a/src/lib/component-file.ts b/src/lib/component-file.ts new file mode 100644 index 0000000..0dd6ea4 --- /dev/null +++ b/src/lib/component-file.ts @@ -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; + } +} diff --git a/src/lib/errors.ts b/src/lib/errors.ts new file mode 100644 index 0000000..b92c44c --- /dev/null +++ b/src/lib/errors.ts @@ -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 {} diff --git a/src/lib/index.ts b/src/lib/index.ts new file mode 100644 index 0000000..e5a4496 --- /dev/null +++ b/src/lib/index.ts @@ -0,0 +1,4 @@ +export * from "./errors"; +export * from "./pfm"; +export * from "./component-file"; +export * from "./binding"; diff --git a/src/lib/pfm.ts b/src/lib/pfm.ts new file mode 100644 index 0000000..9584462 --- /dev/null +++ b/src/lib/pfm.ts @@ -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; + +export function createMarkdownRenderer( + binding: BoundPlaybill, +): MarkdownParserFunction { + // TODO: Allow specifying links to other files via file paths + // In this scenario, assume its a markdown-defined file and find the ID, use that for the link + // to allow for more natural Obsidian-like linking + // For now, this will mostly work, but if you need to specify a unique ID, it wont work in Obsidian + // despite being valid for Muse + + const parser = unified() + .use(remarkParse) + .use(remarkGfm) + .use(remarkWikiLink, { + aliasDivider: "|", + permalinks: binding.playbill.allComponentIds, + pageResolver: (permalink: string) => { + return [toSlug(permalink)]; + }, + hrefTemplate: (permalink: string) => `#component/${permalink}`, + }) + .use(remarkRehype, { allowDangerousHtml: true }) + .use(rehypeRaw) + .use(rehypeShiftHeading, { shift: 1 }) + .use(rehypeStringify); + + // TODO: Add sanitization + + return async (input: string): Promise => { + const parsed = await parser.process(input); + return String(parsed); + }; +} + +export async function compileMarkdownInPlaybill( + boundPlaybill: BoundPlaybill, +): Promise { + const renderMarkdown = createMarkdownRenderer(boundPlaybill); + const playbill = boundPlaybill.playbill; + + // Define a processor function to iterate over all components and process their markdown + const processMarkdownInComponent = async (entry: AnyPlaybillComponent) => { + const preparsed = parseProsceniumScript(entry.description, playbill); + entry.description = await renderMarkdown(preparsed); + + if (entry._component === "resource" && entry.type === "table") { + const newData: string[][] = []; + for (const row of entry.data) { + const newRow: string[] = []; + for (const cell of row) { + newRow.push(await renderMarkdown(cell)); + } + newData.push(newRow); + } + entry.data = newData; + } + }; + + // Process all components + await playbill.processComponents(processMarkdownInComponent); + + return boundPlaybill; +} diff --git a/src/lib/slug.ts b/src/lib/slug.ts new file mode 100644 index 0000000..b6a0de4 --- /dev/null +++ b/src/lib/slug.ts @@ -0,0 +1,5 @@ +import slugify from "slugify"; + +export function toSlug(str: string): string { + return slugify(str, { lower: true }); +} diff --git a/src/preload/http-plugin.js b/src/preload/http-plugin.js deleted file mode 100644 index bb7f732..0000000 --- a/src/preload/http-plugin.js +++ /dev/null @@ -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}`), - ); - }, -}); diff --git a/src/render/html/component/ability.tsx b/src/render/html/component/ability.tsx new file mode 100644 index 0000000..09537da --- /dev/null +++ b/src/render/html/component/ability.tsx @@ -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( + + {ability.ap} AP + , + ); + } + + if (ability.hp > 0) { + costs.push( + + {ability.hp} HP + , + ); + } + + if (ability.ep > 0) { + costs.push( + + {ability.ep} EP + , + ); + } + + const damage = + ability.damage !== null ? ( +
+ {ability.damage.amount} {ability.damage.type === "phy" ? "Phy" : "Arc"} +
+ ) : null; + + const roll = + ability.roll !== "none" ? ( +
{ability.roll}
+ ) : null; + + const type = ( + {ability.type} + ); + + const classList = `ability ability-${ability.type}`; + + return ( +
+
+

{ability.name}

+
+ {type} + {roll} + {damage} + {costs} +
+
+ +
+ ); +} diff --git a/src/render/html/component/base/html.tsx b/src/render/html/component/base/html.tsx new file mode 100644 index 0000000..761e2f1 --- /dev/null +++ b/src/render/html/component/base/html.tsx @@ -0,0 +1,10 @@ +interface HTMLWrapperProps { + html: string; + className?: string; +} + +export function HTML({ html, className }: HTMLWrapperProps) { + return ( +
+ ); +} diff --git a/src/render/html/component/base/section.tsx b/src/render/html/component/base/section.tsx new file mode 100644 index 0000000..2c85f28 --- /dev/null +++ b/src/render/html/component/base/section.tsx @@ -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 ( +
+
+ {preInfo} +

{title}

+ {info} +
+
{leftCornerTag}
+
{rightCornerTag}
+
{children}
+
+ ); +} diff --git a/src/render/html/component/glossary.tsx b/src/render/html/component/glossary.tsx new file mode 100644 index 0000000..a139dc4 --- /dev/null +++ b/src/render/html/component/glossary.tsx @@ -0,0 +1,26 @@ +import { Section } from "./base/section"; + +interface GlossaryTableProps { + terms: [string, string[]][]; +} + +export function GlossaryTable({ terms }: GlossaryTableProps) { + return ( +
+
+
+ {terms.map(([term, defs]) => { + return ( +
+
{term}
+ {defs.map((d, i) => ( +
{d}
+ ))} +
+ ); + })} +
+
+
+ ); +} diff --git a/src/render/html/component/header.tsx b/src/render/html/component/header.tsx new file mode 100644 index 0000000..4c0535e --- /dev/null +++ b/src/render/html/component/header.tsx @@ -0,0 +1,18 @@ +import type { Playbill } from "@proscenium/playbill"; + +interface PlaybillHeaderProps { + playbill: Playbill; +} + +export function PageHeader({ playbill }: PlaybillHeaderProps) { + return ( +
+

{playbill.name}

+

A Playbill by {playbill.author}

+

+ {" "} + Version {playbill.version} +

+
+ ); +} diff --git a/src/render/html/component/item.tsx b/src/render/html/component/item.tsx new file mode 100644 index 0000000..ebe63b7 --- /dev/null +++ b/src/render/html/component/item.tsx @@ -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( +

+ {item.affinity} Affinity +

, + ); + } + + if (item.damage !== null && item.damage.amount > 0) { + infoParts.push( +

+ {item.damage.amount} {capitalize(item.damage.type)} Damage +

, + ); + } + + return ( +
{infoParts}
} + rightCornerTag={capitalize(item.rarity)} + leftCornerTag={itemTypeDescriptor} + > + + + ); +} diff --git a/src/render/html/component/method.tsx b/src/render/html/component/method.tsx new file mode 100644 index 0000000..e39106b --- /dev/null +++ b/src/render/html/component/method.tsx @@ -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 ( +
+

Rank {i + 1}

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

} + info={ +

+ } + > +

{ranks}
+
+ ); +} diff --git a/src/render/html/component/resource.tsx b/src/render/html/component/resource.tsx new file mode 100644 index 0000000..cd07910 --- /dev/null +++ b/src/render/html/component/resource.tsx @@ -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 ( +
+ {resource.categories.map((category) => ( +
  • {category}
  • + ))} + + } + > + +
    + ); +} + +interface ImageResourceSectionProps { + resource: ImageResource; +} + +function ImageResourceSection({ resource }: ImageResourceSectionProps) { + return ( +
    + {resource.categories.map((category) => ( +
  • {category}
  • + ))} + + } + > +
    + {resource.name} +
    +
    +
    + ); +} + +interface TableResourceSectionProps { + resource: TableResource; +} + +function TableResourceSection({ resource }: TableResourceSectionProps) { + const [headers, ...rows] = resource.data; + return ( +
    + {resource.categories.map((category) => ( +
  • {category}
  • + ))} + + } + > + + + + {headers.map((header) => ( + + + + {rows.map((row, i) => ( + + {row.map((cell, j) => ( + + ))} + +
    + ))} +
    + ))} +
    +
    + ); +} + +interface ResourceSectionProps { + resource: Resource; +} + +export function ResourceSection({ resource }: ResourceSectionProps) { + switch (resource.type) { + case "text": + return ; + case "image": + return ; + case "table": + return ; + } +} diff --git a/src/render/html/component/rule.tsx b/src/render/html/component/rule.tsx new file mode 100644 index 0000000..7971be1 --- /dev/null +++ b/src/render/html/component/rule.tsx @@ -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 ( +
    + +
    + ); +} diff --git a/src/render/html/component/species.tsx b/src/render/html/component/species.tsx new file mode 100644 index 0000000..6c11831 --- /dev/null +++ b/src/render/html/component/species.tsx @@ -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 ? ( +
    +

    Innate Abilities

    + {species.abilities.map((abilityId) => { + const ability = playbill.getAbility(abilityId); + + if (ability === null) { + throw new Error(`Ability not found: ${abilityId}`); + } + + return ; + })} +
    + ) : ( + "" + ); + + const sectionContent = ( + <> +
    +
    +

    + HP +

    +

    {hpString}

    +
    + +
    +

    + AP +

    +

    {apString}

    +
    + +
    +

    + EP +

    +

    {epString}

    +
    +
    + +
    +
    +

    Muscle

    +

    + {species.muscle} +

    +
    + +
    +

    Focus

    +

    + {species.focus} +

    +
    + +
    +

    + Knowledge +

    +

    + {species.knowledge} +

    +
    + +
    +

    Charm

    +

    + {species.charm} +

    +
    + +
    +

    Cunning

    +

    + {species.cunning} +

    +
    + +
    +

    Spark

    +

    + {species.spark} +

    +
    +
    + + {abilitySection} + + ); + + return ( +
    Species

    } + > + {sectionContent} +
    + ); +} diff --git a/src/render/html/default.css b/src/render/html/default.css new file mode 100644 index 0000000..f7c5ab5 --- /dev/null +++ b/src/render/html/default.css @@ -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; +} diff --git a/src/render/html/index.tsx b/src/render/html/index.tsx new file mode 100644 index 0000000..bb26ddd --- /dev/null +++ b/src/render/html/index.tsx @@ -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 { + // Make a copy of the playbill to avoid modifying the original + // and compile all description markdown + const playbill = Playbill.fromSerialized(boundPlaybill.playbill.serialize()); + await compileMarkdownInPlaybill(boundPlaybill); + + // Prepare stylesheet + const cssParts: string[] = []; + + if (!boundPlaybill.omitDefaultStyles) { + cssParts.push(``); + } + + if (boundPlaybill.styles.length > 0) { + cssParts.push(``); + } + + const css = cssParts.join("\n"); + + const body = renderToString( + <> +
    + {playbill.allSpecies.map((species) => ( + + ))} +
    + +
    + {playbill.allMethods.map((method) => ( + + ))} +
    + +
    + {playbill.allItems.map((item) => ( + + ))} +
    + +
    + {playbill.allRules.map((rule) => ( + + ))} +
    + +
    + {} +
    + +
    + {playbill.allResources.map((resource) => ( + + ))} +
    + , + ); + + // Main document + const doc = ` + + + + + + Playbill: ${playbill.name} + ${css} + + + + + ${renderToString()} +

    ${playbill.description}

    + + ${body} + + + `; + + return doc; +} diff --git a/src/util.ts b/src/util.ts deleted file mode 100644 index 0c84a67..0000000 --- a/src/util.ts +++ /dev/null @@ -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 { - const normalized = normalize(filePath); - const file = Bun.file(normalized); - const exists = await file.exists(); - - if (!exists) { - return null; - } - - const stat = await file.stat(); - if (stat.isDirectory()) { - return null; - } - - return normalized; -} diff --git a/src/util/args.ts b/src/util/args.ts new file mode 100644 index 0000000..25d2d0f --- /dev/null +++ b/src/util/args.ts @@ -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: + --check Only load and check the current binding and resources, but do not compile + --outfile, -o Specify the output file path. If not specified, output to stdout + --watch, -w Watch the directory for changes and recompile + --renderer, -r Specify the output renderer. Options: json, html + --markdown Compile markdown in descriptions when rendering to JSON + --help, -h Show this help message +`.trim(); + +const Renderers = ["json", "html"] as const; +const RendererSchema = z.enum(Renderers); +type Renderer = z.infer; + +/** + * A shape representing the arguments passed to the CLI + */ +export interface CLIArguments { + inputFilePath: string; + + options: { + check: boolean; + outfile: string; + help: boolean; + renderer: Renderer; + watch: boolean; + markdown: boolean; + }; +} + +/** + * Given an array of CLI arguments, parse them into a structured object + * + * @param argv The arguments to parse + * @returns The parsed CLI arguments + * + * @throws {CLIError} if the arguments are invalid + */ +export function parseCLIArguments(argv: string[]): CLIArguments { + const { values: options, positionals: args } = parseArgs({ + args: argv, + options: { + check: { + short: "c", + type: "boolean", + default: false, + }, + outfile: { + short: "o", + type: "string", + default: "", + }, + help: { + short: "h", + default: false, + type: "boolean", + }, + renderer: { + short: "r", + default: "json", + type: "string", + }, + watch: { + default: false, + type: "boolean", + }, + markdown: { + default: false, + type: "boolean", + }, + }, + strict: true, + allowPositionals: true, + }); + + // -- ARG VALIDATION -- // + if (options.check && options.outfile !== "") { + throw new CLIError("Cannot use --check and --outfile together"); + } + + const parsedRenderer = RendererSchema.safeParse(options.renderer); + + if (!parsedRenderer.success) { + throw new MuseError(`Invalid renderer: ${parsedRenderer.data}`); + } + + return { + inputFilePath: args[0] ?? "./binding.yaml", + + options: { + check: options.check, + outfile: options.outfile, + help: options.help, + renderer: parsedRenderer.data, + watch: options.watch, + markdown: options.markdown, + }, + }; +} diff --git a/src/util/files.ts b/src/util/files.ts new file mode 100644 index 0000000..cd6c865 --- /dev/null +++ b/src/util/files.ts @@ -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 { + const fileToLoad = Bun.file(filePath); + const fileExists = await fileToLoad.exists(); + + if (!fileExists) { + throw new FileNotFoundError(`File not found: ${filePath}`); + } + + return await fileToLoad.text(); +} + +/** + * Load and parse a YAML file or throw an error if the file doesnt exist + * or the yaml fails to parse + * @param filePath The path to the file to load + * @returns The parsed contents of the file + * + * @throws {FileNotFoundError} if the file does not exist + * @throws {FileError} if the file is not valid YAML + */ +export async function loadYAMLFileOrFail(filePath: string): Promise { + try { + return YAML.parse(await loadFileOrFail(filePath)); + } catch (error) { + throw new FileError("Failed to parse YAML file", filePath, `${error}`); + } +} + +/** + * Load and parse a JSON file or throw an error if the file doesnt exist + * or the JSON fails to parse + * @param filePath The path to the file to load + * @returns The parsed contents of the file + * + * @throws {FileNotFoundError} if the file does not exist + * @throws {FileError} if the file is not valid JSON + */ +export async function loadJSONFileOrFail(filePath: string): Promise { + try { + return JSON.parse(await loadFileOrFail(filePath)); + } catch (error) { + throw new FileError("Failed to parse JSON file", filePath, `${error}`); + } +} + +/** + * Returns the absolute path of the directory containing the given file + * + * @param filePath The path to the file + * @returns The absolute path of the directory containing the file + */ +export function getAbsoluteDirname(filePath: string): string { + return path.resolve(path.dirname(filePath)); +} diff --git a/src/util/index.ts b/src/util/index.ts new file mode 100644 index 0000000..1d10193 --- /dev/null +++ b/src/util/index.ts @@ -0,0 +1,2 @@ +export * from "./args"; +export * from "./files"; diff --git a/tsconfig.json b/tsconfig.json index a964c9c..cc9ee32 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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", ] }