Guillemets 0.1.0
dotnet add package Guillemets --version 0.1.0
NuGet\Install-Package Guillemets -Version 0.1.0
<PackageReference Include="Guillemets" Version="0.1.0" />
<PackageVersion Include="Guillemets" Version="0.1.0" />
<PackageReference Include="Guillemets" />
paket add Guillemets --version 0.1.0
#r "nuget: Guillemets, 0.1.0"
#:package Guillemets@0.1.0
#addin nuget:?package=Guillemets&version=0.1.0
#tool nuget:?package=Guillemets&version=0.1.0
Guillemets
A markdown-aware template engine for non-technical authors.
«» — guillemets, pronounced ghee-uh-MAY — were chosen for readability:
they never collide with markdown syntax and stand out visually in prose.
That's a deliberate trade against writability — neither delimiter sits on
a standard keyboard, so typing either takes a shortcut (see below), not a
keystroke. Templates get read far more often than typed by hand, so that
trade favors the reader.
<details> <summary>How to type «»</summary>
| Platform | « | » |
|---|---|---|
| Windows (any keyboard) | Alt+0171 (numpad) |
Alt+0187 (numpad) |
| Windows, French AZERTY | AltGr+Z |
AltGr+X |
| macOS, US/English keyboard | Option+\ |
Option+Shift+\ |
| macOS, French AZERTY | Option+7 |
Option+Shift+7 |
| Linux (any keyboard, GNOME/GTK) | Ctrl+Shift+U 00ab Enter |
Ctrl+Shift+U 00bb Enter |
| Linux, French AZERTY or Turkish Q | AltGr+Z |
AltGr+X |
On Windows/macOS, Turkish (Q) keyboards have no dedicated key for either
character — use the row above for your OS instead. If Alt+0171/ Alt+0187
doesn't work in a given Windows app, try Alt+174/Alt+175 (same numpad
requirement, no leading zero) — a different Alt-code table that some apps read
instead.
On macOS, you can also set up Text Replacement once so <</>> expand to
«/» automatically in any app: System Settings → Keyboard → Text Input → Text
Replacements… → + → add << → «, then >> → ».
</details>
Hi «first name»,
««is member
Welcome back — thanks for being a member!
~
Thanks for placing your first order with us!
»»
**Order #«order id»** — placed «order date / date: MMMM d, yyyy»
««items
| Item | Qty | Price | Total |
| ------ | ---------- | ------------------ | ------------------------ |
| «name» | «quantity» | «price / currency» | «total / currency» |
| | | **Order total** | «order total / currency» |
»»
You ordered «items: name / join last: and / join: , ».
Shipping to «shipping address: city», «shipping address: state».
Thanks for shopping with us!
rendered against
{
"FirstName": "Priya",
"IsMember": true,
"OrderId": "A-1042",
"OrderDate": "2026-03-04",
"OrderTotal": 87.50,
"Items": [
{ "Name": "Wireless Mouse", "Quantity": 1, "Price": 24.99, "Total": 24.99 },
{ "Name": "USB-C Hub", "Quantity": 1, "Price": 39.99, "Total": 39.99 },
{ "Name": "Mousepad", "Quantity": 2, "Price": 11.26, "Total": 22.52 }
],
"ShippingAddress": { "City": "Austin", "State": "TX" }
}
produces
Hi Priya,
Welcome back — thanks for being a member!
Order #A-1042 — placed March 4, 2026
| Item | Qty | Price | Total |
|---|---|---|---|
| Wireless Mouse | 1 | $24.99 | $24.99 |
| USB-C Hub | 1 | $39.99 | $39.99 |
| Mousepad | 2 | $11.26 | $22.52 |
| Order total | $87.50 |
You ordered Wireless Mouse, USB-C Hub and Mousepad.
Shipping to Austin, TX.
Thanks for shopping with us!
<details> <summary>Raw output</summary>
Hi Priya,
Welcome back — thanks for being a member!
**Order #A-1042** — placed March 4, 2026
| Item | Qty | Price | Total |
| ------ | ---------- | ------------------ | ------------------------ |
| Wireless Mouse | 1 | $24.99 | $24.99 |
| USB-C Hub | 1 | $39.99 | $39.99 |
| Mousepad | 2 | $11.26 | $22.52 |
| | | **Order total** | $87.50 |
You ordered Wireless Mouse, USB-C Hub and Mousepad.
Shipping to Austin, TX.
Thanks for shopping with us!
</details>
One template covers both the member and first-time-buyer greeting (an if /
else block, inferred from IsMember being a boolean — no keyword needed),
repeats the order table's one row per item straight from a plain markdown table,
and turns the item list into a natural "A, B and C" sentence with the join /
join last filters. See docs/specs.md for the full language.
Usage
using Guillemets;
var template = Template.Create("Dear «full name»,");
var output = template.RenderObject(new { FullName = "Alice Smith" });
// => "Dear Alice Smith,"
Render/RenderObject also accept System.Text.Json.JsonElement
(template.Render(jsonElement)) and Newtonsoft.Json.Linq.JToken.
A Template is immutable and stateless once created — parse it once and
reuse the same instance for every Render call, including concurrently
across threads, rather than re-parsing per request.
Custom filters
Template.Create's optional configure callback exposes
ParseOptions.Filters, the registry Register(instance) adds a filter to
alongside the built-ins (join, date, upper, ...) — re-registering an
existing name (e.g. Register(new CurrencyFilter("TL"))) replaces it, and
Remove<TFilter>() drops one entirely. A filter's template name drops the
Filter suffix and lowercases the rest — ReverseFilter becomes reverse.
Apply maps over the current sequence: a single-value filter like date
returns one string per input; a collapsing filter like join returns fewer
strings than it received.
using Guillemets.Filters;
public class ReverseFilter : IFilter
{
public IEnumerable<string> Apply(IEnumerable<string> values, string? arg) =>
values.Select(value => new string(value.Reverse().ToArray()));
}
var template = Template.Create(text,
options => options.Filters.Register(new ReverseFilter())
);
// «text / reverse» reverses each value
The same configure callback also sets options.Localizer, an
IStringLocalizer that bridges a template's business vocabulary to model
property names when they don't already match — see
docs/specs.md's Glossary & Localization section.
Documentation
docs/specs.md— full syntax reference and behavior spec.docs/implementations/dotnet.md— this .NET implementation's own behavior, including its runtime-specific filters.docs/architecture.md— how the engine is built.
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Localization.Abstractions (>= 10.0.0)
- Newtonsoft.Json (>= 13.0.3)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.1.0 | 178 | 8/14/2026 |