lm_docstache 3.0.1 → 3.0.6

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: b43bd6c92fe86cdde66926647f6b98a90c68d9cdf2418d7c3db07494d1872bc1
4
- data.tar.gz: 52b819e713189acc459c8290614a46791e7dbc6e0049ba32e2fb7b204f44bd29
3
+ metadata.gz: 32932b369254e26404e4c1aabbddaef0ce65ed73a06e340f4cbef0c9fc495f5d
4
+ data.tar.gz: 6979b799f3c67d3a0d8b2eeaa247b843a62a6cd92945d49ec3a489e9853fa8df
5
5
  SHA512:
6
- metadata.gz: f96c3e1d1f76400286c984309db190326440aaf6c4bf93aaec2896297fa1fad4a1e44fb0c09f0e4c2aa0c8a912876e5aabdf5eadd7edd85a7345d2d819860377
7
- data.tar.gz: fee26975013fe5e92699a5da1ef317c9bf989269f5a369e3e11ef47aa82a8047d1648563ece41d4fed6186c58a7fe7319ce9ba9b1cfab732fbc7dd77e464c981
6
+ metadata.gz: 90f0aa2257a42051c415dbb930bcd1a9fd2931a2fb07a2842ef6486230dd20bc9f4aac8c507bc8eb21a0c45e6fcdbc9d28d1f5d75dc01596629103c7e5aa6f2e
7
+ data.tar.gz: 9192ff9c6940c40f52256a259a445d83439f18f7bb8b24b05d32f722a893f1e2493dc1f5b9f0a287f390810ce2cdbcf2ec0eb3b3d22366fceea92330a9f26d64
data/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.6
4
+
5
+ #### Bug fixes
6
+
7
+ * Fix bug on `LMDocstache::Docstache#unusable_tags` method, where `nil` could be
8
+ passed to `broken_tags.deleted_at` call.
9
+
10
+ ## 3.0.5
11
+
12
+ #### Bug fixes and improvements
13
+
14
+ * Improve the way broken tags are detected, making the algorithm wider in terms
15
+ detecting broken tags, specially if the broken tag is the opening part of
16
+ conditional tag blocks (which was being detected before these improvements).
17
+ * Improve the way the paragraphs with "unusable" tags are traversed and have
18
+ their same-style texts merged (hence the "unusable" tags becoming usable). So,
19
+ from now, `w:hyperlink` elements, for instance, are properly processed as
20
+ well.
21
+
22
+ ## 3.0.4
23
+ * Allow replacement `data` argument to be an `Array`. This feature allow to replace blocks
24
+ in a sequentially order following the sequence of matching blocks order.
25
+
26
+ ## 3.0.3
27
+
28
+ ### Bugfix
29
+
30
+ * Hide custom tags arguments was pushing blocks tags to end of paragraph. There are cases this approach
31
+ doesn't work. I changed to be an ordered replacement when we match hide tags.
32
+ * Avoid to merge Tab tags on fix errors methods. This was causing unexpected document changes.
33
+
34
+ ## 3.0.2
35
+
36
+ ### Bugfix
37
+
38
+ * Fix replacing tags related to hidden custom tags regexp formats. E.g. tab characters.
39
+
3
40
  ## 3.0.1
4
41
 
5
42
  ### Bugfix
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,50 @@ 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)
58
66
 
59
- usable_tags.each do |usable_tag|
60
- index = unusable_tags.index(usable_tag)
61
- unusable_tags.delete_at(index) if index
62
- end
67
+ usable_tags.reduce(tags) do |broken_tags, usable_tag|
68
+ next broken_tags unless index = broken_tags.index(usable_tag)
63
69
 
64
- unusable_tags
70
+ broken_tags.delete_at(index) && broken_tags
71
+ end.reject do |broken_tag|
72
+ operator = broken_tag.is_a?(Regexp) ? :=~ : :==
73
+ start_tags_index = conditional_start_tags.find_index do |start_tag|
74
+ broken_tag.send(operator, start_tag)
75
+ end
76
+
77
+ conditional_start_tags.delete_at(start_tags_index) if start_tags_index
78
+ !!start_tags_index
79
+ end
65
80
  end
66
81
 
67
82
  def fix_errors
68
- problem_paragraphs.each { |pg| flatten_paragraph(pg) if pg }
83
+ problem_paragraphs.each { |pg| flatten_text_blocks(pg) if pg }
69
84
  end
70
85
 
71
86
  def errors?
@@ -99,6 +114,25 @@ module LMDocstache
99
114
 
100
115
  private
101
116
 
117
+ def text_nodes_containing_only_starting_conditionals
118
+ @documents.values.flat_map do |document|
119
+ document.css('w|t').select do |paragraph|
120
+ paragraph.text =~ WHOLE_BLOCK_START_REGEX
121
+ end
122
+ end
123
+ end
124
+
125
+ def extract_tag_names(text, conditional_tag = false)
126
+ if conditional_tag
127
+ text.scan(Parser::BLOCK_MATCHER).map do |match|
128
+ start_block_tag = "{{#{match[0]}#{match[1]} #{match[2]} #{match[3]}}}"
129
+ /#{Regexp.escape(start_block_tag)}/
130
+ end
131
+ else
132
+ text.scan(Parser::VARIABLE_MATCHER).map { |match| "{{#{match[0]}}}" }
133
+ end
134
+ end
135
+
102
136
  def render_documents(data, text = nil, render_options = {})
103
137
  Hash[
104
138
  @documents.map do |(path, document)|
@@ -115,37 +149,48 @@ module LMDocstache
115
149
  def problem_paragraphs
116
150
  unusable_tags.flat_map do |tag|
117
151
  @documents.values.inject([]) do |tags, document|
118
- faulty_paragraphs = document
119
- .css('w|p')
120
- .select { |paragraph| paragraph.text =~ /#{Regexp.escape(tag)}/ }
152
+ faulty_paragraphs = document.css('w|p').select do |paragraph|
153
+ tag_regex = tag.is_a?(Regexp) ? tag : /#{Regexp.escape(tag)}/
154
+ paragraph.text =~ tag_regex
155
+ end
121
156
 
122
157
  tags + faulty_paragraphs
123
158
  end
124
159
  end
125
160
  end
126
161
 
127
- def flatten_paragraph(paragraph)
128
- return if (run_nodes = paragraph.css('w|r')).size < 2
162
+ def flatten_text_blocks(runs_wrapper)
163
+ return if (children = filtered_children(runs_wrapper)).size < 2
129
164
 
130
- while run_node = run_nodes.pop
131
- next if run_nodes.empty?
165
+ while node = children.pop
166
+ is_run_node = node.matches?(RUN_LIKE_ELEMENTS)
167
+ previous_node = children.last
132
168
 
133
- style_node = run_node.at_css('w|rPr')
169
+ if !is_run_node && filtered_children(node, RUN_LIKE_ELEMENTS).any?
170
+ next flatten_text_blocks(node)
171
+ end
172
+ next if !is_run_node || children.empty? || !previous_node.matches?(RUN_LIKE_ELEMENTS)
173
+ next if node.at_css('w|tab') || previous_node.at_css('w|tab')
174
+
175
+ style_node = node.at_css('w|rPr')
134
176
  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')
177
+ previous_style_node = previous_node.at_css('w|rPr')
137
178
  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')
179
+ previous_text_node = previous_node.at_css('w|t')
180
+ current_text_node = node.at_css('w|t')
140
181
 
141
182
  next if style_html != previous_style_html
142
183
  next if current_text_node.nil? || previous_text_node.nil?
143
184
 
144
- previous_text_node.content = previous_text_node.text + run_node.text
145
- run_node.unlink
185
+ previous_text_node.content = previous_text_node.text + current_text_node.text
186
+ node.unlink
146
187
  end
147
188
  end
148
189
 
190
+ def filtered_children(node, selector = BLOCK_CHILDREN_ELEMENTS)
191
+ Nokogiri::XML::NodeSet.new(node.document, node.children.filter(selector))
192
+ end
193
+
149
194
  def unzip_read(zip, zip_path)
150
195
  file = zip.find_entry(zip_path)
151
196
  contents = ""
@@ -28,17 +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
31
  next unless run_node.at_css('w|t')
33
- remainder_run_node = run_node.clone
34
- run_node.unlink
35
- tag_contents = split_tag_content(remainder_run_node.text, full_pattern)
32
+ next unless run_node.text =~ full_pattern
33
+ tag_contents = split_tag_content(run_node.text, full_pattern)
34
+ replacement_nodes = []
36
35
  tag_contents[:content_list].each_with_index do |content, idx|
36
+ remainder_run_node = run_node.clone
37
37
  replace_content(remainder_run_node, content)
38
- run_node_with_match = remainder_run_node.dup
39
38
  matched_tag = tag_contents[:matched_tags][idx]
40
- nodes_list = [remainder_run_node]
39
+ replacement_nodes << remainder_run_node
41
40
  if matched_tag
41
+ run_node_with_match = run_node.clone
42
42
  replace_style(run_node_with_match)
43
43
  matched_content = matched_tag
44
44
  if value
@@ -47,11 +47,11 @@ module LMDocstache
47
47
  value.to_s
48
48
  end
49
49
  replace_content(run_node_with_match, matched_content)
50
- nodes_list << run_node_with_match
50
+ replacement_nodes << run_node_with_match
51
51
  end
52
- paragraph << Nokogiri::XML::NodeSet.new(document, nodes_list)
53
- remainder_run_node = remainder_run_node.clone
54
52
  end
53
+ run_node.add_next_sibling(Nokogiri::XML::NodeSet.new(document, replacement_nodes))
54
+ run_node.unlink
55
55
  end
56
56
  end
57
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.1"
2
+ VERSION = "3.0.6"
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.1
4
+ version: 3.0.6
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-26 00:00:00.000000000 Z
15
+ date: 2021-06-10 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