sheng 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
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