defmodule Localize.Substitution do @moduledoc """ Compiles substitution templates of the form `"{0} something {1}"` into token lists for efficient parameter substitution at runtime. Templates are parsed once into a list of string literals and integer indices. At runtime, values are substituted for the integer indices to produce the final output. """ @doc """ Parses a substitution template into a list of tokens. ### Arguments * `template` is a binary string that may include parameter markers like `{0}`, `{1}`, etc. ### Returns * A list of tokens where substitution markers become integers and literal text remains as strings. * `{:error, reason}` if the template is not a binary. ### Examples iex> Localize.Substitution.parse("{0}, {1}") [0, ", ", 1] iex> Localize.Substitution.parse("{0} something {1} else {2}") [0, " something ", 1, " else ", 2] iex> Localize.Substitution.parse("") [] """ @spec parse(String.t()) :: [String.t() | integer()] | {:error, String.t()} def parse("") do [] end def parse(template) when is_binary(template) do template |> String.split(~r/{[0-9]}/, include_captures: true, trim: true) |> Enum.map(&item_from_token/1) end def parse(_template) do {:error, "Localize.Substitution.parse/1 accepts only a binary parameter"} end @doc """ Substitutes values into a pre-parsed template token list. ### Arguments * `values` is a value or list of values to substitute into the template. * `tokens` is a template token list previously created by `parse/1`. ### Returns * A list with values substituted for integer indices in the template. ### Examples iex> template = Localize.Substitution.parse("{0} and {1}") [0, " and ", 1] iex> Localize.Substitution.substitute(["a", "b"], template) ["a", " and ", "b"] iex> Localize.Substitution.substitute("x", [0, "!"]) ["x", "!"] """ @spec substitute(term() | [term()], [String.t() | integer()]) :: [term()] # Single item, single token def substitute(item, [0]) do [item] end def substitute([item], [0]) do [item] end # No parameters used — just a literal string def substitute([_item], [string]) when is_binary(string) do [string] end def substitute(_item, [string]) when is_binary(string) do [string] end # One parameter: {0}string def substitute([item], [0, string]) when is_binary(string) do [item, string] end def substitute(item, [0, string]) when is_binary(string) do [item, string] end # One parameter: string{0} def substitute([item], [string, 0]) when is_binary(string) do [string, item] end def substitute(item, [string, 0]) when is_binary(string) do [string, item] end # One parameter: string{0}string def substitute(item, [string1, 0, string2]) when is_binary(string1) and is_binary(string2) do [string1, item, string2] end # Two parameters: {0}{1} def substitute([item_0, item_1], [0, 1]) do [item_0, item_1] end # Two parameters: {0}string{1} def substitute([item_0, item_1], [0, string, 1]) when is_binary(string) do [item_0, string, item_1] end # Two parameters: {1}string{0} def substitute([item_0, item_1], [1, string, 0]) when is_binary(string) do [item_1, string, item_0] end # Two parameters with trailing: {0}string{1}string def substitute([item_0, item_1], [0, string1, 1, string2]) do [item_0, string1, item_1, string2] end def substitute([item_0, item_1], [1, string1, 0, string2]) do [item_0, string1, item_1, string2] end # Three parameters: {0}string{1}string{2} def substitute([item_0, item_1, item_2], [0, string_1, 1, string_2, 2]) when is_binary(string_1) and is_binary(string_2) do [item_0, string_1, item_1, string_2, item_2] end # ── Private ───────────────────────────────────────────────── @digits [?0, ?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9] defp item_from_token(<>) when digit in @digits do digit - ?0 end defp item_from_token(string) do string end end