lm_docstache 3.0.3 → 3.0.8

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