|
| 1 | +defmodule Draft.Ranges do |
| 2 | + @moduledoc """ |
| 3 | + Provides functions for adding inline style ranges and entity ranges |
| 4 | + """ |
| 5 | + |
| 6 | + def apply(text, inline_style_ranges, entity_ranges, entity_map) do |
| 7 | + inline_style_ranges ++ entity_ranges |
| 8 | + |> consolidate_ranges() |
| 9 | + |> Enum.reduce(text, fn {start, finish}, acc -> |
| 10 | + {style_opening_tag, style_closing_tag} = |
| 11 | + case get_styles_for_range(start, finish, inline_style_ranges) do |
| 12 | + "" -> {"", ""} |
| 13 | + styles -> {"<span style=\"#{styles}\">", "</span>"} |
| 14 | + end |
| 15 | + entity_opening_tags = get_entity_opening_tags_for_start(start, entity_ranges, entity_map) |
| 16 | + entity_closing_tags = get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map) |
| 17 | + opening_tags = "#{entity_opening_tags}#{style_opening_tag}" |
| 18 | + closing_tags = "#{style_closing_tag}#{entity_closing_tags}" |
| 19 | + |
| 20 | + adjusted_start = start + String.length(acc) - String.length(text) |
| 21 | + adjusted_finish = finish + String.length(acc) - String.length(text) |
| 22 | + |
| 23 | + acc |
| 24 | + |> String.split_at(adjusted_finish) |
| 25 | + |> Tuple.to_list |
| 26 | + |> Enum.join(closing_tags) |
| 27 | + |> String.split_at(adjusted_start) |
| 28 | + |> Tuple.to_list |
| 29 | + |> Enum.join(opening_tags) |
| 30 | + end) |
| 31 | + end |
| 32 | + |
| 33 | + defp css("BOLD") do |
| 34 | + "font-weight: bold;" |
| 35 | + end |
| 36 | + |
| 37 | + defp css("ITALIC") do |
| 38 | + "font-style: italic;" |
| 39 | + end |
| 40 | + |
| 41 | + defp entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}) do |
| 42 | + {"<a href=\"#{url}\">", "</a>"} |
| 43 | + end |
| 44 | + |
| 45 | + defp get_styles_for_range(start, finish, inline_style_ranges) do |
| 46 | + inline_style_ranges |
| 47 | + |> Enum.filter(fn range -> is_in_range(range, start, finish) end) |
| 48 | + |> Enum.map(fn range -> css(range["style"]) end) |
| 49 | + |> Enum.join(" ") |
| 50 | + end |
| 51 | + |
| 52 | + defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map) do |
| 53 | + entity_ranges |
| 54 | + |> Enum.filter(fn range -> range["offset"] === start end) |
| 55 | + |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> entity() |> elem(0) end) |
| 56 | + end |
| 57 | + |
| 58 | + defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map) do |
| 59 | + entity_ranges |
| 60 | + |> Enum.filter(fn range -> range["offset"] + range["length"] === finish end) |
| 61 | + |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> entity() |> elem(1) end) |
| 62 | + |> Enum.reverse() |
| 63 | + end |
| 64 | + |
| 65 | + defp is_in_range(range, start, finish) do |
| 66 | + range_start = range["offset"] |
| 67 | + range_finish = range["offset"] + range["length"] |
| 68 | + |
| 69 | + start >= range_start && finish <= range_finish |
| 70 | + end |
| 71 | + |
| 72 | + @doc """ |
| 73 | + Takes multiple potentially overlapping ranges and breaks them into other mutually exclusive |
| 74 | + ranges, so we can take each mini-range and add the specified, potentially multiple, styles |
| 75 | + and entities to each mini-range |
| 76 | +
|
| 77 | + ## Examples |
| 78 | + iex> ranges = [ |
| 79 | + %{"offset" => 0, "length" => 4, "style" => "ITALIC"}, |
| 80 | + %{"offset" => 4, "length" => 4, "style" => "BOLD"}, |
| 81 | + %{"offset" => 2, "length" => 3, "key" => 0}] |
| 82 | + iex> consolidate_ranges(ranges) |
| 83 | + [{0, 2}, {2, 4}, {4, 5}, {5, 8}] |
| 84 | + """ |
| 85 | + defp consolidate_ranges(ranges) do |
| 86 | + ranges |
| 87 | + |> ranges_to_points() |
| 88 | + |> points_to_ranges() |
| 89 | + end |
| 90 | + |
| 91 | + defp points_to_ranges(points) do |
| 92 | + points |
| 93 | + |> Enum.with_index |
| 94 | + |> Enum.reduce([], fn {point, index}, acc -> |
| 95 | + case Enum.at(points, index + 1) do |
| 96 | + nil -> acc |
| 97 | + next -> acc ++ [{point, next}] |
| 98 | + end |
| 99 | + end) |
| 100 | + end |
| 101 | + |
| 102 | + defp ranges_to_points(ranges) do |
| 103 | + Enum.reduce(ranges, [], fn range, acc -> |
| 104 | + acc ++ [range["offset"], range["offset"] + range["length"]] |
| 105 | + end) |
| 106 | + |> Enum.uniq |
| 107 | + |> Enum.sort |
| 108 | + end |
| 109 | +end |
0 commit comments