Skip to main content

max / makenotwork

2.5 KB · 65 lines History Blame Raw
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 | Filter | Effect |
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