lm_docstache 2.0.0 → 2.1.2

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: 805d05c9872a3562ac59527ada43842f4f28412d4a273d3010986f1182ee8421
4
- data.tar.gz: 1c0ecd9d43788420553310cefcba6bcca8fe24cd6dc14ff3cbac27f2cfb8ac8f
3
+ metadata.gz: f57ee42bd7804fc41051ca123be801e909acfee0b0406a5eac91e8f562827f11
4
+ data.tar.gz: 21ab27cac4e621eb3d3c391b71945a9f29dac1343d790ccb4026a191dbee62ae
5
5
  SHA512:
6
- metadata.gz: 66881d21495aa30890ebfc392e4292bfd6ef0bccb9b31d147381b606478996707d82cb745d3dd387f98a793b743f54d695be7149282add2b4707438262bc1a0e
7
- data.tar.gz: 0a75364125a98fbd22150cbcde7e3826ab2a8d28705e321e0a471290838867d211ebda8a9181fcf7bf4a1428c6351fa1e93fc0ad33c200f850ae71bed3a0808d
6
+ metadata.gz: 642af591848de049083886277afa1fcd082cf150785d7610ebc15ff328ddc53527bc373e4ab18f910eabffe16b747d8f2191332a968b9e5716a52b4a2e5b2a62
7
+ data.tar.gz: e5d7b66bdb616221ccb789841ce6d84eb7ce76b44c2319ebf522c0f8cd4fb53069fbe93df745724c34ec450884b7b462932624dc2bc10a85216dd1c655bf00d5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.1.2
4
+
5
+ ### Bugfix
6
+
7
+ * Giving a document there is no *rPr* tag we should add new *rPr* tag before
8
+ *t* tag.
9
+
10
+ ## 2.1.1
11
+
12
+ ### Improvements
13
+
14
+ * Hidden custom tags now follow your document background color. If there is no background defined
15
+ we assume white font color.
16
+
17
+ ## 2.1.0
18
+
19
+ #### Improvements
20
+
21
+ * Add new feature which you're able to "hide" matching RegExp rule with white font color.
22
+ This change allows you to keep your content and don't loose locations of hidden tags.
23
+ You can use it just adding `hide_custom_tags` list of RegExp options to `Document#render_file`.
24
+ * Added `Document#render_xml` options parameter so you can call it with more documents
25
+ formatting possibilities.
26
+
27
+ ## 2.0.2
28
+
29
+ ### Improvements and bugfixes
30
+
31
+ * Fix a bug related to a bad empty text nodes handling approach inside
32
+ `Document#flatten_paragraph` logic.
33
+
34
+ ## 2.0.1
35
+
36
+ ### Improvements and bugfixes
37
+
38
+ * Improve the approach on fixing errors around paragraphs that have tags through
39
+ multiple `w:r` elements, by merging only `w:r` that have same styles.
40
+
3
41
  ## 2.0.0
4
42
 
5
43
  ### Breaking changes
data/lib/lm_docstache.rb CHANGED
@@ -2,6 +2,7 @@ require 'nokogiri'
2
2
  require 'zip'
3
3
  require "lm_docstache/version"
4
4
  require "lm_docstache/document"
5
+ require 'lm_docstache/hide_custom_tags'
5
6
  require "lm_docstache/parser"
6
7
  require "lm_docstache/condition"
7
8
  require "lm_docstache/conditional_block"
@@ -1,7 +1,7 @@
1
1
  module LMDocstache
2
2
  class Document
3
3
  TAGS_REGEXP = /{{.+?}}/
4
- ROLES_REGEXP = /(\{\{(sig|sigfirm|date|check|text|initial)\|(req|noreq)\|(.+?)\}\})/
4
+ ROLES_REGEXP = /({{(sig|sigfirm|date|check|text|initial)\|(req|noreq)\|(.+?)}})/
5
5
 
6
6
  def initialize(*paths)
7
7
  raise ArgumentError if paths.empty?
@@ -65,7 +65,7 @@ module LMDocstache
65
65
  end
66
66
 
67
67
  def fix_errors
68
- problem_paragraphs.each { |p| flatten_paragraph(p) if p.present? }
68
+ problem_paragraphs.each { |pg| flatten_paragraph(pg) if pg }
69
69
  end
70
70
 
71
71
  def errors?
@@ -93,8 +93,8 @@ module LMDocstache
93
93
  buffer.sysread
94
94
  end
95
95
 
96
- def render_xml(data = {})
97
- render_documents(data)
96
+ def render_xml(data = {}, render_options = {})
97
+ render_documents(data, nil, render_options)
98
98
  end
99
99
 
100
100
  private
@@ -125,15 +125,23 @@ module LMDocstache
125
125
  end
126
126
 
127
127
  def flatten_paragraph(paragraph)
128
- run_nodes = paragraph.css('w|r')
129
- host_run_node = run_nodes.shift
128
+ return if (run_nodes = paragraph.css('w|r')).size < 2
130
129
 
131
- until host_run_node.at_css('w|t') || run_nodes.size == 0
132
- host_run_node = run_nodes.shift
133
- end
130
+ while run_node = run_nodes.pop
131
+ next if run_nodes.empty?
132
+
133
+ style_node = run_node.at_css('w|rPr')
134
+ 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')
137
+ 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
+ next if style_html != previous_style_html
142
+ next if current_text_node.nil? || previous_text_node.nil?
134
143
 
135
- run_nodes.each do |run_node|
136
- host_run_node.at_css('w|t').content += run_node.text
144
+ previous_text_node.content = previous_text_node.text + run_node.text
137
145
  run_node.unlink
138
146
  end
139
147
  end
@@ -0,0 +1,77 @@
1
+ module LMDocstache
2
+ class HideCustomTags
3
+ attr_reader :document, :hide_custom_tags
4
+
5
+ # The +hide_custom_tags+ options is an +Array+ of +Regexp+ or +String+ representing
6
+ # the pattern you expect to keep at the document but with white font color.
7
+ #
8
+ # You have to remember is not acceptable to have capture groups in your +Regexp's+.
9
+ # We don't accept because we need to find all parts of your text, split it in multiple runs
10
+ # and add document background color or white font color to matching custom tags.
11
+ def initialize(document:, hide_custom_tags: [])
12
+ @document = document
13
+ @hide_custom_tags = hide_custom_tags
14
+ end
15
+
16
+ # Find all run nodes matching hide custom tags +Regexp's+ options you defined, split it
17
+ # in multiple runs and replace font color to document background color or white in the matching tag run node.
18
+ def hide_custom_tags!
19
+ hide_custom_tags.each do |full_pattern|
20
+ paragraphs = document.css('w|p')
21
+ while paragraph = paragraphs.shift do
22
+ next unless paragraph.text =~ full_pattern
23
+ run_nodes = paragraph.css('w|r')
24
+ while run_node = run_nodes.shift
25
+ next if run_node.text.to_s.strip.size == 0
26
+ remainder_run_node = run_node.clone
27
+ run_node.unlink
28
+ tag_contents = split_tag_content(remainder_run_node.text, full_pattern)
29
+ tag_contents[:content_list].each_with_index do |content, idx|
30
+ replace_content(remainder_run_node, content)
31
+ run_node_with_match = remainder_run_node.dup
32
+ matched_tag = tag_contents[:matched_tags][idx]
33
+ nodes_list = [remainder_run_node]
34
+ if matched_tag
35
+ replace_style(run_node_with_match)
36
+ replace_content(run_node_with_match, matched_tag)
37
+ nodes_list << run_node_with_match
38
+ end
39
+ paragraph << Nokogiri::XML::NodeSet.new(document, nodes_list)
40
+ remainder_run_node = remainder_run_node.clone
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ def font_color
50
+ @font_color ||= document.at_css('w|background')&.attr('w:color') || 'FFFFFF'
51
+ end
52
+
53
+ def split_tag_content(text, full_pattern)
54
+ content_list = text.split(full_pattern)
55
+ content_list = content_list.empty? ? [''] : content_list
56
+ matched_tags = text.scan(full_pattern)
57
+ { content_list: content_list, matched_tags: matched_tags}
58
+ end
59
+
60
+ def replace_style(run_node)
61
+ style = run_node.at_css('w|rPr')
62
+ if style
63
+ w_color = style.at_css('w|color')
64
+ w_color.unlink if w_color
65
+ style << "<w:color w:val=\"#{font_color}\"/>"
66
+ else
67
+ run_node.prepend_child("<w:rPr><w:color w:val=\"#{font_color}\"/></w:rPr>")
68
+ end
69
+ end
70
+
71
+ def replace_content(run_node, content)
72
+ run_text = run_node.at_css('w|t')
73
+ run_text['xml:space'] = 'preserve'
74
+ run_text.content = content
75
+ end
76
+ end
77
+ end
@@ -17,7 +17,7 @@ module LMDocstache
17
17
  BLOCK_MATCHER = /#{BLOCK_PATTERN}/
18
18
  VARIABLE_MATCHER = /{{([^#\^\/].*?)}}/
19
19
 
20
- attr_reader :document, :data, :blocks, :special_variable_replacements
20
+ attr_reader :document, :data, :blocks, :special_variable_replacements, :hide_custom_tags
21
21
 
22
22
  # The +special_variable_replacements+ option is a +Hash+ where the key is
23
23
  # expected to be either a +Regexp+ or a +String+ representing the pattern
@@ -34,13 +34,27 @@ module LMDocstache
34
34
  # * any other value that will be turned into a string -> in this case, this
35
35
  # will be the value that will replace the matched string
36
36
  #
37
+ # The +hide_custom_tags+ options is an +Array+ of +Regexp+ or +String+ representing
38
+ # the pattern you expect to keep at the document but with white font color.
39
+ #
40
+ # You have to remember is not acceptable to have capture groups in your +Regexp's+.
41
+ # We don't accept because we need to find all parts of your text, split it in multiple runs
42
+ # and add document background color or white font color to matching custom tags.
37
43
  def initialize(document, data, options = {})
38
44
  @document = document
39
45
  @data = data.transform_keys(&:to_s)
40
46
  @special_variable_replacements = options.fetch(:special_variable_replacements, {})
47
+ @hide_custom_tags = load_hide_custom_tags(options)
48
+ end
49
+
50
+ def load_hide_custom_tags(options)
51
+ options.fetch(:hide_custom_tags, []).map do |regexp_str|
52
+ regexp_str.is_a?(String) ? Regexp.new("{{#{regexp_str}}}") : /{{#{regexp_str.source}}/
53
+ end
41
54
  end
42
55
 
43
56
  def parse_and_update_document!
57
+ hide_custom_tags!
44
58
  find_blocks
45
59
  replace_conditional_blocks_in_document!
46
60
  replace_variables_in_document!
@@ -81,6 +95,11 @@ module LMDocstache
81
95
  @blocks
82
96
  end
83
97
 
98
+ def hide_custom_tags!
99
+ custom_tags = HideCustomTags.new(document: document, hide_custom_tags: hide_custom_tags)
100
+ custom_tags.hide_custom_tags!
101
+ end
102
+
84
103
  # Evaluates all conditional blocks inside the given XML document and keep or
85
104
  # remove their content inside the document, depending on the truthiness of
86
105
  # the condition on each given conditional block.
@@ -115,6 +134,7 @@ module LMDocstache
115
134
  end
116
135
 
117
136
  def has_skippable_variable?(text)
137
+ return true if hide_custom_tags.find { |pattern| text =~ pattern }
118
138
  !!special_variable_replacements.find do |(pattern, value)|
119
139
  pattern = pattern.is_a?(String) ? /{{#{pattern}}}/ : /{{#{pattern.source}}}/
120
140
  text =~ pattern && value == false
@@ -6,7 +6,7 @@ module LMDocstache
6
6
 
7
7
  def initialize(xml, data, options = {})
8
8
  @content = xml
9
- @parser = Parser.new(xml, data, options.slice(:special_variable_replacements))
9
+ @parser = Parser.new(xml, data, options.slice(:special_variable_replacements, :hide_custom_tags))
10
10
  end
11
11
 
12
12
  def render
@@ -1,3 +1,3 @@
1
1
  module LMDocstache
2
- VERSION = "2.0.0"
2
+ VERSION = "2.1.2"
3
3
  end
@@ -24,9 +24,7 @@ describe LMDocstache::Renderer do
24
24
  def render_docx(doc_text)
25
25
  # create doc from blank
26
26
  blank_doc.render_replace(temp_file, doc_text)
27
-
28
- doc = LMDocstache::Document.new(temp_file).render_file(result_file, data)
29
-
27
+ LMDocstache::Document.new(temp_file).render_file(result_file, data)
30
28
  result_doc = LMDocstache::Document.new(result_file).render_xml(data)
31
29
  result_doc["word/document.xml"].text
32
30
  end
Binary file
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ describe LMDocstache::HideCustomTags do
4
+
5
+ context '#example' do
6
+ let(:output_dir) { "#{base_path}/tmp/" }
7
+ let(:output_file) { File.new("#{output_dir}/BlankTestOutput.docx", 'w') }
8
+
9
+ before do
10
+ FileUtils.rm_rf(output_dir) if File.exist?(output_dir)
11
+ Dir.mkdir(output_dir)
12
+ end
13
+
14
+ after do
15
+ File.delete(output_file.path)
16
+ end
17
+
18
+ let(:base_path) { SPEC_BASE_PATH.join('example_input') }
19
+ let(:document) { LMDocstache::Document.new(input_file).instance_variable_get(:@document) }
20
+ let(:regexp_tag) { /{{(?:sig|sigfirm|date|check|text|initial)\|(?:req|noreq)\|.+?}}/ }
21
+ let(:hide_custom_tags) {
22
+ LMDocstache::HideCustomTags.new(document: document, hide_custom_tags: [ regexp_tag ])
23
+ }
24
+
25
+ context "giving a document with blue background" do
26
+ let(:input_file) { "#{base_path}/sample-signature-blue.docx" }
27
+
28
+ it 'expect to have a white color on all hide custom tags matching and have first child node equal rPr tag' do
29
+ hide_custom_tags.hide_custom_tags!
30
+ d = hide_custom_tags.document
31
+ run_nodes = d.css('w|p w|r')
32
+ while run_node = run_nodes.shift
33
+ if run_node.text =~ regexp_tag
34
+ expect(run_node.at_css('w|rPr w|color').first[1]).to eq('4472C4')
35
+ expect(run_node.children.first.name).to eq('rPr')
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ context 'giving a document with white background' do
42
+ let(:input_file) { "#{base_path}/sample-signature.docx" }
43
+
44
+ it 'expect to have a white color on all hide custom tags matching and have first child node equal rPr tag' do
45
+ hide_custom_tags.hide_custom_tags!
46
+ d = hide_custom_tags.document
47
+ run_nodes = d.css('w|p w|r')
48
+ while run_node = run_nodes.shift
49
+ if run_node.text =~ regexp_tag
50
+ expect(run_node.at_css('w|rPr w|color').first[1]).to eq('FFFFFF')
51
+ expect(run_node.children.first.name).to eq('rPr')
52
+ end
53
+ end
54
+ end
55
+ end
56
+ context 'giving a document without rpr' do
57
+ let(:input_file) { "#{base_path}/docx-no-rpr.docx" }
58
+
59
+ it 'expect to have a white color on all hide custom tags matching and have first child node equal rPr tag' do
60
+ hide_custom_tags.hide_custom_tags!
61
+ d = hide_custom_tags.document
62
+ run_nodes = d.css('w|p w|r')
63
+ while run_node = run_nodes.shift
64
+ if run_node.text =~ regexp_tag
65
+ expect(run_node.at_css('w|rPr w|color').first[1]).to eq('FFFFFF')
66
+ expect(run_node.children.first.name).to eq('rPr')
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -33,18 +33,20 @@ module LMDocstache
33
33
  end
34
34
 
35
35
  describe 'integration test', integration: true do
36
- let(:data) { LMDocstache::TestData::DATA }
37
36
  let(:base_path) { SPEC_BASE_PATH.join('example_input') }
38
- let(:input_file) { "#{base_path}/ExampleTemplate.docx" }
39
37
  let(:output_dir) { "#{base_path}/tmp" }
40
- let(:output_file) { "#{output_dir}/IntegrationTestOutput.docx" }
41
- let(:document) { LMDocstache::Document.new(input_file) }
42
- before do
43
- FileUtils.rm_rf(output_dir) if File.exist?(output_dir)
44
- Dir.mkdir(output_dir)
45
- end
46
38
 
47
39
  context 'should process that incoming docx' do
40
+ let(:data) { LMDocstache::TestData::DATA }
41
+ let(:input_file) { "#{base_path}/ExampleTemplate.docx" }
42
+ let(:output_file) { "#{output_dir}/IntegrationTestOutput.docx" }
43
+ let(:document) { LMDocstache::Document.new(input_file) }
44
+
45
+ before do
46
+ FileUtils.rm_rf(output_dir) if File.exist?(output_dir)
47
+ Dir.mkdir(output_dir)
48
+ end
49
+
48
50
  it 'loads the input file' do
49
51
  expect(document).to_not be_nil
50
52
  end
@@ -58,26 +60,84 @@ describe 'integration test', integration: true do
58
60
  end
59
61
 
60
62
  it 'fixes nested xml errors breaking tags' do
61
- expect(document.send(:problem_paragraphs)).to_not be_empty
62
- document.fix_errors
63
- expect(document.send(:problem_paragraphs)).to be_empty
63
+ expect { document.fix_errors }.to change {
64
+ document.send(:problem_paragraphs).size
65
+ }.from(6).to(1)
66
+
67
+ expect(document.send(:problem_paragraphs).first.text).to eq(
68
+ '{{TAG123-\\-//WITH WE👻IRD CHARS}}'
69
+ )
64
70
  end
65
71
 
66
72
  it 'has the expected amount of usable tags' do
67
- expect(document.usable_tags.count).to be(30)
73
+ expect(document.usable_tags.count).to eq(43)
68
74
  end
69
75
 
70
76
  it 'has the expected amount of usable roles tags' do
71
77
  document.fix_errors
72
- expect(document.usable_role_tags.count).to be(6)
78
+ expect(document.usable_role_tags.count).to eq(6)
73
79
  end
74
80
 
75
81
  it 'has the expected amount of unique tag names' do
76
- expect(document.usable_tag_names.count).to be(18)
82
+ expect(document.usable_tag_names.count).to eq(19)
77
83
  end
78
84
 
79
85
  it 'renders file using data' do
80
86
  document.render_file(output_file, data)
81
87
  end
82
88
  end
89
+ context "testing hide custom tags" do
90
+ before do
91
+ FileUtils.rm_rf(output_dir) if File.exist?(output_dir)
92
+ Dir.mkdir(output_dir)
93
+ end
94
+
95
+ let(:render_options) {
96
+ {
97
+ hide_custom_tags: ['(?:sig|sigfirm|date|check|text|initial)\|(?:req|noreq)\|.+?']
98
+ }
99
+ }
100
+ let(:document) { LMDocstache::Document.new(input_file) }
101
+
102
+ context "witth document with blue background" do
103
+ let(:input_file) { "#{base_path}/sample-signature-blue.docx" }
104
+
105
+ it 'should have content replacement aligned with hide custom tags' do
106
+ doc = document
107
+ doc.fix_errors
108
+ noko = doc.render_xml({}, render_options)
109
+ output = noko['word/document.xml'].to_xml
110
+ expect(output).to include('<w:r>
111
+ <w:rPr>
112
+ <w:rFonts w:cstheme="minorHAnsi"/>
113
+ <w:lang w:val="en-US"/>
114
+ <w:color w:val="4472C4"/>
115
+ </w:rPr>
116
+ <w:t xml:space="preserve">{{sig|req|client}}</w:t>
117
+ </w:r>')
118
+ expect(output).to include('<w:t xml:space="preserve">Test Multiple text in the same line </w:t>')
119
+ end
120
+ end
121
+
122
+ context "with document without backgorund" do
123
+ let(:input_file) { "#{base_path}/sample-signature.docx" }
124
+ let(:document) { LMDocstache::Document.new(input_file) }
125
+
126
+ it 'should have content replacement aligned with hide custom tags' do
127
+ doc = document
128
+ doc.fix_errors
129
+ noko = doc.render_xml({}, render_options)
130
+ output = noko['word/document.xml'].to_xml
131
+ expect(output).to include('<w:r>
132
+ <w:rPr>
133
+ <w:rFonts w:cstheme="minorHAnsi"/>
134
+ <w:lang w:val="en-US"/>
135
+ <w:color w:val="FFFFFF"/>
136
+ </w:rPr>
137
+ <w:t xml:space="preserve">{{sig|req|client}}</w:t>
138
+ </w:r>')
139
+ expect(output).to include('<w:t xml:space="preserve">Test Multiple text in the same line </w:t>')
140
+ end
141
+ end
142
+ end
83
143
  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: 2.0.0
4
+ version: 2.1.2
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-02-17 00:00:00.000000000 Z
15
+ date: 2021-03-22 00:00:00.000000000 Z
16
16
  dependencies:
17
17
  - !ruby/object:Gem::Dependency
18
18
  name: nokogiri
@@ -113,6 +113,7 @@ files:
113
113
  - lib/lm_docstache/condition.rb
114
114
  - lib/lm_docstache/conditional_block.rb
115
115
  - lib/lm_docstache/document.rb
116
+ - lib/lm_docstache/hide_custom_tags.rb
116
117
  - lib/lm_docstache/parser.rb
117
118
  - lib/lm_docstache/renderer.rb
118
119
  - lib/lm_docstache/version.rb
@@ -120,6 +121,10 @@ files:
120
121
  - spec/conditional_block_spec.rb
121
122
  - spec/example_input/ExampleTemplate.docx
122
123
  - spec/example_input/blank.docx
124
+ - spec/example_input/docx-no-rpr.docx
125
+ - spec/example_input/sample-signature-blue.docx
126
+ - spec/example_input/sample-signature.docx
127
+ - spec/hide_custom_tags_spec.rb
123
128
  - spec/integration_spec.rb
124
129
  - spec/spec_helper.rb
125
130
  - spec/template_processor_spec.rb
@@ -150,6 +155,10 @@ test_files:
150
155
  - spec/conditional_block_spec.rb
151
156
  - spec/example_input/ExampleTemplate.docx
152
157
  - spec/example_input/blank.docx
158
+ - spec/example_input/docx-no-rpr.docx
159
+ - spec/example_input/sample-signature-blue.docx
160
+ - spec/example_input/sample-signature.docx
161
+ - spec/hide_custom_tags_spec.rb
153
162
  - spec/integration_spec.rb
154
163
  - spec/spec_helper.rb
155
164
  - spec/template_processor_spec.rb