draftjs_html 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/draftjs_html/draftjs/block.rb +2 -1
- data/lib/draftjs_html/html_defaults.rb +50 -0
- data/lib/draftjs_html/html_depth.rb +65 -0
- data/lib/draftjs_html/node.rb +1 -1
- data/lib/draftjs_html/overrideable_map.rb +25 -0
- data/lib/draftjs_html/to_html.rb +21 -84
- data/lib/draftjs_html/unicode_rtl_detector.rb +27 -0
- data/lib/draftjs_html/version.rb +1 -1
- metadata +10 -6
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 80c53506fa59d06daa51517049055d61a21740a9cb6f68d7cc04a22fc40d116c
|
4
|
+
data.tar.gz: 9e678df882452e6917e88250884619e242ea38a28bd61a33c1bbdfb9c2ffc6f8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b866a4dc37c5c776c174fafb0cd87f110d386a692e97d39147bbb74669ce0a5ed9b50c8bf250cc6bf58bc7e5db05291d156053131c15e96f2be95c5ca2e31af9
|
7
|
+
data.tar.gz: c53aa1b233e2a4ad68a9ecdc70fe4531565c418cdaa603512e3df8ff986fcbfe0d1f1adb6590b433c285fbddcf7c18370875e17cf0d5a550b56780e0dc85d8c4
|
@@ -2,12 +2,13 @@
|
|
2
2
|
|
3
3
|
module DraftjsHtml
|
4
4
|
module Draftjs
|
5
|
-
Block = Struct.new(:key, :text, :type, :inline_style_ranges, :raw_entity_ranges, keyword_init: true) do
|
5
|
+
Block = Struct.new(:key, :text, :type, :depth, :inline_style_ranges, :raw_entity_ranges, keyword_init: true) do
|
6
6
|
def self.parse(raw)
|
7
7
|
new(
|
8
8
|
key: raw['key'],
|
9
9
|
text: raw['text'],
|
10
10
|
type: raw['type'],
|
11
|
+
depth: raw['depth'],
|
11
12
|
inline_style_ranges: Array(raw['inlineStyleRanges']),
|
12
13
|
raw_entity_ranges: Array(raw['entityRanges']),
|
13
14
|
)
|
@@ -0,0 +1,50 @@
|
|
1
|
+
module DraftjsHtml
|
2
|
+
module HtmlDefaults
|
3
|
+
BLOCK_TYPE_TO_HTML = {
|
4
|
+
'unstyled' => 'p',
|
5
|
+
'paragraph' => 'p',
|
6
|
+
'header-one' => 'h1',
|
7
|
+
'header-two' => 'h2',
|
8
|
+
'header-three' => 'h3',
|
9
|
+
'header-four' => 'h4',
|
10
|
+
'header-five' => 'h5',
|
11
|
+
'header-six' => 'h6',
|
12
|
+
'blockquote' => 'blockquote',
|
13
|
+
'code-block' => 'code',
|
14
|
+
'ordered-list-item' => 'li',
|
15
|
+
'unordered-list-item' => 'li',
|
16
|
+
'atomic' => 'figure',
|
17
|
+
}.freeze
|
18
|
+
|
19
|
+
STYLE_MAP = {
|
20
|
+
'BOLD' => 'b',
|
21
|
+
'ITALIC' => 'i',
|
22
|
+
'STRIKETHROUGH' => 'del',
|
23
|
+
'UNDERLINE' => 'u',
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
ENTITY_ATTRIBUTE_NAME_MAP = {
|
27
|
+
'className' => 'class',
|
28
|
+
'url' => 'href',
|
29
|
+
}.freeze
|
30
|
+
|
31
|
+
DEFAULT_ENTITY_STYLE_FN = ->(_entity, chars, _doc) { chars }
|
32
|
+
|
33
|
+
ENTITY_CONVERSION_MAP = {
|
34
|
+
'LINK' => ->(entity, content, *) {
|
35
|
+
attributes = entity.data.slice('url', 'rel', 'target', 'title', 'className').each_with_object({}) do |(attr, value), h|
|
36
|
+
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
|
37
|
+
end
|
38
|
+
|
39
|
+
DraftjsHtml::Node.new('a', attributes, content)
|
40
|
+
},
|
41
|
+
'IMAGE' => ->(entity, *) {
|
42
|
+
attributes = entity.data.slice('src', 'alt', 'className', 'width', 'height').each_with_object({}) do |(attr, value), h|
|
43
|
+
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
DraftjsHtml::Node.new('img', attributes)
|
47
|
+
}
|
48
|
+
}.freeze
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module DraftjsHtml
|
2
|
+
# This class manages the depth and nesting of the myriad HTML tags generated by DraftjsHtml::ToHtml.
|
3
|
+
# It is intended to be a private implementation detail.
|
4
|
+
class HtmlDepth # :nodoc:
|
5
|
+
BLOCK_TYPE_TO_HTML_WRAPPER = {
|
6
|
+
'code-block' => 'pre',
|
7
|
+
'ordered-list-item' => 'ol',
|
8
|
+
'unordered-list-item' => 'ul',
|
9
|
+
}.freeze
|
10
|
+
|
11
|
+
attr_reader :body
|
12
|
+
|
13
|
+
def initialize(body)
|
14
|
+
@current_depth = 0
|
15
|
+
@body = body
|
16
|
+
@previous_parents = [body.parent]
|
17
|
+
end
|
18
|
+
|
19
|
+
def apply(block)
|
20
|
+
new_wrapper_tag = BLOCK_TYPE_TO_HTML_WRAPPER[block.type]
|
21
|
+
if body.parent.name != new_wrapper_tag || block.depth != @current_depth
|
22
|
+
if @current_depth < block.depth
|
23
|
+
push_depth(body, new_wrapper_tag)
|
24
|
+
elsif @current_depth > block.depth
|
25
|
+
pop_depth(body, times: @current_depth - block.depth)
|
26
|
+
pop_nesting(body) unless new_wrapper_tag
|
27
|
+
elsif new_wrapper_tag
|
28
|
+
push_nesting(body, new_wrapper_tag)
|
29
|
+
elsif @previous_parents.size > 1
|
30
|
+
pop_nesting(body)
|
31
|
+
end
|
32
|
+
@current_depth = block.depth
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def push_depth(builder, tagname)
|
39
|
+
@previous_parents << builder.parent
|
40
|
+
builder.parent = builder.parent.last_element_child
|
41
|
+
push_nesting(builder, tagname)
|
42
|
+
end
|
43
|
+
|
44
|
+
def push_nesting(builder, tagname)
|
45
|
+
node = create_child(builder, tagname)
|
46
|
+
@previous_parents << builder.parent
|
47
|
+
builder.parent = node
|
48
|
+
end
|
49
|
+
|
50
|
+
def pop_depth(builder, times:)
|
51
|
+
times.times do
|
52
|
+
pop_nesting(builder)
|
53
|
+
pop_nesting(builder)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def pop_nesting(builder)
|
58
|
+
builder.parent = @previous_parents.pop
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_child(builder, tagname)
|
62
|
+
builder.parent.add_child(builder.doc.create_element(tagname))
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
data/lib/draftjs_html/node.rb
CHANGED
@@ -18,7 +18,7 @@ module DraftjsHtml
|
|
18
18
|
lines = raw.lines
|
19
19
|
text_nodes = lines.flat_map.with_index do |text, i|
|
20
20
|
nodes = [Nokogiri::XML::Text.new(text.chomp, document)]
|
21
|
-
nodes << Nokogiri::XML::Node.new('br', document) if
|
21
|
+
nodes << Nokogiri::XML::Node.new('br', document) if text.end_with?("\n")
|
22
22
|
nodes
|
23
23
|
end
|
24
24
|
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DraftjsHtml
|
2
|
+
class OverrideableMap
|
3
|
+
def initialize(defaults = {})
|
4
|
+
@map = defaults.dup.transform_keys(&:to_s)
|
5
|
+
end
|
6
|
+
|
7
|
+
def with_overrides(overrides)
|
8
|
+
@map.merge!((overrides || {}).transform_keys(&:to_s))
|
9
|
+
self
|
10
|
+
end
|
11
|
+
|
12
|
+
def with_default(default)
|
13
|
+
@default = default
|
14
|
+
self
|
15
|
+
end
|
16
|
+
|
17
|
+
def value_of!(key)
|
18
|
+
@map.fetch(key.to_s)
|
19
|
+
end
|
20
|
+
|
21
|
+
def value_of(key)
|
22
|
+
@map.fetch(key.to_s, @default)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
data/lib/draftjs_html/to_html.rb
CHANGED
@@ -1,59 +1,15 @@
|
|
1
1
|
require_relative 'node'
|
2
|
+
require_relative 'html_depth'
|
3
|
+
require_relative 'html_defaults'
|
4
|
+
require_relative 'overrideable_map'
|
5
|
+
require_relative 'unicode_rtl_detector'
|
2
6
|
|
3
7
|
module DraftjsHtml
|
4
8
|
class ToHtml
|
5
|
-
BLOCK_TYPE_TO_HTML = {
|
6
|
-
'unstyled' => 'p',
|
7
|
-
'paragraph' => 'p',
|
8
|
-
'header-one' => 'h1',
|
9
|
-
'header-two' => 'h2',
|
10
|
-
'header-three' => 'h3',
|
11
|
-
'header-four' => 'h4',
|
12
|
-
'header-five' => 'h5',
|
13
|
-
'header-six' => 'h6',
|
14
|
-
'blockquote' => 'blockquote',
|
15
|
-
'code-block' => 'code',
|
16
|
-
'ordered-list-item' => 'li',
|
17
|
-
'unordered-list-item' => 'li',
|
18
|
-
'atomic' => 'figure',
|
19
|
-
}.freeze
|
20
|
-
BLOCK_TYPE_TO_HTML_WRAPPER = {
|
21
|
-
'code-block' => 'pre',
|
22
|
-
'ordered-list-item' => 'ol',
|
23
|
-
'unordered-list-item' => 'ul',
|
24
|
-
}.freeze
|
25
|
-
STYLE_MAP = {
|
26
|
-
'BOLD' => 'b',
|
27
|
-
'ITALIC' => 'i',
|
28
|
-
'STRIKETHROUGH' => 'del',
|
29
|
-
'UNDERLINE' => 'u',
|
30
|
-
}.freeze
|
31
|
-
|
32
|
-
DEFAULT_ENTITY_STYLE_FN = ->(_entity, chars, _doc) { chars }
|
33
|
-
ENTITY_ATTRIBUTE_NAME_MAP = {
|
34
|
-
'className' => 'class',
|
35
|
-
'url' => 'href',
|
36
|
-
}.freeze
|
37
|
-
ENTITY_CONVERSION_MAP = {
|
38
|
-
'LINK' => ->(entity, content, *) {
|
39
|
-
attributes = entity.data.slice('url', 'rel', 'target', 'title', 'className').each_with_object({}) do |(attr, value), h|
|
40
|
-
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
|
41
|
-
end
|
42
|
-
|
43
|
-
DraftjsHtml::Node.new('a', attributes, content)
|
44
|
-
},
|
45
|
-
'IMAGE' => ->(entity, *) {
|
46
|
-
attributes = entity.data.slice('src', 'alt', 'className', 'width', 'height').each_with_object({}) do |(attr, value), h|
|
47
|
-
h[ENTITY_ATTRIBUTE_NAME_MAP.fetch(attr, attr)] = value
|
48
|
-
end
|
49
|
-
|
50
|
-
DraftjsHtml::Node.new('img', attributes)
|
51
|
-
}
|
52
|
-
}.freeze
|
53
|
-
|
54
9
|
def initialize(options)
|
55
10
|
@options = ensure_options!(options)
|
56
11
|
@document = Nokogiri::HTML::Builder.new(encoding: @options.fetch(:encoding, 'UTF-8'))
|
12
|
+
@html_depth = HtmlDepth.new(@document)
|
57
13
|
end
|
58
14
|
|
59
15
|
def convert(raw_draftjs)
|
@@ -61,10 +17,10 @@ module DraftjsHtml
|
|
61
17
|
|
62
18
|
@document.html do |html|
|
63
19
|
html.body do |body|
|
64
|
-
@
|
20
|
+
@previous_parents = [body.parent]
|
65
21
|
|
66
22
|
draftjs.blocks.each do |block|
|
67
|
-
|
23
|
+
@html_depth.apply(block)
|
68
24
|
|
69
25
|
body.public_send(block_element_for(block)) do |block_body|
|
70
26
|
block.each_range do |char_range|
|
@@ -84,18 +40,7 @@ module DraftjsHtml
|
|
84
40
|
private
|
85
41
|
|
86
42
|
def squeeze_newlines(char_range)
|
87
|
-
char_range.text = @options[:newline_squeezer].call(char_range.text)
|
88
|
-
end
|
89
|
-
|
90
|
-
def ensure_nesting_depth(block, body)
|
91
|
-
new_wrapper_tag = BLOCK_TYPE_TO_HTML_WRAPPER[block.type]
|
92
|
-
if body.parent.name != new_wrapper_tag
|
93
|
-
if new_wrapper_tag
|
94
|
-
push_nesting(body, new_wrapper_tag)
|
95
|
-
else
|
96
|
-
pop_nesting(body)
|
97
|
-
end
|
98
|
-
end
|
43
|
+
char_range.text = @options[:newline_squeezer].call(char_range.text.chomp)
|
99
44
|
end
|
100
45
|
|
101
46
|
def apply_styles_to(html, style_names, child)
|
@@ -111,49 +56,41 @@ module DraftjsHtml
|
|
111
56
|
end
|
112
57
|
|
113
58
|
def append_child(nokogiri, child)
|
114
|
-
|
59
|
+
new_node = DraftjsHtml::Node.of(child).to_nokogiri(@document.doc)
|
60
|
+
nokogiri.parent['dir'] = 'rtl' if UnicodeRtlDetector.new.contains_rtl?(new_node.inner_text)
|
61
|
+
nokogiri.parent.add_child(new_node)
|
115
62
|
end
|
116
63
|
|
117
64
|
def block_element_for(block)
|
118
65
|
return 'br' if block.blank?
|
119
66
|
|
120
|
-
@options[:block_type_mapping].
|
67
|
+
@options[:block_type_mapping].value_of!(block.type)
|
121
68
|
end
|
122
69
|
|
123
70
|
def style_element_for(style)
|
124
|
-
@options[:inline_style_mapping]
|
71
|
+
@options[:inline_style_mapping].value_of!(style)
|
125
72
|
end
|
126
73
|
|
127
74
|
def try_apply_entity_to(draftjs, char_range)
|
128
75
|
entity = draftjs.find_entity(char_range.entity_key)
|
129
76
|
content = char_range.text
|
130
77
|
if entity
|
131
|
-
style_fn =
|
78
|
+
style_fn = @options[:entity_style_mappings].value_of(entity.type)
|
132
79
|
content = style_fn.call(entity, Node.of(content), @document.parent)
|
133
80
|
end
|
134
81
|
|
135
82
|
content
|
136
83
|
end
|
137
84
|
|
138
|
-
def push_nesting(builder, tagname)
|
139
|
-
node = create_child(builder, tagname)
|
140
|
-
@previous_parent = builder.parent
|
141
|
-
builder.parent = node
|
142
|
-
end
|
143
|
-
|
144
|
-
def pop_nesting(builder)
|
145
|
-
builder.parent = @previous_parent
|
146
|
-
end
|
147
|
-
|
148
|
-
def create_child(builder, tagname)
|
149
|
-
builder.parent.add_child(builder.doc.create_element(tagname))
|
150
|
-
end
|
151
|
-
|
152
85
|
def ensure_options!(opts)
|
153
|
-
opts[:entity_style_mappings] =
|
154
|
-
|
86
|
+
opts[:entity_style_mappings] = OverrideableMap.new(HtmlDefaults::ENTITY_CONVERSION_MAP)
|
87
|
+
.with_overrides(opts[:entity_style_mappings])
|
88
|
+
.with_default(HtmlDefaults::DEFAULT_ENTITY_STYLE_FN)
|
89
|
+
opts[:block_type_mapping] = OverrideableMap.new(HtmlDefaults::BLOCK_TYPE_TO_HTML)
|
90
|
+
.with_overrides(opts[:block_type_mapping])
|
155
91
|
opts[:newline_squeezer] = opts[:squeeze_newlines] ? ->(text) { text.gsub(/(\n|\r\n)+/, "\n") } : ->(text) { text }
|
156
|
-
opts[:inline_style_mapping] =
|
92
|
+
opts[:inline_style_mapping] = OverrideableMap.new(HtmlDefaults::STYLE_MAP)
|
93
|
+
.with_overrides(opts[:inline_style_mapping])
|
157
94
|
opts[:inline_style_renderer] ||= ->(*) { nil }
|
158
95
|
opts
|
159
96
|
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module DraftjsHtml
|
2
|
+
class UnicodeRtlDetector
|
3
|
+
# This regex was copied from fbjs's UnicodeBidi detection
|
4
|
+
# See https://github.com/facebook/fbjs/blob/main/packages/fbjs/src/unicode/UnicodeBidi.js.
|
5
|
+
STRONG_RTL_CHAR_RANGES = [
|
6
|
+
'\u0590\u05BE\u05C0\u05C3\u05C6\u05C8-\u05CF\u05D0-\u05EA\u05EB-\u05EF',
|
7
|
+
'\u05F0-\u05F2\u05F3-\u05F4\u05F5-\u05FF\u07C0-\u07C9\u07CA-\u07EA',
|
8
|
+
'\u07F4-\u07F5\u07FA\u07FB-\u07FF\u0800-\u0815\u081A\u0824\u0828',
|
9
|
+
'\u082E-\u082F\u0830-\u083E\u083F\u0840-\u0858\u085C-\u085D\u085E',
|
10
|
+
'\u085F-\u089F\u200F\uFB1D\uFB1F-\uFB28\uFB2A-\uFB36\uFB37\uFB38-\uFB3C',
|
11
|
+
'\uFB3D\uFB3E\uFB3F\uFB40-\uFB41\uFB42\uFB43-\uFB44\uFB45\uFB46-\uFB4F',
|
12
|
+
'\u0608\u060B\u060D\u061B\u061C\u061D\u061E-\u061F\u0620-\u063F\u0640',
|
13
|
+
'\u0641-\u064A\u066D\u066E-\u066F\u0671-\u06D3\u06D4\u06D5\u06E5-\u06E6',
|
14
|
+
'\u06EE-\u06EF\u06FA-\u06FC\u06FD-\u06FE\u06FF\u0700-\u070D\u070E\u070F',
|
15
|
+
'\u0710\u0712-\u072F\u074B-\u074C\u074D-\u07A5\u07B1\u07B2-\u07BF',
|
16
|
+
'\u08A0-\u08B2\u08B3-\u08E3\uFB50-\uFBB1\uFBB2-\uFBC1\uFBC2-\uFBD2',
|
17
|
+
'\uFBD3-\uFD3D\uFD40-\uFD4F\uFD50-\uFD8F\uFD90-\uFD91\uFD92-\uFDC7',
|
18
|
+
'\uFDC8-\uFDCF\uFDF0-\uFDFB\uFDFC\uFDFE-\uFDFF\uFE70-\uFE74\uFE75',
|
19
|
+
'\uFE76-\uFEFC\uFEFD-\uFEFE',
|
20
|
+
].join.freeze
|
21
|
+
RTL_MATCHER = /[#{STRONG_RTL_CHAR_RANGES}]/
|
22
|
+
|
23
|
+
def contains_rtl?(text)
|
24
|
+
RTL_MATCHER.match?(text)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
data/lib/draftjs_html/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: draftjs_html
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- TJ Taylor
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-10-
|
11
|
+
date: 2022-10-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -65,8 +65,12 @@ files:
|
|
65
65
|
- lib/draftjs_html/draftjs/entity.rb
|
66
66
|
- lib/draftjs_html/draftjs/entity_map.rb
|
67
67
|
- lib/draftjs_html/draftjs/to_raw.rb
|
68
|
+
- lib/draftjs_html/html_defaults.rb
|
69
|
+
- lib/draftjs_html/html_depth.rb
|
68
70
|
- lib/draftjs_html/node.rb
|
71
|
+
- lib/draftjs_html/overrideable_map.rb
|
69
72
|
- lib/draftjs_html/to_html.rb
|
73
|
+
- lib/draftjs_html/unicode_rtl_detector.rb
|
70
74
|
- lib/draftjs_html/version.rb
|
71
75
|
homepage: https://github.com/dugancathal/draftjs_html
|
72
76
|
licenses:
|
@@ -75,7 +79,7 @@ metadata:
|
|
75
79
|
homepage_uri: https://github.com/dugancathal/draftjs_html
|
76
80
|
source_code_uri: https://github.com/dugancathal/draftjs_html
|
77
81
|
changelog_uri: https://github.com/dugancathal/draftjs_html/tree/main/CHANGELOG.md
|
78
|
-
post_install_message:
|
82
|
+
post_install_message:
|
79
83
|
rdoc_options: []
|
80
84
|
require_paths:
|
81
85
|
- lib
|
@@ -90,8 +94,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
94
|
- !ruby/object:Gem::Version
|
91
95
|
version: '0'
|
92
96
|
requirements: []
|
93
|
-
rubygems_version: 3.1.
|
94
|
-
signing_key:
|
97
|
+
rubygems_version: 3.1.2
|
98
|
+
signing_key:
|
95
99
|
specification_version: 4
|
96
100
|
summary: A tool for converting DraftJS JSON to HTML (and back again)
|
97
101
|
test_files: []
|