defmodule Textwrap do @moduledoc """ `Textwrap` provides a set of functions for wrapping, indenting, and dedenting text. It wraps the Rust [`textwrap`](https://lib.rs/textwrap) crate. ## Wrapping Use `wrap/2` to turn a `String` into a list of `String`s each no more than `width` characters long. iex> Textwrap.wrap("foo bar baz", 3) ["foo", "bar", "baz"] iex> Textwrap.wrap("foo bar baz", 7) ["foo bar", "baz"] `fill/2` is like `wrap/2`, except that it retuns the wrapped text as a single `String`. iex> Textwrap.fill("foo bar baz", 3) "foo\\nbar\\nbaz" iex> Textwrap.fill("foo bar baz", 7) "foo bar\\nbaz" Both `wrap/2` and `fill/2` can either take the width to wrap too as their second argument, or take a keyword list including a `:width` key and any number of other options. See the docs for `wrap/2` for details. ## Displayed Width vs Byte Width `Textwrap` wraps text based on measured display width, not simply counting bytes. For ascii text this gives exactly the same result, but many non-ASCII characters take up more than one byte in the UTF-8 encoding. See the documentation of the [`textwrap` crate](https://docs.rs/textwrap/0.13.2/textwrap/index.html#displayed-width-vs-byte-size) for more details. ## Terminal Width The `width` passed to `wrap/2` or `fill/2` can either by a positive integer, or the atom `:termwidth`. When standard output is connected to a terminal, passing `:termwidth` will wrap the text to the width of the terminal. Otherwise, it will use a width of 80 characters as a fallback. ## Indenting and Dedenting Use `indent/2` and `dedent/1` to indent and dedent text: iex> Textwrap.indent("hello\\nworld\\n", " ") " hello\\n world\\n" iex> Textwrap.dedent(" hello\\n world\\n") "hello\\nworld\\n" """ use Rustler, otp_app: :textwrap, crate: "textwrap_nif" @type wrap_opts() :: pos_integer() | :termwidth | [wrap_opt()] @type wrap_opt() :: {:width, pos_integer() | :termwidth} | {:break_words, boolean()} | {:initial_indent, String.t()} | {:word_splitter, nil | :en_us | false} | {:subsequent_indent, String.t()} | {:wrap_algorithm, wrap_algorithm()} # Depecrated, kept for backwards compatibility | {:splitter, nil | :en_us | false} @type wrap_algorithm() :: :first_fit | :optimal_fit | {:optimal_fit, penalties()} @type penalties() :: %{ optional(:nline_penalty) => non_neg_integer(), optional(:overflow_penalty) => non_neg_integer(), optional(:short_last_line_fraction) => non_neg_integer(), optional(:short_last_line_penalty) => non_neg_integer(), optional(:hyphen_penalty) => non_neg_integer() } @spec wrap(text :: String.t(), opts :: wrap_opts()) :: [String.t()] @doc """ Wraps text to the given width. `wrap/2` returns a list of `Strings`, each of no more than `width` charecters. Options can be either passed as a keyword list (which must include the key `:width`), or, if using no options other than `:width`, the width can be passed on its own as the second argument. `width` can either by a positive integer or the atom `:termwidth`. See the [module docs](#module-terminal-width) on `:termwidth` for more details. ## Options - `:width` — the width to wrap at, a positive integer. - `:break_words` — allow long words to be broken, if they won't fit on a single line. Setting this to false may cause some lines to be longer than `:width`. - `:inital_indent` — will be added as a prefix to the first line of the result. - `:subsequent_indent` — will be added as a prefix to each line other than the first line of the result. - `:word_splitter` — when set to `false`, hyphens within words won't be treated specially as a place to split words. When set to `:en_us`, a language-aware hyphenation system will be used to try to break words in appropriate places. - `:wrap_algorithm` — by default, or when set to `:optimal_fit`, `wrap/2` will do its best to balance the gaps left at the ends of lines. When set to `:first_fit`, a simpler greedy algorithm is used instead. You can also pass `{:optimal_fit, penalties}` where `penalties` is a map containing custom penalty values to override the defaults (which are suitable for monospace fonts). This allows you to tune the algorithm for proportional fonts. See the [Penalties documentation](https://docs.rs/textwrap/0.16.2/textwrap/wrap_algorithms/struct.Penalties.html) for details on the available penalty fields. For more on the algorithms, see the [`textwrap` crate docs](https://docs.rs/textwrap/0.16.2/textwrap/wrap_algorithms/enum.WrapAlgorithm.html). ## Examples iex> Textwrap.wrap("hello world", 5) ["hello", "world"] iex> Textwrap.wrap("hello world", width: 5) ["hello", "world"] iex> Textwrap.wrap("Antidisestablishmentarianism", width: 10) ["Antidisest", "ablishment", "arianism"] iex> Textwrap.wrap("Antidisestablishmentarianism", width: 10, break_words: false) ["Antidisestablishmentarianism"] iex> Textwrap.wrap("Antidisestablishmentarianism", width: 10, word_splitter: :en_us) ["Antidis-", "establish-", "mentarian-", "ism"] iex> Textwrap.wrap("foo bar baz", ...> width: 5, ...> initial_indent: "> ", ...> subsequent_indent: " ") ["> foo", " bar", " baz"] iex> Textwrap.wrap("Lorem ipsum dolor sit amet, consectetur adipisicing elit", ...> width: 25, ...> wrap_algorithm: :optimal_fit) ["Lorem ipsum dolor", "sit amet, consectetur", "adipisicing elit"] iex> Textwrap.wrap("Lorem ipsum dolor sit amet, consectetur adipisicing elit", ...> width: 25, ...> wrap_algorithm: :first_fit) ["Lorem ipsum dolor sit", "amet, consectetur", "adipisicing elit"] """ def wrap(text, width_or_opts) def wrap(text, width) when is_integer(width) or width == :termwidth do wrap(text, width: width) end def wrap(text, opts) do width = fetch_width!(opts) wrap_nif(text, width, opts) end @spec fill(text :: String.t(), opts :: wrap_opts()) :: String.t() @doc """ Fills text to the given width. The result is a `String`, with lines seperated by newline. The `wrap/2` function does the same thing, except that it returns a list of `Strings`, one for each line. See the docs for `wrap/2` for details about the options it takes. ## Examples iex> Textwrap.fill("hello world", 5) "hello\\nworld" iex> Textwrap.fill("hello world", width: 5) "hello\\nworld" iex> Textwrap.fill("Antidisestablishmentarianism", width: 10) "Antidisest\\nablishment\\narianism" iex> Textwrap.fill("Antidisestablishmentarianism", width: 10, break_words: false) "Antidisestablishmentarianism" iex> Textwrap.fill("Antidisestablishmentarianism", width: 10, word_splitter: :en_us) "Antidis-\\nestablish-\\nmentarian-\\nism" iex> Textwrap.fill("foo bar baz", ...> width: 5, ...> initial_indent: "> ", ...> subsequent_indent: " ") "> foo\\n bar\\n baz" iex> Textwrap.fill("Lorem ipsum dolor sit amet, consectetur adipisicing elit", ...> width: 25, ...> wrap_algorithm: :optimal_fit) "Lorem ipsum dolor\\nsit amet, consectetur\\nadipisicing elit" iex> Textwrap.fill("Lorem ipsum dolor sit amet, consectetur adipisicing elit", ...> width: 25, ...> wrap_algorithm: :first_fit) "Lorem ipsum dolor sit\\namet, consectetur\\nadipisicing elit" """ def fill(text, width_or_opts) def fill(text, width) when is_integer(width) or width == :termwidth do fill(text, width: width) end def fill(text, opts) do width = fetch_width!(opts) fill_nif(text, width, opts) end @spec dedent(text :: String.t()) :: String.t() @doc """ Removes as much common leading whitespace as possible from each line. Each non-empty line has an equal amount of whitespace removed from its start. Empty lines (containing only whitespace) are normalized to a single `\\n`, with no other whitespace on the line. ## Examples: iex> Textwrap.dedent(" hello world") "hello world" iex> Textwrap.dedent(" ...> foo ...> bar ...> baz ...> ") "\\nfoo\\n bar\\nbaz\\n" """ def dedent(_text), do: :erlang.nif_error(:nif_not_loaded) @spec indent(text :: String.t(), prefix :: String.t()) :: String.t() @doc """ Adds a given prefix to each non-empty line. Empty lines (containing only whitespace) are normalized to a single `\\n`, and not indented. Any leading and trailing whitespace on non-empty lines is left unchanged. Examples: iex> Textwrap.indent("hello world", ">") ">hello world" iex> Textwrap.indent("foo\\nbar\\nbaz\\n", " ") " foo\\n bar\\n baz\\n" """ def indent(_text, _prefix), do: :erlang.nif_error(:nif_not_loaded) defp fill_nif(_text, _width, _opts), do: :erlang.nif_error(:nif_not_loaded) defp wrap_nif(_text, _width, _opts), do: :erlang.nif_error(:nif_not_loaded) defp fetch_width!(opts) do case Keyword.fetch!(opts, :width) do :termwidth -> termwidth() width -> width end end defp termwidth, do: :erlang.nif_error(:nif_not_loaded) end