PhoenixKit.Mentions.Token (phoenix_kit v2.4.0)

Copy Markdown View Source

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 #hashtag feature 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

Types

:user for an @ ping, :resource for a # record link.

t()

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

kind()

@type kind() :: :user | :resource

:user for an @ ping, :resource for a # record link.

t()

@type t() :: %PhoenixKit.Mentions.Token{
  kind: kind(),
  label: String.t(),
  raw: String.t(),
  type: String.t(),
  uuid: String.t()
}

Functions

max_label_length()

@spec max_label_length() :: pos_integer()

Longest label a token may carry.

parse(text)

@spec parse(String.t() | nil) :: [t()]

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.

pattern()

@spec pattern() :: Regex.t()

The regex that recognises a token. Exposed so consumers can reuse it.

split(text)

@spec split(String.t() | nil) :: [String.t() | t()]

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.

to_plain_text(text)

@spec to_plain_text(String.t() | nil) :: String.t()

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.

to_string(kind, type, uuid, label)

@spec to_string(kind(), String.t(), String.t(), String.t()) ::
  {:ok, String.t()} | :error

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.