sheng 0.3.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.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/.gitattributes +1 -0
  3. data/.gitignore +18 -0
  4. data/.rspec +3 -0
  5. data/.ruby-version +1 -0
  6. data/.watchr +99 -0
  7. data/Gemfile +4 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +62 -0
  10. data/Rakefile +10 -0
  11. data/docs/creating_templates.docx +0 -0
  12. data/docs/creating_templates.md +392 -0
  13. data/lib/sheng/block.rb +59 -0
  14. data/lib/sheng/check_box.rb +43 -0
  15. data/lib/sheng/conditional_block.rb +30 -0
  16. data/lib/sheng/data_set.rb +45 -0
  17. data/lib/sheng/docx.rb +75 -0
  18. data/lib/sheng/merge_field.rb +208 -0
  19. data/lib/sheng/merge_field_set.rb +90 -0
  20. data/lib/sheng/path_helpers.rb +15 -0
  21. data/lib/sheng/sequence.rb +66 -0
  22. data/lib/sheng/support.rb +57 -0
  23. data/lib/sheng/version.rb +3 -0
  24. data/lib/sheng/wml_file.rb +47 -0
  25. data/lib/sheng.rb +21 -0
  26. data/sheng.gemspec +31 -0
  27. data/spec/fixtures/bad_docx_files/with_field_not_in_dataset.docx +0 -0
  28. data/spec/fixtures/bad_docx_files/with_missing_sequence_start.docx +0 -0
  29. data/spec/fixtures/bad_docx_files/with_old_mergefields.docx +0 -0
  30. data/spec/fixtures/bad_docx_files/with_poorly_nested_sequences.docx +0 -0
  31. data/spec/fixtures/bad_docx_files/with_unended_sequence.docx +0 -0
  32. data/spec/fixtures/docx_files/input_document.docx +0 -0
  33. data/spec/fixtures/docx_files/old_style/input_document.docx +0 -0
  34. data/spec/fixtures/docx_files/old_style/output_document.docx +0 -0
  35. data/spec/fixtures/docx_files/output_document.docx +0 -0
  36. data/spec/fixtures/inputs/complete.json +61 -0
  37. data/spec/fixtures/inputs/incomplete.json +52 -0
  38. data/spec/fixtures/trees/embedded_sequence.yml +13 -0
  39. data/spec/fixtures/trees/merge_field_set.yml +16 -0
  40. data/spec/fixtures/xml_fragments/input/check_box/check_box.xml +11 -0
  41. data/spec/fixtures/xml_fragments/input/conditional_block/bad/badly_nested_conditional.xml +105 -0
  42. data/spec/fixtures/xml_fragments/input/conditional_block/bad/unclosed_conditional.xml +35 -0
  43. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_if.xml +84 -0
  44. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_inline.xml +59 -0
  45. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_unless.xml +51 -0
  46. data/spec/fixtures/xml_fragments/input/conditional_block/conditional_in_table.xml +176 -0
  47. data/spec/fixtures/xml_fragments/input/conditional_block/embedded_conditional.xml +79 -0
  48. data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml +19 -0
  49. data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml +9 -0
  50. data/spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml +25 -0
  51. data/spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml +26 -0
  52. data/spec/fixtures/xml_fragments/input/merge_field/merge_field.xml +14 -0
  53. data/spec/fixtures/xml_fragments/input/merge_field/new_merge_field.xml +28 -0
  54. data/spec/fixtures/xml_fragments/input/merge_field/split_merge_field.xml +33 -0
  55. data/spec/fixtures/xml_fragments/input/merge_field_set/complex_nesting_and_reuse.xml +231 -0
  56. data/spec/fixtures/xml_fragments/input/merge_field_set/merge_field_set.xml +89 -0
  57. data/spec/fixtures/xml_fragments/input/merge_field_set/with_non_mergefield_fields.xml +43 -0
  58. data/spec/fixtures/xml_fragments/input/sequence/array_sequence.xml +23 -0
  59. data/spec/fixtures/xml_fragments/input/sequence/bad/badly_nested_sequence.xml +62 -0
  60. data/spec/fixtures/xml_fragments/input/sequence/bad/unclosed_sequence.xml +32 -0
  61. data/spec/fixtures/xml_fragments/input/sequence/embedded_sequence.xml +68 -0
  62. data/spec/fixtures/xml_fragments/input/sequence/inline_sequence.xml +24 -0
  63. data/spec/fixtures/xml_fragments/input/sequence/overridden_iterator_array_sequence.xml +23 -0
  64. data/spec/fixtures/xml_fragments/input/sequence/sequence.xml +39 -0
  65. data/spec/fixtures/xml_fragments/input/sequence/sequence_in_table.xml +125 -0
  66. data/spec/fixtures/xml_fragments/input/sequence/sequence_with_section_formatting.xml +41 -0
  67. data/spec/fixtures/xml_fragments/input/sequence/series_with_commas.xml +73 -0
  68. data/spec/fixtures/xml_fragments/output/check_box/check_box.xml +11 -0
  69. data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_does_not_exist.xml +52 -0
  70. data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_exists.xml +84 -0
  71. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_both.xml +11 -0
  72. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_inside.xml +5 -0
  73. data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_outside.xml +8 -0
  74. data/spec/fixtures/xml_fragments/output/conditional_block/if_does_not_exist.xml +5 -0
  75. data/spec/fixtures/xml_fragments/output/conditional_block/if_exists.xml +19 -0
  76. data/spec/fixtures/xml_fragments/output/conditional_block/inline_does_not_exist.xml +10 -0
  77. data/spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml +13 -0
  78. data/spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml +11 -0
  79. data/spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml +5 -0
  80. data/spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml +11 -0
  81. data/spec/fixtures/xml_fragments/output/merge_field/merge_field.xml +12 -0
  82. data/spec/fixtures/xml_fragments/output/merge_field/split_merge_field.xml +10 -0
  83. data/spec/fixtures/xml_fragments/output/merge_field_set/complex_nesting_and_reuse.xml +190 -0
  84. data/spec/fixtures/xml_fragments/output/merge_field_set/merge_field_set.xml +75 -0
  85. data/spec/fixtures/xml_fragments/output/merge_field_set/with_non_mergefield_fields.xml +31 -0
  86. data/spec/fixtures/xml_fragments/output/sequence/array_sequence.xml +17 -0
  87. data/spec/fixtures/xml_fragments/output/sequence/embedded_sequence.xml +56 -0
  88. data/spec/fixtures/xml_fragments/output/sequence/inline_sequence.xml +16 -0
  89. data/spec/fixtures/xml_fragments/output/sequence/overridden_iterator_array_sequence.xml +12 -0
  90. data/spec/fixtures/xml_fragments/output/sequence/sequence.xml +28 -0
  91. data/spec/fixtures/xml_fragments/output/sequence/sequence_in_table.xml +120 -0
  92. data/spec/fixtures/xml_fragments/output/sequence/sequence_with_section_formatting.xml +46 -0
  93. data/spec/fixtures/xml_fragments/output/sequence/series_with_commas.xml +43 -0
  94. data/spec/fixtures/xml_fragments/output/sequence/series_with_commas_two_items.xml +31 -0
  95. data/spec/lib/sheng/check_box_spec.rb +87 -0
  96. data/spec/lib/sheng/conditional_block_spec.rb +151 -0
  97. data/spec/lib/sheng/data_set_spec.rb +65 -0
  98. data/spec/lib/sheng/docx_spec.rb +146 -0
  99. data/spec/lib/sheng/merge_field_set_spec.rb +165 -0
  100. data/spec/lib/sheng/merge_field_spec.rb +276 -0
  101. data/spec/lib/sheng/sequence_spec.rb +201 -0
  102. data/spec/lib/sheng/wml_file_spec.rb +38 -0
  103. data/spec/spec_helper.rb +16 -0
  104. data/spec/support/path_helper.rb +15 -0
  105. data/spec/support/xml_helper.rb +28 -0
  106. metadata +355 -0
@@ -0,0 +1,59 @@
1
+ module Sheng
2
+ class Block < MergeFieldSet
3
+ class MissingEndTag < StandardError; end
4
+ class ImproperNesting < StandardError; end
5
+
6
+ def initialize(merge_field)
7
+ @start_field = merge_field
8
+ @key = merge_field.key
9
+ @xml_document = merge_field.xml_document
10
+ @xml_fragment, @end_field = get_node_set_and_end_field
11
+ end
12
+
13
+ def raw_key
14
+ @start_field.raw_key
15
+ end
16
+
17
+ def extract_mergefields(fragment)
18
+ if fragment.name == "fldSimple"
19
+ return [MergeField.new(fragment)]
20
+ end
21
+ fragment.xpath(".//#{mergefield_element_path}|.//#{new_mergefield_element_path}").map do |field_simple|
22
+ unless field_simple.at('.//w:checkBox')
23
+ MergeField.new(field_simple)
24
+ end
25
+ end.compact
26
+ end
27
+
28
+ def get_node_set_and_end_field
29
+ node_set = Nokogiri::XML::NodeSet.new(@start_field.xml_document)
30
+ next_node, end_field = @start_field, nil
31
+ embedded_starts = [@start_field]
32
+ while !end_field
33
+ next_node = next_node.next_element
34
+
35
+ if next_node.nil?
36
+ raise MissingEndTag, "no end tag for #{@start_field.block_prefix}:#{key}"
37
+ end
38
+
39
+ extract_mergefields(next_node).each do |mergefield|
40
+ if mergefield.is_start?
41
+ embedded_starts.push mergefield
42
+ elsif mergefield.is_end?
43
+ last_start = embedded_starts.pop
44
+ if last_start.key != mergefield.key
45
+ raise ImproperNesting, "expected end tag for #{last_start.block_prefix}:#{last_start.key}, got #{mergefield.block_prefix}:#{mergefield.key}"
46
+ elsif embedded_starts.empty?
47
+ end_field = mergefield
48
+ end
49
+ end
50
+ end
51
+
52
+ unless end_field
53
+ node_set << next_node
54
+ end
55
+ end
56
+ [node_set, end_field]
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,43 @@
1
+ module Sheng
2
+ class CheckBox
3
+ attr_reader :element, :xml_document
4
+
5
+ class << self
6
+ def from_element(element)
7
+ new(element)
8
+ end
9
+ end
10
+
11
+ def initialize(element = nil)
12
+ @element = element
13
+ @xml_document = element.document
14
+ end
15
+
16
+ def ==(other)
17
+ other.is_a?(self.class) && other.element == element
18
+ end
19
+
20
+ def key
21
+ @element.xpath('.//w:name').first['w:val']
22
+ end
23
+
24
+ def raw_key
25
+ key
26
+ end
27
+
28
+ def interpolate(data_set)
29
+ value = data_set.fetch(key)
30
+ checked_attribute = @element.search('.//w:default').first.attribute('val')
31
+ checked_attribute.value = value_is_truthy?(value) ? '1' : '0'
32
+ rescue DataSet::KeyNotFound
33
+ # Ignore this error; if the key for this checkbox is not found in the
34
+ # data set, we don't want to uncheck the checkbox; we just want to leave
35
+ # it alone.
36
+ nil
37
+ end
38
+
39
+ def value_is_truthy?(value)
40
+ ['true', '1', 'yes'].include? value.to_s.downcase
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,30 @@
1
+ require_relative "block"
2
+
3
+ module Sheng
4
+ class ConditionalBlock < Block
5
+ def interpolate(data_set)
6
+ variable = data_set.fetch(key, :default => nil)
7
+ @start_field.remove
8
+ @end_field.remove
9
+ if criterion_met?(variable)
10
+ merge_field_set = MergeFieldSet.new("#{conditional_type}_#{key}", xml_fragment)
11
+ merge_field_set.interpolate(data_set)
12
+ else
13
+ xml_fragment.remove
14
+ end
15
+ end
16
+
17
+ def conditional_type
18
+ @start_field.block_prefix
19
+ end
20
+
21
+ def criterion_met?(variable)
22
+ variable_exists = variable && (variable == true || !variable.empty?)
23
+ if conditional_type == "if"
24
+ variable_exists
25
+ else
26
+ !variable_exists
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,45 @@
1
+ module Sheng
2
+ class DataSet
3
+ class KeyNotFound < StandardError; end
4
+
5
+ attr_accessor :raw_hash
6
+
7
+ def initialize(hsh)
8
+ raise ArgumentError.new("must be initialized with a Hash") unless hsh.is_a?(Hash)
9
+ @raw_hash = hsh.deep_symbolize_keys
10
+ end
11
+
12
+ def raise_key_too_long(key, key_part)
13
+ raise KeyNotFound, "in #{key}, #{key_part} did not return a Hash"
14
+ end
15
+
16
+ def raise_key_too_short(key)
17
+ raise KeyNotFound, "result at #{key} is a Hash"
18
+ end
19
+
20
+ def fetch(key, **options)
21
+ raise ArgumentError.new("must provide a string") unless key.is_a?(String)
22
+ key_parts = key.split(/\./)
23
+ current_result = raw_hash
24
+
25
+ key_parts.each_with_index do |key_part, i|
26
+ begin
27
+ value = current_result.fetch(key_part.to_sym)
28
+ rescue KeyError
29
+ if options.has_key?(:default)
30
+ value = options[:default]
31
+ else
32
+ raise KeyNotFound, "did not find in dataset: #{key} (#{key_part} not found)"
33
+ end
34
+ end
35
+ if (i + 1) < key_parts.length
36
+ raise_key_too_long(key, key_part) if !(value.is_a?(Hash))
37
+ end
38
+ current_result = value
39
+ end
40
+
41
+ raise_key_too_short(key) if current_result.is_a?(Hash)
42
+ current_result
43
+ end
44
+ end
45
+ end
data/lib/sheng/docx.rb ADDED
@@ -0,0 +1,75 @@
1
+ #
2
+ # Sheng::Docx - is a base Mediator which delegates responsibilities
3
+ # to another sheng singleton classes, which replace their part of xml.
4
+ #
5
+ module Sheng
6
+ class Docx
7
+ class InvalidFile < StandardError; end
8
+ class FileAlreadyExists < StandardError; end
9
+
10
+ WMLFileNamePatterns = [
11
+ /word\/document.xml/,
12
+ /word\/numbering.xml/,
13
+ /word\/header(\d)*.xml/,
14
+ /word\/footer(\d)*.xml/
15
+ ]
16
+
17
+ def initialize(input_file_path, params)
18
+ @input_zip_file = Zip::File.new(input_file_path)
19
+ @data_set = DataSet.new(params)
20
+ rescue Zip::Error => e
21
+ raise InvalidFile.new(e.message)
22
+ end
23
+
24
+ def wml_files
25
+ @wml_files ||= @input_zip_file.entries.map do |entry|
26
+ if is_wml_file?(entry.name)
27
+ WMLFile.new(entry.name, entry.get_input_stream)
28
+ end
29
+ end.compact
30
+ end
31
+
32
+ def to_tree
33
+ wml_files.map { |wml| { :file => wml.filename, :tree => wml.to_tree } }
34
+ end
35
+
36
+ def required_hash
37
+ wml_files.inject({}) { |memo, wml| Sheng::Support.merge_required_hashes(memo, wml.required_hash) }
38
+ end
39
+
40
+ def generate(path, force: false)
41
+ if File.exists?(path) && !force
42
+ raise FileAlreadyExists, "File at #{path} already exists"
43
+ end
44
+
45
+ buffer = Zip::OutputStream.write_buffer do |out|
46
+ begin
47
+ @input_zip_file.entries.each do |entry|
48
+ write_converted_zip_file_to_buffer(entry, out)
49
+ end
50
+ ensure
51
+ out.close_buffer
52
+ end
53
+ end
54
+
55
+ File.open(path, "w") { |f| f.write(buffer.string) }
56
+ end
57
+
58
+ private
59
+
60
+ def write_converted_zip_file_to_buffer(entry, buffer)
61
+ contents = entry.get_input_stream.read
62
+ buffer.put_next_entry(entry.name)
63
+ if is_wml_file?(entry.name)
64
+ wml_file = WMLFile.new(entry.name, contents)
65
+ buffer.write wml_file.interpolate(@data_set)
66
+ else
67
+ buffer.write contents
68
+ end
69
+ end
70
+
71
+ def is_wml_file?(file_name)
72
+ WMLFileNamePatterns.any? { |regex| file_name.match(regex) }
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,208 @@
1
+ module Sheng
2
+ class MergeField
3
+ AllowedFilters = [:upcase, :downcase, :capitalize, :titleize, :reverse]
4
+ InstructionTextRegex = /^\s*MERGEFIELD(.*)\\\* MERGEFORMAT\s*$/
5
+ KeyRegex = /^(start:|end:|if:|end_if:|unless:|end_unless:)?\s*([^\|\s]+)\s*\|?(.*)?/
6
+
7
+ class NotAMergeFieldError < StandardError; end
8
+
9
+ class << self
10
+ def from_element(element)
11
+ new(element)
12
+ rescue NotAMergeFieldError => e
13
+ nil
14
+ end
15
+ end
16
+
17
+ attr_reader :element, :xml_document
18
+
19
+ def initialize(element)
20
+ @element = element
21
+ @xml_document = element.document
22
+ @instruction_text = mergefield_instruction_text
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(self.class) && other.element == element
27
+ end
28
+
29
+ def new_style?
30
+ element.name == 'fldChar'
31
+ end
32
+
33
+ def key
34
+ raw_key.gsub(KeyRegex, '\2')
35
+ end
36
+
37
+ def filters
38
+ match = raw_key.match(KeyRegex)
39
+ match.captures[2].split("|").map(&:strip)
40
+ end
41
+
42
+ def raw_key
43
+ @raw_key ||= mergefield_instruction_text.gsub(InstructionTextRegex, '\1').strip
44
+ end
45
+
46
+ def mergefield_instruction_text
47
+ Sheng::Support.extract_mergefield_instruction_text(element)
48
+ end
49
+
50
+ def styling_paragraph
51
+ return nil if inline?
52
+ containing_element.at_xpath(".//w:pPr")
53
+ end
54
+
55
+ def styling_run
56
+ if new_style?
57
+ separator_field = element.ancestors[1].at_xpath(".//w:fldChar[contains(@w:fldCharType, 'separate')]")
58
+ if separator_field
59
+ separator_field.parent.next_element.at_xpath(".//w:rPr")
60
+ end
61
+ else
62
+ element.at_xpath(".//w:rPr")
63
+ end
64
+ end
65
+
66
+ def start_key
67
+ if is_start?
68
+ "#{block_prefix}:#{key}"
69
+ elsif block_prefix == "end"
70
+ "start:#{key}"
71
+ else
72
+ "#{block_prefix.gsub(/^end_/, '')}:#{key}"
73
+ end
74
+ end
75
+
76
+ def block_type
77
+ return nil unless block_prefix
78
+ if ["start", "end"].include?(block_prefix)
79
+ Sequence
80
+ else
81
+ ConditionalBlock
82
+ end
83
+ end
84
+
85
+ def block_prefix
86
+ @potential_prefix ||= begin
87
+ potential_prefix = raw_key.match(KeyRegex).captures[0]
88
+ potential_prefix && potential_prefix.gsub(/\:$/, '')
89
+ end
90
+ end
91
+
92
+ def is_start?
93
+ block_prefix && !block_prefix.match(/^end/)
94
+ end
95
+
96
+ def iteration_variable
97
+ if filters.detect { |f| f =~ /^as\((.*)\)$/ }
98
+ $1.to_sym
99
+ else
100
+ :item
101
+ end
102
+ end
103
+
104
+ def series_with_commas?
105
+ filters.detect { |f| f =~ /^series_with_commas/ }
106
+ end
107
+
108
+ def comma_series_conjunction
109
+ if filters.detect { |f| f =~ /^series_with_commas\((.*)\)$/ }
110
+ $1
111
+ else
112
+ "and"
113
+ end
114
+ end
115
+
116
+ def is_end?
117
+ block_prefix && block_prefix.match(/^end/)
118
+ end
119
+
120
+ def remove
121
+ if inline?
122
+ xml.remove
123
+ elsif is_table_row_marker?
124
+ containing_element.ancestors[1].remove
125
+ else
126
+ containing_element.remove
127
+ end
128
+ end
129
+
130
+ def containing_element
131
+ parents_until_container = new_style? ? 2 : 1
132
+ element.ancestors[parents_until_container - 1]
133
+ end
134
+
135
+ def is_table_row_marker?
136
+ in_table_row? && (is_start? || is_end?)
137
+ end
138
+
139
+ def in_table_row?
140
+ containing_element.ancestors[1] && containing_element.ancestors[1].name == "tr"
141
+ end
142
+
143
+ def inline?
144
+ containing_element.children.text != xml.text
145
+ end
146
+
147
+ def add_previous_sibling(fragment_to_add)
148
+ if inline?
149
+ [xml].flatten.first.add_previous_sibling(fragment_to_add)
150
+ elsif is_table_row_marker?
151
+ containing_element.ancestors[1].add_previous_sibling(fragment_to_add)
152
+ else
153
+ containing_element.add_previous_sibling(fragment_to_add)
154
+ end
155
+ end
156
+
157
+ def next_element
158
+ if inline?
159
+ [xml].flatten.last.next_element
160
+ elsif is_table_row_marker?
161
+ containing_element.ancestors[1].next_element
162
+ else
163
+ containing_element.next_element
164
+ end
165
+ end
166
+
167
+ def xml
168
+ return element unless new_style?
169
+ nodeset = Nokogiri::XML::NodeSet.new(xml_document)
170
+ current_node = element.parent
171
+ nodeset << current_node
172
+ loop do
173
+ current_node = current_node.next_element
174
+ nodeset << current_node
175
+ break if current_node.at_xpath("./w:fldChar[contains(@w:fldCharType, 'end')]")
176
+ end
177
+ nodeset
178
+ end
179
+
180
+ def replace_mergefield(value)
181
+ new_run = Sheng::Support.new_text_run(
182
+ value, xml_document: xml_document, style_run: styling_run
183
+ )
184
+ xml.before(new_run)
185
+ xml.remove
186
+ end
187
+
188
+ def interpolate(data_set)
189
+ value = data_set.fetch(key)
190
+ replace_mergefield(filter_value(value))
191
+ rescue DataSet::KeyNotFound
192
+ # Ignore this error; we'll collect all uninterpolated fields later and
193
+ # raise a new exception, so we can list all the fields in an error
194
+ # message.
195
+ nil
196
+ end
197
+
198
+ def filter_value(value)
199
+ filters.inject(value) { |val, filter|
200
+ if AllowedFilters.include?(filter.to_sym) && val.respond_to?(filter.to_sym)
201
+ val.send(filter)
202
+ else
203
+ val
204
+ end
205
+ }
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,90 @@
1
+ require_relative "path_helpers"
2
+
3
+ module Sheng
4
+ class MergeFieldSet
5
+ include PathHelpers
6
+
7
+ attr_reader :xml_fragment, :xml_document, :key
8
+
9
+ def initialize(key, xml_fragment)
10
+ @key = key
11
+ @xml_fragment = xml_fragment
12
+ @xml_document = xml_fragment.document
13
+ end
14
+
15
+ def interpolate(data_set)
16
+ nodes.each do |node|
17
+ node.interpolate(data_set)
18
+ end
19
+ end
20
+
21
+ def to_tree
22
+ nodes.map do |node|
23
+ hsh = {
24
+ :type => node.class.name.underscore.gsub(/^sheng\//, ''),
25
+ :key => node.raw_key
26
+ }
27
+ if node.is_a? MergeFieldSet
28
+ hsh[:nodes] = node.to_tree
29
+ end
30
+ hsh
31
+ end
32
+ end
33
+
34
+ def required_hash(placeholder = nil)
35
+ return nil if is_a?(Sequence) && array_of_primitives_expected?
36
+ nodes.inject({}) do |node_list, node|
37
+ hsh = if node.is_a?(ConditionalBlock)
38
+ node.required_hash(placeholder)
39
+ else
40
+ value = node.is_a?(Block) ? [node.required_hash(placeholder)].compact : placeholder
41
+ key_parts = node.key.split(/\./)
42
+ last_key = key_parts.pop
43
+ key_parts.reverse.inject(last_key => value) do |memo, key|
44
+ memo = { key => memo }; memo
45
+ end
46
+ end
47
+ Sheng::Support.merge_required_hashes(node_list, hsh)
48
+ end
49
+ end
50
+
51
+ # Returns an array of nodes for interpolation, which can be a mix of
52
+ # MergeField, CheckBox, ConditionalBlock, and Sequence instances.
53
+ def nodes
54
+ @nodes ||= begin
55
+ current_block_key = nil
56
+ basic_nodes.map do |node|
57
+ next if node.is_a?(CheckBox) && current_block_key
58
+ if node.is_a? MergeField
59
+ if current_block_key
60
+ if node.is_end? && node.start_key == current_block_key
61
+ current_block_key = nil
62
+ end
63
+ next
64
+ elsif node.is_start?
65
+ current_block_key = node.start_key
66
+ node = node.block_type.new(node)
67
+ end
68
+ end
69
+ node
70
+ end.compact
71
+ end
72
+ end
73
+
74
+ def basic_nodes
75
+ basic_node_elements.map { |element|
76
+ if element.xpath('.//w:checkBox').first
77
+ CheckBox.from_element(element)
78
+ else
79
+ MergeField.from_element(element)
80
+ end
81
+ }.compact
82
+ end
83
+
84
+ def basic_node_elements
85
+ child_nodes = xml_fragment.xpath(".//#{mergefield_element_path}|.//#{new_mergefield_element_path}|.//#{checkbox_element_path}").to_a
86
+ child_nodes += xml_fragment.select { |element| element.name == "fldSimple" }
87
+ child_nodes
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,15 @@
1
+ module Sheng
2
+ module PathHelpers
3
+ def new_mergefield_element_path
4
+ "w:fldChar[contains(@w:fldCharType, 'begin')]"
5
+ end
6
+
7
+ def mergefield_element_path
8
+ "w:fldSimple[contains(@w:instr, 'MERGEFIELD')]"
9
+ end
10
+
11
+ def checkbox_element_path
12
+ "w:checkBox/.."
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,66 @@
1
+ require_relative "block"
2
+
3
+ module Sheng
4
+ class Sequence < Block
5
+ def interpolate(data_set)
6
+ collection = data_set.fetch(key)
7
+ if collection.respond_to?(:each_with_index)
8
+ collection.each_with_index do |item, i|
9
+ add_sequence_element(item, i, last: i == collection.length - 1)
10
+ end
11
+ clean_up
12
+ end
13
+ rescue DataSet::KeyNotFound
14
+ nil
15
+ end
16
+
17
+ def array_of_primitives_expected?
18
+ return true if nodes.map(&:key).uniq == ["item"]
19
+ @start_field.iteration_variable != :item
20
+ end
21
+
22
+ def add_sequence_element(member, index, last: false)
23
+ if series_with_commas? && index > 0
24
+ @start_field.add_previous_sibling(serial_comma_node(index, last: last))
25
+ end
26
+ new_node_set = @start_field.add_previous_sibling(dup_node_set)
27
+ merge_field_set = MergeFieldSet.new("#{key}_#{index}", new_node_set)
28
+ member = { @start_field.iteration_variable => member } unless member.is_a?(Hash)
29
+ merge_field_set.interpolate(DataSet.new(member))
30
+ if index == 0 || last
31
+ copy_section_formatting(new_node_set, side: last ? "end" : "start")
32
+ end
33
+ end
34
+
35
+ def copy_section_formatting(node_set, side:)
36
+ field = instance_variable_get(:"@#{side}_field")
37
+ if field.styling_paragraph && field.styling_paragraph.at_xpath(".//w:sectPr")
38
+ existing_ppr = node_set.at_xpath(".//w:pPr")
39
+ existing_ppr && existing_ppr.remove
40
+ node_set.first.prepend_child(field.styling_paragraph.dup)
41
+ end
42
+ end
43
+
44
+ def clean_up
45
+ [@start_field, @end_field, xml_fragment].map(&:remove)
46
+ end
47
+
48
+ def dup_node_set
49
+ xml_fragment.each_with_object(Nokogiri::XML::NodeSet.new(xml_document)) do |child, dup_content|
50
+ dup_content << child.dup
51
+ end
52
+ end
53
+
54
+ def series_with_commas?
55
+ @start_field.series_with_commas?
56
+ end
57
+
58
+ def serial_comma_node(index, last: false)
59
+ content = ", #{last ? "#{@start_field.comma_series_conjunction} " : ""}"
60
+ content.gsub!(/\,/, '') if last && index == 1
61
+ Sheng::Support.new_text_run(
62
+ content, xml_document: xml_document, space_preserve: true
63
+ )
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,57 @@
1
+ module Sheng
2
+ module Support
3
+ class << self
4
+ def merge_required_hashes(hsh1, hsh2)
5
+ hsh1.merge(hsh2) do |key, old_value, new_value|
6
+ if [old_value, new_value].all? { |v| v.is_a?(Hash) }
7
+ merge_required_hashes(old_value, new_value)
8
+ elsif [old_value, new_value].all? { |v| v.is_a?(Array) } && !old_value.empty?
9
+ [merge_required_hashes(old_value.first, new_value.first)]
10
+ else
11
+ new_value
12
+ end
13
+ end
14
+ end
15
+
16
+ def new_text_run(value, xml_document:, style_run: nil, space_preserve: false)
17
+ r_tag = new_tag('r', xml_document: xml_document)
18
+ if style_run
19
+ r_tag.add_child(style_run)
20
+ end
21
+ t_tag = new_tag('t', xml_document: xml_document)
22
+ if space_preserve
23
+ t_tag["xml:space"] = "preserve"
24
+ end
25
+ t_tag.content = value
26
+ r_tag.add_child(t_tag)
27
+ r_tag
28
+ end
29
+
30
+ def new_tag(tag_name, xml_document:)
31
+ tag = Nokogiri::XML::Node.new(tag_name, xml_document)
32
+ tag.namespace = xml_document.root.namespace_definitions.find { |ns| ns.prefix == "w" }
33
+ tag
34
+ end
35
+
36
+ def extract_mergefield_instruction_text(element)
37
+ if element.name == 'fldSimple'
38
+ label = element['w:instr']
39
+ else
40
+ current_element = element.parent.next_element
41
+ label = current_element.at_xpath(".//w:instrText").text
42
+ loop do
43
+ current_element = current_element.next_element
44
+ next if ["bookmarkStart", "bookmarkEnd"].include?(current_element.name)
45
+ label_part = current_element.at_xpath(".//w:instrText")
46
+ break unless label_part
47
+ label << label_part.text
48
+ end
49
+ end
50
+ unless label.match(MergeField::InstructionTextRegex)
51
+ raise MergeField::NotAMergeFieldError.new(label)
52
+ end
53
+ label
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Sheng
2
+ VERSION = "0.3.2"
3
+ end