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 +4 -4
- data/CHANGELOG.md +37 -0
- data/lib/lm_docstache.rb +1 -1
- data/lib/lm_docstache/document.rb +70 -27
- data/lib/lm_docstache/hide_custom_tags.rb +10 -9
- data/lib/lm_docstache/parser.rb +46 -2
- data/lib/lm_docstache/version.rb +1 -1
- data/spec/example_input/ExampleTemplate.docx +0 -0
- data/spec/example_input/sample-signature-with-tabs-spacing.docx +0 -0
- data/spec/hide_custom_tags_spec.rb +9 -0
- data/spec/integration_spec.rb +3 -3
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cc1a3839b3cfabfd78b144d3f2862aa4a8bf1650bc4037220b2e5af4feaa4a31
|
4
|
+
data.tar.gz: d573c864a49f7e2dcc07122fc242664b597a588253408f065ad94cb0f2823f0a
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
-
|
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.
|
51
|
-
tag.
|
52
|
-
|
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
|
-
|
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
|
-
|
60
|
-
|
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|
|
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
|
-
.
|
120
|
-
|
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
|
128
|
-
return if (
|
160
|
+
def flatten_text_blocks(runs_wrapper)
|
161
|
+
return if (children = filtered_children(runs_wrapper)).size < 2
|
129
162
|
|
130
|
-
while
|
131
|
-
|
163
|
+
while node = children.pop
|
164
|
+
is_run_node = node.matches?(RUN_LIKE_ELEMENTS)
|
165
|
+
previous_node = children.last
|
132
166
|
|
133
|
-
|
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
|
-
|
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 =
|
139
|
-
current_text_node =
|
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 +
|
145
|
-
|
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
|
32
|
-
|
33
|
-
run_node.
|
34
|
-
|
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
|
-
|
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
|
-
|
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
|
data/lib/lm_docstache/parser.rb
CHANGED
@@ -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,
|
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
|
data/lib/lm_docstache/version.rb
CHANGED
Binary file
|
Binary file
|
@@ -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
|
data/spec/integration_spec.rb
CHANGED
@@ -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(
|
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(
|
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(
|
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.
|
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-
|
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
|