lm_docstache 3.0.1 → 3.0.6

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: 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