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.
- 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
|