| 1 |
# subst |
| 2 |
|
| 3 |
A markdown-aware `{{ dotted.path | filter(args) }}` value-substitution engine. |
| 4 |
|
| 5 |
[mnw-assumptions](../mnw-assumptions) is its only consumer today. The engine is |
| 6 |
domain-free: it knows nothing about where values come from. Callers populate a table and |
| 7 |
register whatever filters they need. |
| 8 |
|
| 9 |
```rust |
| 10 |
use subst::{Substituter, Value}; |
| 11 |
|
| 12 |
let s = Substituter::new() |
| 13 |
.with_value("price.basic", Value::Int(16)) |
| 14 |
.with_value("stripe.percent", Value::Float(0.029)); |
| 15 |
|
| 16 |
let out = s.substitute("Basic is ${{ price.basic }} ({{ stripe.percent | percent }} fee).")?; |
| 17 |
assert_eq!(out, "Basic is $16 (2.9% fee)."); |
| 18 |
``` |
| 19 |
|
| 20 |
## Markers |
| 21 |
|
| 22 |
A marker is `{{ path (| filter (args)?)* }}`. The path is a dotted key looked up verbatim |
| 23 |
in the table; filters run left-to-right, each taking the previous filter's output. |
| 24 |
Arguments are integer, float, or quoted string literals (single or double): |
| 25 |
`{{ x | round(2) | money("$") }}`. |
| 26 |
|
| 27 |
Markers inside inline code spans and fenced code blocks are **left verbatim**, so |
| 28 |
documentation showing literal template syntax survives untouched. That is the whole reason |
| 29 |
the pass is markdown-aware; everything else about it is plain text substitution. |
| 30 |
|
| 31 |
## Values and filters |
| 32 |
|
| 33 |
`Value` is `Int(i64) | Float(f64) | String(String)`. Built-in filters: |
| 34 |
|
| 35 |
|
| 36 |
|
| 37 |
| `int` | Truncate toward zero, render as an integer | |
| 38 |
| `ceil` / `floor` / `round(n?)` | Round up / down / to `n` decimal places (0..=10, default 0) | |
| 39 |
| `money(symbol?)` | Two decimals behind a symbol, `$` unless given: `0.3` becomes `$0.30` | |
| 40 |
| `percent(n?)` | Multiply by 100, `n` decimals (default 1), trailing `%`: `0.029` becomes `2.9%` | |
| 41 |
| `upper` / `lower` | ASCII case conversion | |
| 42 |
|
| 43 |
Register a custom filter with `with_filter(name, f)`. `Filter` is a single-method trait |
| 44 |
(`apply(Value, &[FilterArg]) -> Result<Value, FilterError>`) with a blanket impl over |
| 45 |
`Fn`, so a closure works without writing a struct. A custom filter overrides a built-in of |
| 46 |
the same name. |
| 47 |
|
| 48 |
## Errors |
| 49 |
|
| 50 |
`substitute` returns `SubstError::Unresolved(Vec<String>)` listing every path that was |
| 51 |
missing from the table, or every filter that failed, formatted for a log line. Unresolved |
| 52 |
markers are left in place in the returned string, so a caller can grep the output rather |
| 53 |
than diff it against the input to find them. |
| 54 |
|
| 55 |
## Dependencies |
| 56 |
|
| 57 |
`regex-lite`, and nothing else. |
| 58 |
|
| 59 |
The `code_span_ranges` helper is duplicated from docengine rather than shared. Depending |
| 60 |
on docengine here would invert the separation this crate exists to keep. |
| 61 |
|
| 62 |
## License |
| 63 |
|
| 64 |
MIT. |
| 65 |
|