Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Ranges #2

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions lib/draft.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,33 +3,20 @@ defmodule Draft do
Provides functions for parsing DraftJS content.
"""

@doc """
Parses the given DraftJS input and returns the blocks as a list of
maps.

## Examples
iex> draft = ~s({"entityMap":{},"blocks":[{"key":"1","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]})
iex> Draft.blocks draft
[%{"key" => "1", "text" => "Hello", "type" => "unstyled",
"depth" => 0, "inlineStyleRanges" => [], "entityRanges" => [],
"data" => %{}}]
"""
def blocks(input) do
Poison.Parser.parse!(input)["blocks"]
end

@doc """
Renders the given DraftJS input as html.

## Examples
iex> draft = ~s({"entityMap":{},"blocks":[{"key":"1","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]})
iex> draft = %{"entityMap"=>%{},"blocks"=>[%{"key"=>"1","text"=>"Hello","type"=>"unstyled","depth"=>0,"inlineStyleRanges"=>[],"entityRanges"=>[],"data"=>%{}}]}
iex> Draft.to_html draft
"<p>Hello</p>"
"""
def to_html(input) do
entity_map = Map.get(input, "entityMap")

input
|> blocks
|> Enum.map(&Draft.Block.to_html/1)
|> Map.get("blocks")
|> Enum.map(&(Draft.Block.to_html(&1, entity_map)))
|> Enum.join("")
end
end
32 changes: 19 additions & 13 deletions lib/draft/block.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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
"<p>Hello</p>"
"""
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",
Expand All @@ -23,7 +26,7 @@ defmodule Draft.Block do
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"inlineStyleRanges" => _}, _) do
"<br>"
end

Expand All @@ -32,30 +35,33 @@ 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}>"
"<#{tag}>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</#{tag}>"
end

defp process_block(%{"type" => "blockquote",
"text" => text,
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"<blockquote>#{text}</blockquote>"
"entityRanges" => entity_ranges,
"inlineStyleRanges" => inline_style_ranges},
entity_map) do
"<blockquote>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</blockquote>"
end

defp process_block(%{"type" => "unstyled",
"text" => text,
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"<p>#{text}</p>"
"entityRanges" => entity_ranges,
"inlineStyleRanges" => inline_style_ranges},
entity_map) do
"<p>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</p>"
end

defp header_tags do
Expand Down
109 changes: 109 additions & 0 deletions lib/draft/ranges.ex
Original file line number Diff line number Diff line change
@@ -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 -> {"<span style=\"#{styles}\">", "</span>"}
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
{"<a href=\"#{url}\">", "</a>"}
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
3 changes: 1 addition & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 3 additions & 2 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -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], []}}
}
62 changes: 56 additions & 6 deletions test/draft_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,88 @@ defmodule DraftTest do
doctest Draft

test "generate a <p>" 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 = "<p>Hello</p>"
assert Draft.to_html(input) == output
end

test "generate a <h1>" 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 = "<h1>Hello</h1>"
assert Draft.to_html(input) == output
end

test "generate a <h2>" 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 = "<h2>Hello</h2>"
assert Draft.to_html(input) == output
end

test "generate a <h3>" 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 = "<h3>Hello</h3>"
assert Draft.to_html(input) == output
end

test "generate a <blockquote>" 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 = "<blockquote>Hello</blockquote>"
assert Draft.to_html(input) == output
end

test "generate a <br>" 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 = "<br>"
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 = "<p>He<span style=\"font-weight: bold;\">ll</span>o</p>"
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 = "<p>He<span style=\"font-weight: bold;\">ll</span>o Wo<span style=\"font-style: italic;\">rld</span>!</p>"
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 = "<p>He<span style=\"font-style: italic; font-weight: bold;\">ll</span><span style=\"font-style: italic;\">o W</span>orld!</p>"
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 = "<p>He<span style=\"font-style: italic;\">ll</span><span style=\"font-style: italic; font-weight: bold;\">o W</span><span style=\"font-weight: bold;\">or</span>ld!</p>"
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 = "<p>He<a href=\"http://google.com\">llo</a> World!</p>"
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 = "<p><span style=\"font-style: italic;\">He</span><a href=\"http://google.com\"><span style=\"font-style: italic;\">ll</span><span style=\"font-weight: bold;\">o</span></a><span style=\"font-weight: bold;\"> Wo</span>rld!</p>"
assert Draft.to_html(input) == output
end
end