todo.normalise-tags
Clean up tags as typed: trim, drop the #, lower-case slug form, drop empties and duplicates, keep the order.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 13 tests, run in TypeScript, Python and Rust.
What it does
Turns tags as a person typed them into the one form a todo stores, so that `#Home`, `home` and ` HOME ` are the same tag and a filter on one finds all three.
Each tag loses its leading `#` characters and becomes a slug with text.slugify: lower-case ASCII letters and digits, runs of anything else collapsed to a single hyphen, never a hyphen at either end. So "Home Office" is `home-office`, "Café" is `cafe` and "Straße" is `strasse`. The transliteration covers Latin-1 letters only (see text.slugify): a tag written in another script, or made only of punctuation, comes out empty and is dropped rather than stored as an empty tag.
For example
normaliseTags(Home)→ home a capital letter is lower-casednormaliseTags(#home, HOME, home )→ home #home, HOME and a padded home are one tagnormaliseTags(Home Office)→ home-office spaces inside become one hyphen
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function normaliseTags(tags: readonly string[]): readonly string[]
| tags | string[] | as typed, e.g. ["#Home", " home office ", "HOME"] |
| returns | string[] | e.g. ["home", "home-office"], first appearance first |
Your code names it in one line, in the file that uses it
import { normaliseTags } from "#fune/todo.normalise-tags@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { slugify } from "./text_slugify.ts"; ← from text.slugify ^1.0.0 · built alongside by fune
/**
* Tags as a person types them, in the one form every todo stores and
* compares: "#Home", "home" and " HOME " are one tag. Each tag is stripped of
* its leading #s and turned into a slug (text.slugify), so "Home Office" is
* "home-office" and "Café" is "cafe". Tags that come out empty are dropped;
* repeats keep their first position.
*/
export function normaliseTags(tags: readonly string[]): readonly string[] {
const out: string[] = [];
for (const tag of tags) {
const slug = slugify(tag.replace(/^[ \t\n\r\v\f#]+/, ""));
if (slug !== "" && !out.includes(slug)) out.push(slug);
}
return out;
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add todo.normalise-tags
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./todo.normalise-tags-1.0.0-typescript.fune, or fetch it from a terminal with fune pull todo.normalise-tags@1.0.0:typescript.
The whole function, every language, is one file too: todo.normalise-tags-1.0.0.fune, 6,037 bytes, sha256 c098ae9cc4cff3719a9100ca369754dcc598f716578252f1b23114de7be44139. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before todo.normalise-tags
after — your function gets the result and the arguments, and returns the final result.
// fune: after todo.normalise-tags
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace text.slugify in todo.normalise-tags
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show todo.normalise-tags --steps.
// fune: step todo.normalise-tags after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| a capital letter is lower-cased | Home | → | home |
| #home, HOME and a padded home are one tag | #home, HOME, home | → | home |
| spaces inside become one hyphen | Home Office | → | home-office |
| accented Latin letters lose their accent | Café | → | cafe |
| sharp s becomes ss | Straße | → | strasse |
| several leading #s go | ##urgent | → | urgent |
| tags that come out empty are dropped | #, , !!!, | → | |
| no tags | → | ||
| the first appearance keeps its place | work, Errands, #WORK, errands | → | work, errands |
| digits are kept and punctuation becomes a hyphen | Q3 2026 plan, a_b, c++ | → | q3-2026-plan, a-b, c |
Show the other 3 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a tag in a script slugify does not transliterate is dropped | 日本, home | → | home |
| an emoji tag is dropped, one beside a word leaves the word | 🏠, 🏠 home | → | home |
| already normal tags are unchanged | home, bills-2026 | → | home, bills-2026 |
More from the author
Repeats are removed after normalising, keeping the first appearance, and the order is otherwise the order typed.
It does not enforce the limits (10 tags, 30 characters each): cutting a tag short or dropping the eleventh silently would lose what was typed. todo.item's validators report those, so the form can say so.
Files
| Path | Bytes |
|---|---|
| README.md | 978 |
| impl/python.py | 549 |
| impl/rust.rs | 748 |
| impl/typescript.ts | 671 |
| vectors.json | 1,370 |