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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 570a70e02425c105494fa4abd36c9b51e1e667c4022446dbf83fa7c83b1e4d3d
4
- data.tar.gz: b6be5723042425ce04408ec2b996f94b30ab289354829b78eafd3fbde7d17ed1
3
+ metadata.gz: 80c53506fa59d06daa51517049055d61a21740a9cb6f68d7cc04a22fc40d116c
4
+ data.tar.gz: 9e678df882452e6917e88250884619e242ea38a28bd61a33c1bbdfb9c2ffc6f8
5
5
  SHA512:
6
- metadata.gz: 3a1f74891f8942d709dda34cc9d3f2893cba092d09ca797a02c796a75aa314b7966856d5f80564304dd9d45b75e0a0ce649d792e82399af8d8429b6362f3e343
7
- data.tar.gz: 1ee7fe5a0df0073220a29acd055cf64ddfa988755c1ad53dd6f58822cad3ec7ffcff368fa8f89f3c972419b10a8b7a4e4a723466a9e5d37ca9bd7eecfe040e9e
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
@@ -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 i < lines.size - 1
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
@@ -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
- @previous_parent = body.parent
20
+ @previous_parents = [body.parent]
65
21
 
66
22
  draftjs.blocks.each do |block|
67
- ensure_nesting_depth(block, body)
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
- nokogiri.parent.add_child(DraftjsHtml::Node.of(child).to_nokogiri(@document.doc))
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].fetch(block.type)
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][style]
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 = (@options[:entity_style_mappings][entity.type] || DEFAULT_ENTITY_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] = ENTITY_CONVERSION_MAP.merge(opts[:entity_style_mappings] || {}).transform_keys(&:to_s)
154
- opts[:block_type_mapping] = BLOCK_TYPE_TO_HTML.merge(opts[:block_type_mapping] || {})
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] = STYLE_MAP.merge(opts[:inline_style_mapping] || {}).transform_keys(&:to_s)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DraftjsHtml
4
- VERSION = "0.8.0"
4
+ VERSION = "0.10.0"
5
5
  end
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.8.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-07 00:00:00.000000000 Z
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.6
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: []