draftjs_html 0.21.0 → 0.23.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: 662afcc40856fc071f8933862c939e45da447bfcf089287ef486c20eaa9ad9ea
4
- data.tar.gz: e3c86eb5fd81a6daf446d3cd6a2a8394d4db8c2a79a4d7e3dfc9aa0ff1524514
3
+ metadata.gz: c01b8b4b8ca17c90c9d53f7d85caebc7fe6c18aab14a79ff635110d8a0fb81dc
4
+ data.tar.gz: 6e0d85454c9d8ee4e6998e8e398c72c01f6097fc3c5e9b61c165fb07ac01af0f
5
5
  SHA512:
6
- metadata.gz: 91d4da6dd384c6b38fda6edaa8653af2a968b04e53ab3b0bdc4b1305242043f816c25f906508fb6800ab306bbe8967d6608e3a411294152b53c22f375b0cf6dd
7
- data.tar.gz: 8840930b9e1d680477ca458b7b44faff3343dfd1d4129b4e28c931e62c2318dddee8d8c4f8c32bf1f5eaa13e8b542f6704b09ee766c95c9049b9bfa4d37441cf
6
+ metadata.gz: 76cdd86c495a5900653f060f8d4f9702a29b1a7b3ebdb792b00e3f7071642c2fd7547d56aa1e3f6684ab6f873e8c377a09d8fe0c344e2ecf400db106076ebe66
7
+ data.tar.gz: 0c65eac79da80da23d667c62352542dc03717e3db1999fffb35f5a7ac19a1afe871345c1c7449dd29c0570fd780d17c1ea44f3c75b02c034e7cd224eb2c002bb
data/README.md CHANGED
@@ -229,6 +229,15 @@ The callable should return a Hash with symbol keys. The supported values are:
229
229
  - `data` (optional, default `{}`)
230
230
  - an arbitrary data-bag (Hash) of entity data
231
231
 
232
+ #### `:is_semantic_markup:`
233
+
234
+ Defaults to `true`.
235
+
236
+ By setting to `false`, the user is stating they want to treat `div` tags as semantic,
237
+ block-level tags. In some markup (emails, for example), there are no semantic tags
238
+ (read, no `p` tags), so the only indications of whitespace and structure come from
239
+ `div` tags. This flag will flush content wrapped in a `div` as a DraftJS block.
240
+
232
241
  ## Development
233
242
 
234
243
  After checking out the repo, run `bin/setup` to install dependencies. Then, run
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+ require 'set'
3
+
4
+ module DraftjsHtml
5
+ class FromHtml < Nokogiri::XML::SAX::Document
6
+ class CharList
7
+ module FinishableRange
8
+ def range
9
+ start..finish
10
+ end
11
+
12
+ def try_finish(index)
13
+ self[:finish] ||= index
14
+ end
15
+ end
16
+
17
+ Char = Struct.new(:it, :styles, :entity, :atomic, keyword_init: true) do
18
+ def atomic?
19
+ self.atomic
20
+ end
21
+
22
+ def styles
23
+ self[:styles] ||= Set.new
24
+ end
25
+ end
26
+
27
+ EntityRange = Struct.new(:entity, :start, :finish, keyword_init: true) do
28
+ include FinishableRange
29
+ end
30
+
31
+ StyleRange = Struct.new(:style, :start, :finish, keyword_init: true) do
32
+ include FinishableRange
33
+ end
34
+
35
+ attr_reader :chars
36
+
37
+ def initialize(initial = [])
38
+ @chars = initial.dup
39
+ end
40
+
41
+ def append(str, styles: Set.new, entity: nil)
42
+ @chars << Char.new(it: "\n") if @chars.last&.atomic?
43
+
44
+ @chars += str.chars.map { Char.new(it: _1, styles: Set.new(styles), entity: entity) }
45
+ end
46
+
47
+ def append_char(char)
48
+ @chars << char
49
+ end
50
+
51
+ def text
52
+ @chars.map(&:it).join
53
+ end
54
+
55
+ def size
56
+ @chars.size
57
+ end
58
+
59
+ def any?
60
+ size > 0
61
+ end
62
+
63
+ def atomic?
64
+ @chars.any? && @chars.all?(&:atomic?)
65
+ end
66
+
67
+ def each_line
68
+ return to_enum(:each_line) unless block_given?
69
+
70
+ line = self.class.new
71
+ chars.each do |c|
72
+ if c.it == "\n"
73
+ yield line
74
+ line = self.class.new
75
+ next
76
+ end
77
+
78
+ line.append_char(c)
79
+ end
80
+
81
+ yield line if line.any?
82
+ end
83
+
84
+ def apply_entity(range, entity)
85
+ @chars[range].each { _1.entity = entity }
86
+ end
87
+
88
+ def append_atomic_entity(entity)
89
+ append("\n") if @chars.any?
90
+ append_char(Char.new(it: ' ', atomic: true, entity: entity))
91
+ end
92
+
93
+ def append_styles(range, styles)
94
+ @chars[range].each { _1.styles += Array(styles) }
95
+ end
96
+
97
+ def +(other)
98
+ self.class.new(@chars + other.chars)
99
+ end
100
+
101
+ def entity_ranges
102
+ current_entity = nil
103
+
104
+ entity_ranges = @chars.each_with_object(Array.new).with_index do |(char, ranges), i|
105
+ next if char.entity == current_entity
106
+
107
+ current_entity = char.entity
108
+ ranges.last&.try_finish(i - 1)
109
+ ranges << EntityRange.new(entity: char.entity, start: i) if char.entity
110
+ end
111
+
112
+ entity_ranges.last&.try_finish(@chars.size - 1)
113
+ entity_ranges
114
+ end
115
+
116
+ def style_ranges
117
+ style_ranges = @chars.each_with_object({}).with_index do |(char, ranges_by_style), i|
118
+ char.styles.each do |style|
119
+ ranges_by_style[style] ||= []
120
+ if i > 0 && @chars[i-1].styles.include?(style)
121
+ ranges_by_style[style].last << i
122
+ else
123
+ ranges_by_style[style] << [i]
124
+ end
125
+ end
126
+ end
127
+
128
+ style_ranges.flat_map do |style, ranges|
129
+ ranges.map do |range|
130
+ StyleRange.new(style: style, start: range.first, finish: range.last)
131
+ end
132
+ end
133
+ end
134
+ end
135
+ end
136
+ end
@@ -1,15 +1,16 @@
1
1
  module DraftjsHtml
2
2
  class FromHtml < Nokogiri::XML::SAX::Document
3
3
  class DepthStack
4
- def initialize
4
+ def initialize(is_semantic_markup: true)
5
5
  @stack = []
6
6
  @nodes = []
7
7
  @list_depth = -1
8
- @style_stack = StyleStack.new
8
+ @active_styles = []
9
+ @is_semantic_markup = is_semantic_markup
9
10
  end
10
11
 
11
12
  def push(tagname, attrs)
12
- @stack << PendingBlock.from_tag(tagname, attrs, @nodes.dup, @list_depth)
13
+ @stack << PendingBlock.from_tag(tagname, attrs, @nodes.dup, @list_depth, is_semantic_markup: @is_semantic_markup)
13
14
  track_block_node(tagname)
14
15
  end
15
16
 
@@ -26,8 +27,7 @@ module DraftjsHtml
26
27
  @nodes.pop
27
28
  end
28
29
  blocks.reverse_each do |pending_block|
29
- pending_block.flush_to(draftjs, @style_stack)
30
- pending_block.apply_entities_to(draftjs)
30
+ pending_block.flush_to(draftjs)
31
31
  end
32
32
  @list_depth -= 1
33
33
  end
@@ -59,34 +59,35 @@ module DraftjsHtml
59
59
  next unless user_created_entity
60
60
 
61
61
  if content == '' && !user_created_entity[:atomic]
62
- current.text_buffer << ' '
63
- range = range.begin..(range.end+1)
62
+ current.text_buffer.append(' ', entity: user_created_entity)
63
+ elsif content == '' && user_created_entity[:atomic]
64
+ current.text_buffer.append_atomic_entity(user_created_entity)
65
+ else
66
+ current.text_buffer.apply_entity(range, user_created_entity)
64
67
  end
65
- current.entities << user_created_entity.merge(start: range.begin, finish: range.end)
66
68
  end
67
69
  end
68
70
 
69
71
  def style_start(tagname)
70
- @style_stack.track_start(tagname, current_character_offset + 1)
72
+ @active_styles += [DraftjsHtml::HtmlDefaults::HTML_STYLE_TAGS_TO_STYLE[tagname]]
71
73
  end
72
74
 
73
75
  def style_end(tagname)
74
- @style_stack.track_end(tagname, current_character_offset)
76
+ @active_styles.delete_at(@active_styles.index(DraftjsHtml::HtmlDefaults::HTML_STYLE_TAGS_TO_STYLE[tagname]))
75
77
  end
76
78
 
77
79
  def flush_to(draftjs)
78
- current.flush_to(draftjs, @style_stack)
79
- current.apply_entities_to(draftjs)
80
+ current.flush_to(draftjs)
80
81
  end
81
82
 
82
83
  def append_text(chars)
83
- current.text_buffer << chars unless chars.empty?
84
+ current.text_buffer.append(chars, styles: @active_styles) unless chars.empty?
84
85
  end
85
86
 
86
87
  private
87
88
 
88
89
  def current_text_buffer
89
- current.text_buffer.join
90
+ current.text_buffer.text
90
91
  end
91
92
 
92
93
  def current_character_offset
@@ -1,15 +1,16 @@
1
1
  module DraftjsHtml
2
2
  class FromHtml < Nokogiri::XML::SAX::Document
3
- PendingBlock = Struct.new(:tagname, :attrs, :chars, :entities, :pending_entities, :parent_tagnames, :depth, keyword_init: true) do
4
- def self.from_tag(name, attrs, parent_tagnames, depth)
3
+ PendingBlock = Struct.new(:tagname, :attrs, :chars, :entities, :pending_entities, :parent_tagnames, :depth, :is_semantic_markup, keyword_init: true) do
4
+ def self.from_tag(name, attrs, parent_tagnames, depth, is_semantic_markup: true)
5
5
  self.new(
6
6
  tagname: name,
7
7
  attrs: attrs,
8
8
  entities: [],
9
- chars: [],
9
+ chars: CharList.new,
10
10
  pending_entities: [],
11
11
  depth: depth,
12
12
  parent_tagnames: parent_tagnames,
13
+ is_semantic_markup: is_semantic_markup,
13
14
  )
14
15
  end
15
16
 
@@ -17,17 +18,14 @@ module DraftjsHtml
17
18
  self[:chars]
18
19
  end
19
20
 
20
- def clear_text_buffer
21
- self[:chars] = []
22
- end
23
-
24
21
  def character_offset
25
- text_buffer.join.length - 1
22
+ text_buffer.size - 1
26
23
  end
27
24
 
28
25
  def flushable?
29
26
  %w[OPENING ol ul li table].include?(parent_tagnames.last) ||
30
- (parent_tagnames.last == 'div' && tagname != 'div')
27
+ (parent_tagnames.last == 'div' && tagname != 'div') ||
28
+ (!is_semantic_markup && tagname == 'div')
31
29
  end
32
30
 
33
31
  def consume(other_pending_block)
@@ -36,31 +34,21 @@ module DraftjsHtml
36
34
  self.entities += other_pending_block.entities
37
35
  end
38
36
 
39
- def flush_to(draftjs, styles)
37
+ def flush_to(draftjs)
40
38
  if text_buffer.any?
41
- chars.join.lines.each do |line|
42
- draftjs.typed_block(block_name, line.chomp, depth: [depth, 0].max)
43
- end
44
-
45
- styles.each do |descriptor|
46
- finish = descriptor[:finish] || character_offset
47
- draftjs.inline_style(descriptor[:style], descriptor[:start]..finish)
48
- end
49
- end
39
+ text_buffer.each_line do |line|
40
+ block_type = line.atomic? ? 'atomic' : block_name
41
+ draftjs.typed_block(block_type, line.text, depth: [depth, 0].max)
50
42
 
51
- clear_text_buffer
52
- styles.clear_finished
53
- end
43
+ line.entity_ranges.each do |entity_range|
44
+ entity = entity_range.entity
45
+ draftjs.apply_entity entity[:type], entity_range.range, data: entity[:data], mutability: entity.fetch(:mutability, 'IMMUTABLE')
46
+ end
54
47
 
55
- def apply_entities_to(draftjs)
56
- Array(entities).each do |entity|
57
- range = entity[:start]..entity[:finish]
58
- if entity[:atomic]
59
- draftjs.typed_block('atomic', ' ', depth: [depth, 0].max)
60
- range = 0..0
48
+ line.style_ranges.each do |style_range|
49
+ draftjs.inline_style(style_range.style, style_range.range)
50
+ end
61
51
  end
62
-
63
- draftjs.apply_entity entity[:type], range, data: entity[:data], mutability: entity.fetch(:mutability, 'IMMUTABLE')
64
52
  end
65
53
  end
66
54
 
@@ -1,9 +1,9 @@
1
1
  require 'stringio'
2
2
  require_relative 'html_defaults'
3
3
  require_relative 'from_html/elements'
4
- require_relative 'from_html/style_stack'
5
4
  require_relative 'from_html/pending_block'
6
5
  require_relative 'from_html/depth_stack'
6
+ require_relative 'from_html/char_list'
7
7
 
8
8
  module DraftjsHtml
9
9
  class FromHtml < Nokogiri::XML::SAX::Document
@@ -11,7 +11,7 @@ module DraftjsHtml
11
11
  def initialize(options = {})
12
12
  @draftjs = Draftjs::RawBuilder.new
13
13
  @parser = Nokogiri::HTML4::SAX::Parser.new(self)
14
- @depth_stack = DepthStack.new
14
+ @depth_stack = DepthStack.new(is_semantic_markup: options.fetch(:is_semantic_markup, true))
15
15
  @options = ensure_options!(options.dup)
16
16
  end
17
17
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DraftjsHtml
4
- VERSION = "0.21.0"
4
+ VERSION = "0.23.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.21.0
4
+ version: 0.23.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - TJ Taylor
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-11-20 00:00:00.000000000 Z
11
+ date: 2022-12-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -68,10 +68,10 @@ files:
68
68
  - lib/draftjs_html/draftjs/raw_builder.rb
69
69
  - lib/draftjs_html/draftjs/to_raw.rb
70
70
  - lib/draftjs_html/from_html.rb
71
+ - lib/draftjs_html/from_html/char_list.rb
71
72
  - lib/draftjs_html/from_html/depth_stack.rb
72
73
  - lib/draftjs_html/from_html/elements.rb
73
74
  - lib/draftjs_html/from_html/pending_block.rb
74
- - lib/draftjs_html/from_html/style_stack.rb
75
75
  - lib/draftjs_html/html_defaults.rb
76
76
  - lib/draftjs_html/html_depth.rb
77
77
  - lib/draftjs_html/node.rb
@@ -100,7 +100,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
100
  - !ruby/object:Gem::Version
101
101
  version: '0'
102
102
  requirements: []
103
- rubygems_version: 3.1.6
103
+ rubygems_version: 3.3.26
104
104
  signing_key:
105
105
  specification_version: 4
106
106
  summary: A tool for converting DraftJS JSON to HTML (and back again)
@@ -1,50 +0,0 @@
1
- module DraftjsHtml
2
- class FromHtml < Nokogiri::XML::SAX::Document
3
- class StyleStack
4
- def initialize
5
- @stack = []
6
- end
7
-
8
- def clear_finished
9
- @stack.delete_if { !!_1[:finish] }
10
- end
11
-
12
- def each(&block)
13
- @stack.reverse_each.group_by { _1[:tagname] }.each do |_, descriptors|
14
- overlapping_ranges = find_overlapping_styles(descriptors)
15
- widest_descriptor = overlapping_ranges.max_by { (_1[:start].._1[:finish]).size }
16
-
17
- applicable_styles = descriptors - overlapping_ranges + [widest_descriptor].compact
18
- applicable_styles.each(&block)
19
- end
20
- end
21
-
22
- def track_start(tagname, current_character_offset)
23
- style = DraftjsHtml::HtmlDefaults::HTML_STYLE_TAGS_TO_STYLE[tagname]
24
- @stack.unshift({ tagname: tagname, style: style, start: current_character_offset })
25
- end
26
-
27
- def track_end(tagname, current_character_offset)
28
- descriptor_index = @stack.find_index { _1[:tagname] == tagname && !_1[:finish] }
29
- descriptor = @stack[descriptor_index]
30
- descriptor[:finish] = current_character_offset
31
- end
32
-
33
- private
34
-
35
- def find_overlapping_styles(descriptors)
36
- descriptors.select do |candidate_a|
37
- candidate_range = candidate_a[:start]..candidate_a[:finish]
38
- (descriptors - [candidate_a]).any? do |other|
39
- other_range = other[:start]..other[:finish]
40
- range_overlaps?(candidate_range, other_range)
41
- end
42
- end
43
- end
44
-
45
- def range_overlaps?(candidate_range, other_range)
46
- other_range.begin == candidate_range.begin || candidate_range.cover?(other_range.begin) || other_range.cover?(candidate_range.begin)
47
- end
48
- end
49
- end
50
- end