lm_docstache 3.0.0 → 3.0.5

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: f59dcaa224f4e8e899df674bc5bd296432ebb24e8e4f929be6b414b8f3a8e0a9
4
- data.tar.gz: b05e837bccad1978807e82bd7d13da786d24fc796213f4f2859f6ebf023522f1
3
+ metadata.gz: cc1a3839b3cfabfd78b144d3f2862aa4a8bf1650bc4037220b2e5af4feaa4a31
4
+ data.tar.gz: d573c864a49f7e2dcc07122fc242664b597a588253408f065ad94cb0f2823f0a
5
5
  SHA512:
6
- metadata.gz: 900ff62d4ada49c3645abfd6fbb64f30b6d7f7e0bf0d619d378c1702890dc25d15685e0c950b152242ca3438d9c4e09658788cc36171d0dacad57c37046eb545
7
- data.tar.gz: abd44d43c73c3012b1512e56c2c0fa985295876217d66d86a83670c73e23054362a29899608a86e90893cc1b82ce5306770a57e45fdbf71f0bec5cacf6518900
6
+ metadata.gz: 7b0fb9ff483de6b2e4315206d1961f3ff847612519ed7ff11203f8ca9e7aabd35fc8a5f8730ff552c171082a0d29d2a1a126f86e89a3a117e3a10ab0a5fb5222
7
+ data.tar.gz: a8c510d591b10ee2d66e6c3ac85995ee9eadb8d91f2178c127e70c12b4184d840cfa725929d4d02b40e62b2e2f9f2c7d629bf65866164833e421a5fdbb5e5c7b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.5
4
+
5
+ #### Bug fixes and improvements
6
+
7
+ * Improve the way broken tags are detected, making the algorithm wider in terms
8
+ detecting broken tags, specially if the broken tag is the opening part of
9
+ conditional tag blocks (which was being detected before these improvements).
10
+ * Improve the way the paragraphs with "unusable" tags are traversed and have
11
+ their same-style texts merged (hence the "unusable" tags becoming usable). So,
12
+ from now, `w:hyperlink` elements, for instance, are properly processed as
13
+ well.
14
+
15
+ ## 3.0.4
16
+ * Allow replacement `data` argument to be an `Array`. This feature allow to replace blocks
17
+ in a sequentially order following the sequence of matching blocks order.
18
+
19
+ ## 3.0.3
20
+
21
+ ### Bugfix
22
+
23
+ * Hide custom tags arguments was pushing blocks tags to end of paragraph. There are cases this approach
24
+ doesn't work. I changed to be an ordered replacement when we match hide tags.
25
+ * Avoid to merge Tab tags on fix errors methods. This was causing unexpected document changes.
26
+
27
+ ## 3.0.2
28
+
29
+ ### Bugfix
30
+
31
+ * Fix replacing tags related to hidden custom tags regexp formats. E.g. tab characters.
32
+
33
+ ## 3.0.1
34
+
35
+ ### Bugfix
36
+
37
+ * Fix Hide Custom Tag feature when document there is no text inside a w|r we
38
+ can't split content.
39
+
3
40
  ## 3.0.0
4
41
 
5
42
  ## Breaking Changes
data/lib/lm_docstache.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  require 'nokogiri'
2
2
  require 'zip'
3
3
  require "lm_docstache/version"
4
+ require "lm_docstache/parser"
4
5
  require "lm_docstache/document"
5
6
  require 'lm_docstache/hide_custom_tags'
6
- require "lm_docstache/parser"
7
7
  require "lm_docstache/condition"
8
8
  require "lm_docstache/conditional_block"
9
9
  require "lm_docstache/renderer"
@@ -1,7 +1,10 @@
1
1
  module LMDocstache
2
2
  class Document
3
- TAGS_REGEXP = /{{.+?}}/
3
+ WHOLE_BLOCK_START_REGEX = /^#{Parser::BLOCK_START_PATTERN}$/
4
+ GENERAL_TAG_REGEX = /\{\{[\/#^]?(.+?)(?:(\s((?:==|~=))\s?.+?))?\}\}/
4
5
  ROLES_REGEXP = /({{(sig|sigfirm|date|check|text|initial)\|(req|noreq)\|(.+?)}})/
6
+ BLOCK_CHILDREN_ELEMENTS = 'w|r,w|hyperlink,w|ins,w|del'
7
+ RUN_LIKE_ELEMENTS = 'w|r,w|ins'
5
8
 
6
9
  def initialize(*paths)
7
10
  raise ArgumentError if paths.empty?
@@ -34,38 +37,48 @@ module LMDocstache
34
37
 
35
38
  def tags
36
39
  @documents.values.flat_map do |document|
37
- document.text.strip.scan(TAGS_REGEXP)
40
+ document_text = document.text
41
+ extract_tag_names(document_text) + extract_tag_names(document_text, true)
38
42
  end
39
43
  end
40
44
 
41
45
  def usable_tags
42
46
  @documents.values.reduce([]) do |tags, document|
43
47
  document.css('w|t').reduce(tags) do |document_tags, text_node|
44
- document_tags.push(*text_node.text.scan(TAGS_REGEXP))
48
+ text = text_node.text
49
+ document_tags.push(*extract_tag_names(text))
50
+ document_tags.push(*extract_tag_names(text, true))
45
51
  end
46
52
  end
47
53
  end
48
54
 
49
55
  def usable_tag_names
50
- usable_tags.reject { |tag| tag =~ ROLES_REGEXP }.map do |tag|
51
- tag.scan(/\{\{[\/#^]?(.+?)(?:(\s((?:==|~=))\s?.+?))?\}\}/)
52
- $1
56
+ usable_tags.reduce([]) do |memo, tag|
57
+ next memo if !tag.is_a?(Regexp) && tag =~ ROLES_REGEXP
58
+
59
+ tag = tag.source if tag.is_a?(Regexp)
60
+ memo << (tag.scan(GENERAL_TAG_REGEX) && $1)
53
61
  end.compact.uniq
54
62
  end
55
63
 
56
64
  def unusable_tags
57
- unusable_tags = tags
65
+ conditional_start_tags = text_nodes_containing_only_starting_conditionals.map(&:text)
66
+
67
+ usable_tags.reduce(tags) do |broken_tags, usable_tag|
68
+ broken_tags.delete_at(broken_tags.index(usable_tag)) && broken_tags
69
+ end.reject do |broken_tag|
70
+ operator = broken_tag.is_a?(Regexp) ? :=~ : :==
71
+ start_tags_index = conditional_start_tags.find_index do |start_tag|
72
+ broken_tag.send(operator, start_tag)
73
+ end
58
74
 
59
- usable_tags.each do |usable_tag|
60
- index = unusable_tags.index(usable_tag)
61
- unusable_tags.delete_at(index) if index
75
+ conditional_start_tags.delete_at(start_tags_index) if start_tags_index
76
+ !!start_tags_index
62
77
  end
63
-
64
- unusable_tags
65
78
  end
66
79
 
67
80
  def fix_errors
68
- problem_paragraphs.each { |pg| flatten_paragraph(pg) if pg }
81
+ problem_paragraphs.each { |pg| flatten_text_blocks(pg) if pg }
69
82
  end
70
83
 
71
84
  def errors?
@@ -99,6 +112,25 @@ module LMDocstache
99
112
 
100
113
  private
101
114
 
115
+ def text_nodes_containing_only_starting_conditionals
116
+ @documents.values.flat_map do |document|
117
+ document.css('w|t').select do |paragraph|
118
+ paragraph.text =~ WHOLE_BLOCK_START_REGEX
119
+ end
120
+ end
121
+ end
122
+
123
+ def extract_tag_names(text, conditional_tag = false)
124
+ if conditional_tag
125
+ text.scan(Parser::BLOCK_MATCHER).map do |match|
126
+ start_block_tag = "{{#{match[0]}#{match[1]} #{match[2]} #{match[3]}}}"
127
+ /#{Regexp.escape(start_block_tag)}/
128
+ end
129
+ else
130
+ text.scan(Parser::VARIABLE_MATCHER).map { |match| "{{#{match[0]}}}" }
131
+ end
132
+ end
133
+
102
134
  def render_documents(data, text = nil, render_options = {})
103
135
  Hash[
104
136
  @documents.map do |(path, document)|
@@ -115,37 +147,48 @@ module LMDocstache
115
147
  def problem_paragraphs
116
148
  unusable_tags.flat_map do |tag|
117
149
  @documents.values.inject([]) do |tags, document|
118
- faulty_paragraphs = document
119
- .css('w|p')
120
- .select { |paragraph| paragraph.text =~ /#{Regexp.escape(tag)}/ }
150
+ faulty_paragraphs = document.css('w|p').select do |paragraph|
151
+ tag_regex = tag.is_a?(Regexp) ? tag : /#{Regexp.escape(tag)}/
152
+ paragraph.text =~ tag_regex
153
+ end
121
154
 
122
155
  tags + faulty_paragraphs
123
156
  end
124
157
  end
125
158
  end
126
159
 
127
- def flatten_paragraph(paragraph)
128
- return if (run_nodes = paragraph.css('w|r')).size < 2
160
+ def flatten_text_blocks(runs_wrapper)
161
+ return if (children = filtered_children(runs_wrapper)).size < 2
129
162
 
130
- while run_node = run_nodes.pop
131
- next if run_nodes.empty?
163
+ while node = children.pop
164
+ is_run_node = node.matches?(RUN_LIKE_ELEMENTS)
165
+ previous_node = children.last
132
166
 
133
- style_node = run_node.at_css('w|rPr')
167
+ if !is_run_node && filtered_children(node, RUN_LIKE_ELEMENTS).any?
168
+ next flatten_text_blocks(node)
169
+ end
170
+ next if !is_run_node || children.empty? || !previous_node.matches?(RUN_LIKE_ELEMENTS)
171
+ next if node.at_css('w|tab') || previous_node.at_css('w|tab')
172
+
173
+ style_node = node.at_css('w|rPr')
134
174
  style_html = style_node ? style_node.inner_html : ''
135
- previous_run_node = run_nodes.last
136
- previous_style_node = previous_run_node.at_css('w|rPr')
175
+ previous_style_node = previous_node.at_css('w|rPr')
137
176
  previous_style_html = previous_style_node ? previous_style_node.inner_html : ''
138
- previous_text_node = previous_run_node.at_css('w|t')
139
- current_text_node = run_node.at_css('w|t')
177
+ previous_text_node = previous_node.at_css('w|t')
178
+ current_text_node = node.at_css('w|t')
140
179
 
141
180
  next if style_html != previous_style_html
142
181
  next if current_text_node.nil? || previous_text_node.nil?
143
182
 
144
- previous_text_node.content = previous_text_node.text + run_node.text
145
- run_node.unlink
183
+ previous_text_node.content = previous_text_node.text + current_text_node.text
184
+ node.unlink
146
185
  end
147
186
  end
148
187
 
188
+ def filtered_children(node, selector = BLOCK_CHILDREN_ELEMENTS)
189
+ Nokogiri::XML::NodeSet.new(node.document, node.children.filter(selector))
190
+ end
191
+
149
192
  def unzip_read(zip, zip_path)
150
193
  file = zip.find_entry(zip_path)
151
194
  contents = ""
@@ -28,16 +28,17 @@ module LMDocstache
28
28
  next unless paragraph.text =~ full_pattern
29
29
  run_nodes = paragraph.css('w|r')
30
30
  while run_node = run_nodes.shift
31
- next if run_node.text.to_s.strip.size == 0
32
- remainder_run_node = run_node.clone
33
- run_node.unlink
34
- tag_contents = split_tag_content(remainder_run_node.text, full_pattern)
31
+ next unless run_node.at_css('w|t')
32
+ next unless run_node.text =~ full_pattern
33
+ tag_contents = split_tag_content(run_node.text, full_pattern)
34
+ replacement_nodes = []
35
35
  tag_contents[:content_list].each_with_index do |content, idx|
36
+ remainder_run_node = run_node.clone
36
37
  replace_content(remainder_run_node, content)
37
- run_node_with_match = remainder_run_node.dup
38
38
  matched_tag = tag_contents[:matched_tags][idx]
39
- nodes_list = [remainder_run_node]
39
+ replacement_nodes << remainder_run_node
40
40
  if matched_tag
41
+ run_node_with_match = run_node.clone
41
42
  replace_style(run_node_with_match)
42
43
  matched_content = matched_tag
43
44
  if value
@@ -46,11 +47,11 @@ module LMDocstache
46
47
  value.to_s
47
48
  end
48
49
  replace_content(run_node_with_match, matched_content)
49
- nodes_list << run_node_with_match
50
+ replacement_nodes << run_node_with_match
50
51
  end
51
- paragraph << Nokogiri::XML::NodeSet.new(document, nodes_list)
52
- remainder_run_node = remainder_run_node.clone
53
52
  end
53
+ run_node.add_next_sibling(Nokogiri::XML::NodeSet.new(document, replacement_nodes))
54
+ run_node.unlink
54
55
  end
55
56
  end
56
57
  end
@@ -18,7 +18,22 @@ module LMDocstache
18
18
  VARIABLE_MATCHER = /{{([^#\^\/].*?)}}/
19
19
 
20
20
  attr_reader :document, :data, :blocks, :special_variable_replacements, :hide_custom_tags
21
+ attr_reader :data_sequential_replacement
21
22
 
23
+ # Constructor +data+ argument is a +Hash+ where the key is
24
+ # expected to be a +String+ representing the replacement block value. +Hash+
25
+ # key must not contain the `{{}}` part, but only the pattern characters.
26
+ # As for the values of the +Hash+, we have options:
27
+ #
28
+ # * +String+ will be the value that will replace matching string.
29
+ # * +Array<String>+ will be an ordered sequence of values that will replace the matched string following
30
+ # document matching order.
31
+ #
32
+ # Example:
33
+ # { 'full_name' => 'John Doe', 'text|req|Client' => ['John', 'Matt', 'Paul'] }
34
+ #
35
+ # Constructor +options+ argument is a +Hash+ where keys can be:
36
+ #
22
37
  # The +special_variable_replacements+ option is a +Hash+ where the key is
23
38
  # expected to be either a +Regexp+ or a +String+ representing the pattern
24
39
  # of more specific type of variables that deserves a special treatment. The
@@ -47,7 +62,8 @@ module LMDocstache
47
62
  # will be the value that will replace the matched string
48
63
  def initialize(document, data, options = {})
49
64
  @document = document
50
- @data = data.transform_keys(&:to_s)
65
+ @data = data.transform_keys(&:to_s).select {|e, v| !v.is_a?(Array) }
66
+ @data_sequential_replacement = data.transform_keys(&:to_s).select {|e, v| v.is_a?(Array) }
51
67
  @special_variable_replacements = add_blocks_to_regexp(options.fetch(:special_variable_replacements, {}))
52
68
  @hide_custom_tags = add_blocks_to_regexp(options.fetch(:hide_custom_tags, {}))
53
69
  end
@@ -65,6 +81,7 @@ module LMDocstache
65
81
  hide_custom_tags!
66
82
  find_blocks
67
83
  replace_conditional_blocks_in_document!
84
+ replace_data_sequentially_in_document!
68
85
  replace_variables_in_document!
69
86
  end
70
87
 
@@ -140,8 +157,35 @@ module LMDocstache
140
157
  end
141
158
  end
142
159
 
160
+ def replace_data_sequentially_in_document!
161
+ data_sequential_replacement.each do |tag_key, values|
162
+
163
+ tag = Regexp.escape("{{#{tag_key}}}")
164
+ pattern_found = 0
165
+
166
+ document.css('w|t').each do |text_node|
167
+ text = text_node.text
168
+
169
+ if text.match(tag)
170
+
171
+ text.gsub!(/#{tag}/) do |_match|
172
+ value = values[pattern_found]
173
+ # if there is no more available value replace the content with empty string
174
+ return '' unless value
175
+
176
+ pattern_found +=1
177
+ value
178
+ end
179
+
180
+ text_node.content = text
181
+ end
182
+ end
183
+ end
184
+ end
185
+
143
186
  def has_skippable_variable?(text)
144
- return true if hide_custom_tags.find { |(pattern, value)| text =~ pattern }
187
+ return true if hide_custom_tags.find { |(pattern, _)| text =~ pattern }
188
+
145
189
  !!special_variable_replacements.find do |(pattern, value)|
146
190
  text =~ pattern && value == false
147
191
  end
@@ -1,3 +1,3 @@
1
1
  module LMDocstache
2
- VERSION = "3.0.0"
2
+ VERSION = "3.0.5"
3
3
  end
@@ -85,5 +85,14 @@ describe LMDocstache::HideCustomTags do
85
85
  expect(total_replacement).to eq(2)
86
86
  end
87
87
  end
88
+
89
+ context 'giving a document with tabs spacing in the middle of replacement tags' do
90
+ let(:input_file) { "#{base_path}/sample-signature-with-tabs-spacing.docx" }
91
+ it 'expect to not replace tabs' do
92
+ hide_custom_tags.hide_custom_tags!
93
+ d = hide_custom_tags.document
94
+ expect(d.css('w|p w|tab').size).to eq(11)
95
+ end
96
+ end
88
97
  end
89
98
  end
@@ -62,7 +62,7 @@ describe 'integration test', integration: true do
62
62
  it 'fixes nested xml errors breaking tags' do
63
63
  expect { document.fix_errors }.to change {
64
64
  document.send(:problem_paragraphs).size
65
- }.from(6).to(1)
65
+ }.from(7).to(1)
66
66
 
67
67
  expect(document.send(:problem_paragraphs).first.text).to eq(
68
68
  '{{TAG123-\\-//WITH WE👻IRD CHARS}}'
@@ -70,7 +70,7 @@ describe 'integration test', integration: true do
70
70
  end
71
71
 
72
72
  it 'has the expected amount of usable tags' do
73
- expect(document.usable_tags.count).to eq(43)
73
+ expect(document.usable_tags.count).to eq(21)
74
74
  end
75
75
 
76
76
  it 'has the expected amount of usable roles tags' do
@@ -79,7 +79,7 @@ describe 'integration test', integration: true do
79
79
  end
80
80
 
81
81
  it 'has the expected amount of unique tag names' do
82
- expect(document.usable_tag_names.count).to eq(19)
82
+ expect(document.usable_tag_names.count).to eq(14)
83
83
  end
84
84
 
85
85
  it 'renders file using data' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lm_docstache
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.0
4
+ version: 3.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roey Chasman
@@ -12,7 +12,7 @@ authors:
12
12
  autorequire:
13
13
  bindir: bin
14
14
  cert_chain: []
15
- date: 2021-03-23 00:00:00.000000000 Z
15
+ date: 2021-06-04 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: nokogiri
@@ -123,6 +123,7 @@ files:
123
123
  - spec/example_input/blank.docx
124
124
  - spec/example_input/docx-no-rpr.docx
125
125
  - spec/example_input/sample-signature-blue.docx
126
+ - spec/example_input/sample-signature-with-tabs-spacing.docx
126
127
  - spec/example_input/sample-signature.docx
127
128
  - spec/hide_custom_tags_spec.rb
128
129
  - spec/integration_spec.rb
@@ -157,6 +158,7 @@ test_files:
157
158
  - spec/example_input/blank.docx
158
159
  - spec/example_input/docx-no-rpr.docx
159
160
  - spec/example_input/sample-signature-blue.docx
161
+ - spec/example_input/sample-signature-with-tabs-spacing.docx
160
162
  - spec/example_input/sample-signature.docx
161
163
  - spec/hide_custom_tags_spec.rb
162
164
  - spec/integration_spec.rb