defmodule Perch do @moduledoc """ Documentation for `Perch`. """ defstruct tables: %{}, output: nil require Logger def parse(str) when is_binary(str) do parsed = str |> extract_groups() |> Enum.reduce(%__MODULE__{}, &do_parse_thing/2) {:ok, parsed} end def generate_output(perch, output_template \\ nil, recursions \\ 10) when recursions > 0 do input = output_template || perch.output output = perch |> replace_roll_macros(input) |> replace_inline_macros() |> replace_inline_ranges() if has_macro?(output) do generate_output(perch, output, recursions - 1) else output end end def generate_output(_, _, _), do: raise("too many recursions") def roll(%__MODULE__{} = perch, tab) do perch |> Map.get(:tables) |> Map.get(tab) |> Enum.random() end ### Private @section_regex ~r/\[(\w*)\]/ @inline_macro_regex ~r/\{(.*\|.*)\}/ @inline_range_regex ~r/\{(\d*)-(\d*)\}/ def macro_patterns() do [ @section_regex, @inline_macro_regex, @inline_range_regex ] end def has_macro?(str) do Enum.any?(macro_patterns(), fn rx -> String.match?(str, rx) end) end defp replace_roll_macros(perch, input) do Regex.replace(@section_regex, input, fn _, match -> match |> String.to_atom() |> then(&Perch.roll(perch, &1)) end) end defp replace_inline_macros(output) do Regex.replace(@inline_macro_regex, output, fn _full_match, match -> match |> String.split("|") |> Enum.random() end) end defp replace_inline_ranges(output) do Regex.replace(@inline_range_regex, output, fn _match, a, b -> Enum.random(String.to_integer(a)..String.to_integer(b)) |> Integer.to_string() end) end defp extract_groups(tab_string) do tab_string |> String.split("\n", trim: true) |> Enum.chunk_by(&String.starts_with?(&1, " ")) |> Enum.chunk_every(2) end # handles the special "output" group defp do_parse_thing([["output"], [output]], acc) do %{acc | output: String.trim(output)} end # handles a table defp do_parse_thing([[a], b], acc) do new_key = String.to_atom(a) new_tab = Enum.map(b, &String.trim/1) %{acc | tables: Map.put(acc.tables, new_key, new_tab)} end end