Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f1b5e64
Remove json parsing
buob Feb 6, 2019
5b6be92
Add ranges to draft
buob Feb 14, 2019
58a0676
Merge pull request #1 from digitalonboarding/jb/inline-and-entities
buob Feb 15, 2019
43bb62c
`use` to allow custom processors
buob Feb 14, 2019
1b02753
Allow for `context` variable to be provided
buob Feb 14, 2019
b77cf54
Merge pull request #2 from digitalonboarding/jb/use
buob Feb 19, 2019
714312b
Whoops, add context to ranges too
buob Feb 19, 2019
1daf21b
Specify language in README
buob Feb 19, 2019
2726949
Use string keys instead of integers for getting from entityMap
buob Feb 21, 2019
ef5058f
Process `ul`s and `ol`s
buob Feb 22, 2019
a6936a3
Merge pull request #3 from digitalonboarding/jb/lists
crossman Feb 22, 2019
c01bf6d
Fix derp function call
buob Feb 22, 2019
59c994c
Fix broken tests
buob Feb 22, 2019
580495e
Rewrite inline ranges as a tree
buob Mar 1, 2019
c6572fc
Merge pull request #5 from digitalonboarding/jb/tree
buob Mar 4, 2019
31184ee
Format project
crossman Mar 13, 2019
bfd447a
Fix warnings for library users
crossman Mar 13, 2019
a7b7e3d
Merge pull request #6 from digitalonboarding/jc/warnings
crossman Mar 13, 2019
ec45c57
Handle headers four through six in process_block
estermer Jun 20, 2019
0bd7a7e
Merge pull request #7 from estermer/es/handle-h4-to-h6
crossman Jun 21, 2019
f3ac31f
remove unused variable declaration
Jan 23, 2020
c415107
support outlook for ul|ol li
Jan 23, 2020
5c00a3f
Merge pull request #8 from digitalonboarding/mf/fix-lists-outlook
buob Jan 23, 2020
fd83069
Join with newlines instead of nothing
buob Jan 23, 2020
346c877
Add a newline in the span tag?
buob Jan 24, 2020
9660730
Revert "Join with newlines instead of nothing"
Feb 13, 2020
590231d
Revert "Add a newline in the span tag?"
Feb 13, 2020
3ebb1a5
Fix tests for lists so they pass
Feb 13, 2020
9fb50c1
Handle UNDERLINE with process_style
Feb 12, 2020
5cc0fe4
Merge pull request #9 from estermer/es/underline
buob Feb 13, 2020
a8d8f3e
Add ability to control the order in which styles are applied to entities
BruceBC Aug 23, 2022
fa39087
Merge pull request #10 from digitalonboarding/bc/ex-draft-styling-order
BruceBC Aug 23, 2022
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
5 changes: 5 additions & 0 deletions .formatter.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
import_deps: [],
inputs: ["*.{ex,exs}", "{config,lib,test}/**/*.{ex,exs}"],
subdirectories: []
]
93 changes: 92 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,95 @@

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

```elixir
# 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
"<img src='#{url}' />"
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)
```

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


def process_block(%{"type" => "atomic",
"text" => "",
"key" => _,
"data" => %{"type" => "image", "url" => url},
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}, [user: user]) do
"<img src='#{url}' alt='#{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)
```
88 changes: 60 additions & 28 deletions lib/draft.ex
Original file line number Diff line number Diff line change
@@ -1,35 +1,67 @@
defmodule Draft do
@moduledoc """
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
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
iex> draft = ~s({"entityMap":{},"blocks":[{"key":"1","text":"Hello","type":"unstyled","depth":0,"inlineStyleRanges":[],"entityRanges":[],"data":{}}]})
## Examples
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
input
|> blocks
|> Enum.map(&Draft.Block.to_html/1)
|> Enum.join("")
"""

use Draft.Block

def to_html(input, context \\ []) do
entity_map = Map.get(input, "entityMap")

input
|> Map.get("blocks")
|> Enum.reduce([], &group_list_items/2)
|> 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"=>%{}}]}}]
"""

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", "")

# 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]
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
123 changes: 65 additions & 58 deletions lib/draft/block.ex
Original file line number Diff line number Diff line change
@@ -1,68 +1,75 @@
defmodule Draft.Block do
@moduledoc """
Converts a single DraftJS block to html.
"""
defmacro __using__(_) do
quote do
@moduledoc """
Converts a single DraftJS block to html.
"""

@doc """
Renders the given DraftJS input as html.
use Draft.Ranges

## Examples
iex> block = %{"key" => "1", "text" => "Hello", "type" => "unstyled",
...> "depth" => 0, "inlineStyleRanges" => [], "entityRanges" => [],
...> "data" => %{}}
iex> Draft.Block.to_html block
"<p>Hello</p>"
"""
def to_html(block) do
process_block(block)
end
def process_block(%{"type" => "unstyled", "text" => ""}, _, _) do
"<br>"
end

defp process_block(%{"type" => "unstyled",
"text" => "",
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"<br>"
end
def process_block(
%{"type" => "header-" <> header, "text" => text} = block,
entity_map,
context
) do
tag = header_tags()[header]
"<#{tag}>#{apply_ranges(block, entity_map, context)}</#{tag}>"
end

defp process_block(%{"type" => "header-" <> header,
"text" => text,
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
tag = header_tags[header]
"<#{tag}>#{text}</#{tag}>"
end
def process_block(%{"type" => "blockquote", "text" => text} = block, entity_map, context) do
"<blockquote>#{apply_ranges(block, entity_map, context)}</blockquote>"
end

defp process_block(%{"type" => "blockquote",
"text" => text,
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"<blockquote>#{text}</blockquote>"
end
def process_block(%{"type" => "unstyled", "text" => text} = block, entity_map, context) do
"<p>#{apply_ranges(block, entity_map, context)}</p>"
end

defp process_block(%{"type" => "unstyled",
"text" => text,
"key" => _,
"data" => _,
"depth" => _,
"entityRanges" => _,
"inlineStyleRanges" => _}) do
"<p>#{text}</p>"
end
def process_block(
%{"type" => "unordered-list", "data" => %{"children" => children}},
entity_map,
context
) do
"<ul>#{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}</ul>"
end

def process_block(
%{"type" => "ordered-list", "data" => %{"children" => children}},
entity_map,
context
) do
"<ol>#{Enum.map(children, &process_block(&1, entity_map, context)) |> Enum.join("")}</ol>"
end

def process_block(
%{"type" => "ordered-list-item", "text" => text} = block,
entity_map,
context
) do
"<li style=\"mso-special-format:numbullet;\">#{apply_ranges(block, entity_map, context)}</li>"
end

def process_block(
%{"type" => "unordered-list-item", "text" => text} = block,
entity_map,
context
) do
"<li style=\"mso-special-format:bullet;\">#{apply_ranges(block, entity_map, context)}</li>"
end

defp header_tags do
%{
"one" => "h1",
"two" => "h2",
"three" => "h3"
}
def header_tags do
%{
"one" => "h1",
"two" => "h2",
"three" => "h3",
"four" => "h4",
"five" => "h5",
"six" => "h6"
}
end
end
end
end
Loading