sheng 0.3.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitattributes +1 -0
- data/.gitignore +18 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/.watchr +99 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +62 -0
- data/Rakefile +10 -0
- data/docs/creating_templates.docx +0 -0
- data/docs/creating_templates.md +392 -0
- data/lib/sheng/block.rb +59 -0
- data/lib/sheng/check_box.rb +43 -0
- data/lib/sheng/conditional_block.rb +30 -0
- data/lib/sheng/data_set.rb +45 -0
- data/lib/sheng/docx.rb +75 -0
- data/lib/sheng/merge_field.rb +208 -0
- data/lib/sheng/merge_field_set.rb +90 -0
- data/lib/sheng/path_helpers.rb +15 -0
- data/lib/sheng/sequence.rb +66 -0
- data/lib/sheng/support.rb +57 -0
- data/lib/sheng/version.rb +3 -0
- data/lib/sheng/wml_file.rb +47 -0
- data/lib/sheng.rb +21 -0
- data/sheng.gemspec +31 -0
- data/spec/fixtures/bad_docx_files/with_field_not_in_dataset.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_missing_sequence_start.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_old_mergefields.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_poorly_nested_sequences.docx +0 -0
- data/spec/fixtures/bad_docx_files/with_unended_sequence.docx +0 -0
- data/spec/fixtures/docx_files/input_document.docx +0 -0
- data/spec/fixtures/docx_files/old_style/input_document.docx +0 -0
- data/spec/fixtures/docx_files/old_style/output_document.docx +0 -0
- data/spec/fixtures/docx_files/output_document.docx +0 -0
- data/spec/fixtures/inputs/complete.json +61 -0
- data/spec/fixtures/inputs/incomplete.json +52 -0
- data/spec/fixtures/trees/embedded_sequence.yml +13 -0
- data/spec/fixtures/trees/merge_field_set.yml +16 -0
- data/spec/fixtures/xml_fragments/input/check_box/check_box.xml +11 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/bad/badly_nested_conditional.xml +105 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/bad/unclosed_conditional.xml +35 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_if.xml +84 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_inline.xml +59 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_block_unless.xml +51 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/conditional_in_table.xml +176 -0
- data/spec/fixtures/xml_fragments/input/conditional_block/embedded_conditional.xml +79 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_new.xml +19 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/not_a_real_mergefield_old.xml +9 -0
- data/spec/fixtures/xml_fragments/input/merge_field/bad/unclosed_merge_field.xml +25 -0
- data/spec/fixtures/xml_fragments/input/merge_field/inline_merge_field.xml +26 -0
- data/spec/fixtures/xml_fragments/input/merge_field/merge_field.xml +14 -0
- data/spec/fixtures/xml_fragments/input/merge_field/new_merge_field.xml +28 -0
- data/spec/fixtures/xml_fragments/input/merge_field/split_merge_field.xml +33 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/complex_nesting_and_reuse.xml +231 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/merge_field_set.xml +89 -0
- data/spec/fixtures/xml_fragments/input/merge_field_set/with_non_mergefield_fields.xml +43 -0
- data/spec/fixtures/xml_fragments/input/sequence/array_sequence.xml +23 -0
- data/spec/fixtures/xml_fragments/input/sequence/bad/badly_nested_sequence.xml +62 -0
- data/spec/fixtures/xml_fragments/input/sequence/bad/unclosed_sequence.xml +32 -0
- data/spec/fixtures/xml_fragments/input/sequence/embedded_sequence.xml +68 -0
- data/spec/fixtures/xml_fragments/input/sequence/inline_sequence.xml +24 -0
- data/spec/fixtures/xml_fragments/input/sequence/overridden_iterator_array_sequence.xml +23 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence.xml +39 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence_in_table.xml +125 -0
- data/spec/fixtures/xml_fragments/input/sequence/sequence_with_section_formatting.xml +41 -0
- data/spec/fixtures/xml_fragments/input/sequence/series_with_commas.xml +73 -0
- data/spec/fixtures/xml_fragments/output/check_box/check_box.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_does_not_exist.xml +52 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/conditional_in_table_exists.xml +84 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_both.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_inside.xml +5 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/embedded_conditional_outside.xml +8 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/if_does_not_exist.xml +5 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/if_exists.xml +19 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/inline_does_not_exist.xml +10 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/inline_exists.xml +13 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/unless_does_not_exist.xml +11 -0
- data/spec/fixtures/xml_fragments/output/conditional_block/unless_exists.xml +5 -0
- data/spec/fixtures/xml_fragments/output/merge_field/inline_merge_field.xml +11 -0
- data/spec/fixtures/xml_fragments/output/merge_field/merge_field.xml +12 -0
- data/spec/fixtures/xml_fragments/output/merge_field/split_merge_field.xml +10 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/complex_nesting_and_reuse.xml +190 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/merge_field_set.xml +75 -0
- data/spec/fixtures/xml_fragments/output/merge_field_set/with_non_mergefield_fields.xml +31 -0
- data/spec/fixtures/xml_fragments/output/sequence/array_sequence.xml +17 -0
- data/spec/fixtures/xml_fragments/output/sequence/embedded_sequence.xml +56 -0
- data/spec/fixtures/xml_fragments/output/sequence/inline_sequence.xml +16 -0
- data/spec/fixtures/xml_fragments/output/sequence/overridden_iterator_array_sequence.xml +12 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence.xml +28 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence_in_table.xml +120 -0
- data/spec/fixtures/xml_fragments/output/sequence/sequence_with_section_formatting.xml +46 -0
- data/spec/fixtures/xml_fragments/output/sequence/series_with_commas.xml +43 -0
- data/spec/fixtures/xml_fragments/output/sequence/series_with_commas_two_items.xml +31 -0
- data/spec/lib/sheng/check_box_spec.rb +87 -0
- data/spec/lib/sheng/conditional_block_spec.rb +151 -0
- data/spec/lib/sheng/data_set_spec.rb +65 -0
- data/spec/lib/sheng/docx_spec.rb +146 -0
- data/spec/lib/sheng/merge_field_set_spec.rb +165 -0
- data/spec/lib/sheng/merge_field_spec.rb +276 -0
- data/spec/lib/sheng/sequence_spec.rb +201 -0
- data/spec/lib/sheng/wml_file_spec.rb +38 -0
- data/spec/spec_helper.rb +16 -0
- data/spec/support/path_helper.rb +15 -0
- data/spec/support/xml_helper.rb +28 -0
- metadata +355 -0
data/lib/sheng/block.rb
ADDED
@@ -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
|