From f1b5e64ae2b6f42ac970dc03c636d185e5cfa08f Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Wed, 6 Feb 2019 10:12:13 -0600 Subject: [PATCH 1/2] Remove json parsing This solves a couple problems: 1. Poison was pretty out of date (so trying to use the lib caused version conflicts with the version we were already using in our app) 2. Our draftjs content wasn't stored as a string, it was stored as a map. Removing json parsing will let the user parse it if they need to (in whatever way they want), and let those who don't need to use it out of the box. --- lib/draft.ex | 19 ++----------------- mix.exs | 3 +-- mix.lock | 5 +++-- test/draft_test.exs | 12 ++++++------ 4 files changed, 12 insertions(+), 27 deletions(-) diff --git a/lib/draft.ex b/lib/draft.ex index 7fbe389..e6a8f7f 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -3,32 +3,17 @@ 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 input - |> blocks + |> Map.get("blocks") |> Enum.map(&Draft.Block.to_html/1) |> Enum.join("") 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..6e3df3a 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -3,37 +3,37 @@ 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":"header-one","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-one","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":"header-two","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-two","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":"header-three","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]}) + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-three","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 From 5b6be923e5ef2be9ba7bb0f9b8ab85ef23186fde Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 14 Feb 2019 12:03:43 -0600 Subject: [PATCH 2/2] Add ranges to draft Inline style ranges (right now only `BOLD` and `ITALIC`) and entity ranges (right now only `LINK`) --- lib/draft.ex | 4 +- lib/draft/block.ex | 32 +++++++------ lib/draft/ranges.ex | 109 ++++++++++++++++++++++++++++++++++++++++++++ test/draft_test.exs | 50 ++++++++++++++++++++ 4 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 lib/draft/ranges.ex diff --git a/lib/draft.ex b/lib/draft.ex index e6a8f7f..3c9b6b8 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -12,9 +12,11 @@ defmodule Draft do "

Hello

" """ def to_html(input) do + entity_map = Map.get(input, "entityMap") + input |> Map.get("blocks") - |> Enum.map(&Draft.Block.to_html/1) + |> 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 "
" end @@ -32,10 +35,11 @@ defmodule Draft.Block do "key" => _, "data" => _, "depth" => _, - "entityRanges" => _, - "inlineStyleRanges" => _}) do + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do tag = header_tags[header] - "<#{tag}>#{text}" + "<#{tag}>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}" end defp process_block(%{"type" => "blockquote", @@ -43,9 +47,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 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/test/draft_test.exs b/test/draft_test.exs index 6e3df3a..7e0c12e 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -37,4 +37,54 @@ defmodule DraftTest do 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