sablon 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -2
  3. data/README.md +36 -5
  4. data/lib/sablon.rb +0 -3
  5. data/lib/sablon/configuration/html_tag.rb +1 -1
  6. data/lib/sablon/content.rb +56 -0
  7. data/lib/sablon/context.rb +2 -0
  8. data/lib/sablon/document_object_model/content_types.rb +35 -0
  9. data/lib/sablon/document_object_model/file_handler.rb +26 -0
  10. data/lib/sablon/document_object_model/model.rb +94 -0
  11. data/lib/sablon/document_object_model/numbering.rb +94 -0
  12. data/lib/sablon/document_object_model/relationships.rb +111 -0
  13. data/lib/sablon/environment.rb +13 -16
  14. data/lib/sablon/html/ast.rb +14 -13
  15. data/lib/sablon/html/ast_builder.rb +18 -5
  16. data/lib/sablon/html/node_properties.rb +3 -3
  17. data/lib/sablon/operations.rb +59 -0
  18. data/lib/sablon/processor/document.rb +48 -11
  19. data/lib/sablon/processor/section_properties.rb +11 -4
  20. data/lib/sablon/template.rb +88 -47
  21. data/lib/sablon/version.rb +1 -1
  22. data/misc/image-example.png +0 -0
  23. data/test/configuration_test.rb +22 -22
  24. data/test/content_test.rb +50 -0
  25. data/test/context_test.rb +37 -1
  26. data/test/environment_test.rb +4 -1
  27. data/test/executable_test.rb +0 -2
  28. data/test/fixtures/cv_sample.docx +0 -0
  29. data/test/fixtures/html_sample.docx +0 -0
  30. data/test/fixtures/images/c3po.jpg +0 -0
  31. data/test/fixtures/images/clone.jpg +0 -0
  32. data/test/fixtures/images/darth_vader.jpg +0 -0
  33. data/test/fixtures/images/r2d2.jpg +0 -0
  34. data/test/fixtures/images_sample.docx +0 -0
  35. data/test/fixtures/images_template.docx +0 -0
  36. data/test/fixtures/loops_sample.docx +0 -0
  37. data/test/fixtures/loops_template.docx +0 -0
  38. data/test/fixtures/recipe_sample.docx +0 -0
  39. data/test/fixtures/xml/image.xml +91 -0
  40. data/test/fixtures/xml/loop_with_unique_ids.xml +152 -0
  41. data/test/fixtures/xml/mock_document/word/document.xml +12 -0
  42. data/test/html/ast_test.rb +10 -5
  43. data/test/html/converter_style_test.rb +9 -9
  44. data/test/html/converter_test.rb +66 -81
  45. data/test/html/node_properties_test.rb +2 -2
  46. data/test/html_test.rb +2 -6
  47. data/test/processor/document_test.rb +80 -3
  48. data/test/processor/section_properties_test.rb +68 -0
  49. data/test/sablon_test.rb +77 -5
  50. data/test/test_helper.rb +109 -9
  51. metadata +33 -9
  52. data/lib/sablon/numbering.rb +0 -23
  53. data/lib/sablon/processor/numbering.rb +0 -47
  54. data/lib/sablon/relationship.rb +0 -47
  55. data/lib/sablon/test/assertions.rb +0 -22
  56. data/test/section_properties_test.rb +0 -41
@@ -81,13 +81,13 @@ class NodePropertiesTest < Sablon::TestCase
81
81
  props = {}
82
82
  props = Sablon::HTMLConverter::NodeProperties.new('w:pPr', props, @inc_props.new)
83
83
  props['rStyle'] = 'FootnoteText'
84
- assert_equal({ 'rStyle' => 'FootnoteText' }, props.instance_variable_get(:@properties))
84
+ assert_equal({ rStyle: 'FootnoteText' }, props.instance_variable_get(:@properties))
85
85
  end
86
86
 
87
87
  def test_properties_filtered_on_init
88
88
  props = { 'pStyle' => 'Paragraph', 'rStyle' => 'EndnoteText' }
89
89
  props = Sablon::HTMLConverter::NodeProperties.new('w:rPr', props, %w[rStyle])
90
- assert_equal({ 'rStyle' => 'EndnoteText' }, props.instance_variable_get(:@properties))
90
+ assert_equal({ rStyle: 'EndnoteText' }, props.instance_variable_get(:@properties))
91
91
  end
92
92
 
93
93
  def test_transferred_properties
@@ -3,7 +3,6 @@ require "test_helper"
3
3
  require "support/html_snippets"
4
4
 
5
5
  class SablonHTMLTest < Sablon::TestCase
6
- include Sablon::Test::Assertions
7
6
  include HTMLSnippets
8
7
 
9
8
  def setup
@@ -14,7 +13,6 @@ class SablonHTMLTest < Sablon::TestCase
14
13
  end
15
14
 
16
15
  def test_generate_document_from_template_with_styles_and_html
17
- uid_generator = UIDTestGenerator.new
18
16
  template_path = @base_path + "fixtures/insertion_template.docx"
19
17
  output_path = @base_path + "sandbox/html.docx"
20
18
  template = Sablon.template template_path
@@ -25,9 +23,7 @@ class SablonHTMLTest < Sablon::TestCase
25
23
  'html:github' => '<a href="http://www.github.com" style="color: #0000FF">GitHub</a>'
26
24
  }
27
25
  }
28
- SecureRandom.stub(:uuid, uid_generator.method(:new_uid)) do
29
- template.render_to_file output_path, context
30
- end
26
+ template.render_to_file output_path, context
31
27
  #
32
28
  assert_docx_equal @sample_path, output_path
33
29
  end
@@ -41,7 +37,7 @@ class SablonHTMLTest < Sablon::TestCase
41
37
  e = assert_raises(ArgumentError) do
42
38
  template.render_to_file output_path, context
43
39
  end
44
- assert_equal 'Could not find w:abstractNum definition for style: "ListNumber"', e.message
40
+ assert_equal "Could not find w:abstractNum definition for style: 'ListNumber'", e.message
45
41
 
46
42
  skip 'implement default styles'
47
43
  end
@@ -311,7 +311,6 @@ class ProcessorDocumentTest < Sablon::TestCase
311
311
  </w:tc>
312
312
  </w:tr>
313
313
  </w:tbl>
314
-
315
314
  document
316
315
  end
317
316
 
@@ -348,6 +347,22 @@ class ProcessorDocumentTest < Sablon::TestCase
348
347
  assert_equal "Could not find end field for «technologies:each(technology)». Was looking for «technologies:endEach»", e.message
349
348
  end
350
349
 
350
+ def test_loop_incrementing_unique_ids
351
+ context = {
352
+ fruits: %w[Apple Blueberry Cranberry Date].map { |i| { name: i } },
353
+ cars: %w[Silverado Serria Ram Tundra].map { |i| { name: i } }
354
+ }
355
+ #
356
+ xml = Nokogiri::XML(process(snippet('loop_with_unique_ids'), context))
357
+ #
358
+ # all unique ids should get incremented to stay unique
359
+ ids = xml.xpath("//*[local-name() = 'docPr']").map { |n| n.attr('id') }
360
+ assert_equal %w[1 2 3 4], ids
361
+ #
362
+ ids = xml.xpath("//*[local-name() = 'cNvPr']").map { |n| n.attr('id') }
363
+ assert_equal %w[1 2 3 4], ids
364
+ end
365
+
351
366
  def test_conditional_with_missing_end_raises_error
352
367
  e = assert_raises Sablon::TemplateError do
353
368
  process(snippet("conditional_without_ending"), {})
@@ -446,7 +461,7 @@ class ProcessorDocumentTest < Sablon::TestCase
446
461
  assert_xml_equal <<-document, result
447
462
  <w:r><w:t xml:space="preserve">Before </w:t></w:r>
448
463
  <w:r><w:t xml:space="preserve">After </w:t></w:r>
449
- <w:p>
464
+ <w:p>
450
465
  <w:r w:rsidR="004B49F0">
451
466
  <w:rPr><w:noProof/></w:rPr>
452
467
  <w:t>Contents of comment key</w:t>
@@ -455,10 +470,72 @@ class ProcessorDocumentTest < Sablon::TestCase
455
470
  document
456
471
  end
457
472
 
473
+ def test_image_replacement
474
+ base_path = Pathname.new(File.expand_path("../../", __FILE__))
475
+ image = Sablon.content(:image, base_path + "fixtures/images/r2d2.jpg")
476
+ result = process(snippet("image"), { "item" => { "image" => image } })
477
+
478
+ assert_xml_equal <<-document, result
479
+ <w:p>
480
+ </w:p>
481
+ <w:p>
482
+ <w:r>
483
+ <w:rPr>
484
+ <w:noProof/>
485
+ </w:rPr>
486
+ <w:drawing>
487
+ <wp:inline distT="0" distB="0" distL="0" distR="0">
488
+ <wp:extent cx="1875155" cy="1249045"/>
489
+ <wp:effectExtent l="0" t="0" r="0" b="0"/>
490
+ <wp:docPr id="2" name="Picture 2"/>
491
+ <wp:cNvGraphicFramePr>
492
+ <a:graphicFrameLocks xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main" noChangeAspect="1"/>
493
+ </wp:cNvGraphicFramePr>
494
+ <a:graphic xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">
495
+ <a:graphicData uri="http://schemas.openxmlformats.org/drawingml/2006/picture">
496
+ <pic:pic xmlns:pic="http://schemas.openxmlformats.org/drawingml/2006/picture">
497
+ <pic:nvPicPr>
498
+ <pic:cNvPr id="2" name="r2d2.jpg"/>
499
+ <pic:cNvPicPr/>
500
+ </pic:nvPicPr>
501
+ <pic:blipFill>
502
+ <a:blip r:embed="rId1235\">
503
+ <a:extLst>
504
+ <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
505
+ <a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
506
+ </a:ext>
507
+ </a:extLst>
508
+ </a:blip>
509
+ <a:stretch>
510
+ <a:fillRect/>
511
+ </a:stretch>
512
+ </pic:blipFill>
513
+ <pic:spPr>
514
+ <a:xfrm>
515
+ <a:off x="0" y="0"/>
516
+ <a:ext cx="1875155" cy="1249045"/>
517
+ </a:xfrm>
518
+ <a:prstGeom prst="rect">
519
+ <a:avLst/>
520
+ </a:prstGeom>
521
+ </pic:spPr>
522
+ </pic:pic>
523
+ </a:graphicData>
524
+ </a:graphic>
525
+ </wp:inline>
526
+ </w:drawing>
527
+ </w:r>
528
+ </w:p>
529
+ <w:p>
530
+ </w:p>
531
+ document
532
+ end
533
+
458
534
  private
459
535
 
460
536
  def process(document, context)
461
- env = Sablon::Environment.new(nil, context)
537
+ env = Sablon::Environment.new(MockTemplate.new, context)
538
+ env.document.current_entry = 'word/document.xml'
462
539
  @processor.process(wrap(document), env).to_xml
463
540
  end
464
541
  end
@@ -0,0 +1,68 @@
1
+ require "test_helper"
2
+ require "support/document_xml_helper"
3
+
4
+ class SectionPropertiesTest < Sablon::TestCase
5
+ include DocumentXMLHelper
6
+
7
+ def setup
8
+ @env = Sablon::Environment.new(nil, {})
9
+ end
10
+
11
+ def test_process
12
+ xml = <<-XML
13
+ <w:body>
14
+ <w:sectPr w:rsidR="00FC1AFD" w:rsidSect="006745DF">
15
+ <w:pgSz w:w="11900" w:h="16840"/>
16
+ <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="708" w:footer="708" w:gutter="0"/>
17
+ <w:pgNumType w:start="1"/>
18
+ <w:cols w:space="708"/>
19
+ <w:docGrid w:linePitch="360"/>
20
+ </w:sectPr>
21
+ </w:body>
22
+ XML
23
+ expected = xml.gsub(/w:start="1"/, 'w:start="123"')
24
+ xml = wrap(xml)
25
+ #
26
+ @env.section_properties = { start_page_number: 123 }
27
+ Sablon::Processor::SectionProperties.process(xml, @env)
28
+ #
29
+ assert_xml_equal expected, xml.to_s
30
+ end
31
+
32
+ def test_assign_start_page_number_with_pgNumType_tag
33
+ xml = wrap <<-XML
34
+ <w:body>
35
+ <w:sectPr w:rsidR="00FC1AFD" w:rsidSect="006745DF">
36
+ <w:pgSz w:w="11900" w:h="16840"/>
37
+ <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="708" w:footer="708" w:gutter="0"/>
38
+ <w:pgNumType w:start="1"/>
39
+ <w:cols w:space="708"/>
40
+ <w:docGrid w:linePitch="360"/>
41
+ </w:sectPr>
42
+ </w:body>
43
+ XML
44
+ #
45
+ properties = Sablon::Processor::SectionProperties.new(xml)
46
+ assert_equal "1", properties.start_page_number
47
+ properties.start_page_number = "23"
48
+ assert_equal "23", properties.start_page_number
49
+ end
50
+
51
+ def test_assign_start_page_number_without_pgNumType_tag
52
+ xml = wrap <<-XML
53
+ <w:body>
54
+ <w:sectPr w:rsidR="00FC1AFD" w:rsidSect="006745DF">
55
+ <w:pgSz w:w="11900" w:h="16840"/>
56
+ <w:pgMar w:top="1440" w:right="1800" w:bottom="1440" w:left="1800" w:header="708" w:footer="708" w:gutter="0"/>
57
+ <w:cols w:space="708"/>
58
+ <w:docGrid w:linePitch="360"/>
59
+ </w:sectPr>
60
+ </w:body>
61
+ XML
62
+ #
63
+ properties = Sablon::Processor::SectionProperties.new(xml)
64
+ assert_nil properties.start_page_number
65
+ properties.start_page_number = "16"
66
+ assert_equal "16", properties.start_page_number
67
+ end
68
+ end
@@ -3,7 +3,6 @@ require "test_helper"
3
3
  require "support/xml_snippets"
4
4
 
5
5
  class SablonTest < Sablon::TestCase
6
- include Sablon::Test::Assertions
7
6
  include XMLSnippets
8
7
 
9
8
  def setup
@@ -24,7 +23,7 @@ class SablonTest < Sablon::TestCase
24
23
  referee = Struct.new(:name, :company, :position, :phone)
25
24
 
26
25
  context = {
27
- current_time: Time.now.strftime("%d.%m.%Y %H:%M"),
26
+ current_time: '15.04.2015 14:57',
28
27
  metadata: { generator: "Sablon" },
29
28
  title: "Resume",
30
29
  person: OpenStruct.new("first_name" => "Ronald", "last_name" => "Anderson",
@@ -85,8 +84,7 @@ class SablonTest < Sablon::TestCase
85
84
  end
86
85
  end
87
86
 
88
- class SablonTest < Sablon::TestCase
89
- include Sablon::Test::Assertions
87
+ class SablonConditionalsTest < Sablon::TestCase
90
88
  include XMLSnippets
91
89
 
92
90
  def setup
@@ -99,7 +97,81 @@ class SablonTest < Sablon::TestCase
99
97
 
100
98
  def test_generate_document_from_template
101
99
  template = Sablon.template @template_path
102
- context = {paragraph: true, inline: true, table: true, table_inline: true, content: "Some Content"}
100
+ context = {
101
+ paragraph: true,
102
+ inline: true,
103
+ table: true,
104
+ table_inline: true,
105
+ content: "Some Content"
106
+ }
107
+ #
108
+ context = { paragraph: true, inline: true, table: true, table_inline: true, content: "Some Content" }
109
+ template.render_to_file @output_path, context
110
+ assert_docx_equal @sample_path, @output_path
111
+ end
112
+ end
113
+
114
+ class SablonLoopsTest < Sablon::TestCase
115
+ include XMLSnippets
116
+
117
+ def setup
118
+ super
119
+ @base_path = Pathname.new(File.expand_path("../", __FILE__))
120
+ @template_path = @base_path + "fixtures/loops_template.docx"
121
+ @output_path = @base_path + "sandbox/loops.docx"
122
+ @sample_path = @base_path + "fixtures/loops_sample.docx"
123
+ end
124
+
125
+ def test_generate_document_from_template
126
+ template = Sablon.template @template_path
127
+ context = {
128
+ fruits: %w[Apple Blueberry Cranberry Date].map { |i| { name: i } },
129
+ cars: %w[Silverado Serria Ram Tundra].map { |i| { name: i } }
130
+ }
131
+
132
+ template.render_to_file @output_path, context
133
+ assert_docx_equal @sample_path, @output_path
134
+ end
135
+ end
136
+
137
+ class SablonImagesTest < Sablon::TestCase
138
+ def setup
139
+ super
140
+ @base_path = Pathname.new(File.expand_path("../", __FILE__))
141
+ @template_path = @base_path + "fixtures/images_template.docx"
142
+ @output_path = @base_path + "sandbox/images.docx"
143
+ @sample_path = @base_path + "fixtures/images_sample.docx"
144
+ @image_fixtures = @base_path + "fixtures/images"
145
+ end
146
+
147
+ def test_generate_document_from_template
148
+ template = Sablon.template @template_path
149
+ #
150
+ # setup two image contents to allow quick reuse
151
+ r2d2 = Sablon.content(:image, @image_fixtures.join('r2d2.jpg').to_s)
152
+ c3po = Sablon.content(:image, @image_fixtures.join('c3po.jpg'))
153
+ darth = Sablon.content(:image, @image_fixtures.join('darth_vader.jpg'))
154
+ #
155
+ im_data = StringIO.new(IO.binread(@image_fixtures.join('clone.jpg')))
156
+ trooper = Sablon.content(:image, im_data, filename: 'clone.jpg')
157
+ #
158
+ # with the following context setup all trooper should be reused and
159
+ # only a single file added to media. R2D2 should get duplicated in the
160
+ # media folder because it is used in two different context keys as
161
+ # separate instances. Darth Vader should not be duplicated because
162
+ # the ket "unused_darth" doesn't appear in the template
163
+ context = {
164
+ items: [
165
+ { title: 'C-3PO', image: c3po },
166
+ { title: 'R2-D2', image: r2d2 },
167
+ { title: 'Darth Vader', 'image:image' => @image_fixtures.join('darth_vader.jpg') },
168
+ { title: 'Storm Trooper', image: trooper }
169
+ ],
170
+ 'image:r2d2' => @image_fixtures.join('r2d2.jpg'),
171
+ 'unused_darth' => darth,
172
+ trooper: trooper
173
+ }
174
+
103
175
  template.render_to_file @output_path, context
104
176
  assert_docx_equal @sample_path, @output_path
105
177
  end
@@ -1,5 +1,5 @@
1
1
  require "bundler/setup"
2
-
2
+ require 'minitest/assertions'
3
3
  require "minitest/autorun"
4
4
  require "minitest/mock"
5
5
  require "xmlsimple"
@@ -8,26 +8,126 @@ require "pathname"
8
8
 
9
9
  $: << File.expand_path('../../lib', __FILE__)
10
10
  require "sablon"
11
- require "sablon/test"
11
+
12
+ module Minitest
13
+ module Assertions
14
+ def assert_docx_equal(expected_path, actual_path)
15
+ #
16
+ # Parse document archives and generate a diff
17
+ xml_diffs = diff_docx_files(expected_path, actual_path)
18
+ #
19
+ # build error message
20
+ msg = 'The generated document does not match the sample. Please investigate file(s): '
21
+ msg += xml_diffs.keys.sort.join(', ')
22
+ xml_diffs.each do |name, diff_text|
23
+ msg += "\n#{'-' * 72}\nFile: #{name}\n#{diff_text}\n"
24
+ end
25
+ msg += '-' * 72 + "\n"
26
+ msg += "If the generated document is correct, the sample needs to be updated:\n"
27
+ msg += "\t cp #{actual_path} #{expected_path}"
28
+ #
29
+ raise Minitest::Assertion, msg unless xml_diffs.empty?
30
+ end
31
+
32
+ # Returns a hash of all XML files that differ in the docx file. This
33
+ # only checks files that have the extension ".xml" or ".rels".
34
+ def diff_docx_files(expected_path, actual_path)
35
+ expected = parse_docx(expected_path)
36
+ actual = parse_docx(actual_path)
37
+ xml_diffs = {}
38
+ #
39
+ expected.each do |entry_name, expect|
40
+ next unless entry_name =~ /.xml$|.rels$/
41
+ next unless expect != actual[entry_name]
42
+ #
43
+ xml_diffs[entry_name] = diff(expect, actual[entry_name])
44
+ end
45
+ #
46
+ xml_diffs
47
+ end
48
+
49
+ def parse_docx(path)
50
+ contents = {}
51
+ #
52
+ # step over all entries adding them to the hash to diff against
53
+ Zip::File.open(path).each do |entry|
54
+ next unless entry.file?
55
+ content = entry.get_input_stream.read
56
+ # normalize xml content
57
+ if entry.name =~ /.xml$|.rels$/
58
+ content = Nokogiri::XML(content).to_xml(indent: 2)
59
+ end
60
+ contents[entry.name] = content
61
+ end
62
+ #
63
+ contents
64
+ end
65
+ end
66
+ end
12
67
 
13
68
  class Sablon::TestCase < MiniTest::Test
14
69
  def teardown
15
70
  super
16
71
  end
17
72
 
18
- class UIDTestGenerator
73
+ class MockTemplate
74
+ attr_reader :document
75
+
76
+ def initialize
77
+ @path = nil
78
+ @document = MockDomModel.new
79
+ end
80
+ end
81
+
82
+ # catch all for method stubs that are needed during testing
83
+ class MockDomModel
84
+ attr_accessor :current_entry
85
+ attr_reader :current_rid, :zip_contents
86
+
87
+ # Simple class to reload mock document components from fixtures on demand
88
+ class ZipContents
89
+ def [](entry_name)
90
+ load_mock_content(entry_name)
91
+ end
92
+
93
+ private
94
+
95
+ # Loads and parses individual files to build the mock document
96
+ def load_mock_content(entry_name)
97
+ root = Pathname.new(File.dirname(__FILE__))
98
+ xml_path = root.join('fixtures', 'xml', 'mock_document', entry_name)
99
+ Nokogiri::XML(File.read(xml_path))
100
+ end
101
+ end
102
+
19
103
  def initialize
20
- @current_id = 1234
21
- @current_id_start = @current_id
104
+ @current_entry = nil
105
+ @current_rid = 1234
106
+ @current_rid_start = @current_rid
107
+ @current_numid = 0
108
+ @current_numid_start = @current_numid
109
+ @zip_contents = ZipContents.new
110
+ end
111
+
112
+ # Returns the corresponding DOM handled file
113
+ def [](entry_name)
114
+ Sablon::DOM.wrap_with_handler(entry_name, @zip_contents[entry_name])
22
115
  end
23
116
 
24
- def new_uid
25
- @current_id += 1
26
- @current_id.to_s
117
+ def add_relationship(*)
118
+ "rId#{@current_rid += 1}"
119
+ end
120
+
121
+ def add_list_definition(style)
122
+ @current_numid += 1
123
+ Struct.new(:style, :numid).new(style, @current_numid)
27
124
  end
28
125
 
29
126
  def reset
30
- @current_id = @current_id_start
127
+ @current_rid = @current_rid_start
128
+ @current_numid = @current_numid_start
31
129
  end
130
+
131
+ alias add_media add_relationship
32
132
  end
33
133
  end