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