draftjs_html 0.7.0 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0e6d6f1d7f1c978841fe029f93f5db47688459ec70f2b33de5ed7c9cfa6d2758
4
- data.tar.gz: 40ee34b8ba65efaf7c3c627046c06bd1e03c32547a17b111cfc661b7bd7a0a7f
3
+ metadata.gz: 4955cd2cbaade2ea9ae659cb65028e4097fb1d8d95d5058bc165e758f40e7794
4
+ data.tar.gz: 5bf0f0fbf4e6e73eb3da8b069fe536505a56db67cfed802c04baa2079a8641ff
5
5
  SHA512:
6
- metadata.gz: 4427355fcb9c1578edc43d67c7fd04ba9c64001dae800996c7124938c0fe2c28d5e4da29e35fda0f2b4659656f829c62dd16b9e482a0e4b6fc780cffe8a1223c
7
- data.tar.gz: 1277ff4825ce7a8bb972c4c0cbca2b3b0f440eceee3e6c00346b78b99d9bc900f8497f4a562b56d2cc8427b545418f578c64358d90618dcb7ce39fff5e196b43
6
+ metadata.gz: a36d06cdc9383a8c27d3d93c08fc59c4a022120aa9951cdf6e9774f0fc77f3e69439ea761a6765469b0f9a4c4879b76b216b465473894b35fbd7e57b5e5ff740
7
+ data.tar.gz: 16c84f495363d08c997b55de7f53b496d8f92760c936a755c1c1960d024bae7e9fb3235bcbcdb32d05649dc2668c16d4833577fce9b9861273a32f7cae33082a
data/README.md CHANGED
@@ -77,6 +77,17 @@ HTML, it's also possible to return `Nokogiri::Node` objects or String objects.
77
77
  Specify the HTML generation encoding.
78
78
  Defaults to `UTF-8`.
79
79
 
80
+ #### `squeeze_newlines`
81
+
82
+ Often times, we'll get text in our blocks that will generate unexpected HTML.
83
+ Most of this is caused by whitespace.
84
+ You can use the `squeeze_newlines` option to collapse consecutive newline/CRLF characters to one, resulting in a single `<br>` tag.
85
+ Defaults to `false`.
86
+
87
+ ```ruby
88
+
89
+ ```
90
+
80
91
  #### `:entity_style_mappings`
81
92
 
82
93
  Allows the author to specify special mapping functions for entities.
@@ -93,12 +104,11 @@ These may be overridden and appended to, like so:
93
104
 
94
105
  ```ruby
95
106
  DraftjsHtml.to_html(raw_draftjs, options: {
96
- block_type_mapping: {
97
- 'unstyled' => 'span',
98
- },
107
+ squeeze_newlines: true,
99
108
  })
100
109
 
101
- # This would generate <span> tags instead of <p> tags for "unstyled" DraftJS blocks.
110
+ # Given a DraftJS block like: `{ text: 'Hi!\n\n\nWelcome to Westeros!\n\n\n'}`
111
+ # This would generate `<p>Hi!<br>Welcome to Westeros!<br></p>`
102
112
  ```
103
113
 
104
114
  #### `:inline_style_mapping`
@@ -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,14 @@
1
1
  require_relative 'node'
2
+ require_relative 'html_depth'
3
+ require_relative 'html_defaults'
4
+ require_relative 'overrideable_map'
2
5
 
3
6
  module DraftjsHtml
4
7
  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
8
  def initialize(options)
55
9
  @options = ensure_options!(options)
56
10
  @document = Nokogiri::HTML::Builder.new(encoding: @options.fetch(:encoding, 'UTF-8'))
11
+ @html_depth = HtmlDepth.new(@document)
57
12
  end
58
13
 
59
14
  def convert(raw_draftjs)
@@ -61,13 +16,14 @@ module DraftjsHtml
61
16
 
62
17
  @document.html do |html|
63
18
  html.body do |body|
64
- @previous_parent = body.parent
19
+ @previous_parents = [body.parent]
65
20
 
66
21
  draftjs.blocks.each do |block|
67
- ensure_nesting_depth(block, body)
22
+ @html_depth.apply(block)
68
23
 
69
24
  body.public_send(block_element_for(block)) do |block_body|
70
25
  block.each_range do |char_range|
26
+ squeeze_newlines(char_range)
71
27
  content = try_apply_entity_to(draftjs, char_range)
72
28
 
73
29
  apply_styles_to(block_body, char_range.style_names, Node.of(content))
@@ -82,15 +38,8 @@ module DraftjsHtml
82
38
 
83
39
  private
84
40
 
85
- def ensure_nesting_depth(block, body)
86
- new_wrapper_tag = BLOCK_TYPE_TO_HTML_WRAPPER[block.type]
87
- if body.parent.name != new_wrapper_tag
88
- if new_wrapper_tag
89
- push_nesting(body, new_wrapper_tag)
90
- else
91
- pop_nesting(body)
92
- end
93
- end
41
+ def squeeze_newlines(char_range)
42
+ char_range.text = @options[:newline_squeezer].call(char_range.text.chomp)
94
43
  end
95
44
 
96
45
  def apply_styles_to(html, style_names, child)
@@ -112,42 +61,33 @@ module DraftjsHtml
112
61
  def block_element_for(block)
113
62
  return 'br' if block.blank?
114
63
 
115
- @options[:block_type_mapping].fetch(block.type)
64
+ @options[:block_type_mapping].value_of!(block.type)
116
65
  end
117
66
 
118
67
  def style_element_for(style)
119
- @options[:inline_style_mapping][style]
68
+ @options[:inline_style_mapping].value_of!(style)
120
69
  end
121
70
 
122
71
  def try_apply_entity_to(draftjs, char_range)
123
72
  entity = draftjs.find_entity(char_range.entity_key)
124
73
  content = char_range.text
125
74
  if entity
126
- style_fn = (@options[:entity_style_mappings][entity.type] || DEFAULT_ENTITY_STYLE_FN)
75
+ style_fn = @options[:entity_style_mappings].value_of(entity.type)
127
76
  content = style_fn.call(entity, Node.of(content), @document.parent)
128
77
  end
129
78
 
130
79
  content
131
80
  end
132
81
 
133
- def push_nesting(builder, tagname)
134
- node = create_child(builder, tagname)
135
- @previous_parent = builder.parent
136
- builder.parent = node
137
- end
138
-
139
- def pop_nesting(builder)
140
- builder.parent = @previous_parent
141
- end
142
-
143
- def create_child(builder, tagname)
144
- builder.parent.add_child(builder.doc.create_element(tagname))
145
- end
146
-
147
82
  def ensure_options!(opts)
148
- opts[:entity_style_mappings] = ENTITY_CONVERSION_MAP.merge(opts[:entity_style_mappings] || {}).transform_keys(&:to_s)
149
- opts[:block_type_mapping] = BLOCK_TYPE_TO_HTML.merge(opts[:block_type_mapping] || {})
150
- opts[:inline_style_mapping] = STYLE_MAP.merge(opts[:inline_style_mapping] || {}).transform_keys(&:to_s)
83
+ opts[:entity_style_mappings] = OverrideableMap.new(HtmlDefaults::ENTITY_CONVERSION_MAP)
84
+ .with_overrides(opts[:entity_style_mappings])
85
+ .with_default(HtmlDefaults::DEFAULT_ENTITY_STYLE_FN)
86
+ opts[:block_type_mapping] = OverrideableMap.new(HtmlDefaults::BLOCK_TYPE_TO_HTML)
87
+ .with_overrides(opts[:block_type_mapping])
88
+ opts[:newline_squeezer] = opts[:squeeze_newlines] ? ->(text) { text.gsub(/(\n|\r\n)+/, "\n") } : ->(text) { text }
89
+ opts[:inline_style_mapping] = OverrideableMap.new(HtmlDefaults::STYLE_MAP)
90
+ .with_overrides(opts[:inline_style_mapping])
151
91
  opts[:inline_style_renderer] ||= ->(*) { nil }
152
92
  opts
153
93
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DraftjsHtml
4
- VERSION = "0.7.0"
4
+ VERSION = "0.9.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.7.0
4
+ version: 0.9.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,7 +65,10 @@ 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
70
73
  - lib/draftjs_html/version.rb
71
74
  homepage: https://github.com/dugancathal/draftjs_html
@@ -75,7 +78,7 @@ metadata:
75
78
  homepage_uri: https://github.com/dugancathal/draftjs_html
76
79
  source_code_uri: https://github.com/dugancathal/draftjs_html
77
80
  changelog_uri: https://github.com/dugancathal/draftjs_html/tree/main/CHANGELOG.md
78
- post_install_message:
81
+ post_install_message:
79
82
  rdoc_options: []
80
83
  require_paths:
81
84
  - lib
@@ -90,8 +93,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
90
93
  - !ruby/object:Gem::Version
91
94
  version: '0'
92
95
  requirements: []
93
- rubygems_version: 3.1.6
94
- signing_key:
96
+ rubygems_version: 3.1.2
97
+ signing_key:
95
98
  specification_version: 4
96
99
  summary: A tool for converting DraftJS JSON to HTML (and back again)
97
100
  test_files: []