require_relative 'node'
module DraftjsHtml
class ToHtml
BLOCK_TYPE_TO_HTML = {
'unstyled' => 'p',
'paragraph' => 'p',
'header-one' => 'h1',
'header-two' => 'h2',
'header-three' => 'h3',
'header-four' => 'h4',
'header-five' => 'h5',
'header-six' => 'h6',
'blockquote' => 'blockquote',
'code-block' => 'code',
'ordered-list-item' => 'li',
'unordered-list-item' => 'li',
'atomic' => 'figure',
}.freeze
BLOCK_TYPE_TO_HTML_WRAPPER = {
'code-block' => 'pre',
'ordered-list-item' => 'ol',
'unordered-list-item' => 'ul',
}.freeze
STYLE_MAP = {
'BOLD' => 'b',
'ITALIC' => 'i',
'STRIKETHROUGH' => 'del',
'UNDERLINE' => 'u',
}.freeze
DEFAULT_ENTITY_STYLE_FN = ->(_entity, chars, _doc) { chars }
ENTITY_ATTRIBUTE_NAME_MAP = {
'className' => 'class',
'url' => 'href',
}.freeze
ENTITY_CONVERSION_MAP = {
'LINK' => ->(entity, content, *) {
attributes = entity.data.slice('url', 'rel', 'target', 'title', 'className').each_with_object({}) do |(attr, value), h|
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
end
DraftjsHtml::Node.new('a', attributes, content)
},
'IMAGE' => ->(entity, *) {
attributes = entity.data.slice('src', 'alt', 'className', 'width', 'height').each_with_object({}) do |(attr, value), h|
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
end
DraftjsHtml::Node.new('img', attributes)
}
}.freeze
def initialize(options)
@options = ensure_options!(options)
@document = Nokogiri::HTML::Builder.new(encoding: @options.fetch(:encoding, 'UTF-8'))
end
def convert(raw_draftjs)
draftjs = Draftjs.parse(raw_draftjs)
@document.html do |html|
html.body do |body|
@previous_parent = body.parent
draftjs.blocks.each do |block|
ensure_nesting_depth(block, body)
body.public_send(block_element_for(block)) do |block_body|
block.each_range do |char_range|
content = try_apply_entity_to(draftjs, char_range)
apply_styles_to(block_body, char_range.style_names, Node.of(content))
end
end
end
end
end
@document.doc.css('body').first.children.to_html.strip
end
private
def ensure_nesting_depth(block, body)
new_wrapper_tag = BLOCK_TYPE_TO_HTML_WRAPPER[block.type]
if body.parent.name != new_wrapper_tag
if new_wrapper_tag
push_nesting(body, new_wrapper_tag)
else
pop_nesting(body)
end
end
end
def apply_styles_to(html, style_names, child)
return append_child(html, child) if style_names.empty?
custom_render_content = @options[:inline_style_renderer].call(style_names, child, @document.parent)
return append_child(html, custom_render_content) if custom_render_content
style, *rest = style_names
html.public_send(style_element_for(style)) do |builder|
apply_styles_to(builder, rest, child)
end
end
def append_child(nokogiri, child)
nokogiri.parent.add_child(DraftjsHtml::Node.of(child).to_nokogiri(@document.doc))
end
def block_element_for(block)
return 'br' if block.blank?
@options[:block_type_mapping].fetch(block.type)
end
def style_element_for(style)
@options[:inline_style_mapping][style]
end
def try_apply_entity_to(draftjs, char_range)
entity = draftjs.find_entity(char_range.entity_key)
content = char_range.text
if entity
style_fn = (@options[:entity_style_mappings][entity.type] || DEFAULT_ENTITY_STYLE_FN)
content = style_fn.call(entity, Node.of(content), @document.parent)
end
content
end
def push_nesting(builder, tagname)
node = create_child(builder, tagname)
@previous_parent = builder.parent
builder.parent = node
end
def pop_nesting(builder)
builder.parent = @previous_parent
end
def create_child(builder, tagname)
builder.parent.add_child(builder.doc.create_element(tagname))
end
def ensure_options!(opts)
opts[:entity_style_mappings] = ENTITY_CONVERSION_MAP.merge(opts[:entity_style_mappings] || {}).transform_keys(&:to_s)
opts[:block_type_mapping] = BLOCK_TYPE_TO_HTML.merge(opts[:block_type_mapping] || {})
opts[:inline_style_mapping] = STYLE_MAP.merge(opts[:inline_style_mapping] || {}).transform_keys(&:to_s)
opts[:inline_style_renderer] ||= ->(*) { nil }
opts
end
end
end