diff --git a/lib/draft.ex b/lib/draft.ex index 7fbe389..3c9b6b8 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -3,33 +3,20 @@ defmodule Draft do Provides functions for parsing DraftJS content. """ - @doc """ - Parses the given DraftJS input and returns the blocks as a list of - maps. - - ## Examples - iex> draft = ~s({"entityMap":{},"blocks":[{"key":"1","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) - iex> Draft.blocks draft - [%{"key" => "1", "text" => "Hello", "type" => "unstyled", - "depth" => 0, "inlineStyleRanges" => [], "entityRanges" => [], - "data" => %{}}] - """ - def blocks(input) do - Poison.Parser.parse!(input)["blocks"] - end - @doc """ Renders the given DraftJS input as html. ## Examples - iex> draft = ~s({"entityMap":{},"blocks":[{"key":"1","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + iex> draft = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"1","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} iex> Draft.to_html draft "
Hello
" """ def to_html(input) do + entity_map = Map.get(input, "entityMap") + input - |> blocks - |> Enum.map(&Draft.Block.to_html/1) + |> Map.get("blocks") + |> Enum.map(&(Draft.Block.to_html(&1, entity_map))) |> Enum.join("") end end diff --git a/lib/draft/block.ex b/lib/draft/block.ex index b73be18..a7a7597 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -3,18 +3,21 @@ defmodule Draft.Block do Converts a single DraftJS block to html. """ + alias Draft.Ranges + @doc """ Renders the given DraftJS input as html. ## Examples + iex> entity_map = %{} iex> block = %{"key" => "1", "text" => "Hello", "type" => "unstyled", ...> "depth" => 0, "inlineStyleRanges" => [], "entityRanges" => [], ...> "data" => %{}} - iex> Draft.Block.to_html block + iex> Draft.Block.to_html block, entity_map "Hello
" """ - def to_html(block) do - process_block(block) + def to_html(block, entity_map) do + process_block(block, entity_map) end defp process_block(%{"type" => "unstyled", @@ -23,7 +26,7 @@ defmodule Draft.Block do "data" => _, "depth" => _, "entityRanges" => _, - "inlineStyleRanges" => _}) do + "inlineStyleRanges" => _}, _) do "#{text}" + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do + "
#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}" end defp process_block(%{"type" => "unstyled", @@ -53,9 +58,10 @@ defmodule Draft.Block do "key" => _, "data" => _, "depth" => _, - "entityRanges" => _, - "inlineStyleRanges" => _}) do - "
#{text}
" + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do + "#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}
" end defp header_tags do diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex new file mode 100644 index 0000000..186994b --- /dev/null +++ b/lib/draft/ranges.ex @@ -0,0 +1,109 @@ +defmodule Draft.Ranges do + @moduledoc """ + Provides functions for adding inline style ranges and entity ranges + """ + + def apply(text, inline_style_ranges, entity_ranges, entity_map) do + inline_style_ranges ++ entity_ranges + |> consolidate_ranges() + |> Enum.reduce(text, fn {start, finish}, acc -> + {style_opening_tag, style_closing_tag} = + case get_styles_for_range(start, finish, inline_style_ranges) do + "" -> {"", ""} + styles -> {"", ""} + end + entity_opening_tags = get_entity_opening_tags_for_start(start, entity_ranges, entity_map) + entity_closing_tags = get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map) + opening_tags = "#{entity_opening_tags}#{style_opening_tag}" + closing_tags = "#{style_closing_tag}#{entity_closing_tags}" + + adjusted_start = start + String.length(acc) - String.length(text) + adjusted_finish = finish + String.length(acc) - String.length(text) + + acc + |> String.split_at(adjusted_finish) + |> Tuple.to_list + |> Enum.join(closing_tags) + |> String.split_at(adjusted_start) + |> Tuple.to_list + |> Enum.join(opening_tags) + end) + end + + defp process_style("BOLD") do + "font-weight: bold;" + end + + defp process_style("ITALIC") do + "font-style: italic;" + end + + defp process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}) do + {"", ""} + end + + defp get_styles_for_range(start, finish, inline_style_ranges) do + inline_style_ranges + |> Enum.filter(fn range -> is_in_range(range, start, finish) end) + |> Enum.map(fn range -> process_style(range["style"]) end) + |> Enum.join(" ") + end + + defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map) do + entity_ranges + |> Enum.filter(fn range -> range["offset"] === start end) + |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> process_entity() |> elem(0) end) + end + + defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map) do + entity_ranges + |> Enum.filter(fn range -> range["offset"] + range["length"] === finish end) + |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> process_entity() |> elem(1) end) + |> Enum.reverse() + end + + defp is_in_range(range, start, finish) do + range_start = range["offset"] + range_finish = range["offset"] + range["length"] + + start >= range_start && finish <= range_finish + end + + @doc """ + Takes multiple potentially overlapping ranges and breaks them into other mutually exclusive + ranges, so we can take each mini-range and add the specified, potentially multiple, styles + and entities to each mini-range + + ## Examples + iex> ranges = [ + %{"offset" => 0, "length" => 4, "style" => "ITALIC"}, + %{"offset" => 4, "length" => 4, "style" => "BOLD"}, + %{"offset" => 2, "length" => 3, "key" => 0}] + iex> consolidate_ranges(ranges) + [{0, 2}, {2, 4}, {4, 5}, {5, 8}] + """ + defp consolidate_ranges(ranges) do + ranges + |> ranges_to_points() + |> points_to_ranges() + end + + defp points_to_ranges(points) do + points + |> Enum.with_index + |> Enum.reduce([], fn {point, index}, acc -> + case Enum.at(points, index + 1) do + nil -> acc + next -> acc ++ [{point, next}] + end + end) + end + + defp ranges_to_points(ranges) do + Enum.reduce(ranges, [], fn range, acc -> + acc ++ [range["offset"], range["offset"] + range["length"]] + end) + |> Enum.uniq + |> Enum.sort + end +end diff --git a/mix.exs b/mix.exs index 2213426..88d3b66 100644 --- a/mix.exs +++ b/mix.exs @@ -29,8 +29,7 @@ defmodule Draft.Mixfile do defp deps do [ {:credo, "~> 0.3", only: [:dev, :test]}, - {:ex_doc, "~> 0.14", only: :dev}, - {:poison, "~> 2.0"} + {:ex_doc, "~> 0.14", only: :dev} ] end end diff --git a/mix.lock b/mix.lock index 5540876..bcae6a9 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,6 @@ -%{"bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, +%{ + "bunt": {:hex, :bunt, "0.1.6", "5d95a6882f73f3b9969fdfd1953798046664e6f77ec4e486e6fafc7caad97c6f", [:mix], []}, "credo": {:hex, :credo, "0.5.2", "92e8c9f86e0ffbf9f688595e9f4e936bc96a52e5606d2c19713e9e4d191d5c74", [:mix], [{:bunt, "~> 0.1.6", [hex: :bunt, optional: false]}]}, "earmark": {:hex, :earmark, "1.0.3", "89bdbaf2aca8bbb5c97d8b3b55c5dd0cff517ecc78d417e87f1d0982e514557b", [:mix], []}, "ex_doc": {:hex, :ex_doc, "0.14.5", "c0433c8117e948404d93ca69411dd575ec6be39b47802e81ca8d91017a0cf83c", [:mix], [{:earmark, "~> 1.0", [hex: :earmark, optional: false]}]}, - "poison": {:hex, :poison, "2.2.0", "4763b69a8a77bd77d26f477d196428b741261a761257ff1cf92753a0d4d24a63", [:mix], []}} +} diff --git a/test/draft_test.exs b/test/draft_test.exs index 9fd1359..7e0c12e 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -3,38 +3,88 @@ defmodule DraftTest do doctest Draft test "generate a" do - input = ~s({"entityMap":{},"blocks":[{"key":"9d21d","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "
Hello
" assert Draft.to_html(input) == output end test "generate a" do - input = ~s({"entityMap":{},"blocks":[{"key":"9d21d","text":"Hello","type":"blockquote","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"blockquote","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "Hello" assert Draft.to_html(input) == output end test "generate a
" do - input = ~s({"entityMap":{},"blocks":[{"key":"9d21d","text":"","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "
" assert Draft.to_html(input) == output end + + test "wraps single inline style" do + input = %{"entityMap"=>%{},"blocks"=>[%{"text"=>"Hello","inlineStyleRanges"=>[%{"style"=>"BOLD","offset"=>2,"length"=>2}],"type"=>"unstyled","depth"=>0,"entityRanges"=>[],"data"=>%{},"key"=>"9d21d"}]} + output = "Hello
" + assert Draft.to_html(input) == output + end + + test "wraps multiple inline styles" do + input = %{"entityMap"=>%{},"blocks"=>[%{"text"=>"Hello World!","inlineStyleRanges"=>[%{"style"=>"ITALIC","offset"=>8,"length"=>3},%{"style"=>"BOLD","offset"=>2,"length"=>2}],"type"=>"unstyled","depth"=>0,"entityRanges"=>[],"data"=>%{},"key"=>"9d21d"}]} + output = "Hello World!
" + assert Draft.to_html(input) == output + end + + test "wraps nested inline styles" do + input = %{"entityMap"=>%{},"blocks"=>[%{"text"=>"Hello World!","inlineStyleRanges"=>[%{"style"=>"ITALIC","offset"=>2,"length"=>5},%{"style"=>"BOLD","offset"=>2,"length"=>2}],"type"=>"unstyled","depth"=>0,"entityRanges"=>[],"data"=>%{},"key"=>"9d21d"}]} + output = "Hello World!
" + assert Draft.to_html(input) == output + end + + test "wraps overlapping inline styles" do + input = %{"entityMap"=>%{},"blocks"=>[%{"text"=>"Hello World!","inlineStyleRanges"=>[%{"style"=>"ITALIC","offset"=>2,"length"=>5}, %{"style"=>"BOLD","offset"=>4,"length"=>5}],"type"=>"unstyled","depth"=>0,"entityRanges"=>[],"data"=>%{},"key"=>"9d21d"}]} + output = "Hello World!
" + assert Draft.to_html(input) == output + end + + test "wraps anchor entities" do + input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}}, + "blocks"=>[%{"text"=>"Hello World!","inlineStyleRanges"=>[],"type"=>"unstyled","depth"=>0,"entityRanges"=>[ + %{"offset"=>2,"length"=>3,"key"=>0} + ],"data"=>%{},"key"=>"9d21d"}]} + output = "Hello World!
" + assert Draft.to_html(input) == output + end + + test "wraps overlapping entities and inline styles" do + input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}}, + "blocks"=>[%{"text"=>"Hello World!", + "inlineStyleRanges"=>[ + %{"style"=>"ITALIC","offset"=>0,"length"=>4}, + %{"style"=>"BOLD","offset"=>4,"length"=>4}, + ], + "entityRanges"=>[ + %{"offset"=>2,"length"=>3,"key"=>0} + ], + "type"=>"unstyled", + "depth"=>0, + "data"=>%{},"key"=>"9d21d"}]} + output = "Hello World!
" + assert Draft.to_html(input) == output + end end