The stored form of a mention, and the only thing that turns free text into a link.
Two shapes, both closed and both carrying their own label:
@[user:018e3c4a-9f6b-7890-abcd-ef1234567890|Alice Smith]
#[project:018e3c4a-9f6b-7890-abcd-ef1234567890|Q3 Launch]Why the label lives in the text
The token is self-contained on purpose. Text gets copied between records, exported, kept in edit history, and read by people with JavaScript off — and a module can be uninstalled entirely. A bare foreign key survives none of that; this survives all of it, degrading to something a human can still read. It also keeps full-text search working: searching "Alice" finds the comment, because "Alice" is literally in the column.
The label is a SNAPSHOT of what the author saw when they picked. It is not
the display title — PhoenixKit.Mentions re-resolves that per viewer at
render, and deliberately never shows a refreshed title to someone who
cannot open the record.
What is NOT a mention
A bare @alice or #launch is ordinary prose and is never linked. Only
the closed form above counts, which is what makes escaping mostly a
non-problem: a token needs the trigger, a known type, a syntactically valid
UUID, a |, a label and a ], so typing one by accident is not a thing
that happens.
Two deliberate consequences:
- Publishing's
#hashtagfeature is untouched — the trigger character is shared, the stored form is not. - An unfinished token (someone typing, or a truncated paste) stays plain text rather than becoming a broken link.
For the rare case of writing a complete-looking token that should NOT
link, prefix it with a backslash: \@[user:…|Alice]. render/2 strips the
backslash and leaves the rest as text.
Summary
Functions
Longest label a token may carry.
Every mention in text, in order, ignoring backslash-escaped ones.
The regex that recognises a token. Exposed so consumers can reuse it.
Splits text into a list of plain strings and t() structs, in order.
The text with every mention replaced by its label — what a plain-text channel (an email digest, a search index, a notification preview) should show instead of raw tokens.
Builds the stored form. Returns :error when the label can't be
represented — the picker should then pick a different label rather than
the caller escaping anything.
Types
Functions
@spec max_label_length() :: pos_integer()
Longest label a token may carry.
Every mention in text, in order, ignoring backslash-escaped ones.
Returns [] for nil or non-binary input so callers can pipe a possibly
empty field straight in.
@spec pattern() :: Regex.t()
The regex that recognises a token. Exposed so consumers can reuse it.
Splits text into a list of plain strings and t() structs, in order.
This is what a renderer walks: everything that isn't a mention comes back as a binary to be escaped and printed as-is, and an escaped token comes back as text with its backslash removed.
The text with every mention replaced by its label — what a plain-text channel (an email digest, a search index, a notification preview) should show instead of raw tokens.
Builds the stored form. Returns :error when the label can't be
represented — the picker should then pick a different label rather than
the caller escaping anything.