Skip to content

Commit 678a4f3

Browse files
committed
Add ranges to draft
Inline style ranges (right now only `BOLD` and `ITALIC`) and entity ranges (right now only `LINK`)
1 parent f1b5e64 commit 678a4f3

File tree

4 files changed

+181
-14
lines changed

4 files changed

+181
-14
lines changed

lib/draft.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,11 @@ defmodule Draft do
1212
"<p>Hello</p>"
1313
"""
1414
def to_html(input) do
15+
entity_map = Map.get(input, "entityMap")
16+
1517
input
1618
|> Map.get("blocks")
17-
|> Enum.map(&Draft.Block.to_html/1)
19+
|> Enum.map(&(Draft.Block.to_html(&1, entity_map)))
1820
|> Enum.join("")
1921
end
2022
end

lib/draft/block.ex

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,21 @@ defmodule Draft.Block do
33
Converts a single DraftJS block to html.
44
"""
55

6+
alias Draft.Ranges
7+
68
@doc """
79
Renders the given DraftJS input as html.
810
911
## Examples
12+
iex> entity_map = %{}
1013
iex> block = %{"key" => "1", "text" => "Hello", "type" => "unstyled",
1114
...> "depth" => 0, "inlineStyleRanges" => [], "entityRanges" => [],
1215
...> "data" => %{}}
13-
iex> Draft.Block.to_html block
16+
iex> Draft.Block.to_html block, entity_map
1417
"<p>Hello</p>"
1518
"""
16-
def to_html(block) do
17-
process_block(block)
19+
def to_html(block, entity_map) do
20+
process_block(block, entity_map)
1821
end
1922

2023
defp process_block(%{"type" => "unstyled",
@@ -23,7 +26,7 @@ defmodule Draft.Block do
2326
"data" => _,
2427
"depth" => _,
2528
"entityRanges" => _,
26-
"inlineStyleRanges" => _}) do
29+
"inlineStyleRanges" => _}, _) do
2730
"<br>"
2831
end
2932

@@ -32,30 +35,33 @@ defmodule Draft.Block do
3235
"key" => _,
3336
"data" => _,
3437
"depth" => _,
35-
"entityRanges" => _,
36-
"inlineStyleRanges" => _}) do
38+
"entityRanges" => entity_ranges,
39+
"inlineStyleRanges" => inline_style_ranges},
40+
entity_map) do
3741
tag = header_tags[header]
38-
"<#{tag}>#{text}</#{tag}>"
42+
"<#{tag}>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</#{tag}>"
3943
end
4044

4145
defp process_block(%{"type" => "blockquote",
4246
"text" => text,
4347
"key" => _,
4448
"data" => _,
4549
"depth" => _,
46-
"entityRanges" => _,
47-
"inlineStyleRanges" => _}) do
48-
"<blockquote>#{text}</blockquote>"
50+
"entityRanges" => entity_ranges,
51+
"inlineStyleRanges" => inline_style_ranges},
52+
entity_map) do
53+
"<blockquote>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</blockquote>"
4954
end
5055

5156
defp process_block(%{"type" => "unstyled",
5257
"text" => text,
5358
"key" => _,
5459
"data" => _,
5560
"depth" => _,
56-
"entityRanges" => _,
57-
"inlineStyleRanges" => _}) do
58-
"<p>#{text}</p>"
61+
"entityRanges" => entity_ranges,
62+
"inlineStyleRanges" => inline_style_ranges},
63+
entity_map) do
64+
"<p>#{Ranges.apply(text, inline_style_ranges, entity_ranges, entity_map)}</p>"
5965
end
6066

6167
defp header_tags do

lib/draft/ranges.ex

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

test/draft_test.exs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,54 @@ defmodule DraftTest do
3737
output = "<br>"
3838
assert Draft.to_html(input) == output
3939
end
40+
41+
test "wraps single inline style" do
42+
input = %{"entityMap"=>%{},"blocks"=>[%{"text"=>"Hello","inlineStyleRanges"=>[%{"style"=>"BOLD","offset"=>2,"length"=>2}],"type"=>"unstyled","depth"=>0,"entityRanges"=>[],"data"=>%{},"key"=>"9d21d"}]}
43+
output = "<p>He<span style=\"font-weight: bold;\">ll</span>o</p>"
44+
assert Draft.to_html(input) == output
45+
end
46+
47+
test "wraps multiple inline styles" do
48+
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"}]}
49+
output = "<p>He<span style=\"font-weight: bold;\">ll</span>o Wo<span style=\"font-style: italic;\">rld</span>!</p>"
50+
assert Draft.to_html(input) == output
51+
end
52+
53+
test "wraps nested inline styles" do
54+
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"}]}
55+
output = "<p>He<span style=\"font-style: italic; font-weight: bold;\">ll</span><span style=\"font-style: italic;\">o W</span>orld!</p>"
56+
assert Draft.to_html(input) == output
57+
end
58+
59+
test "wraps overlapping inline styles" do
60+
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"}]}
61+
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>"
62+
assert Draft.to_html(input) == output
63+
end
64+
65+
test "wraps anchor entities" do
66+
input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}},
67+
"blocks"=>[%{"text"=>"Hello World!","inlineStyleRanges"=>[],"type"=>"unstyled","depth"=>0,"entityRanges"=>[
68+
%{"offset"=>2,"length"=>3,"key"=>0}
69+
],"data"=>%{},"key"=>"9d21d"}]}
70+
output = "<p>He<a href=\"http://google.com\">llo</a> World!</p>"
71+
assert Draft.to_html(input) == output
72+
end
73+
74+
test "wraps overlapping entities and inline styles" do
75+
input = %{"entityMap"=>%{0=>%{"type"=>"LINK","mutability"=>"MUTABLE","data"=>%{"url"=>"http://google.com"}}},
76+
"blocks"=>[%{"text"=>"Hello World!",
77+
"inlineStyleRanges"=>[
78+
%{"style"=>"ITALIC","offset"=>0,"length"=>4},
79+
%{"style"=>"BOLD","offset"=>4,"length"=>4},
80+
],
81+
"entityRanges"=>[
82+
%{"offset"=>2,"length"=>3,"key"=>0}
83+
],
84+
"type"=>"unstyled",
85+
"depth"=>0,
86+
"data"=>%{},"key"=>"9d21d"}]}
87+
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>"
88+
assert Draft.to_html(input) == output
89+
end
4090
end

0 commit comments

Comments
 (0)