From f1b5e64ae2b6f42ac970dc03c636d185e5cfa08f Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Wed, 6 Feb 2019 10:12:13 -0600 Subject: [PATCH 01/23] 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 02/23] 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 From 43bb62ceef5d61f412923294020817e08daa520d Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 14 Feb 2019 15:35:40 -0600 Subject: [PATCH 03/23] `use` to allow custom processors --- README.md | 46 ++++++++++- lib/draft.ex | 33 ++++---- lib/draft/block.ex | 131 +++++++++++++++---------------- lib/draft/ranges.ex | 184 ++++++++++++++++++++++---------------------- test/draft_test.exs | 26 +++---- 5 files changed, 232 insertions(+), 188 deletions(-) diff --git a/README.md b/README.md index 4fef7de..00212a1 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,48 @@ Draft is a module that parses a JSON representation of [Draft.js](https://facebook.github.io/draft-js/) `convertToRaw` output and turns it into HTML. -It is very work in progress. +## Usage + +You can `use Draft` in your module, and then extend it with custom +`process_block`, `process_style`, and `process_entity` function signatures + +``` + # define your draft module + defmodule MyCustomDraft do + use Draft + + + def process_block(%{"type" => "atomic", + "text" => "", + "key" => _, + "data" => %{"type" => "image", "url" => url}, + "depth" => _, + "entityRanges" => _, + "inlineStyleRanges" => _}, _) do + "" + end + end + + # somewhere else you can pass your custom draft map to `to_html` + input = %{ + "entityMap"=>%{}, + "blocks"=>[ + %{"key"=>"9d21d", + "text"=>"Hello", + "type"=>"header-one", + "depth"=>0, + "inlineStyleRanges"=>[], + "entityRanges"=>[], + "data"=>%{}} + %{"key"=>"d12d9", + "text"=>"", + "type"=>"atomic", + "depth"=>0, + "inlineStyleRanges"=>[], + "entityRanges"=>[], + "data"=>%{ + "type"=>"atomic", + "url"=>"https://uploads.digitalonboarding.com/do_logo_long.png"}}]} + + MyCustomDraft.to_html(input) +``` diff --git a/lib/draft.ex b/lib/draft.ex index 3c9b6b8..3cafc89 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -1,22 +1,29 @@ defmodule Draft do - @moduledoc """ - Provides functions for parsing DraftJS content. - """ + defmacro __using__(_) do + quote do + @moduledoc """ + Provides functions for parsing DraftJS content. + """ - @doc """ - Renders the given DraftJS input as html. + @doc """ + Renders the given DraftJS input as html. - ## Examples + ## Examples 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 - |> Map.get("blocks") - |> Enum.map(&(Draft.Block.to_html(&1, entity_map))) - |> Enum.join("") + use Draft.Block + + def to_html(input) do + entity_map = Map.get(input, "entityMap") + + input + |> Map.get("blocks") + |> Enum.map(&(process_block(&1, entity_map))) + |> Enum.join("") + end + end end end diff --git a/lib/draft/block.ex b/lib/draft/block.ex index a7a7597..c5c68d4 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -1,74 +1,63 @@ defmodule Draft.Block do - @moduledoc """ - 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, entity_map - "

Hello

" - """ - def to_html(block, entity_map) do - process_block(block, entity_map) - end - - defp process_block(%{"type" => "unstyled", - "text" => "", - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => _, - "inlineStyleRanges" => _}, _) do - "
" - end - - defp process_block(%{"type" => "header-" <> header, - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, - entity_map) do - tag = header_tags[header] - "<#{tag}>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}" - end - - defp process_block(%{"type" => "blockquote", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "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", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "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 - %{ - "one" => "h1", - "two" => "h2", - "three" => "h3" - } + defmacro __using__(_) do + quote do + @moduledoc """ + Converts a single DraftJS block to html. + """ + + use Draft.Ranges + + def process_block(%{"type" => "unstyled", + "text" => "", + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => _, + "inlineStyleRanges" => _}, _) do + "
" + end + + def process_block(%{"type" => "header-" <> header, + "text" => text, + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do + tag = header_tags[header] + "<#{tag}>#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}" + end + + def process_block(%{"type" => "blockquote", + "text" => text, + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do + "
#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}
" + end + + def process_block(%{"type" => "unstyled", + "text" => text, + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map) do + "

#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}

" + end + + def header_tags do + %{ + "one" => "h1", + "two" => "h2", + "three" => "h3" + } + end + end end end diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 186994b..f2ef58e 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -1,109 +1,113 @@ defmodule Draft.Ranges do - @moduledoc """ - Provides functions for adding inline style ranges and entity ranges - """ + defmacro __using__(_) do + quote 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}" + def apply_ranges(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) + 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 + 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 + def process_style("BOLD") do + "font-weight: bold;" + end - defp process_style("ITALIC") do - "font-style: italic;" - end + def process_style("ITALIC") do + "font-style: italic;" + end - defp process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}) do - {"", ""} - end + def 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_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_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 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"] + 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 + 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 + @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 + ## 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}] + 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 - 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 + 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 end end diff --git a/test/draft_test.exs b/test/draft_test.exs index 7e0c12e..ea434b8 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -1,65 +1,65 @@ defmodule DraftTest do use ExUnit.Case - doctest Draft + use Draft test "generate a

" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "

Hello

" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "generate a

" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-one","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "

Hello

" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "generate a

" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-two","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "

Hello

" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "generate a

" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"header-three","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "

Hello

" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "generate a
" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"Hello","type"=>"blockquote","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "
Hello
" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "generate a
" do input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} output = "
" - assert Draft.to_html(input) == output + assert 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 + assert 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 + assert 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 + assert 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 + assert to_html(input) == output end test "wraps anchor entities" do @@ -68,7 +68,7 @@ defmodule DraftTest do %{"offset"=>2,"length"=>3,"key"=>0} ],"data"=>%{},"key"=>"9d21d"}]} output = "

Hello World!

" - assert Draft.to_html(input) == output + assert to_html(input) == output end test "wraps overlapping entities and inline styles" do @@ -85,6 +85,6 @@ defmodule DraftTest do "depth"=>0, "data"=>%{},"key"=>"9d21d"}]} output = "

Hello World!

" - assert Draft.to_html(input) == output + assert to_html(input) == output end end From 1b02753cecf52ecb6b0be00a5e04d453aca76e51 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 14 Feb 2019 15:38:16 -0600 Subject: [PATCH 04/23] Allow for `context` variable to be provided Which can be hooked into in custom processors --- README.md | 47 ++++++++++++++++++++++++++++++++++++++++++++++ lib/draft.ex | 4 ++-- lib/draft/block.ex | 8 ++++---- 3 files changed, 53 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 00212a1..86e07fb 100644 --- a/README.md +++ b/README.md @@ -47,3 +47,50 @@ You can `use Draft` in your module, and then extend it with custom MyCustomDraft.to_html(input) ``` + +You can pass a third variable for context that your custom processors can hook +into. + +``` + # capture the relevant context vars in your custom processors + defmodule MyCustomDraft do + use Draft + + + def process_block(%{"type" => "atomic", + "text" => "", + "key" => _, + "data" => %{"type" => "image", "url" => url}, + "depth" => _, + "entityRanges" => _, + "inlineStyleRanges" => _}, [user: user]) do + "#{user.name}" + end + end + + # somewhere else you can pass any number of vars as the third argument + + user = %{name: "Pablo"} + + input = %{ + "entityMap"=>%{}, + "blocks"=>[ + %{"key"=>"9d21d", + "text"=>"Hello", + "type"=>"header-one", + "depth"=>0, + "inlineStyleRanges"=>[], + "entityRanges"=>[], + "data"=>%{}} + %{"key"=>"d12d9", + "text"=>"", + "type"=>"atomic", + "depth"=>0, + "inlineStyleRanges"=>[], + "entityRanges"=>[], + "data"=>%{ + "type"=>"atomic", + "url"=>"https://uploads.digitalonboarding.com/do_logo_long.png"}}]} + + MyCustomDraft.to_html(input, user: user) +``` diff --git a/lib/draft.ex b/lib/draft.ex index 3cafc89..1318299 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -16,12 +16,12 @@ defmodule Draft do use Draft.Block - def to_html(input) do + def to_html(input, context \\ []) do entity_map = Map.get(input, "entityMap") input |> Map.get("blocks") - |> Enum.map(&(process_block(&1, entity_map))) + |> Enum.map(&(process_block(&1, entity_map, context))) |> Enum.join("") end end diff --git a/lib/draft/block.ex b/lib/draft/block.ex index c5c68d4..d861a76 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -13,7 +13,7 @@ defmodule Draft.Block do "data" => _, "depth" => _, "entityRanges" => _, - "inlineStyleRanges" => _}, _) do + "inlineStyleRanges" => _}, _, _) do "
" end @@ -24,7 +24,7 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map) do + entity_map, _) do tag = header_tags[header] "<#{tag}>#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}" end @@ -36,7 +36,7 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map) do + entity_map, _) do "
#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}
" end @@ -47,7 +47,7 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map) do + entity_map, _) do "

#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}

" end From 714312b9702decd6a108f87e8b8d8e794cc8cd45 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Tue, 19 Feb 2019 10:22:48 -0600 Subject: [PATCH 05/23] Whoops, add context to ranges too --- lib/draft/block.ex | 12 ++++++------ lib/draft/ranges.ex | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/draft/block.ex b/lib/draft/block.ex index d861a76..b1fbb54 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -24,9 +24,9 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map, _) do + entity_map, context) do tag = header_tags[header] - "<#{tag}>#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}" + "<#{tag}>#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}" end def process_block(%{"type" => "blockquote", @@ -36,8 +36,8 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map, _) do - "
#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}
" + entity_map, context) do + "
#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
" end def process_block(%{"type" => "unstyled", @@ -47,8 +47,8 @@ defmodule Draft.Block do "depth" => _, "entityRanges" => entity_ranges, "inlineStyleRanges" => inline_style_ranges}, - entity_map, _) do - "

#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map)}

" + entity_map, context) do + "

#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}

" end def header_tags do diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index f2ef58e..7329549 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -5,17 +5,17 @@ defmodule Draft.Ranges do Provides functions for adding inline style ranges and entity ranges """ - def apply_ranges(text, inline_style_ranges, entity_ranges, entity_map) do + def apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context) 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 + case get_styles_for_range(start, finish, inline_style_ranges, context) 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) + entity_opening_tags = get_entity_opening_tags_for_start(start, entity_ranges, entity_map, context) + entity_closing_tags = get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map, context) opening_tags = "#{entity_opening_tags}#{style_opening_tag}" closing_tags = "#{style_closing_tag}#{entity_closing_tags}" @@ -32,35 +32,35 @@ defmodule Draft.Ranges do end) end - def process_style("BOLD") do + def process_style("BOLD", _) do "font-weight: bold;" end - def process_style("ITALIC") do + def process_style("ITALIC", _) do "font-style: italic;" end - def process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}) do + def process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}, _) do {"", ""} end - defp get_styles_for_range(start, finish, inline_style_ranges) do + defp get_styles_for_range(start, finish, inline_style_ranges, context) 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.map(fn range -> process_style(range["style"], context) end) |> Enum.join(" ") end - defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map) do + defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map, context) 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) + |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> process_entity(context) |> elem(0) end) end - defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map) do + defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map, context) 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.map(fn range -> Map.get(entity_map, range["key"]) |> process_entity(context) |> elem(1) end) |> Enum.reverse() end From 1daf21be4c8ffb51177c507ed6c2e4154e09d658 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Tue, 19 Feb 2019 10:29:06 -0600 Subject: [PATCH 06/23] Specify language in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 86e07fb..c0a8e1c 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Draft is a module that parses a JSON representation of [Draft.js](https://facebo You can `use Draft` in your module, and then extend it with custom `process_block`, `process_style`, and `process_entity` function signatures -``` +```elixir # define your draft module defmodule MyCustomDraft do use Draft @@ -51,7 +51,7 @@ You can `use Draft` in your module, and then extend it with custom You can pass a third variable for context that your custom processors can hook into. -``` +```elixir # capture the relevant context vars in your custom processors defmodule MyCustomDraft do use Draft From 2726949129305c83739eafd9a26030edfa1a42d9 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 21 Feb 2019 10:18:58 -0600 Subject: [PATCH 07/23] Use string keys instead of integers for getting from entityMap --- lib/draft/ranges.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 7329549..6a66ccf 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -54,13 +54,13 @@ defmodule Draft.Ranges do defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map, context) do entity_ranges |> Enum.filter(fn range -> range["offset"] === start end) - |> Enum.map(fn range -> Map.get(entity_map, range["key"]) |> process_entity(context) |> elem(0) end) + |> Enum.map(fn range -> Map.get(entity_map, Integer.to_string(range["key"])) |> process_entity(context) |> elem(0) end) end defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map, context) 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(context) |> elem(1) end) + |> Enum.map(fn range -> Map.get(entity_map, Integer.to_string(range["key"])) |> process_entity(context) |> elem(1) end) |> Enum.reverse() end From ef5058f2974f5b8434d9df9346802e335004f2cb Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Fri, 22 Feb 2019 15:05:43 -0600 Subject: [PATCH 08/23] Process `ul`s and `ol`s --- lib/draft.ex | 37 +++++++++++++++++++++++++++++++++++++ lib/draft/block.ex | 34 ++++++++++++++++++++++++++++++++++ test/draft_test.exs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 104 insertions(+) diff --git a/lib/draft.ex b/lib/draft.ex index 1318299..26d04f8 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -21,9 +21,46 @@ defmodule Draft do input |> Map.get("blocks") + |> Enum.reduce([], group_list_items()) |> Enum.map(&(process_block(&1, entity_map, context))) |> Enum.join("") end + + @doc """ + Groups pertinent block types (i.e. ordered and unordered lists), allowing us to define + `process_block` signatures for both the wrapper component and their children (see + process_block signature for `unordered-list`, it's responsible for rendering its children) + + ## Examples + iex> blocks = [%{"key"=>"1","text"=>"Hello","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}] + iex> Enum.reduce(blocks, [], group_list_items()) + [%{"type"=>"unordered-list","data"=>%{"children"=>[%{"key"=>"1","text"=>"Hello","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]}}] + """ + + defp group_list_items(block, acc) do + case block["type"] do + type when type in ["unordered-list-item", "ordered-list-item"] -> + list_type = String.replace(block["type"], "-item", "") + + with {last_item, all_but_last_item} when not is_nil(last_item) <- List.pop_at(acc, length(acc) - 1), + type when type == list_type <- Map.get(last_item, "type") + #FIXME: this ignores depth + do + all_but_last_item ++ [add_block_item_to_previous_list(last_item, block)] + else + _ -> acc ++ [%{"type" => list_type, + "data" => %{"children" => [block]}}] + end + _ -> acc ++ [block] + end + end + + defp add_block_item_to_previous_list(previous_list, block) do + updated_children = previous_list["data"]["children"] ++ [block] + updated_data = Map.put(previous_list["data"], "children", updated_children) + + Map.put(previous_list, "data", updated_data) + end end end end diff --git a/lib/draft/block.ex b/lib/draft/block.ex index b1fbb54..d93a12d 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -51,6 +51,40 @@ defmodule Draft.Block do "

#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}

" end + def process_block(%{"type" => "unordered-list", + "data" => %{"children" => children}}, + entity_map, context) do + "
    #{Enum.map(children, &(process_block(&1, entity_map, context))) |> Enum.join("")}
" + end + + def process_block(%{"type" => "ordered-list", + "data" => %{"children" => children}}, + entity_map, context) do + "
    #{Enum.map(children, &(process_block(&1, entity_map, context))) |> Enum.join("")}
" + end + + def process_block(%{"type" => "ordered-list-item", + "text" => text, + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map, context) do + "
  • #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
  • " + end + + def process_block(%{"type" => "unordered-list-item", + "text" => text, + "key" => _, + "data" => _, + "depth" => _, + "entityRanges" => entity_ranges, + "inlineStyleRanges" => inline_style_ranges}, + entity_map, context) do + "
  • #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
  • " + end + def header_tags do %{ "one" => "h1", diff --git a/test/draft_test.exs b/test/draft_test.exs index ea434b8..3095126 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -87,4 +87,37 @@ defmodule DraftTest do output = "

    Hello World!

    " assert to_html(input) == output end + + test "wraps ordered lists in
      " do + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"one","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} + output = "
      1. one
      " + assert to_html(input) == output + end + + test "wraps unordered lists in
        " do + input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} + output = "
        • one
        " + assert to_html(input) == output + end + + test "wraps multiple unordered list items in the same
          " do + input = %{"entityMap"=>%{},"blocks"=>[ + %{"key"=>"9d21d","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, + %{"key"=>"9d21e","text"=>"two","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}} + ]} + output = "
          • one
          • two
          " + assert to_html(input) == output + end + + test "wraps multiple complex lists" do + input = %{"entityMap"=>%{},"blocks"=>[ + %{"key"=>"9d21c","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, + %{"key"=>"9d21e","text"=>"whoops","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, + %{"key"=>"9d21d","text"=>"two","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, + %{"key"=>"9d21f","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, + %{"key"=>"9d21g","text"=>"and another","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}} + ]} + output = "
          • one
          1. whoops
          • two

          Hello

          1. and another
          " + assert to_html(input) == output + end end From c01bf6d43955b217f092d024220fede76e3c1c43 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Fri, 22 Feb 2019 15:39:12 -0600 Subject: [PATCH 09/23] Fix derp function call --- lib/draft.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/draft.ex b/lib/draft.ex index 26d04f8..5cba0aa 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -21,7 +21,7 @@ defmodule Draft do input |> Map.get("blocks") - |> Enum.reduce([], group_list_items()) + |> Enum.reduce([], &group_list_items/2) |> Enum.map(&(process_block(&1, entity_map, context))) |> Enum.join("") end From 59c994cb83a608a13f0df2c36f113aa599ec3346 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Fri, 22 Feb 2019 15:39:20 -0600 Subject: [PATCH 10/23] Fix broken tests Broke from 2726949129305c83739eafd9a26030edfa1a42d9 --- test/draft_test.exs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/draft_test.exs b/test/draft_test.exs index 3095126..befc967 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -63,7 +63,7 @@ defmodule DraftTest do end test "wraps anchor entities" do - input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}}, + 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"}]} @@ -72,7 +72,7 @@ defmodule DraftTest do end test "wraps overlapping entities and inline styles" do - input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}}, + input = %{"entityMap"=>%{"0"=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}}, "blocks"=>[%{"text"=>"Hello World!", "inlineStyleRanges"=>[ %{"style"=>"ITALIC","offset"=>0,"length"=>4}, From 580495ec80f0fa9ddf0e1a7c09c45bfcca1a66cb Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Fri, 1 Mar 2019 18:03:26 -0500 Subject: [PATCH 11/23] Rewrite inline ranges as a tree This made us able to do some unique things, like transforming content in our custom entities, that we were unable to do with our simpler approach. --- lib/draft/block.ex | 53 +++----------- lib/draft/ranges.ex | 131 ++++++++++++++++------------------ lib/draft/tree.ex | 53 ++++++++++++++ test/custom_entity_test.exs | 138 ++++++++++++++++++++++++++++++++++++ test/draft/tree_test.exs | 63 ++++++++++++++++ 5 files changed, 325 insertions(+), 113 deletions(-) create mode 100644 lib/draft/tree.ex create mode 100644 test/custom_entity_test.exs create mode 100644 test/draft/tree_test.exs diff --git a/lib/draft/block.ex b/lib/draft/block.ex index d93a12d..118b337 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -7,48 +7,27 @@ defmodule Draft.Block do use Draft.Ranges - def process_block(%{"type" => "unstyled", - "text" => "", - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => _, - "inlineStyleRanges" => _}, _, _) do + def process_block(%{"type" => "unstyled", "text" => ""}, _, _) do "
          " end def process_block(%{"type" => "header-" <> header, - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, + "text" => text} = block, entity_map, context) do tag = header_tags[header] - "<#{tag}>#{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}" + "<#{tag}>#{apply_ranges(block, entity_map, context)}" end def process_block(%{"type" => "blockquote", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, + "text" => text} = block, entity_map, context) do - "
          #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
          " + "
          #{apply_ranges(block, entity_map, context)}
          " end def process_block(%{"type" => "unstyled", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, + "text" => text} = block, entity_map, context) do - "

          #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}

          " + "

          #{apply_ranges(block, entity_map, context)}

          " end def process_block(%{"type" => "unordered-list", @@ -64,25 +43,15 @@ defmodule Draft.Block do end def process_block(%{"type" => "ordered-list-item", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, + "text" => text} = block, entity_map, context) do - "
        • #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
        • " + "
        • #{apply_ranges(block, entity_map, context)}
        • " end def process_block(%{"type" => "unordered-list-item", - "text" => text, - "key" => _, - "data" => _, - "depth" => _, - "entityRanges" => entity_ranges, - "inlineStyleRanges" => inline_style_ranges}, + "text" => text} = block, entity_map, context) do - "
        • #{apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context)}
        • " + "
        • #{apply_ranges(block, entity_map, context)}
        • " end def header_tags do diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 6a66ccf..f328b6c 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -5,30 +5,28 @@ defmodule Draft.Ranges do Provides functions for adding inline style ranges and entity ranges """ - def apply_ranges(text, inline_style_ranges, entity_ranges, entity_map, context) 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, context) do - "" -> {"", ""} - styles -> {"", ""} - end - entity_opening_tags = get_entity_opening_tags_for_start(start, entity_ranges, entity_map, context) - entity_closing_tags = get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map, context) - opening_tags = "#{entity_opening_tags}#{style_opening_tag}" - closing_tags = "#{style_closing_tag}#{entity_closing_tags}" + def apply_ranges(block, entity_map, context) do + block["inlineStyleRanges"] + |> divvy_style_ranges(block["entityRanges"]) + |> group_style_ranges() + |> Kernel.++(block["entityRanges"]) + |> sort_by_offset_and_length_then_styles_first() + |> DraftTree.build_tree(block["text"]) + |> DraftTree.process_tree(fn text, styles, key -> + cond do + !is_nil(styles) -> + css = styles + |> Enum.map(fn style -> process_style(style, context) end) + |> Enum.join(" ") - adjusted_start = start + String.length(acc) - String.length(text) - adjusted_finish = finish + String.length(acc) - String.length(text) + "#{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) + !is_nil(key) -> + process_entity(entity_map |> Map.get(Integer.to_string(key)), text, context) + + true -> + text + end end) end @@ -40,63 +38,54 @@ defmodule Draft.Ranges do "font-style: italic;" end - def process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}, _) do - {"", ""} - end - - defp get_styles_for_range(start, finish, inline_style_ranges, context) do - inline_style_ranges - |> Enum.filter(fn range -> is_in_range(range, start, finish) end) - |> Enum.map(fn range -> process_style(range["style"], context) end) - |> Enum.join(" ") - end - - defp get_entity_opening_tags_for_start(start, entity_ranges, entity_map, context) do - entity_ranges - |> Enum.filter(fn range -> range["offset"] === start end) - |> Enum.map(fn range -> Map.get(entity_map, Integer.to_string(range["key"])) |> process_entity(context) |> elem(0) end) - end - - defp get_entity_closing_tags_for_finish(finish, entity_ranges, entity_map, context) do - entity_ranges - |> Enum.filter(fn range -> range["offset"] + range["length"] === finish end) - |> Enum.map(fn range -> Map.get(entity_map, Integer.to_string(range["key"])) |> process_entity(context) |> elem(1) end) - |> Enum.reverse() + def process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}, text, _) do + "#{text}" 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 + defp group_style_ranges(ranges) do + ranges + |> Enum.group_by(fn range -> {range["offset"], range["length"]} end) + |> Enum.map(fn {{offset, length}, ranges} -> + %{"offset" => offset, + "length" => length, + "styles" => ranges |> Enum.map(&(&1["style"]))} + end) 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}] + Cuts up multiple potentially overlapping ranges into more mutually exclusive ranges """ - defp consolidate_ranges(ranges) do - ranges - |> ranges_to_points() - |> points_to_ranges() + defp divvy_style_ranges(style_ranges, entity_ranges) do + Enum.map(style_ranges, fn style_range -> + ranges_to_points(entity_ranges ++ style_ranges) + |> Enum.filter(fn point -> + point > style_range["offset"] && point < style_range["offset"] + style_range["length"] + end) + |> Enum.reduce([style_range], fn point, acc -> + {already_split, [main]} = Enum.split(acc, length(acc) - 1) + + already_split ++ [ + %{"style" => style_range["style"], + "offset" => main["offset"], + "length" => point - main["offset"]}, + %{"style" => style_range["style"], + "offset" => point, + "length" => main["length"] - (point - main["offset"])}] + end) + end) + |> List.flatten() 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}] + defp sort_by_offset_and_length_then_styles_first(ranges) do + ranges + |> Enum.sort(fn range1, range2 -> + cond do + range1["offset"] != range2["offset"] -> + range1["offset"] < range2["offset"] + range1["length"] != range2["length"] -> + range1["length"] >= range2["length"] + true -> + is_nil(range2["styles"]) end end) end diff --git a/lib/draft/tree.ex b/lib/draft/tree.ex new file mode 100644 index 0000000..a85cb24 --- /dev/null +++ b/lib/draft/tree.ex @@ -0,0 +1,53 @@ +defmodule DraftTree do + defmodule Node do + defstruct offset: 0, length: 0, children: [], styles: [], key: nil, text: "" + end + + def build_tree(ranges, text) do + root_node = %Node{text: text, length: String.length(text), styles: nil} + + Enum.reduce(ranges, root_node, fn range, tree -> insert_node(tree, range, text) end) + end + + defp insert_node(tree, range, text) do + first_included_child = Enum.find(tree.children, fn child -> + range["offset"] >= child.offset && + (range["length"] + range["offset"]) <= (child.length + child.offset) + end) + + case first_included_child do + nil -> + Map.put(tree, :children, tree.children ++ [ + %Node{ + length: range["length"], + offset: range["offset"], + styles: range["styles"], + key: range["key"], + text: String.slice(text, range["offset"], range["length"]) + } + ]) + child -> + {all_but_last_item, _} = Enum.split(tree.children, length(tree.children) - 1) + + Map.put(tree, :children, all_but_last_item ++ [ + insert_node(child, range, text) + ]) + end + end + + def process_tree(%{children: [], key: key, styles: styles, text: text}, processor) do + processor.(text, styles, key) + end + + def process_tree(%{children: children, key: key, styles: styles, offset: offset, length: length, text: text}, processor) do + Enum.map(children, fn child -> {child, process_tree(child, processor)} end) + |> Enum.reverse() + |> Enum.reduce(text, fn {child, child_text}, acc -> + {start, rest} = String.split_at(acc, child.offset - offset) + {_, finish} = String.split_at(rest, child.offset - offset + child.length - String.length(start)) + + start <> child_text <> finish + end) + |> processor.(styles, key) + end +end diff --git a/test/custom_entity_test.exs b/test/custom_entity_test.exs new file mode 100644 index 0000000..5048a5c --- /dev/null +++ b/test/custom_entity_test.exs @@ -0,0 +1,138 @@ +defmodule CustomEntityTest do + use ExUnit.Case + use Draft + + def process_entity( + %{"type"=>"PERSONALIZATION", + "mutability"=>"IMMUTABLE", + "data"=>%{"value"=>value}}, _text, [contact: contact]) do + contact |> Map.get(value) + end + + test "nests entities that potentially replace content inside styles" do + input = %{ + "blocks" => [%{ + "key" => "ck6bi", + "data" => %{}, + "text" => "#CONTACT.NAME_FULL", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [%{ + "key" => 0, + "length" => 18, + "offset" => 0 + }], + "inlineStyleRanges" => [%{ + "style" => "BOLD", + "length" => 18, + "offset" => 0 + }] + }], + "entityMap" => %{ + "0" => %{ + "data" => %{ + "value" => "name" + }, + "type" => "PERSONALIZATION", + "mutability" => "IMMUTABLE" + } + } + } + + assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == + "

          Frodo

          " + end + + test "ranges adjacent to entities that potentially replace content" do + input = %{ + "blocks" => [%{ + "key" => "ck6bi", + "data" => %{}, + "text" => "It's going great #CONTACT.NAME_FULL. Right?", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [%{ + "key" => 0, + "length" => 5, + "offset" => 5 + }, %{ + "key" => 1, + "length" => 18, + "offset" => 17 + }], + "inlineStyleRanges" => [ + %{"style" => "BOLD", + "length" => 5, + "offset" => 37}] + }], + "entityMap" => %{ + "0" => %{ + "data" => %{ + "url" => "http://google.com" + }, + "type" => "LINK", + "mutability" => "MUTABLE" + }, + "1" => %{ + "data" => %{ + "value" => "name" + }, + "type" => "PERSONALIZATION", + "mutability" => "IMMUTABLE" + } + } + } + + assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == + "

          It's going great Frodo. Right?

          " + end + + test "a random complex combination" do + input = %{ + "blocks" => [%{ + "key" => "ck6bi", + "data" => %{}, + "text" => "It's going great #CONTACT.NAME_FULL. Right?", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [%{ + "key" => 0, + "length" => 38, + "offset" => 5 + }, %{ + "key" => 1, + "length" => 18, + "offset" => 17 + }], + "inlineStyleRanges" => [%{ + "style" => "BOLD", + "length" => 1, + "offset" => 0 + }, %{ + "style" => "BOLD", + "length" => 26, + "offset" => 11 + }] + }], + "entityMap" => %{ + "0" => %{ + "data" => %{ + "url" => "http://google.com" + }, + "type" => "LINK", + "mutability" => "MUTABLE" + }, + "1" => %{ + "data" => %{ + "value" => "name" + }, + "type" => "PERSONALIZATION", + "mutability" => "IMMUTABLE" + } + } + } + + assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == + "

          It's going great Frodo. Right?

          " + end +end diff --git a/test/draft/tree_test.exs b/test/draft/tree_test.exs new file mode 100644 index 0000000..fbe4233 --- /dev/null +++ b/test/draft/tree_test.exs @@ -0,0 +1,63 @@ +defmodule TreeTest do + use ExUnit.Case + + test "builds a tree" do + assert DraftTree.build_tree([ + %{"length" => 1, "offset" => 0, "styles" => ["BOLD"]}, + %{"key" => 0, "length" => 38, "offset" => 5}, + %{"length" => 6, "offset" => 11, "styles" => ["BOLD"]}, + %{"length" => 18, "offset" => 17, "styles" => ["BOLD"]}, + %{"key" => 1, "length" => 18, "offset" => 17}, + %{"length" => 2, "offset" => 35, "styles" => ["BOLD"]} + ], "It's going great #CONTACT.NAME_FULL. Right?") == + %DraftTree.Node{ + key: nil, + length: 43, + offset: 0, + styles: nil, + text: "It's going great #CONTACT.NAME_FULL. Right?", + children: [ + %DraftTree.Node{ + key: nil, + length: 1, + offset: 0, + styles: ["BOLD"], + text: "I", + children: []}, + %DraftTree.Node{ + key: 0, + length: 38, + offset: 5, + styles: nil, + text: "going great #CONTACT.NAME_FULL. Right?", + children: [ + %DraftTree.Node{ + key: nil, + length: 6, + offset: 11, + styles: ["BOLD"], + text: "great ", + children: []}, + %DraftTree.Node{ + key: nil, + length: 18, + offset: 17, + styles: ["BOLD"], + text: "#CONTACT.NAME_FULL", + children: [ + %DraftTree.Node{ + key: 1, + length: 18, + offset: 17, + styles: nil, + text: "#CONTACT.NAME_FULL", + children: []}]}, + %DraftTree.Node{ + key: nil, + length: 2, + offset: 35, + styles: ["BOLD"], + text: ". ", + children: []}]}]} + end +end From 31184eee51d7618eb3469bd960d293677287b1ad Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Wed, 13 Mar 2019 10:00:23 -0400 Subject: [PATCH 12/23] Format project --- .formatter.exs | 5 + lib/draft.ex | 21 +- lib/draft/block.ex | 56 +++--- lib/draft/ranges.ex | 43 +++-- lib/draft/tree.ex | 60 ++++-- mix.exs | 14 +- test/custom_entity_test.exs | 157 ++++++++------- test/draft/tree_test.exs | 127 ++++++------ test/draft_test.exs | 372 +++++++++++++++++++++++++++++++----- 9 files changed, 611 insertions(+), 244 deletions(-) create mode 100644 .formatter.exs diff --git a/.formatter.exs b/.formatter.exs new file mode 100644 index 0000000..3b4db1a --- /dev/null +++ b/.formatter.exs @@ -0,0 +1,5 @@ +[ + import_deps: [], + inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"], + subdirectories: [] +] diff --git a/lib/draft.ex b/lib/draft.ex index 5cba0aa..098f4d7 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -22,7 +22,7 @@ defmodule Draft do input |> Map.get("blocks") |> Enum.reduce([], &group_list_items/2) - |> Enum.map(&(process_block(&1, entity_map, context))) + |> Enum.map(&process_block(&1, entity_map, context)) |> Enum.join("") end @@ -42,16 +42,17 @@ defmodule Draft do type when type in ["unordered-list-item", "ordered-list-item"] -> list_type = String.replace(block["type"], "-item", "") - with {last_item, all_but_last_item} when not is_nil(last_item) <- List.pop_at(acc, length(acc) - 1), - type when type == list_type <- Map.get(last_item, "type") - #FIXME: this ignores depth - do - all_but_last_item ++ [add_block_item_to_previous_list(last_item, block)] - else - _ -> acc ++ [%{"type" => list_type, - "data" => %{"children" => [block]}}] + # FIXME: this ignores depth + with {last_item, all_but_last_item} when not is_nil(last_item) <- + List.pop_at(acc, length(acc) - 1), + type when type == list_type <- Map.get(last_item, "type") do + all_but_last_item ++ [add_block_item_to_previous_list(last_item, block)] + else + _ -> acc ++ [%{"type" => list_type, "data" => %{"children" => [block]}}] end - _ -> acc ++ [block] + + _ -> + acc ++ [block] end end diff --git a/lib/draft/block.ex b/lib/draft/block.ex index 118b337..fce1f60 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -11,53 +11,59 @@ defmodule Draft.Block do "
          " end - def process_block(%{"type" => "header-" <> header, - "text" => text} = block, - entity_map, context) do + def process_block( + %{"type" => "header-" <> header, "text" => text} = block, + entity_map, + context + ) do tag = header_tags[header] "<#{tag}>#{apply_ranges(block, entity_map, context)}" end - def process_block(%{"type" => "blockquote", - "text" => text} = block, - entity_map, context) do + def process_block(%{"type" => "blockquote", "text" => text} = block, entity_map, context) do "
          #{apply_ranges(block, entity_map, context)}
          " end - def process_block(%{"type" => "unstyled", - "text" => text} = block, - entity_map, context) do + def process_block(%{"type" => "unstyled", "text" => text} = block, entity_map, context) do "

          #{apply_ranges(block, entity_map, context)}

          " end - def process_block(%{"type" => "unordered-list", - "data" => %{"children" => children}}, - entity_map, context) do - "
            #{Enum.map(children, &(process_block(&1, entity_map, context))) |> Enum.join("")}
          " + def process_block( + %{"type" => "unordered-list", "data" => %{"children" => children}}, + entity_map, + context + ) do + "
            #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
          " end - def process_block(%{"type" => "ordered-list", - "data" => %{"children" => children}}, - entity_map, context) do - "
            #{Enum.map(children, &(process_block(&1, entity_map, context))) |> Enum.join("")}
          " + def process_block( + %{"type" => "ordered-list", "data" => %{"children" => children}}, + entity_map, + context + ) do + "
            #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
          " end - def process_block(%{"type" => "ordered-list-item", - "text" => text} = block, - entity_map, context) do + def process_block( + %{"type" => "ordered-list-item", "text" => text} = block, + entity_map, + context + ) do "
        • #{apply_ranges(block, entity_map, context)}
        • " end - def process_block(%{"type" => "unordered-list-item", - "text" => text} = block, - entity_map, context) do + def process_block( + %{"type" => "unordered-list-item", "text" => text} = block, + entity_map, + context + ) do "
        • #{apply_ranges(block, entity_map, context)}
        • " end def header_tags do %{ - "one" => "h1", - "two" => "h2", + "one" => "h1", + "two" => "h2", "three" => "h3" } end diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index f328b6c..f9e2ccf 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -15,9 +15,10 @@ defmodule Draft.Ranges do |> DraftTree.process_tree(fn text, styles, key -> cond do !is_nil(styles) -> - css = styles - |> Enum.map(fn style -> process_style(style, context) end) - |> Enum.join(" ") + css = + styles + |> Enum.map(fn style -> process_style(style, context) end) + |> Enum.join(" ") "#{text}" @@ -38,7 +39,11 @@ defmodule Draft.Ranges do "font-style: italic;" end - def process_entity(%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>url}}, text, _) do + def process_entity( + %{"type" => "LINK", "mutability" => "MUTABLE", "data" => %{"url" => url}}, + text, + _ + ) do "#{text}" end @@ -46,9 +51,7 @@ defmodule Draft.Ranges do ranges |> Enum.group_by(fn range -> {range["offset"], range["length"]} end) |> Enum.map(fn {{offset, length}, ranges} -> - %{"offset" => offset, - "length" => length, - "styles" => ranges |> Enum.map(&(&1["style"]))} + %{"offset" => offset, "length" => length, "styles" => ranges |> Enum.map(& &1["style"])} end) end @@ -64,13 +67,19 @@ defmodule Draft.Ranges do |> Enum.reduce([style_range], fn point, acc -> {already_split, [main]} = Enum.split(acc, length(acc) - 1) - already_split ++ [ - %{"style" => style_range["style"], - "offset" => main["offset"], - "length" => point - main["offset"]}, - %{"style" => style_range["style"], - "offset" => point, - "length" => main["length"] - (point - main["offset"])}] + already_split ++ + [ + %{ + "style" => style_range["style"], + "offset" => main["offset"], + "length" => point - main["offset"] + }, + %{ + "style" => style_range["style"], + "offset" => point, + "length" => main["length"] - (point - main["offset"]) + } + ] end) end) |> List.flatten() @@ -82,8 +91,10 @@ defmodule Draft.Ranges do cond do range1["offset"] != range2["offset"] -> range1["offset"] < range2["offset"] + range1["length"] != range2["length"] -> range1["length"] >= range2["length"] + true -> is_nil(range2["styles"]) end @@ -94,8 +105,8 @@ defmodule Draft.Ranges do Enum.reduce(ranges, [], fn range, acc -> acc ++ [range["offset"], range["offset"] + range["length"]] end) - |> Enum.uniq - |> Enum.sort + |> Enum.uniq() + |> Enum.sort() end end end diff --git a/lib/draft/tree.ex b/lib/draft/tree.ex index a85cb24..dd26fb2 100644 --- a/lib/draft/tree.ex +++ b/lib/draft/tree.ex @@ -10,28 +10,40 @@ defmodule DraftTree do end defp insert_node(tree, range, text) do - first_included_child = Enum.find(tree.children, fn child -> - range["offset"] >= child.offset && - (range["length"] + range["offset"]) <= (child.length + child.offset) - end) + first_included_child = + Enum.find(tree.children, fn child -> + range["offset"] >= child.offset && + range["length"] + range["offset"] <= child.length + child.offset + end) case first_included_child do nil -> - Map.put(tree, :children, tree.children ++ [ - %Node{ - length: range["length"], - offset: range["offset"], - styles: range["styles"], - key: range["key"], - text: String.slice(text, range["offset"], range["length"]) - } - ]) + Map.put( + tree, + :children, + tree.children ++ + [ + %Node{ + length: range["length"], + offset: range["offset"], + styles: range["styles"], + key: range["key"], + text: String.slice(text, range["offset"], range["length"]) + } + ] + ) + child -> {all_but_last_item, _} = Enum.split(tree.children, length(tree.children) - 1) - Map.put(tree, :children, all_but_last_item ++ [ - insert_node(child, range, text) - ]) + Map.put( + tree, + :children, + all_but_last_item ++ + [ + insert_node(child, range, text) + ] + ) end end @@ -39,12 +51,24 @@ defmodule DraftTree do processor.(text, styles, key) end - def process_tree(%{children: children, key: key, styles: styles, offset: offset, length: length, text: text}, processor) do + def process_tree( + %{ + children: children, + key: key, + styles: styles, + offset: offset, + length: length, + text: text + }, + processor + ) do Enum.map(children, fn child -> {child, process_tree(child, processor)} end) |> Enum.reverse() |> Enum.reduce(text, fn {child, child_text}, acc -> {start, rest} = String.split_at(acc, child.offset - offset) - {_, finish} = String.split_at(rest, child.offset - offset + child.length - String.length(start)) + + {_, finish} = + String.split_at(rest, child.offset - offset + child.length - String.length(start)) start <> child_text <> finish end) diff --git a/mix.exs b/mix.exs index 88d3b66..0b7d348 100644 --- a/mix.exs +++ b/mix.exs @@ -2,12 +2,14 @@ defmodule Draft.Mixfile do use Mix.Project def project do - [app: :draft, - version: "0.1.0", - elixir: "~> 1.3", - build_embedded: Mix.env == :prod, - start_permanent: Mix.env == :prod, - deps: deps()] + [ + app: :draft, + version: "0.1.0", + elixir: "~> 1.3", + build_embedded: Mix.env() == :prod, + start_permanent: Mix.env() == :prod, + deps: deps() + ] end # Configuration for the OTP application diff --git a/test/custom_entity_test.exs b/test/custom_entity_test.exs index 5048a5c..676ca4b 100644 --- a/test/custom_entity_test.exs +++ b/test/custom_entity_test.exs @@ -3,31 +3,42 @@ defmodule CustomEntityTest do use Draft def process_entity( - %{"type"=>"PERSONALIZATION", - "mutability"=>"IMMUTABLE", - "data"=>%{"value"=>value}}, _text, [contact: contact]) do + %{ + "type" => "PERSONALIZATION", + "mutability" => "IMMUTABLE", + "data" => %{"value" => value} + }, + _text, + contact: contact + ) do contact |> Map.get(value) end test "nests entities that potentially replace content inside styles" do input = %{ - "blocks" => [%{ - "key" => "ck6bi", - "data" => %{}, - "text" => "#CONTACT.NAME_FULL", - "type" => "unstyled", - "depth" => 0, - "entityRanges" => [%{ - "key" => 0, - "length" => 18, - "offset" => 0 - }], - "inlineStyleRanges" => [%{ - "style" => "BOLD", - "length" => 18, - "offset" => 0 - }] - }], + "blocks" => [ + %{ + "key" => "ck6bi", + "data" => %{}, + "text" => "#CONTACT.NAME_FULL", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [ + %{ + "key" => 0, + "length" => 18, + "offset" => 0 + } + ], + "inlineStyleRanges" => [ + %{ + "style" => "BOLD", + "length" => 18, + "offset" => 0 + } + ] + } + ], "entityMap" => %{ "0" => %{ "data" => %{ @@ -40,31 +51,33 @@ defmodule CustomEntityTest do } assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == - "

          Frodo

          " + "

          Frodo

          " end test "ranges adjacent to entities that potentially replace content" do input = %{ - "blocks" => [%{ - "key" => "ck6bi", - "data" => %{}, - "text" => "It's going great #CONTACT.NAME_FULL. Right?", - "type" => "unstyled", - "depth" => 0, - "entityRanges" => [%{ - "key" => 0, - "length" => 5, - "offset" => 5 - }, %{ - "key" => 1, - "length" => 18, - "offset" => 17 - }], - "inlineStyleRanges" => [ - %{"style" => "BOLD", - "length" => 5, - "offset" => 37}] - }], + "blocks" => [ + %{ + "key" => "ck6bi", + "data" => %{}, + "text" => "It's going great #CONTACT.NAME_FULL. Right?", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [ + %{ + "key" => 0, + "length" => 5, + "offset" => 5 + }, + %{ + "key" => 1, + "length" => 18, + "offset" => 17 + } + ], + "inlineStyleRanges" => [%{"style" => "BOLD", "length" => 5, "offset" => 37}] + } + ], "entityMap" => %{ "0" => %{ "data" => %{ @@ -84,36 +97,44 @@ defmodule CustomEntityTest do } assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == - "

          It's going great Frodo. Right?

          " + "

          It's going great Frodo. Right?

          " end test "a random complex combination" do input = %{ - "blocks" => [%{ - "key" => "ck6bi", - "data" => %{}, - "text" => "It's going great #CONTACT.NAME_FULL. Right?", - "type" => "unstyled", - "depth" => 0, - "entityRanges" => [%{ - "key" => 0, - "length" => 38, - "offset" => 5 - }, %{ - "key" => 1, - "length" => 18, - "offset" => 17 - }], - "inlineStyleRanges" => [%{ - "style" => "BOLD", - "length" => 1, - "offset" => 0 - }, %{ - "style" => "BOLD", - "length" => 26, - "offset" => 11 - }] - }], + "blocks" => [ + %{ + "key" => "ck6bi", + "data" => %{}, + "text" => "It's going great #CONTACT.NAME_FULL. Right?", + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [ + %{ + "key" => 0, + "length" => 38, + "offset" => 5 + }, + %{ + "key" => 1, + "length" => 18, + "offset" => 17 + } + ], + "inlineStyleRanges" => [ + %{ + "style" => "BOLD", + "length" => 1, + "offset" => 0 + }, + %{ + "style" => "BOLD", + "length" => 26, + "offset" => 11 + } + ] + } + ], "entityMap" => %{ "0" => %{ "data" => %{ @@ -133,6 +154,6 @@ defmodule CustomEntityTest do } assert to_html(input, contact: %{"name" => "Frodo", "email" => "frodo@middleearth.com"}) == - "

          It's going great Frodo. Right?

          " + "

          It's going great Frodo. Right?

          " end end diff --git a/test/draft/tree_test.exs b/test/draft/tree_test.exs index fbe4233..4a61680 100644 --- a/test/draft/tree_test.exs +++ b/test/draft/tree_test.exs @@ -2,62 +2,75 @@ defmodule TreeTest do use ExUnit.Case test "builds a tree" do - assert DraftTree.build_tree([ - %{"length" => 1, "offset" => 0, "styles" => ["BOLD"]}, - %{"key" => 0, "length" => 38, "offset" => 5}, - %{"length" => 6, "offset" => 11, "styles" => ["BOLD"]}, - %{"length" => 18, "offset" => 17, "styles" => ["BOLD"]}, - %{"key" => 1, "length" => 18, "offset" => 17}, - %{"length" => 2, "offset" => 35, "styles" => ["BOLD"]} - ], "It's going great #CONTACT.NAME_FULL. Right?") == - %DraftTree.Node{ - key: nil, - length: 43, - offset: 0, - styles: nil, - text: "It's going great #CONTACT.NAME_FULL. Right?", - children: [ - %DraftTree.Node{ - key: nil, - length: 1, - offset: 0, - styles: ["BOLD"], - text: "I", - children: []}, - %DraftTree.Node{ - key: 0, - length: 38, - offset: 5, - styles: nil, - text: "going great #CONTACT.NAME_FULL. Right?", - children: [ - %DraftTree.Node{ - key: nil, - length: 6, - offset: 11, - styles: ["BOLD"], - text: "great ", - children: []}, - %DraftTree.Node{ - key: nil, - length: 18, - offset: 17, - styles: ["BOLD"], - text: "#CONTACT.NAME_FULL", - children: [ - %DraftTree.Node{ - key: 1, - length: 18, - offset: 17, - styles: nil, - text: "#CONTACT.NAME_FULL", - children: []}]}, - %DraftTree.Node{ - key: nil, - length: 2, - offset: 35, - styles: ["BOLD"], - text: ". ", - children: []}]}]} + assert DraftTree.build_tree( + [ + %{"length" => 1, "offset" => 0, "styles" => ["BOLD"]}, + %{"key" => 0, "length" => 38, "offset" => 5}, + %{"length" => 6, "offset" => 11, "styles" => ["BOLD"]}, + %{"length" => 18, "offset" => 17, "styles" => ["BOLD"]}, + %{"key" => 1, "length" => 18, "offset" => 17}, + %{"length" => 2, "offset" => 35, "styles" => ["BOLD"]} + ], + "It's going great #CONTACT.NAME_FULL. Right?" + ) == + %DraftTree.Node{ + key: nil, + length: 43, + offset: 0, + styles: nil, + text: "It's going great #CONTACT.NAME_FULL. Right?", + children: [ + %DraftTree.Node{ + key: nil, + length: 1, + offset: 0, + styles: ["BOLD"], + text: "I", + children: [] + }, + %DraftTree.Node{ + key: 0, + length: 38, + offset: 5, + styles: nil, + text: "going great #CONTACT.NAME_FULL. Right?", + children: [ + %DraftTree.Node{ + key: nil, + length: 6, + offset: 11, + styles: ["BOLD"], + text: "great ", + children: [] + }, + %DraftTree.Node{ + key: nil, + length: 18, + offset: 17, + styles: ["BOLD"], + text: "#CONTACT.NAME_FULL", + children: [ + %DraftTree.Node{ + key: 1, + length: 18, + offset: 17, + styles: nil, + text: "#CONTACT.NAME_FULL", + children: [] + } + ] + }, + %DraftTree.Node{ + key: nil, + length: 2, + offset: 35, + styles: ["BOLD"], + text: ". ", + children: [] + } + ] + } + ] + } end end diff --git a/test/draft_test.exs b/test/draft_test.exs index befc967..2df917d 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -3,121 +3,405 @@ defmodule DraftTest do use Draft test "generate a

          " do - input = %{"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 to_html(input) == output end test "generate a

          " do - input = %{"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 to_html(input) == output end test "generate a

          " do - input = %{"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 to_html(input) == output end test "generate a

          " do - input = %{"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 to_html(input) == output end test "generate a
          " do - input = %{"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 to_html(input) == output end test "generate a
          " do - input = %{"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 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"}]} + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "text" => "Hello", + "inlineStyleRanges" => [%{"style" => "BOLD", "offset" => 2, "length" => 2}], + "type" => "unstyled", + "depth" => 0, + "entityRanges" => [], + "data" => %{}, + "key" => "9d21d" + } + ] + } + output = "

          Hello

          " assert 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!

          " + 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 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!

          " + 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 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!

          " + 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 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"}]} + 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 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!

          " + 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 to_html(input) == output end test "wraps ordered lists in
            " do - input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"one","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "one", + "type" => "ordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + output = "
            1. one
            " assert to_html(input) == output end test "wraps unordered lists in
              " do - input = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"9d21d","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]} + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "one", + "type" => "unordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + output = "
              • one
              " assert to_html(input) == output end test "wraps multiple unordered list items in the same
                " do - input = %{"entityMap"=>%{},"blocks"=>[ - %{"key"=>"9d21d","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, - %{"key"=>"9d21e","text"=>"two","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}} - ]} + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "one", + "type" => "unordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + }, + %{ + "key" => "9d21e", + "text" => "two", + "type" => "unordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + output = "
                • one
                • two
                " assert to_html(input) == output end test "wraps multiple complex lists" do - input = %{"entityMap"=>%{},"blocks"=>[ - %{"key"=>"9d21c","text"=>"one","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, - %{"key"=>"9d21e","text"=>"whoops","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, - %{"key"=>"9d21d","text"=>"two","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, - %{"key"=>"9d21f","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}, - %{"key"=>"9d21g","text"=>"and another","type"=>"ordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}} - ]} - output = "
                • one
                1. whoops
                • two

                Hello

                1. and another
                " + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21c", + "text" => "one", + "type" => "unordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + }, + %{ + "key" => "9d21e", + "text" => "whoops", + "type" => "ordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + }, + %{ + "key" => "9d21d", + "text" => "two", + "type" => "unordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + }, + %{ + "key" => "9d21f", + "text" => "Hello", + "type" => "unstyled", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + }, + %{ + "key" => "9d21g", + "text" => "and another", + "type" => "ordered-list-item", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + + output = + "
                • one
                1. whoops
                • two

                Hello

                1. and another
                " + assert to_html(input) == output end end From bfd447aa014d15a263aa91960dc421ebd6133980 Mon Sep 17 00:00:00 2001 From: Jonathan Crossman Date: Wed, 13 Mar 2019 10:03:13 -0400 Subject: [PATCH 13/23] Fix warnings for library users --- lib/draft.ex | 2 +- lib/draft/block.ex | 2 +- lib/draft/ranges.ex | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/draft.ex b/lib/draft.ex index 098f4d7..8520320 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -37,7 +37,7 @@ defmodule Draft do [%{"type"=>"unordered-list","data"=>%{"children"=>[%{"key"=>"1","text"=>"Hello","type"=>"unordered-list-item","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]}}] """ - defp group_list_items(block, acc) do + def group_list_items(block, acc) do case block["type"] do type when type in ["unordered-list-item", "ordered-list-item"] -> list_type = String.replace(block["type"], "-item", "") diff --git a/lib/draft/block.ex b/lib/draft/block.ex index fce1f60..77ede94 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -16,7 +16,7 @@ defmodule Draft.Block do entity_map, context ) do - tag = header_tags[header] + tag = header_tags()[header] "<#{tag}>#{apply_ranges(block, entity_map, context)}" end diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index f9e2ccf..305056b 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -58,7 +58,7 @@ defmodule Draft.Ranges do @doc """ Cuts up multiple potentially overlapping ranges into more mutually exclusive ranges """ - defp divvy_style_ranges(style_ranges, entity_ranges) do + def divvy_style_ranges(style_ranges, entity_ranges) do Enum.map(style_ranges, fn style_range -> ranges_to_points(entity_ranges ++ style_ranges) |> Enum.filter(fn point -> From ec45c578268323ead8fc9aecc4afb691c9831055 Mon Sep 17 00:00:00 2001 From: estermer Date: Thu, 20 Jun 2019 13:23:13 -0400 Subject: [PATCH 14/23] Handle headers four through six in process_block --- lib/draft/block.ex | 5 +++- test/draft_test.exs | 60 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/lib/draft/block.ex b/lib/draft/block.ex index 77ede94..d472293 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -64,7 +64,10 @@ defmodule Draft.Block do %{ "one" => "h1", "two" => "h2", - "three" => "h3" + "three" => "h3", + "four" => "h4", + "five" => "h5", + "six" => "h6" } end end diff --git a/test/draft_test.exs b/test/draft_test.exs index 2df917d..a32d41d 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -82,6 +82,66 @@ defmodule DraftTest do assert to_html(input) == output end + test "generate a

                " do + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "Hello", + "type" => "header-four", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + + output = "

                Hello

                " + assert to_html(input) == output + end + + test "generate a
                " do + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "Hello", + "type" => "header-five", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + + output = "
                Hello
                " + assert to_html(input) == output + end + + test "generate a
                " do + input = %{ + "entityMap" => %{}, + "blocks" => [ + %{ + "key" => "9d21d", + "text" => "Hello", + "type" => "header-six", + "depth" => 0, + "inlineStyleRanges" => [], + "entityRanges" => [], + "data" => %{} + } + ] + } + + output = "
                Hello
                " + assert to_html(input) == output + end + test "generate a
                " do input = %{ "entityMap" => %{}, From f3ac31fed31d40fee82ecf61b5c6823cb49d9d9f Mon Sep 17 00:00:00 2001 From: Mike Fisher Date: Thu, 23 Jan 2020 12:55:10 -0500 Subject: [PATCH 15/23] remove unused variable declaration --- lib/draft/tree.ex | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/draft/tree.ex b/lib/draft/tree.ex index dd26fb2..db36891 100644 --- a/lib/draft/tree.ex +++ b/lib/draft/tree.ex @@ -57,7 +57,6 @@ defmodule DraftTree do key: key, styles: styles, offset: offset, - length: length, text: text }, processor From c4151072f394f757e0826f18817d4621c4db284f Mon Sep 17 00:00:00 2001 From: Mike Fisher Date: Thu, 23 Jan 2020 12:55:47 -0500 Subject: [PATCH 16/23] support outlook for ul|ol li --- lib/draft/block.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/draft/block.ex b/lib/draft/block.ex index d472293..aff640a 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -49,7 +49,7 @@ defmodule Draft.Block do entity_map, context ) do - "
              • #{apply_ranges(block, entity_map, context)}
              • " + "
              • #{apply_ranges(block, entity_map, context)}
              • " end def process_block( @@ -57,7 +57,7 @@ defmodule Draft.Block do entity_map, context ) do - "
              • #{apply_ranges(block, entity_map, context)}
              • " + "
              • #{apply_ranges(block, entity_map, context)}
              • " end def header_tags do From fd83069f79c790fb0bdda2f2b0a0ad982cfd90d1 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 23 Jan 2020 17:43:22 -0600 Subject: [PATCH 17/23] Join with newlines instead of nothing Because, yes, line length matters :grimacing: --- lib/draft.ex | 2 +- lib/draft/block.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/draft.ex b/lib/draft.ex index 8520320..1a77cfe 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -23,7 +23,7 @@ defmodule Draft do |> Map.get("blocks") |> Enum.reduce([], &group_list_items/2) |> Enum.map(&process_block(&1, entity_map, context)) - |> Enum.join("") + |> Enum.join("\n") end @doc """ diff --git a/lib/draft/block.ex b/lib/draft/block.ex index aff640a..7fb3888 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -33,7 +33,7 @@ defmodule Draft.Block do entity_map, context ) do - "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
                " + "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("\n")}
                " end def process_block( @@ -41,7 +41,7 @@ defmodule Draft.Block do entity_map, context ) do - "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
                " + "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("\n")}
                " end def process_block( From 346c877227a5b96a3e177464a478a821c75c4172 Mon Sep 17 00:00:00 2001 From: Jake Buob Date: Thu, 23 Jan 2020 18:17:58 -0600 Subject: [PATCH 18/23] Add a newline in the span tag? I hope this doesn't bite us in the butt, buttttt... there is a line length limit and this is really the best place IMO to put the newline because otherwise it could get converted to a space and be in a bad spot... --- lib/draft/ranges.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 305056b..940f19e 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -20,7 +20,7 @@ defmodule Draft.Ranges do |> Enum.map(fn style -> process_style(style, context) end) |> Enum.join(" ") - "#{text}" + "#{text}" !is_nil(key) -> process_entity(entity_map |> Map.get(Integer.to_string(key)), text, context) From 9660730f19751742b8647fb812083a56c0a1869f Mon Sep 17 00:00:00 2001 From: Eric Stermer Date: Thu, 13 Feb 2020 10:11:10 -0500 Subject: [PATCH 19/23] Revert "Join with newlines instead of nothing" This reverts commit fd83069f79c790fb0bdda2f2b0a0ad982cfd90d1. --- lib/draft.ex | 2 +- lib/draft/block.ex | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/draft.ex b/lib/draft.ex index 1a77cfe..8520320 100644 --- a/lib/draft.ex +++ b/lib/draft.ex @@ -23,7 +23,7 @@ defmodule Draft do |> Map.get("blocks") |> Enum.reduce([], &group_list_items/2) |> Enum.map(&process_block(&1, entity_map, context)) - |> Enum.join("\n") + |> Enum.join("") end @doc """ diff --git a/lib/draft/block.ex b/lib/draft/block.ex index 7fb3888..aff640a 100644 --- a/lib/draft/block.ex +++ b/lib/draft/block.ex @@ -33,7 +33,7 @@ defmodule Draft.Block do entity_map, context ) do - "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("\n")}
                " + "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
                " end def process_block( @@ -41,7 +41,7 @@ defmodule Draft.Block do entity_map, context ) do - "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("\n")}
                " + "
                  #{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}
                " end def process_block( From 590231d4099a501aad617c31a8754f31b870925f Mon Sep 17 00:00:00 2001 From: Eric Stermer Date: Thu, 13 Feb 2020 10:11:29 -0500 Subject: [PATCH 20/23] Revert "Add a newline in the span tag?" This reverts commit 346c877227a5b96a3e177464a478a821c75c4172. --- lib/draft/ranges.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 940f19e..305056b 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -20,7 +20,7 @@ defmodule Draft.Ranges do |> Enum.map(fn style -> process_style(style, context) end) |> Enum.join(" ") - "#{text}" + "#{text}" !is_nil(key) -> process_entity(entity_map |> Map.get(Integer.to_string(key)), text, context) From 3ebb1a58da6dc32781f982ca3f919ba35564174f Mon Sep 17 00:00:00 2001 From: Eric Stermer Date: Thu, 13 Feb 2020 10:17:41 -0500 Subject: [PATCH 21/23] Fix tests for lists so they pass --- test/draft_test.exs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/test/draft_test.exs b/test/draft_test.exs index a32d41d..97e2e32 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -354,7 +354,7 @@ defmodule DraftTest do ] } - output = "
                1. one
                " + output = "
                1. one
                " assert to_html(input) == output end @@ -374,7 +374,7 @@ defmodule DraftTest do ] } - output = "
                • one
                " + output = "
                • one
                " assert to_html(input) == output end @@ -403,7 +403,9 @@ defmodule DraftTest do ] } - output = "
                • one
                • two
                " + output = + "
                • one
                • two
                " + assert to_html(input) == output end @@ -460,7 +462,7 @@ defmodule DraftTest do } output = - "
                • one
                1. whoops
                • two

                Hello

                1. and another
                " + "
                • one
                1. whoops
                • two

                Hello

                1. and another
                " assert to_html(input) == output end From 9fb50c1bdcc80688306da34be6e4bff98479338d Mon Sep 17 00:00:00 2001 From: Eric Stermer Date: Wed, 12 Feb 2020 12:15:07 -0500 Subject: [PATCH 22/23] Handle UNDERLINE with process_style --- lib/draft/ranges.ex | 4 ++++ test/draft_test.exs | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 305056b..1009d37 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -39,6 +39,10 @@ defmodule Draft.Ranges do "font-style: italic;" end + def process_style("UNDERLINE", _) do + "text-decoration: underline;" + end + def process_entity( %{"type" => "LINK", "mutability" => "MUTABLE", "data" => %{"url" => url}}, text, diff --git a/test/draft_test.exs b/test/draft_test.exs index 97e2e32..de095bf 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -210,7 +210,8 @@ defmodule DraftTest do "text" => "Hello World!", "inlineStyleRanges" => [ %{"style" => "ITALIC", "offset" => 8, "length" => 3}, - %{"style" => "BOLD", "offset" => 2, "length" => 2} + %{"style" => "BOLD", "offset" => 2, "length" => 2}, + %{"style" => "UNDERLINE", "offset" => 4, "length" => 4} ], "type" => "unstyled", "depth" => 0, @@ -222,7 +223,7 @@ defmodule DraftTest do } output = - "

                Hello World!

                " + "

                Hello World!

                " assert to_html(input) == output end From a8d8f3ec26bc13230d864f49216da26cadb19d1a Mon Sep 17 00:00:00 2001 From: Bruce Colby Date: Tue, 23 Aug 2022 15:17:01 -0400 Subject: [PATCH 23/23] Add ability to control the order in which styles are applied to entities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://trello.com/c/DXA1F6ab Fixes a bug in Outlook that doesn’t respect an outer span’s styling on an inner a tag. --- lib/draft/ranges.ex | 31 ++++++++++++++++++++++++++++--- test/draft_test.exs | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/lib/draft/ranges.ex b/lib/draft/ranges.ex index 1009d37..94a014c 100644 --- a/lib/draft/ranges.ex +++ b/lib/draft/ranges.ex @@ -10,7 +10,7 @@ defmodule Draft.Ranges do |> divvy_style_ranges(block["entityRanges"]) |> group_style_ranges() |> Kernel.++(block["entityRanges"]) - |> sort_by_offset_and_length_then_styles_first() + |> sort_by_offset_and_length_then_styles(entity_map) |> DraftTree.build_tree(block["text"]) |> DraftTree.process_tree(fn text, styles, key -> cond do @@ -43,6 +43,23 @@ defmodule Draft.Ranges do "text-decoration: underline;" end + # NB: styling will be processed first + # This fixes a bug in Outlook where a `span`'s styling has no effect on the `a` tag. + # So instead of `Google` we want + # `Google` + def process_style_order(range, %{ + "type" => "LINK", + "mutability" => "MUTABLE", + "data" => _data + }) do + !is_nil(range["styles"]) + end + + # NB: styling will be processed last + def process_style_order(range, _entity) do + is_nil(range["styles"]) + end + def process_entity( %{"type" => "LINK", "mutability" => "MUTABLE", "data" => %{"url" => url}}, text, @@ -89,7 +106,7 @@ defmodule Draft.Ranges do |> List.flatten() end - defp sort_by_offset_and_length_then_styles_first(ranges) do + defp sort_by_offset_and_length_then_styles(ranges, entity_map) do ranges |> Enum.sort(fn range1, range2 -> cond do @@ -100,7 +117,15 @@ defmodule Draft.Ranges do range1["length"] >= range2["length"] true -> - is_nil(range2["styles"]) + key = + case range2["key"] do + nil -> nil + key -> Integer.to_string(key) + end + + entity = Map.get(entity_map, key) + + process_style_order(range2, entity) end end) end diff --git a/test/draft_test.exs b/test/draft_test.exs index de095bf..093fa60 100644 --- a/test/draft_test.exs +++ b/test/draft_test.exs @@ -339,6 +339,49 @@ defmodule DraftTest do assert to_html(input) == output end + test "anchor entities text is wrapped by an inline style span tag" do + input = %{ + "blocks" => [ + %{ + "depth" => 0, + "entityRanges" => [ + %{ + "key" => 0, + "offset" => 0, + "length" => 6 + } + ], + "inlineStyleRanges" => [ + %{ + "style" => "BOLD", + "offset" => 0, + "length" => 6 + } + ], + "data" => %{}, + "text" => "Google", + "key" => "c2jk5", + "type" => "unstyled" + } + ], + "entityMap" => %{ + "0" => %{ + "type" => "LINK", + "data" => %{ + "url" => "https=>\/\/www.google.com", + "target" => "_blank" + }, + "mutability" => "MUTABLE" + } + } + } + + output = + "

                //www.google.com\">Google

                " + + assert to_html(input) == output + end + test "wraps ordered lists in
                  " do input = %{ "entityMap" => %{},