From 696578a1c8f25e9eb44d819359da274e809f9c3a Mon Sep 17 00:00:00 2001 From: backwardspy Date: Sun, 31 Mar 2024 15:52:56 +0100 Subject: [PATCH] feat(whiskers)!: switch to tera, use rich context objects (#150) * feat(whiskers)!: rewrite from scratch with tera and rich context * feat(whiskers): convert demo example to tera * feat(whiskers): convert frontmatter example to tera * feat(whiskers): convert simple single-file example to tera * feat(whiskers): convert overrides example to tera * docs(whiskers): update for 2.0 * docs(whiskers): update overrides section(s) * chore(whiskers): add beta suffix to version number * Revert "chore(whiskers): add beta suffix to version number" This reverts commit 2d60034e8e2b20f8b73c7144c6a5178cd5c03eac. * feat(whiskers): bring back 0.6 opacity red in demo example * docs(whiskers): document the --dry-run flag --- Cargo.toml | 37 +- README.md | 495 ++++++++++---------- examples/demo/input.hbs | 27 -- examples/demo/input.tera | 28 ++ examples/demo/justfile | 2 +- examples/demo/output/frappe.md | 13 +- examples/demo/output/latte.md | 13 +- examples/demo/output/macchiato.md | 11 +- examples/demo/output/mocha.md | 11 +- examples/{errors.hbs => errors.tera} | 0 examples/frontmatter/input.hbs | 41 -- examples/frontmatter/input.tera | 39 ++ examples/frontmatter/justfile | 2 +- examples/frontmatter/output/frappe.md | 14 +- examples/frontmatter/output/latte.md | 14 +- examples/frontmatter/output/macchiato.md | 12 +- examples/frontmatter/output/mocha.md | 12 +- examples/single-file/overrides/input.hbs | 32 -- examples/single-file/overrides/input.tera | 35 ++ examples/single-file/overrides/justfile | 2 +- examples/single-file/overrides/output.md | 1 - examples/single-file/simple/input.hbs | 25 - examples/single-file/simple/input.tera | 29 ++ examples/single-file/simple/justfile | 2 +- examples/single-file/simple/output.md | 64 +-- src/cli.rs | 125 +++++ src/context.rs | 60 +++ src/filters.rs | 134 ++++++ src/frontmatter.rs | 478 ++----------------- src/functions.rs | 69 +++ src/helper.rs | 123 ----- src/lib.rs | 58 +-- src/main.rs | 463 +++++++++++------- src/markdown.rs | 136 ++++++ src/matrix.rs | 73 +++ src/models.rs | 415 ++++++++++++++++ src/parse.rs | 70 --- src/postprocess.rs | 24 - src/template.rs | 249 ---------- src/templating.rs | 238 ++++++++++ tests/cli.rs | 80 ++-- tests/fixtures/multi/multi.md | 124 +++++ tests/fixtures/multi/multi.tera | 22 + tests/fixtures/multifile.tera | 12 + tests/fixtures/single/single.md | 35 ++ tests/fixtures/single/single.tera | 26 + tests/fixtures/singlefile-multiflavor.tera | 10 + tests/fixtures/singlefile-singleflavor.tera | 8 + 48 files changed, 2350 insertions(+), 1643 deletions(-) delete mode 100644 examples/demo/input.hbs create mode 100644 examples/demo/input.tera rename examples/{errors.hbs => errors.tera} (100%) delete mode 100644 examples/frontmatter/input.hbs create mode 100644 examples/frontmatter/input.tera delete mode 100644 examples/single-file/overrides/input.hbs create mode 100644 examples/single-file/overrides/input.tera delete mode 100644 examples/single-file/simple/input.hbs create mode 100644 examples/single-file/simple/input.tera create mode 100644 src/cli.rs create mode 100644 src/context.rs create mode 100644 src/filters.rs create mode 100644 src/functions.rs delete mode 100644 src/helper.rs create mode 100644 src/markdown.rs create mode 100644 src/matrix.rs create mode 100644 src/models.rs delete mode 100644 src/parse.rs delete mode 100644 src/postprocess.rs delete mode 100644 src/template.rs create mode 100644 src/templating.rs create mode 100644 tests/fixtures/multi/multi.md create mode 100644 tests/fixtures/multi/multi.tera create mode 100644 tests/fixtures/multifile.tera create mode 100644 tests/fixtures/single/single.md create mode 100644 tests/fixtures/single/single.tera create mode 100644 tests/fixtures/singlefile-multiflavor.tera create mode 100644 tests/fixtures/singlefile-singleflavor.tera diff --git a/Cargo.toml b/Cargo.toml index a02d077f..8e9e605a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "catppuccin-whiskers" -version = "1.1.4" +version = "2.0.0" authors = ["backwardspy "] edition = "2021" description = "Soothing port creation tool for the high-spirited!" @@ -9,27 +9,42 @@ homepage = "https://github.com/catppuccin/toolbox/tree/main/whiskers" repository = "https://github.com/catppuccin/toolbox" license = "MIT" +[lib] +name = "whiskers" +path = "src/lib.rs" + [[bin]] name = "whiskers" path = "src/main.rs" +[lints.clippy] +all = "warn" +pedantic = "warn" +nursery = "warn" +unwrap_used = "warn" +missing_errors_doc = "allow" +implicit_hasher = "allow" +cast_possible_truncation = "allow" +cast_sign_loss = "allow" + [dependencies] +anyhow = "1.0" base64 = "0.22" -catppuccin = { version = "2.1", features = ["css-colors"] } -indexmap = { version = "2.2", features = ["serde"] } +catppuccin = { version = "2.1", features = ["serde", "css-colors"] } clap = { version = "4.5", features = ["derive"] } -clap-stdin = "0.4" -color-eyre = { version = "0.6", default-features = false } +clap-stdin = "0.4.0" css-colors = "1.0" -handlebars = "5.1" -regex = "1.10" +indexmap = { version = "2.2", features = ["serde"] } +itertools = "0.12" +lzma-rust = "0.1" +rmp-serde = "1.1" +semver = { version = "1.0.22", features = ["serde"] } serde = { version = "1.0", features = ["derive"] } -serde_json = { version = "1.0", features = ["preserve_order"] } +serde_json = "1.0" serde_yaml = "0.9" -tempfile = "3.10" +tempfile = "3.10.1" +tera = { version = "1.19", features = ["preserve_order"] } thiserror = "1.0" -titlecase = "2.2" -json-patch = "1.2" [dev-dependencies] assert_cmd = "2.0" diff --git a/README.md b/README.md index 3cb0e6f0..cc81c28b 100644 --- a/README.md +++ b/README.md @@ -37,77 +37,168 @@ into. $ whiskers --help Soothing port creation tool for the high-spirited! -Usage: whiskers [OPTIONS] [TEMPLATE] [FLAVOR] +Usage: whiskers [OPTIONS] [TEMPLATE] Arguments: - [TEMPLATE] Path to the template file to render, or `-` for stdin - [FLAVOR] Flavor to get colors from [possible values: latte, frappe, macchiato, mocha, all] + [TEMPLATE] + Path to the template file, or - for stdin Options: - --overrides The overrides to apply to the template in JSON format - -o, --output-path Path to write to instead of stdout - --check Instead of printing a result, check if anything would change - -l, --list-helpers List all template helpers in Markdown format - -h, --help Print help - -V, --version Print version + -f, --flavor + Render a single flavor instead of all four + + [possible values: latte, frappe, macchiato, mocha] + + --color-overrides + Set color overrides + + --overrides + Set frontmatter overrides + + --check [] + Instead of creating an output, check it against an example + + In single-output mode, a path to the example file must be provided. In multi-output mode, no path is required and, if one is provided, it will be ignored. + + --dry-run + Dry run, don't write anything to disk + + -l, --list-functions + List all Tera filters and functions + + -o, --output-format + Output format of --list-functions + + [default: json] + [possible values: json, yaml, markdown, markdown-table] + + -h, --help + Print help (see a summary with '-h') + + -V, --version + Print version + ``` ## Template -Please familiarize yourself with [Handlebars](https://handlebarsjs.com/guide/), -which is the templating engine used in whiskers. +Please familiarize yourself with [Tera](https://keats.github.io/tera/), +which is the templating engine used in Whiskers. ### Context Variables The following variables are available for use in your templates: -| Variable | Description | -| ----------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `flavor` (string) | The name of the flavor being templated. Possible values: `latte`, `frappé`, `macchiato`, `mocha`. | -| `isLight` (bool) | True if `flavor` is `latte`, false otherwise. | -| `isDark` (bool) | True unless `flavor` is `latte`. | -| `rosewater`, `flamingo`, `pink`, [(etc.)](https://github.com/catppuccin/rust/blob/5124eb99eb98d7111dca24537d428a6078e5bbb6/src/flavour.rs#L41-L66) (string) | All named colors in each flavor, each color is formatted as hex by default. | -| `colors` (array) | An array containing all of the named colors. | -| `flavors` (array) | An array containing all of the named flavors, with every other context variable.
See [Single File Support](#Single-File-Support) for more information. | -| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | - -### Helpers - -The following custom helpers are available: - -| Helper
(`<>` values are args) | Input | Output | Description | -| ------------------------------------- | --------------------------------- | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| uppercase \ | `{{ uppercase "hello" }}` | `HELLO` | Convert a string to uppercase. | -| lowercase \ | `{{ lowercase "HELLO" }}` | `hello` | Convert a string to lowercase. | -| titlecase \ | `{{ titlecase "hello there" }}` | `Hello There` | Convert a string to titlecase. | -| trunc \ \ | `{{ trunc 3.14159265 2 }}` | `3.14` | Format a number to a string with a given number of places. | -| lighten \ \ | `{{ lighten red 0.1 }}` | `f8bacc` / `hsl(343, 81%, 85%)` | Lighten a color by a percentage. | -| darken \ \ | `{{ darken red 0.1 }}` | `ee5c85` / `hsl(343, 81%, 65%)` | Darken a color by a percentage. | -| mix \ \ \ | `{{ mix red base 0.3 }}` | `5e4054` (30% red, 70% base) | Mix two colors together in a given ratio. | -| opacity \ \ | `{{ opacity red 0.5 }}` | `hsla(343, 81%, 75%, 0.50)` | Set the opacity of a color. | -| unquote \ | `"{{ unquote isLight true }}"` | `true` (the surrounding quotation marks have been removed) | Marks a value to be unquoted. Mostly useful for maintaining JSON syntax highlighting in template files when a non-string value is needed. | -| rgb \ | `{{ rgb red }}` | `rgb(243, 139, 168)` | Convert a color to CSS RGB format. | -| rgba \ | `{{ rgba (opacity red 0.6) }}` | `rgba(243, 139, 168, 0.60)` | Convert a color to CSS RGBA format. | -| hsl \ | `{{ hsl red }}` | `hsl(343, 81%, 75%)` | Convert a color to CSS HSL format. | -| hsla \ | `{{ hsla (opacity red 0.6) }}` | `hsla(343, 81%, 75%, 0.60)` | Convert a color to CSS HSLA format. | -| red_i \ | `{{ red_i red }}` | `243` | Get the red channel of a color as an integer from 0 to 255. | -| green_i \ | `{{ green_i red }}` | `139` | Get the green channel of a color as an integer from 0 to 255. | -| blue_i \ | `{{ blue_i red }}` | `168` | Get the blue channel of a color as an integer from 0 to 255. | -| alpha_i \ | `{{ alpha_i (opacity red 0.6) }}` | `153` | Get the alpha channel of a color as an integer from 0 to 255. | -| red_f \ | `{{ red_f red }}` | `0.95` (truncated to 2 places) | Get the red channel of a color as a float from 0 to 1. | -| green_f \ | `{{ green_f red }}` | `0.55` (truncated to 2 places) | Get the green channel of a color as a float from 0 to 1. | -| blue_f \ | `{{ blue_f red }}` | `0.66` (truncated to 2 places) | Get the blue channel of a color as a float from 0 to 1. | -| alpha_f \ | `{{ alpha_f (opacity red 0.6) }}` | `0.60` (truncated to 2 places) | Get the alpha channel of a color as a float from 0 to 1. | -| red_h \ | `{{ red_h red }}` | `f3` | Get the red channel of a color as a hexadecimal number from 00 to ff. | -| green_h \ | `{{ green_h red }}` | `8b` | Get the green channel of a color as a hexadecimal number from 00 to ff. | -| blue_h \ | `{{ blue_h red }}` | `a8` | Get the blue channel of a color as a hexadecimal number from 00 to ff. | -| alpha_h \ | `{{ alpha_h (opacity red 0.6) }}` | `99` | Get the alpha channel of a color as a hexadecimal number from 00 to ff. | -| darklight \ \ | `{{ darklight "Night" "Day" }}` | `Day` on Latte, `Night` on other flavors | Choose a value depending on the current flavor. Latte is light, while Frappé, Macchiato, and Mocha are all dark. | +#### Single-Flavor Mode + +| Variable | Description | +| - | - | +| `flavor` ([`Flavor`](#flavor)) | The flavor being templated. | +| `rosewater`, `flamingo`, `pink`, [etc.](https://github.com/catppuccin/catppuccin#-palette) ([`Color`](#color)) | All colors of the flavor being templated. | +| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | + +#### Multi-Flavor Mode + +| Variable | Description | +| - | - | +| `flavors` (Map\) | An array containing all of the named flavors, with every other context variable. | +| Any Frontmatter | All frontmatter variables as described in the [Frontmatter](#Frontmatter) section. | + +#### Types + +These types are designed to closely match the [palette.json](https://github.com/catppuccin/palette/blob/main/palette.json). + +##### Flavor + +| Field | Type | Description | Examples | +| - | - | - | - | +| `name` | `String` | The name of the flavor. | `"Latte"`, `"Frappé"`, `"Macchiato"`, `"Mocha"` | +| `identifier` | `String` | The identifier of the flavor. | `"latte"`, `"frappe"`, `"macchiato"`, `"mocha"` | +| `dark` | `bool` | Whether the flavor is dark. | `false` for Latte, `true` for others | +| `light` | `bool` | Whether the flavor is light. | `true` for Latte, `false` for others | +| `colors` | `Map` | A map of color identifiers to their respective values. | | + +##### Color + +| Field | Type | Description | Examples | +| - | - | - | - | +| `name` | `String` | The name of the color. | `"Rosewater"`, `"Surface 0"`, `"Base"` | +| `identifier` | `String` | The identifier of the color. | `"rosewater"`, `"surface0"`, `"base"` | +| `accent` | `bool` | Whether the color is an accent color. | | +| `hex` | `String` | The color in hexadecimal format. | `"1e1e2e"` | +| `rgb` | `RGB` | The color in RGB format. | | +| `hsl` | `HSL` | The color in HSL format. | | +| `opacity` | `u8` | The opacity of the color. | `0` to `255` | + +##### RGB + +| Field | Type | Description | +| - | - | - | +| `r` | `u8` | The red channel of the color. | +| `g` | `u8` | The green channel of the color. | +| `b` | `u8` | The blue channel of the color. | + +##### HSL + +| Field | Type | Description | +| - | - | - | +| `h` | `u16` | The hue of the color. | +| `s` | `u8` | The saturation of the color. | +| `l` | `u8` | The lightness of the color. | + +### Functions + +| Name | Description | Examples | +|------|-------------|----------| +| `if` | Return one value if a condition is true, and another if it's false | `if(cond=true, t=1, f=0)` => `1` | +| `object` | Create an object from the input | `object(a=1, b=2)` => `{a: 1, b: 2}` | +| `css_rgb` | Convert a color to an RGB CSS string | `css_rgb(color=red)` => `rgb(255, 0, 0)` | +| `css_rgba` | Convert a color to an RGBA CSS string | `css_rgba(color=red)` => `rgba(255, 0, 0, 1)` | +| `css_hsl` | Convert a color to an HSL CSS string | `css_hsl(color=red)` => `hsl(0, 100%, 50%)` | +| `css_hsla` | Convert a color to an HSLA CSS string | `css_hsla(color=red)` => `hsla(0, 100%, 50%, 1)` | + +### Filters + +| Name | Description | Examples | +|------|-------------|----------| +| `add` | Add a value to a color | `red \| add(hue=30)` => `#ff6666` | +| `sub` | Subtract a value from a color | `red \| sub(hue=30)` => `#ff6666` | +| `mod` | Modify a color | `red \| mod(lightness=0.5)` => `#ff6666` | +| `mix` | Mix two colors together | `red \| mix(color=base, amount=0.5)` => `#804040` | +| `urlencode_lzma` | Serialize an object into a URL-safe string with LZMA compression | `red \| urlencode_lzma()` => `#ff6666` | +| `trunc` | Truncate a number to a certain number of places | `1.123456 \| trunc(places=3)` => `1.123` | ## Frontmatter -You can include additional context variables in the templating process by adding -it to an optional YAML frontmatter section at the top of your template file. +Whiskers templates may include a frontmatter section at the top of the file. + +The frontmatter is a YAML block that contains metadata about the template. If +present, the frontmatter section must be the first thing in the file and must +take the form of valid YAML set between triple-dashed lines. + +### Template Version + +The most important frontmatter key is the Whiskers version. This key allows +Whiskers to ensure that it is rendering a template that it can understand. + +Example: + +```yaml +--- +whiskers: + version: "2.0.0" +--- +... standard template content goes here ... +``` + +If the version key is not present, Whiskers will display a warning and attempt +to render the template anyway. However, it is recommended to always include the +version key to ensure compatibility with future versions of Whiskers. + +### Frontmatter Variables + +You can also include additional context variables in the templating process by +adding them to your template's frontmatter. As a simple example, given the following template (`example.cfg`): @@ -118,11 +209,11 @@ author: 'winston' --- # Catppuccin for {{app}} # by {{author}} -bg = '{{base}}' -fg = '{{text}}' +bg = '{{base.hex}}' +fg = '{{text.hex}}' ``` -Running `whiskers example.cfg mocha` produces the following output: +Running `whiskers example.cfg -f mocha` produces the following output: ```yaml # Catppuccin for Pepperjack @@ -131,20 +222,18 @@ bg = '1e1e2e' fg = 'cdd6f4' ``` -Values in YAML frontmatter are rendered in the same way as the rest of the -template, which means you can also make use of context variables in your -frontmatter. This can be useful for things like setting an accent color: +A common use of frontmatter is setting an accent color for the theme: -```yaml +``` --- -accent: "{{mauve}}" -darkGreen: "{{darken green 0.3}}" +accent: "mauve" --- -bg = "#{{base}}" -fg = "#{{text}}" -border = "#{{accent}}" -diffAddFg = "#{{green}}" -diffAddBg = "#{{darkGreen}}" +{% set darkGreen = green | sub(lightness=30) %} +bg = "#{{base.hex}}" +fg = "#{{text.hex}}" +border = "#{{flavor.colors[accent].hex}}" +diffAddFg = "#{{green.hex}}" +diffAddBg = "#{{darkGreen.hex}}" ``` Rendering the above template produces the following output: @@ -159,231 +248,151 @@ diffaddbg = "#40b436" ## Overrides -### Frontmatter - -Whiskers supports overriding template values in the frontmatter itself. For -example, this can be useful for changing variables depending on the flavor: - -`example.yml` - -```yaml ---- -accent: "{{mauve}}" -overrides: - latte: # only applies to Latte - accent: "{{pink}}" - mocha: # only applies to Mocha - accent: "{{blue}}" ---- -{{flavor}} has accent color {{accent}}. -``` - -When running `whiskers example.yml {latte, frappe, macchiato, mocha}`, we see that: - -- Frappé & Macchiato will have the accent `mauve` hex code. -- Latte will have the accent `pink` hex code. -- Mocha will have the accent `blue` hex code. - -### CLI - -Overrides can also be specified through the cli via the `--overrides` flag, taking in a JSON string resembling the -frontmatter. This is particularly useful with build scripts to automatically generate files for each accent: +Frontmatter overrides can also be specified through the cli via the +`--overrides` flag, taking in a JSON string resembling the frontmatter. This is +particularly useful with build scripts to automatically generate files for each +accent: `example.yml` ```yaml --- -accent: "{{mauve}}" +accent: "mauve" --- theme: - accent: "{{accent}}" + accent: "{{flavor.colors[accent].hex}}" ``` -When running `whiskers example.yml latte --overrides '{"accent": "{{pink}}"}'`, +When running `whiskers example.yml -f latte --overrides '{"accent": "pink"}'`, the `accent` will be overridden to pink. -### Frontmatter & CLI +## Color Overrides -Overrides can be specified both in the frontmatter and the CLI but it is -important to understand the order of priority: +Color overrides can be specified through the cli via the `--color-overrides` +flag. This flag takes a JSON string like the following: -1. CLI overrides (`--overrides` flag.) -2. Frontmatter `overrides` block. -3. Frontmatter root context. - -To express this visually, given an `example.yml` file: - -```yaml ---- -accent: "{{mauve}}" # <-- Frontmatter Root Context -background: "{{base}}" -text: "{{text}}" -overrides: # <-- Frontmatter Overrides Block - mocha: - accent: "{{blue}}" ---- -``` - -and the command: - -```shell -whiskers example.yml mocha --overrides '{"accent": "{{pink}}"}' # <-- CLI Overrides +```json +{ + "all": { + "text": "ff0000" + }, + "mocha": { + "base": "000000", + "mantle": "010101", + "crust": "020202", + } +} ``` -The resulting file will have the accent `pink` as the accent will go through the -following transformations: +Passing these overrides would set the `text` color to bright red for all +flavors, and the `base`, `mantle`, and `crust` colors to black/near-black for +Mocha. -1. accent is set to `mauve` in the root context. -2. accent is overridden to `blue` in the overrides block. -3. accent is overridden again to `pink` in the CLI overrides. +## Single-Flavor Mode -## Single File Support +Running Whiskers with the `--flavor/-f` flag causes it to run in single-flavor mode. +This means the chosen flavor is placed into the template context as `flavor` and, +for convenience, all of its colors are also placed into the context as their respective +identifiers (`red`, `surface0`, et cetera.) -Sometimes, you may not want to generate a file per flavor, but rather use all -the flavors inside one single file. This is achieved specifying the `