lm_docstache 3.0.3 → 3.0.8

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: 233db6459ecd3c23c9d566659730ce1e4f2fcfb11b0ad4054dd83631375587ec
4
- data.tar.gz: 344689f276e851c835b550f6197cc11dc2c698842b57efc29fc981fbb0026238
3
+ metadata.gz: b414a4508c323651394880630f25af46409778dfc205ebc5621d72e07a6e69a3
4
+ data.tar.gz: 896ce5a0ef359f2b3a77646a631b66ef74a67fb6f5d8fa2ac218a3d346b8f0f8
5
5
  SHA512:
6
- metadata.gz: 342e46c6da0b34131af9cabd468fcae78e1dd272a8e08f4b395abfa2343f51b592d88ada3885c0c7db13d1ef89b66df30f49ff7237c1fba6f61356d5130e3e52
7
- data.tar.gz: 03f3a44bf4a93b5d066cb141fabb9a2d6317940f9705213183ece98cdc2861bb41e7a0d12f399b80d4f6217f92be72bf96ca62a91ddbc790d9ccb57f4bf8d25c
6
+ metadata.gz: e14266f143047c25ee683b6544ddca31648ac83b51a7a72756f3f01dfa1800353f684d3c1a82e878f523a29ef829389f99c7e4f60b0c6d5c46708eccffc5c36c
7
+ data.tar.gz: 0f5ee3910f3a77519fec6ee1db604954c49f3ecc5b0e12af4bcc4d7844b0f10bc9ee410070291381fbd90cce25f974c246a8bca6a107b8d63021b8e38ed3cef9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.0.8
4
+
5
+ #### Bug fixes
6
+
7
+ * Fix a bug on `usable_tags` method, so it now properly and expectedly
8
+ includes conditional tag names that have its opening tag markup as the sole
9
+ content of paragraphs (which represents conditional blocks where both
10
+ opening and closing tags are in separate parapraghs sorrounding one or more
11
+ paragraphs as its conditional block content).
12
+
13
+ ## 3.0.7
14
+
15
+ #### Bug fixes
16
+
17
+ * Fix a bug on `usable_tag_names` method, so it now properly and expectedly
18
+ includes conditional tag names as well, as before.
19
+
20
+ ## 3.0.6
21
+
22
+ #### Bug fixes
23
+
24
+ * Fix bug on `LMDocstache::Docstache#unusable_tags` method, where `nil` could be
25
+ passed to `broken_tags.deleted_at` call.
26
+
27
+ ## 3.0.5
28
+
29
+ #### Bug fixes and improvements
30
+
31
+ * Improve the way broken tags are detected, making the algorithm wider in terms
32
+ detecting broken tags, specially if the broken tag is the opening part of
33
+ conditional tag blocks (which was being detected before these improvements).
34
+ * Improve the way the paragraphs with "unusable" tags are traversed and have
35
+ their same-style texts merged (hence the "unusable" tags becoming usable). So,
36
+ from now, `w:hyperlink` elements, for instance, are properly processed as
37
+ well.
38
+
39
+ ## 3.0.4
40
+ * Allow replacement `data` argument to be an `Array`. This feature allow to replace blocks
41
+ in a sequentially order following the sequence of matching blocks order.
42
+
3
43
  ## 3.0.3
4
44
 
5
45
  ### 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,41 @@ 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, :full_block)
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, :start_block))
51
+ document_tags.push(*extract_tag_names(text, :full_block))
45
52
  end
46
53
  end
47
54
  end
48
55
 
49
56
  def usable_tag_names
50
- usable_tags.reject { |tag| tag =~ ROLES_REGEXP }.map do |tag|
51
- tag.scan(/\{\{[\/#^]?(.+?)(?:(\s((?:==|~=))\s?.+?))?\}\}/)
52
- $1
57
+ usable_tags.reduce([]) do |memo, tag|
58
+ next memo if !tag.is_a?(Regexp) && tag =~ ROLES_REGEXP
59
+
60
+ tag = unescape_escaped_start_block(tag.source) if tag.is_a?(Regexp)
61
+ memo << (tag.scan(GENERAL_TAG_REGEX) && $1)
53
62
  end.compact.uniq
54
63
  end
55
64
 
56
65
  def unusable_tags
57
- unusable_tags = tags
66
+ usable_tags.reduce(tags) do |broken_tags, usable_tag|
67
+ next broken_tags unless index = broken_tags.index(usable_tag)
58
68
 
59
- usable_tags.each do |usable_tag|
60
- index = unusable_tags.index(usable_tag)
61
- unusable_tags.delete_at(index) if index
69
+ broken_tags.delete_at(index) && broken_tags
62
70
  end
63
-
64
- unusable_tags
65
71
  end
66
72
 
67
73
  def fix_errors
68
- problem_paragraphs.each { |pg| flatten_paragraph(pg) if pg }
74
+ problem_paragraphs.each { |pg| flatten_text_blocks(pg) if pg }
69
75
  end
70
76
 
71
77
  def errors?
@@ -99,6 +105,28 @@ module LMDocstache
99
105
 
100
106
  private
101
107
 
108
+ def unescape_escaped_start_block(regex_source_string)
109
+ regex_source_string
110
+ .gsub('\\{', '{')
111
+ .gsub('\\#', '#')
112
+ .gsub('\\}', '}')
113
+ .gsub('\\^', '^')
114
+ .gsub('\\ ', ' ')
115
+ end
116
+
117
+ def extract_tag_names(text, tag_type = :variable)
118
+ text, regex, extractor =
119
+ if tag_type == :variable
120
+ [text, Parser::VARIABLE_MATCHER, ->(match) { "{{%s}}" % match }]
121
+ else
122
+ extractor = ->(match) { /#{Regexp.escape("{{%s%s %s %s}}" % match)}/ }
123
+ tag_type == :full_block ? [text, Parser::BLOCK_MATCHER, extractor] :
124
+ [text.strip, WHOLE_BLOCK_START_REGEX, extractor]
125
+ end
126
+
127
+ text.scan(regex).map(&extractor)
128
+ end
129
+
102
130
  def render_documents(data, text = nil, render_options = {})
103
131
  Hash[
104
132
  @documents.map do |(path, document)|
@@ -115,41 +143,48 @@ module LMDocstache
115
143
  def problem_paragraphs
116
144
  unusable_tags.flat_map do |tag|
117
145
  @documents.values.inject([]) do |tags, document|
118
- faulty_paragraphs = document
119
- .css('w|p')
120
- .select { |paragraph| paragraph.text =~ /#{Regexp.escape(tag)}/ }
146
+ faulty_paragraphs = document.css('w|p').select do |paragraph|
147
+ tag_regex = tag.is_a?(Regexp) ? tag : /#{Regexp.escape(tag)}/
148
+ paragraph.text =~ tag_regex
149
+ end
121
150
 
122
151
  tags + faulty_paragraphs
123
152
  end
124
153
  end
125
154
  end
126
155
 
127
- def flatten_paragraph(paragraph)
128
- return if (run_nodes = paragraph.css('w|r')).size < 2
156
+ def flatten_text_blocks(runs_wrapper)
157
+ return if (children = filtered_children(runs_wrapper)).size < 2
129
158
 
130
- while run_node = run_nodes.pop
131
- next if run_nodes.empty?
159
+ while node = children.pop
160
+ is_run_node = node.matches?(RUN_LIKE_ELEMENTS)
161
+ previous_node = children.last
132
162
 
133
- style_node = run_node.at_css('w|rPr')
163
+ if !is_run_node && filtered_children(node, RUN_LIKE_ELEMENTS).any?
164
+ next flatten_text_blocks(node)
165
+ end
166
+ next if !is_run_node || children.empty? || !previous_node.matches?(RUN_LIKE_ELEMENTS)
167
+ next if node.at_css('w|tab') || previous_node.at_css('w|tab')
168
+
169
+ style_node = node.at_css('w|rPr')
134
170
  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')
171
+ previous_style_node = previous_node.at_css('w|rPr')
137
172
  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')
140
-
141
- # avoid to merge blocks with tabs
142
- next if run_node.at_css('w|tab')
143
- next if previous_run_node.at_css('w|tab')
173
+ previous_text_node = previous_node.at_css('w|t')
174
+ current_text_node = node.at_css('w|t')
144
175
 
145
176
  next if style_html != previous_style_html
146
177
  next if current_text_node.nil? || previous_text_node.nil?
147
178
 
148
- previous_text_node.content = previous_text_node.text + run_node.text
149
- run_node.unlink
179
+ previous_text_node.content = previous_text_node.text + current_text_node.text
180
+ node.unlink
150
181
  end
151
182
  end
152
183
 
184
+ def filtered_children(node, selector = BLOCK_CHILDREN_ELEMENTS)
185
+ Nokogiri::XML::NodeSet.new(node.document, node.children.filter(selector))
186
+ end
187
+
153
188
  def unzip_read(zip, zip_path)
154
189
  file = zip.find_entry(zip_path)
155
190
  contents = ""
@@ -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.3"
2
+ VERSION = "3.0.8"
3
3
  end
@@ -1,5 +1,4 @@
1
1
  require 'spec_helper'
2
- require 'securerandom'
3
2
  require 'active_support/core_ext/object/blank.rb'
4
3
 
5
4
  module LMDocstache
@@ -63,7 +62,7 @@ describe 'integration test', integration: true do
63
62
  it 'fixes nested xml errors breaking tags' do
64
63
  expect { document.fix_errors }.to change {
65
64
  document.send(:problem_paragraphs).size
66
- }.from(6).to(1)
65
+ }.from(7).to(1)
67
66
 
68
67
  expect(document.send(:problem_paragraphs).first.text).to eq(
69
68
  '{{TAG123-\\-//WITH WE👻IRD CHARS}}'
@@ -71,7 +70,9 @@ describe 'integration test', integration: true do
71
70
  end
72
71
 
73
72
  it 'has the expected amount of usable tags' do
74
- expect(document.usable_tags.count).to eq(43)
73
+ expect { document.fix_errors }.to change {
74
+ document.usable_tags.count
75
+ }.from(29).to(34)
75
76
  end
76
77
 
77
78
  it 'has the expected amount of usable roles tags' do
@@ -80,7 +81,7 @@ describe 'integration test', integration: true do
80
81
  end
81
82
 
82
83
  it 'has the expected amount of unique tag names' do
83
- expect(document.usable_tag_names.count).to eq(19)
84
+ expect(document.usable_tag_names.count).to eq(20)
84
85
  end
85
86
 
86
87
  it 'renders file using data' do
@@ -140,30 +141,5 @@ describe 'integration test', integration: true do
140
141
  expect(output).to include('<w:t xml:space="preserve">Test Multiple text in the same line </w:t>')
141
142
  end
142
143
  end
143
-
144
- context "yoooo" do
145
- let(:input_file) { "#{base_path}/multi_o.docx" }
146
- let(:render_options) {
147
- {
148
- special_variable_replacements: { "(date|sig|sigfirm|text|check|initial|initials)\\|(req|noreq)\\|(.+?)" => false }.freeze,
149
- hide_custom_tags: ['(?:sig|sigfirm|date|check|text|initial)\|(?:req|noreq)\|.+?']
150
- }
151
- }
152
- let(:document) { LMDocstache::Document.new(input_file) }
153
-
154
- it 'should have content replacement aligned with hide custom tags' do
155
- doc = document
156
- doc.fix_errors
157
- new_file_path = "#{Time.now.to_i}-#{SecureRandom.uuid}.docx"
158
- n = doc.render_file(new_file_path, { 'full_name' => 'fred document01' }, render_options)
159
- noko = doc.render_xml({ 'full_name' => 'fred document01' }, render_options)
160
- output = noko['word/document.xml'].to_xml
161
- #puts output
162
- #doc.render_file(new_file_path, { 'full_name' => 'fred document01' }, render_options)
163
- #noko = doc.render_xml({ 'full_name' => 'fred document01' }, render_options)
164
- #output = noko['word/document.xml'].to_xml
165
- #puts output
166
- end
167
- end
168
144
  end
169
145
  end
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.3
4
+ version: 3.0.8
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-04-27 00:00:00.000000000 Z
15
+ date: 2021-06-25 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: nokogiri