src/gleeps/stdlib/string_tree.gleam

import gleeps/stdlib/list

/// `StringTree` is a type used for efficiently building text content to be
/// written to a file or a socket. Internally it is represented as a tree so to
/// append or prepend to a string tree is a constant time operation that
/// allocates a new node in the tree without copying any of the content. When
/// writing to an output stream the tree is traversed and the content is sent
/// directly rather than copying it into a single buffer beforehand.
///
/// On Erlang this type is compatible with Erlang's iodata. On JavaScript this
/// type is compatible with normal strings.
///
/// The BEAM virtual machine has an optimisation for appending strings, where it
/// will mutate the string buffer when safe to do so, so if you are looking to
/// build a string through appending many small strings then you may get better
/// performance by not using a string tree. Always benchmark your performance
/// sensitive code.
///
pub type StringTree

/// Create an empty `StringTree`. Useful as the start of a pipe chaining many
/// trees together.
///
pub fn new() -> StringTree {
  from_strings([])
}

/// Prepends a `String` onto the start of some `StringTree`.
///
/// Runs in constant time.
///
pub fn prepend(to tree: StringTree, prefix prefix: String) -> StringTree {
  append_tree(from_string(prefix), tree)
}

/// Appends a `String` onto the end of some `StringTree`.
///
/// Runs in constant time.
///
pub fn append(to tree: StringTree, suffix second: String) -> StringTree {
  append_tree(tree, from_string(second))
}

/// Prepends some `StringTree` onto the start of another.
///
/// Runs in constant time.
///
pub fn prepend_tree(
  to tree: StringTree,
  prefix prefix: StringTree,
) -> StringTree {
  append_tree(prefix, tree)
}

/// Appends some `StringTree` onto the end of another.
///
/// Runs in constant time.
///
@external(erlang, "gleam_stdlib", "iodata_append")
@external(javascript, "../gleam_stdlib.mjs", "add")
pub fn append_tree(to tree: StringTree, suffix suffix: StringTree) -> StringTree

/// Converts a list of strings into a `StringTree`.
///
/// Runs in constant time.
///
@external(erlang, "gleam_stdlib", "identity")
@external(javascript, "../gleam_stdlib.mjs", "concat")
pub fn from_strings(strings: List(String)) -> StringTree

/// Joins a list of trees into a single tree.
///
/// Runs in constant time.
///
@external(erlang, "gleam_stdlib", "identity")
@external(javascript, "../gleam_stdlib.mjs", "concat")
pub fn concat(trees: List(StringTree)) -> StringTree

/// Converts a string into a `StringTree`.
///
/// Runs in constant time.
///
@external(erlang, "gleam_stdlib", "identity")
@external(javascript, "../gleam_stdlib.mjs", "identity")
pub fn from_string(string: String) -> StringTree

/// Turns a `StringTree` into a `String`.
///
/// This function is implemented natively by the virtual machine and is highly
/// optimised.
///
@external(erlang, "unicode", "characters_to_binary")
@external(javascript, "../gleam_stdlib.mjs", "identity")
pub fn to_string(tree: StringTree) -> String

/// Returns the size of the `StringTree` in bytes.
///
@external(erlang, "erlang", "iolist_size")
@external(javascript, "../gleam_stdlib.mjs", "length")
pub fn byte_size(tree: StringTree) -> Int

/// Joins the given trees into a new tree separated with the given string.
///
pub fn join(trees: List(StringTree), with sep: String) -> StringTree {
  trees
  |> list.intersperse(from_string(sep))
  |> concat
}

/// Converts a `StringTree` to a new one where the contents have been
/// lowercased.
///
@external(erlang, "string", "lowercase")
@external(javascript, "../gleam_stdlib.mjs", "lowercase")
pub fn lowercase(tree: StringTree) -> StringTree

/// Converts a `StringTree` to a new one where the contents have been
/// uppercased.
///
@external(erlang, "string", "uppercase")
@external(javascript, "../gleam_stdlib.mjs", "uppercase")
pub fn uppercase(tree: StringTree) -> StringTree

/// Converts a `StringTree` to a new one with the contents reversed.
///
@external(erlang, "string", "reverse")
pub fn reverse(tree: StringTree) -> StringTree {
  tree
  |> to_string
  |> do_to_graphemes
  |> list.reverse
  |> from_strings
}

@external(javascript, "../gleam_stdlib.mjs", "graphemes")
fn do_to_graphemes(string: String) -> List(String)

type Direction {
  All
}

/// Splits a `StringTree` on a given pattern into a list of trees.
///
@external(javascript, "../gleam_stdlib.mjs", "split")
pub fn split(tree: StringTree, on pattern: String) -> List(StringTree) {
  erl_split(tree, pattern, All)
}

@external(erlang, "string", "split")
fn erl_split(a: StringTree, b: String, c: Direction) -> List(StringTree)

/// Replaces all instances of a pattern with a given string substitute.
///
@external(erlang, "gleam_stdlib", "string_replace")
@external(javascript, "../gleam_stdlib.mjs", "string_replace")
pub fn replace(
  in tree: StringTree,
  each pattern: String,
  with substitute: String,
) -> StringTree

/// Compares two string trees to determine if they have the same textual
/// content.
///
/// Comparing two string trees using the `==` operator may return `False` even
/// if they have the same content as they may have been built in different ways,
/// so using this function is often preferred.
///
/// ## Examples
///
/// ```gleam
/// assert from_strings(["a", "b"]) != from_string("ab")
/// ```
///
/// ```gleam
/// assert is_equal(from_strings(["a", "b"]), from_string("ab"))
/// ```
///
@external(erlang, "string", "equal")
pub fn is_equal(a: StringTree, b: StringTree) -> Bool {
  a == b
}

/// Inspects a `StringTree` to determine if it is equivalent to an empty string.
///
/// ## Examples
///
/// ```gleam
/// assert !{ from_string("ok") |> is_empty }
/// ```
///
/// ```gleam
/// assert from_string("") |> is_empty
/// ```
///
/// ```gleam
/// assert from_strings([]) |> is_empty
/// ```
///
@external(erlang, "string", "is_empty")
pub fn is_empty(tree: StringTree) -> Bool {
  from_string("") == tree
}