# `ExGram.Dsl.MessageEntityBuilder`
[🔗](https://github.com/rockneurotiko/ex_gram/blob/0.64.0/lib/ex_gram/dsl/message_entity_builder.ex#L1)

Composable builder for Telegram `ExGram.Model.MessageEntity` formatted messages.

Instead of constructing MarkdownV2 strings with escape sequences, this module
produces `{plain_text, [%ExGram.Model.MessageEntity{}]}` tuples. The plain text
carries no formatting syntax; all formatting is expressed via entity annotations
with UTF-16 offsets and lengths.

## Core concept

Every builder function returns a `{text, entities}` tuple (`t:t/0`). Caller code
builds up message content by creating these tuples and then composing them
with `concat/1` or `join/2`. Offsets in the entities are always relative to
the beginning of the text in that specific tuple; `concat/1` automatically
adjusts offsets as tuples are combined.

## Example

    iex> alias EntityBuilder, as: B
    iex> B.concat([B.bold("Hello"), B.text(", "), B.italic("world")])
    {"Hello, world", [
      %ExGram.Model.MessageEntity{type: "bold", offset: 0, length: 5},
      %ExGram.Model.MessageEntity{type: "italic", offset: 7, length: 5}
    ]}

# `entity`

```elixir
@type entity() :: %ExGram.Model.MessageEntity{
  custom_emoji_id: term(),
  date_time_format: term(),
  language: term(),
  length: term(),
  offset: term(),
  type: term(),
  unix_time: term(),
  url: term(),
  user: term()
}
```

# `t`

```elixir
@type t() :: {String.t(), [entity()]}
```

# `blockquote`

```elixir
@spec blockquote(String.t() | t()) :: t()
```

Blockquote.

# `bold`

```elixir
@spec bold(String.t()) :: t()
```

Bold text.

# `bot_command`

```elixir
@spec bot_command(String.t()) :: t()
```

Bot command (/start@bot).

# `cashtag`

```elixir
@spec cashtag(String.t()) :: t()
```

Cashtag ($USD).

# `code`

```elixir
@spec code(String.t()) :: t()
```

Inline code.

# `concat`

```elixir
@spec concat([t() | String.t()]) :: t()
```

Concatenates a list of `{text, entities}` tuples into a single tuple.

Entity offsets from later tuples are shifted by the cumulative UTF-16 length
of all preceding text.

# `custom_emoji`

```elixir
@spec custom_emoji(String.t(), String.t()) :: t()
```

Custom emoji.

# `date_time`

```elixir
@spec date_time(String.t(), integer(), String.t() | nil) :: t()
```

Date/time entity.

# `email`

```elixir
@spec email(String.t()) :: t()
```

Email address.

# `empty`

```elixir
@spec empty() :: t()
```

Empty tuple.

# `empty?`

```elixir
@spec empty?(t()) :: boolean()
```

Checks if a tuple is empty (i.e. has no text and no entities).

# `expandable_blockquote`

```elixir
@spec expandable_blockquote(String.t() | t()) :: t()
```

Expandable blockquote.

# `hashtag`

```elixir
@spec hashtag(String.t()) :: t()
```

Hashtag (#hashtag).

# `italic`

```elixir
@spec italic(String.t()) :: t()
```

Italic text.

# `join`

```elixir
@spec join([t()], String.t()) :: t()
```

Joins a list of `{text, entities}` tuples with a separator string between them.

The separator itself carries no entities.

# `mention`

```elixir
@spec mention(String.t()) :: t()
```

Mention (@username).

# `offset_entities`

```elixir
@spec offset_entities([entity()], non_neg_integer()) :: [entity()]
```

Shifts all entity offsets by the given UTF-16 offset.

Used internally by `concat/1` and by callers that prepend text before
an already-built `{text, entities}` tuple.

# `phone_number`

```elixir
@spec phone_number(String.t()) :: t()
```

Phone number.

# `pre`

```elixir
@spec pre(String.t(), String.t() | nil) :: t()
```

Code block (pre). Optionally specify a programming language.

# `slice_utf16`

```elixir
@spec slice_utf16(String.t(), non_neg_integer(), non_neg_integer()) :: String.t()
```

Slices a string by UTF-16 code unit position.

Takes a string `str`, starts at UTF-16 position `start`, and extracts `length`
UTF-16 code units. Ensures surrogate pairs are not split.

## Examples

    iex> slice_utf16("hello", 0, 5)
    "hello"

    iex> slice_utf16("hello world", 6, 5)
    "world"

# `split`

```elixir
@spec split(t() | String.t(), pos_integer()) :: [t()]
```

Splits a `{text, entities}` tuple into a list of tuples, each with a UTF-16
length of at most `max_length`.

The split respects entity boundaries: if an entity would span a split point it
is moved entirely to the next part. The only exception is when the entity alone
is larger than `max_length` - in that case it is split at the limit (unavoidable).

Returns a list with a single element when no splitting is needed.

# `spoiler`

```elixir
@spec spoiler(String.t()) :: t()
```

Spoiler text.

# `strikethrough`

```elixir
@spec strikethrough(String.t()) :: t()
```

Strikethrough text.

# `text`

```elixir
@spec text(String.t()) :: t()
```

Plain text with no formatting.

# `text_link`

```elixir
@spec text_link(String.t(), String.t()) :: t()
```

Clickable text link.

# `text_mention`

```elixir
@spec text_mention(String.t(), ExGram.Model.User.t()) :: t()
```

Text mention (for users without usernames).

# `trim`

```elixir
@spec trim(t() | String.t()) :: t()
```

Removes leading and trailing whitespace from a `{text, entities}` tuple.

Works like `String.trim/1`. Entity offsets and lengths are adjusted to
reflect the trimmed text. Entities fully within trimmed regions are dropped;
entities partially overlapping are clipped.

# `trim`

```elixir
@spec trim(t() | String.t(), String.t()) :: t()
```

Removes leading and trailing characters from a `{text, entities}` tuple.

Similar to `String.trim/2`, removes all leading and trailing characters that
appear in `chars_to_trim`. Entity offsets and lengths are adjusted to reflect
the trimmed text. Entities fully within trimmed regions are dropped; entities
partially overlapping are clipped.

# `trim_leading`

```elixir
@spec trim_leading(t() | String.t()) :: t()
```

Removes leading whitespace from a `{text, entities}` tuple.

Works like `String.trim_leading/1`. Entity offsets are shifted back and
entities that fall entirely within the trimmed region are dropped. Entities
that partially overlap the trim boundary have their offset set to 0 and
length reduced.

# `trim_leading`

```elixir
@spec trim_leading(t() | String.t(), String.t()) :: t()
```

Removes leading characters from a `{text, entities}` tuple.

Similar to `String.trim_leading/2`, removes all leading characters that
appear in `chars_to_trim`. Entity offsets and lengths are adjusted accordingly.

# `trim_trailing`

```elixir
@spec trim_trailing(t() | String.t()) :: t()
```

Removes trailing whitespace from a `{text, entities}` tuple.

Works like `String.trim_trailing/1`. Entities that extend into the trimmed
region are clipped. Entities entirely in the trimmed region are dropped.

# `trim_trailing`

```elixir
@spec trim_trailing(t() | String.t(), String.t()) :: t()
```

Removes trailing characters from a `{text, entities}` tuple.

Similar to `String.trim_trailing/2`, removes all trailing characters that
appear in `chars_to_trim`. Entity offsets and lengths are adjusted accordingly.

# `truncate`

```elixir
@spec truncate(t() | String.t(), non_neg_integer(), String.t()) :: t()
```

Truncates a `{text, entities}` tuple so the total UTF-16 length does not exceed
`max_size`.

When the text is longer than `max_size`, it is cut and `truncate_text` is
appended. `truncate_text` is always treated as plain text (no entity). The
`max_size` limit is *inclusive* of the `truncate_text` suffix, so the returned
text will never exceed `max_size` UTF-16 code units.

Entities are adjusted as follows:
- Entities that start at or after the cut point are dropped.
- Entities that extend past the cut point are trimmed to end exactly at the
  cut point.

If `max_size` is smaller than or equal to the length of `truncate_text` itself,
only the first `max_size` UTF-16 units of `truncate_text` are kept and no
original text is preserved.

Returns the tuple unchanged when no truncation is needed.

# `underline`

```elixir
@spec underline(String.t()) :: t()
```

Underline text.

# `url`

```elixir
@spec url(String.t()) :: t()
```

URL (clickable URL in text).

# `utf16_length`

```elixir
@spec utf16_length(String.t()) :: non_neg_integer()
```

Returns the number of UTF-16 code units in the given string.

This is what Telegram expects for `ExGram.Model.MessageEntity` `offset` and `length` fields.

# `wrap`

```elixir
@spec wrap(String.t(), String.t() | t(), keyword()) :: t()
```

Wraps an existing `{text, entities}` tuple in an outer entity of the given type.

The outer entity spans the entire inner text. Any extra fields (e.g. `url` for
`text_link` or `language` for `pre`) can be provided via `extra_fields`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
