sablon 0.1.1 → 0.2.0
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 +4 -4
- data/Gemfile.lock +2 -2
- data/README.md +36 -5
- data/lib/sablon.rb +0 -3
- data/lib/sablon/configuration/html_tag.rb +1 -1
- data/lib/sablon/content.rb +56 -0
- data/lib/sablon/context.rb +2 -0
- data/lib/sablon/document_object_model/content_types.rb +35 -0
- data/lib/sablon/document_object_model/file_handler.rb +26 -0
- data/lib/sablon/document_object_model/model.rb +94 -0
- data/lib/sablon/document_object_model/numbering.rb +94 -0
- data/lib/sablon/document_object_model/relationships.rb +111 -0
- data/lib/sablon/environment.rb +13 -16
- data/lib/sablon/html/ast.rb +14 -13
- data/lib/sablon/html/ast_builder.rb +18 -5
- data/lib/sablon/html/node_properties.rb +3 -3
- data/lib/sablon/operations.rb +59 -0
- data/lib/sablon/processor/document.rb +48 -11
- data/lib/sablon/processor/section_properties.rb +11 -4
- data/lib/sablon/template.rb +88 -47
- data/lib/sablon/version.rb +1 -1
- data/misc/image-example.png +0 -0
- data/test/configuration_test.rb +22 -22
- data/test/content_test.rb +50 -0
- data/test/context_test.rb +37 -1
- data/test/environment_test.rb +4 -1
- data/test/executable_test.rb +0 -2
- data/test/fixtures/cv_sample.docx +0 -0
- data/test/fixtures/html_sample.docx +0 -0
- data/test/fixtures/images/c3po.jpg +0 -0
- data/test/fixtures/images/clone.jpg +0 -0
- data/test/fixtures/images/darth_vader.jpg +0 -0
- data/test/fixtures/images/r2d2.jpg +0 -0
- data/test/fixtures/images_sample.docx +0 -0
- data/test/fixtures/images_template.docx +0 -0
- data/test/fixtures/loops_sample.docx +0 -0
- data/test/fixtures/loops_template.docx +0 -0
- data/test/fixtures/recipe_sample.docx +0 -0
- data/test/fixtures/xml/image.xml +91 -0
- data/test/fixtures/xml/loop_with_unique_ids.xml +152 -0
- data/test/fixtures/xml/mock_document/word/document.xml +12 -0
- data/test/html/ast_test.rb +10 -5
- data/test/html/converter_style_test.rb +9 -9
- data/test/html/converter_test.rb +66 -81
- data/test/html/node_properties_test.rb +2 -2
- data/test/html_test.rb +2 -6
- data/test/processor/document_test.rb +80 -3
- data/test/processor/section_properties_test.rb +68 -0
- data/test/sablon_test.rb +77 -5
- data/test/test_helper.rb +109 -9
- metadata +33 -9
- data/lib/sablon/numbering.rb +0 -23
- data/lib/sablon/processor/numbering.rb +0 -47
- data/lib/sablon/relationship.rb +0 -47
- data/lib/sablon/test/assertions.rb +0 -22
- data/test/section_properties_test.rb +0 -41
@@ -1,12 +1,18 @@
|
|
1
1
|
module Sablon
|
2
2
|
module Processor
|
3
3
|
class SectionProperties
|
4
|
-
def self.
|
5
|
-
new
|
4
|
+
def self.process(xml_node, env)
|
5
|
+
processor = new(xml_node)
|
6
|
+
processor.write_properties(env.section_properties)
|
6
7
|
end
|
7
8
|
|
8
|
-
def initialize(
|
9
|
-
@properties_node =
|
9
|
+
def initialize(xml_node)
|
10
|
+
@properties_node = xml_node.at_xpath(".//w:sectPr")
|
11
|
+
end
|
12
|
+
|
13
|
+
def write_properties(properties = {})
|
14
|
+
return unless properties["start_page_number"]
|
15
|
+
self.start_page_number = properties["start_page_number"]
|
10
16
|
end
|
11
17
|
|
12
18
|
def start_page_number
|
@@ -18,6 +24,7 @@ module Sablon
|
|
18
24
|
end
|
19
25
|
|
20
26
|
private
|
27
|
+
|
21
28
|
def find_or_add_pg_num_type
|
22
29
|
pg_num_type || begin
|
23
30
|
node = Nokogiri::XML::Node.new "w:pgNumType", @properties_node.document
|
data/lib/sablon/template.rb
CHANGED
@@ -1,5 +1,38 @@
|
|
1
|
+
require 'sablon/document_object_model/model'
|
2
|
+
require 'sablon/processor/document'
|
3
|
+
require 'sablon/processor/section_properties'
|
4
|
+
|
1
5
|
module Sablon
|
6
|
+
# Creates a template from an MS Word doc that can be easily manipulated
|
2
7
|
class Template
|
8
|
+
attr_reader :document
|
9
|
+
|
10
|
+
class << self
|
11
|
+
# Adds a new processor to the processors hash. The +pattern+ is used
|
12
|
+
# to select which files the processor should handle. Multiple processors
|
13
|
+
# can be added for the same pattern.
|
14
|
+
def register_processor(pattern, klass, replace_all: false)
|
15
|
+
processors[pattern] = [] if replace_all
|
16
|
+
#
|
17
|
+
if processors[pattern].empty?
|
18
|
+
processors[pattern] = [klass]
|
19
|
+
else
|
20
|
+
processors[pattern] << klass
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Returns the processor classes with a pattern matching the
|
25
|
+
# entry name. If none match nil is returned.
|
26
|
+
def get_processors(entry_name)
|
27
|
+
key = processors.keys.detect { |pattern| entry_name =~ pattern }
|
28
|
+
processors[key]
|
29
|
+
end
|
30
|
+
|
31
|
+
def processors
|
32
|
+
@processors ||= Hash.new([])
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
3
36
|
def initialize(path)
|
4
37
|
@path = path
|
5
38
|
end
|
@@ -19,63 +52,71 @@ module Sablon
|
|
19
52
|
private
|
20
53
|
|
21
54
|
def render(context, properties = {})
|
22
|
-
|
23
|
-
|
55
|
+
# initialize environment
|
56
|
+
@document = Sablon::DOM::Model.new(Zip::File.open(@path))
|
24
57
|
env = Sablon::Environment.new(self, context)
|
25
|
-
|
58
|
+
env.section_properties = properties
|
59
|
+
#
|
60
|
+
# process files
|
61
|
+
process(env)
|
62
|
+
#
|
26
63
|
Zip::OutputStream.write_buffer(StringIO.new) do |out|
|
27
|
-
|
28
|
-
entry_name = entry.name
|
29
|
-
created_dirs = create_dirs_in_zipfile(created_dirs, entry_name, out)
|
30
|
-
out.put_next_entry(entry_name)
|
31
|
-
content = entry.get_input_stream.read
|
32
|
-
if entry_name == 'word/document.xml'
|
33
|
-
out.write(process(Processor::Document, content, env, properties))
|
34
|
-
elsif entry_name =~ /word\/header\d*\.xml/ || entry_name =~ /word\/footer\d*\.xml/
|
35
|
-
out.write(process(Processor::Document, content, env))
|
36
|
-
elsif entry_name == 'word/numbering.xml'
|
37
|
-
out.write(process(Processor::Numbering, content, env))
|
38
|
-
elsif entry_name == 'word/_rels/document.xml.rels'
|
39
|
-
relations_file_content = content
|
40
|
-
else
|
41
|
-
out.write(content)
|
42
|
-
end
|
43
|
-
end
|
44
|
-
if relations_file_content
|
45
|
-
env.relationship.add_found_relationships(relations_file_content, out)
|
46
|
-
end
|
64
|
+
generate_output_file(out, @document.zip_contents)
|
47
65
|
end
|
48
66
|
end
|
49
67
|
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
entry_name_tokens.pop()
|
59
|
-
if entry_name_tokens.length > 1
|
60
|
-
prev_dir = ''
|
61
|
-
entry_name_tokens.each do |dir_name|
|
62
|
-
prev_dir += dir_name + '/'
|
63
|
-
unless created_dirs.include? prev_dir
|
64
|
-
output_stream.put_next_entry(prev_dir)
|
65
|
-
created_dirs << prev_dir
|
66
|
-
end
|
67
|
-
end
|
68
|
+
# Processes all of te entries searching for ones that match the pattern.
|
69
|
+
# The hash is converted into an array first to avoid any possible
|
70
|
+
# modification during iteration errors (i.e. creation of a new rels file).
|
71
|
+
def process(env)
|
72
|
+
@document.zip_contents.to_a.each do |(entry_name, content)|
|
73
|
+
@document.current_entry = entry_name
|
74
|
+
processors = Template.get_processors(entry_name)
|
75
|
+
processors.each { |processor| processor.process(content, env) }
|
68
76
|
end
|
69
|
-
created_dirs
|
70
77
|
end
|
71
78
|
|
72
|
-
# process the sablon xml template with the given +context+.
|
73
|
-
#
|
74
79
|
# IMPORTANT: Open Office does not ignore whitespace around tags.
|
75
80
|
# We need to render the xml without indent and whitespace.
|
76
|
-
def
|
77
|
-
|
78
|
-
|
81
|
+
def generate_output_file(zip_out, contents)
|
82
|
+
# output entries to zip file
|
83
|
+
created_dirs = []
|
84
|
+
contents.each do |entry_name, content|
|
85
|
+
create_dirs_in_zipfile(created_dirs, File.dirname(entry_name), zip_out)
|
86
|
+
zip_out.put_next_entry(entry_name)
|
87
|
+
#
|
88
|
+
# convert Nokogiri XML to string
|
89
|
+
if content.instance_of? Nokogiri::XML::Document
|
90
|
+
content = content.to_xml(indent: 0, save_with: 0)
|
91
|
+
end
|
92
|
+
#
|
93
|
+
zip_out.write(content)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# creates directories of the unzipped docx file in the newly created
|
98
|
+
# docx file e.g. in case of word/_rels/document.xml.rels it creates
|
99
|
+
# word/ and _rels directories to apply recursive zipping. This is a
|
100
|
+
# hack to fix the issue of getting a corrupted file when any referencing
|
101
|
+
# between the xml files happen like in the case of implementing hyperlinks.
|
102
|
+
# The created_dirs array is augmented in place using '<<'
|
103
|
+
def create_dirs_in_zipfile(created_dirs, entry_path, output_stream)
|
104
|
+
entry_path_tokens = entry_path.split('/')
|
105
|
+
return created_dirs unless entry_path_tokens.length > 1
|
106
|
+
#
|
107
|
+
prev_dir = ''
|
108
|
+
entry_path_tokens.each do |dir_name|
|
109
|
+
prev_dir += dir_name + '/'
|
110
|
+
next if created_dirs.include? prev_dir
|
111
|
+
#
|
112
|
+
output_stream.put_next_entry(prev_dir)
|
113
|
+
created_dirs << prev_dir
|
114
|
+
end
|
79
115
|
end
|
80
116
|
end
|
117
|
+
|
118
|
+
# Register the standard processors
|
119
|
+
Template.register_processor(%r{word/document.xml}, Sablon::Processor::Document)
|
120
|
+
Template.register_processor(%r{word/document.xml}, Sablon::Processor::SectionProperties)
|
121
|
+
Template.register_processor(%r{word/(?:header|footer)\d*\.xml}, Sablon::Processor::Document)
|
81
122
|
end
|
data/lib/sablon/version.rb
CHANGED
Binary file
|
data/test/configuration_test.rb
CHANGED
@@ -16,24 +16,24 @@ class ConfigurationTest < Sablon::TestCase
|
|
16
16
|
}
|
17
17
|
# test initialization without type
|
18
18
|
tag = @config.register_html_tag(:test_tag, **options)
|
19
|
-
assert_equal @config.permitted_html_tags[:test_tag]
|
20
|
-
assert_equal tag.name
|
21
|
-
assert_equal tag.type
|
22
|
-
assert_equal
|
23
|
-
assert_equal
|
24
|
-
assert_equal
|
25
|
-
assert_equal
|
19
|
+
assert_equal tag, @config.permitted_html_tags[:test_tag]
|
20
|
+
assert_equal :test_tag, tag.name
|
21
|
+
assert_equal :inline, tag.type
|
22
|
+
assert_equal Sablon::HTMLConverter::Paragraph, tag.ast_class
|
23
|
+
assert_equal({ dummy: 'value' }, tag.attributes)
|
24
|
+
assert_equal({ 'pstyle' => 'ListBullet' }, tag.properties)
|
25
|
+
assert_equal %i[_inline ol ul li], tag.allowed_children
|
26
26
|
|
27
27
|
# test initialization with type
|
28
28
|
tag = @config.register_html_tag('test_tag2', :block, **options)
|
29
|
-
assert_equal @config.permitted_html_tags[:test_tag2]
|
30
|
-
assert_equal tag.name
|
31
|
-
assert_equal tag.type
|
29
|
+
assert_equal tag, @config.permitted_html_tags[:test_tag2]
|
30
|
+
assert_equal :test_tag2, tag.name
|
31
|
+
assert_equal :block, tag.type
|
32
32
|
end
|
33
33
|
|
34
34
|
def test_remove_tag
|
35
35
|
tag = @config.register_html_tag(:test)
|
36
|
-
assert_equal @config.remove_html_tag(:test)
|
36
|
+
assert_equal tag, @config.remove_html_tag(:test)
|
37
37
|
assert_nil @config.permitted_html_tags[:test]
|
38
38
|
end
|
39
39
|
|
@@ -77,9 +77,9 @@ class ConfigurationHTMLTagTest < Sablon::TestCase
|
|
77
77
|
def test_html_tag_full_init
|
78
78
|
args = ['a', 'inline', ast_class: Sablon::HTMLConverter::Run]
|
79
79
|
tag = Sablon::Configuration::HTMLTag.new(*args)
|
80
|
-
assert_equal tag.name
|
81
|
-
assert_equal tag.type
|
82
|
-
assert_equal
|
80
|
+
assert_equal :a, tag.name
|
81
|
+
assert_equal :inline, tag.type
|
82
|
+
assert_equal Sablon::HTMLConverter::Run, tag.ast_class
|
83
83
|
#
|
84
84
|
options = {
|
85
85
|
ast_class: :run,
|
@@ -89,12 +89,12 @@ class ConfigurationHTMLTagTest < Sablon::TestCase
|
|
89
89
|
}
|
90
90
|
tag = Sablon::Configuration::HTMLTag.new('a', 'inline', **options)
|
91
91
|
#
|
92
|
-
assert_equal tag.name
|
93
|
-
assert_equal tag.type
|
94
|
-
assert_equal
|
95
|
-
assert_equal
|
96
|
-
assert_equal
|
97
|
-
assert_equal
|
92
|
+
assert_equal :a, tag.name
|
93
|
+
assert_equal :inline, tag.type
|
94
|
+
assert_equal Sablon::HTMLConverter::Run, tag.ast_class
|
95
|
+
assert_equal({ dummy: 'value1' }, tag.attributes)
|
96
|
+
assert_equal({ 'dummy2' => 'value2' }, tag.properties)
|
97
|
+
assert_equal [:text], tag.allowed_children
|
98
98
|
end
|
99
99
|
|
100
100
|
def test_html_tag_init_block_without_class
|
@@ -113,10 +113,10 @@ class ConfigurationHTMLTagTest < Sablon::TestCase
|
|
113
113
|
# test default allowances
|
114
114
|
assert div.allowed_child?(text) # all inline elements allowed
|
115
115
|
assert div.allowed_child?(olist) # tag name is included even though it is bock leve
|
116
|
-
assert_equal div.allowed_child?(div)
|
116
|
+
assert_equal false, div.allowed_child?(div) # other block elms are not allowed
|
117
117
|
|
118
118
|
# test olist with allowances for all blocks but no inline
|
119
119
|
assert olist.allowed_child?(div) # all block elements allowed
|
120
|
-
assert_equal olist.allowed_child?(text)
|
120
|
+
assert_equal false, olist.allowed_child?(text) # no inline elements
|
121
121
|
end
|
122
122
|
end
|
data/test/content_test.rb
CHANGED
@@ -238,3 +238,53 @@ class ContentWordMLTest < Sablon::TestCase
|
|
238
238
|
assert_xml_equal output, @document
|
239
239
|
end
|
240
240
|
end
|
241
|
+
|
242
|
+
class ContentImageTest < Sablon::TestCase
|
243
|
+
def setup
|
244
|
+
base_path = Pathname.new(File.expand_path('../', __FILE__))
|
245
|
+
fixture_dir = base_path.join('fixtures')
|
246
|
+
@image_path = fixture_dir.join('images', 'r2d2.jpg')
|
247
|
+
@expected = Sablon::Content::Image.new(@image_path.to_s)
|
248
|
+
end
|
249
|
+
|
250
|
+
def test_inspect
|
251
|
+
assert_equal '#<Image r2d2.jpg:{}>', @expected.inspect
|
252
|
+
#
|
253
|
+
# set some rid's and retest
|
254
|
+
@expected.rid_by_file['word/test.xml'] = 'rId1'
|
255
|
+
assert_equal '#<Image r2d2.jpg:{"word/test.xml"=>"rId1"}>', @expected.inspect
|
256
|
+
end
|
257
|
+
|
258
|
+
def test_wraps_image_from_string_path
|
259
|
+
#
|
260
|
+
tested = Sablon.content(:image, @image_path.to_s)
|
261
|
+
assert_equal @expected, tested
|
262
|
+
end
|
263
|
+
|
264
|
+
def test_wraps_image_from_readable_object_that_can_be_basenamed
|
265
|
+
tested = Sablon.content(:image, open(@image_path.to_s, 'rb'))
|
266
|
+
assert_equal @expected, tested
|
267
|
+
end
|
268
|
+
|
269
|
+
def test_wraps_image_from_readable_object_with_filename_supplied
|
270
|
+
data = StringIO.new(IO.binread(@image_path.to_s))
|
271
|
+
tested = Sablon.content(:image, data, filename: File.basename(@image_path))
|
272
|
+
assert_equal @expected, tested
|
273
|
+
end
|
274
|
+
|
275
|
+
def test_wraps_readable_object_that_responds_to_filename
|
276
|
+
readable = Struct.new(:data, :filename) { alias read data }
|
277
|
+
#
|
278
|
+
readable = readable.new(IO.binread(@image_path.to_s), File.basename(@image_path))
|
279
|
+
tested = Sablon.content(:image, readable)
|
280
|
+
assert_equal @expected, tested
|
281
|
+
end
|
282
|
+
|
283
|
+
def test_raises_error_when_no_filename
|
284
|
+
data = StringIO.new(IO.binread(@image_path.to_s))
|
285
|
+
#
|
286
|
+
assert_raises ArgumentError do
|
287
|
+
Sablon.content(:image, data)
|
288
|
+
end
|
289
|
+
end
|
290
|
+
end
|
data/test/context_test.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# -*- coding: utf-8 -*-
|
2
2
|
require "test_helper"
|
3
3
|
|
4
|
-
class
|
4
|
+
class ContextTest < Sablon::TestCase
|
5
5
|
def test_converts_symbol_keys_to_string_keys
|
6
6
|
context = Sablon::Context.transform_hash(a: 1, b: { c: 2, "d" => 3 })
|
7
7
|
assert_equal({ "a" => 1, "b" => { "c" => 2, "d" => 3 } }, context)
|
@@ -25,4 +25,40 @@ class EnvironmentTest < Sablon::TestCase
|
|
25
25
|
"otherkey" => nil,
|
26
26
|
"normalkey" => nil}, context)
|
27
27
|
end
|
28
|
+
|
29
|
+
def test_recognizes_image_keys
|
30
|
+
base_path = Pathname.new(File.expand_path("../", __FILE__))
|
31
|
+
img_path = "#{base_path}/fixtures/images/c3po.jpg"
|
32
|
+
context = {
|
33
|
+
test: 'result',
|
34
|
+
'image:image' => img_path
|
35
|
+
}
|
36
|
+
#
|
37
|
+
context = Sablon::Context.transform_hash(context)
|
38
|
+
assert_equal({ "test" => "result",
|
39
|
+
"image" => Sablon.content(:image, img_path) },
|
40
|
+
context)
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_converts_hashes_nested_in_arrays
|
44
|
+
input_context = {
|
45
|
+
test: 'result',
|
46
|
+
items: [
|
47
|
+
{ name: 'Key1', value: 'Value1' },
|
48
|
+
{ 'name' => 'Key2', 'html:value' => '<b>Test</b>' }
|
49
|
+
],
|
50
|
+
'word_ml:runs' => '<w:r><w:t>Text</w:t><w:r>'
|
51
|
+
}
|
52
|
+
expected_context = {
|
53
|
+
'test' => 'result',
|
54
|
+
'items' => [
|
55
|
+
{ 'name' => 'Key1', 'value' => 'Value1' },
|
56
|
+
{ 'name' => 'Key2', 'value' => Sablon.content(:html, '<b>Test</b>') }
|
57
|
+
],
|
58
|
+
'runs' => Sablon.content(:word_ml, '<w:r><w:t>Text</w:t><w:r>')
|
59
|
+
}
|
60
|
+
#
|
61
|
+
context = Sablon::Context.transform_hash(input_context)
|
62
|
+
assert_equal expected_context, context
|
63
|
+
end
|
28
64
|
end
|
data/test/environment_test.rb
CHANGED
@@ -13,14 +13,17 @@ class EnvironmentTest < Sablon::TestCase
|
|
13
13
|
def test_alter_context
|
14
14
|
# set initial context
|
15
15
|
env = Sablon::Environment.new(nil, a: 1, b: { c: 2, "d" => 3 })
|
16
|
+
|
16
17
|
# alter context to change a single key and set a new one
|
17
18
|
env2 = env.alter_context(a: "a", e: "new-key")
|
18
19
|
assert_equal({ "a" => "a", "b" => { "c" => 2, "d" => 3 }, "e" => "new-key" }, env2.context)
|
20
|
+
|
19
21
|
# check that the old context was not modified
|
20
22
|
assert_equal({"a" => 1, "b" => { "c" => 2, "d" => 3 }}, env.context)
|
23
|
+
|
21
24
|
# check that numbering and template are the same references
|
22
25
|
assert env.template.equal?(env2.template), "#{env.template} != #{env2.template}"
|
23
|
-
|
26
|
+
|
24
27
|
# check that a new context reference was created
|
25
28
|
assert !env.context.equal?(env2.context), "#{env.context} == #{env2.context}"
|
26
29
|
end
|
data/test/executable_test.rb
CHANGED
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
@@ -0,0 +1,91 @@
|
|
1
|
+
<w:p>
|
2
|
+
<w:r>
|
3
|
+
<w:fldChar w:fldCharType="begin"/>
|
4
|
+
</w:r>
|
5
|
+
<w:r>
|
6
|
+
<w:instrText xml:space="preserve"> MERGEFIELD @item.image:start \* MERGEFORMAT </w:instrText>
|
7
|
+
</w:r>
|
8
|
+
<w:r>
|
9
|
+
<w:fldChar w:fldCharType="separate"/>
|
10
|
+
</w:r>
|
11
|
+
<w:r>
|
12
|
+
<w:rPr>
|
13
|
+
<w:noProof/>
|
14
|
+
</w:rPr>
|
15
|
+
<w:t>«@item.image:start»</w:t>
|
16
|
+
</w:r>
|
17
|
+
<w:r>
|
18
|
+
<w:fldChar w:fldCharType="end"/>
|
19
|
+
</w:r>
|
20
|
+
</w:p>
|
21
|
+
<w:p >
|
22
|
+
<w:r>
|
23
|
+
<w:rPr>
|
24
|
+
<w:noProof/>
|
25
|
+
</w:rPr>
|
26
|
+
<w:drawing>
|
27
|
+
<wp:inline distT="0" distB="0" distL="0" distR="0">
|
28
|
+
<wp:extent cx="1875155" cy="1249045"/>
|
29
|
+
<wp:effectExtent l="0" t="0" r="0" b="0"/>
|
30
|
+
<wp:docPr id="2" name="Picture 2"/>
|
31
|
+
<wp:cNvGraphicFramePr>
|
32
|
+
<a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
|
33
|
+
</wp:cNvGraphicFramePr>
|
34
|
+
<a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
|
35
|
+
<a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
36
|
+
<pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
|
37
|
+
<pic:nvPicPr>
|
38
|
+
<pic:cNvPr id="2" name="placeholder.png"/>
|
39
|
+
<pic:cNvPicPr/>
|
40
|
+
</pic:nvPicPr>
|
41
|
+
<pic:blipFill>
|
42
|
+
<a:blip r:embed="rId4">
|
43
|
+
<a:extLst>
|
44
|
+
<a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
|
45
|
+
<a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
|
46
|
+
</a:ext>
|
47
|
+
</a:extLst>
|
48
|
+
</a:blip>
|
49
|
+
<a:stretch>
|
50
|
+
<a:fillRect/>
|
51
|
+
</a:stretch>
|
52
|
+
</pic:blipFill>
|
53
|
+
<pic:spPr>
|
54
|
+
<a:xfrm>
|
55
|
+
<a:off x="0" y="0"/>
|
56
|
+
<a:ext cx="1875155" cy="1249045"/>
|
57
|
+
</a:xfrm>
|
58
|
+
<a:prstGeom prst="rect">
|
59
|
+
<a:avLst/>
|
60
|
+
</a:prstGeom>
|
61
|
+
</pic:spPr>
|
62
|
+
</pic:pic>
|
63
|
+
</a:graphicData>
|
64
|
+
</a:graphic>
|
65
|
+
</wp:inline>
|
66
|
+
</w:drawing>
|
67
|
+
</w:r>
|
68
|
+
</w:p>
|
69
|
+
<w:p>
|
70
|
+
<w:r>
|
71
|
+
<w:fldChar w:fldCharType="begin"/>
|
72
|
+
</w:r>
|
73
|
+
<w:r>
|
74
|
+
<w:instrText xml:space="preserve"> MERGEFIELD @item.image:end \* MERGEFORMAT </w:instrText>
|
75
|
+
</w:r>
|
76
|
+
<w:r>
|
77
|
+
<w:fldChar w:fldCharType="separate"/>
|
78
|
+
</w:r>
|
79
|
+
<w:r>
|
80
|
+
<w:rPr>
|
81
|
+
<w:noProof/>
|
82
|
+
</w:rPr>
|
83
|
+
<w:t>«@item.image:end»</w:t>
|
84
|
+
</w:r>
|
85
|
+
<w:r>
|
86
|
+
<w:rPr>
|
87
|
+
<w:noProof/>
|
88
|
+
</w:rPr>
|
89
|
+
<w:fldChar w:fldCharType="end"/>
|
90
|
+
</w:r>
|
91
|
+
</w:p>
|