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