caracal 1.5.0 → 1.6.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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/push_gem.yml +29 -0
  3. data/CHANGELOG.md +22 -0
  4. data/README.md +72 -4
  5. data/caracal.gemspec +9 -3
  6. data/lib/caracal/core/footer.rb +50 -0
  7. data/lib/caracal/core/header.rb +49 -0
  8. data/lib/caracal/core/models/field_model.rb +131 -0
  9. data/lib/caracal/core/models/footer_model.rb +43 -0
  10. data/lib/caracal/core/models/header_model.rb +43 -0
  11. data/lib/caracal/core/models/iframe_model.rb +39 -4
  12. data/lib/caracal/core/models/image_model.rb +1 -1
  13. data/lib/caracal/core/models/list_item_model.rb +57 -3
  14. data/lib/caracal/core/models/list_model.rb +1 -7
  15. data/lib/caracal/core/models/list_style_model.rb +1 -1
  16. data/lib/caracal/core/models/page_number_model.rb +17 -2
  17. data/lib/caracal/core/models/paragraph_model.rb +16 -1
  18. data/lib/caracal/core/models/relationship_model.rb +1 -0
  19. data/lib/caracal/core/models/table_model.rb +1 -1
  20. data/lib/caracal/core/page_numbers.rb +5 -1
  21. data/lib/caracal/document.rb +98 -29
  22. data/lib/caracal/renderers/content_types_renderer.rb +3 -1
  23. data/lib/caracal/renderers/document_renderer.rb +47 -10
  24. data/lib/caracal/renderers/footer_renderer.rb +57 -50
  25. data/lib/caracal/renderers/header_footer_renderer.rb +90 -0
  26. data/lib/caracal/renderers/header_renderer.rb +28 -0
  27. data/lib/caracal/renderers/relationships_renderer.rb +22 -4
  28. data/lib/caracal/version.rb +1 -1
  29. data/lib/caracal.rb +16 -0
  30. data/spec/lib/caracal/core/footer_spec.rb +37 -0
  31. data/spec/lib/caracal/core/header_spec.rb +48 -0
  32. data/spec/lib/caracal/core/images_spec.rb +7 -0
  33. data/spec/lib/caracal/core/models/footer_model_spec.rb +41 -0
  34. data/spec/lib/caracal/core/models/header_model_spec.rb +41 -0
  35. data/spec/lib/caracal/core/models/image_model_spec.rb +1 -1
  36. data/spec/lib/caracal/core/models/list_item_model_spec.rb +18 -0
  37. data/spec/lib/caracal/core/models/list_style_model_spec.rb +1 -1
  38. data/spec/lib/caracal/core/models/option_keys_spec.rb +47 -0
  39. data/spec/lib/caracal/core/models/page_number_model_spec.rb +16 -1
  40. data/spec/lib/caracal/core/models/paragraph_model_spec.rb +22 -1
  41. data/spec/lib/caracal/core/models/table_model_spec.rb +1 -1
  42. data/spec/lib/caracal/core/relationships_spec.rb +1 -1
  43. data/spec/lib/caracal/document_output_spec.rb +503 -0
  44. metadata +35 -13
@@ -17,7 +17,7 @@ module Caracal
17
17
  #-------------------------------------------------------------
18
18
 
19
19
  # accessors
20
- attr_accessor :nested_list
20
+ attr_writer :continuation
21
21
 
22
22
  # readers (create aliases for superclass methods to conform
23
23
  # to expected naming convention.)
@@ -54,6 +54,51 @@ module Caracal
54
54
  end
55
55
 
56
56
 
57
+ #=============== NESTED LISTS =========================
58
+
59
+ # This method returns the nested lists in the order they were
60
+ # added, each with the number of runs that came before it, so text
61
+ # after a nested list can be rendered after it.
62
+ #
63
+ def nested_lists
64
+ @nested_lists ||= []
65
+ end
66
+
67
+ def nested_list
68
+ (pair = nested_lists.last) && pair.last
69
+ end
70
+
71
+ def nested_list=(model)
72
+ nested_lists << [runs.size, model]
73
+ end
74
+
75
+ # This method returns whether this is the text that follows a
76
+ # nested list, which renders as an unnumbered paragraph indented
77
+ # with the item.
78
+ #
79
+ def continuation?
80
+ !!@continuation
81
+ end
82
+
83
+ # This method returns this item and everything nested in it in
84
+ # document order: the item's text up to its first nested list, the
85
+ # nested list's items, then any text that follows, and so on.
86
+ #
87
+ def recursive_items
88
+ return [self] if nested_lists.empty?
89
+
90
+ items = []
91
+ start = 0
92
+ nested_lists.each do |(index, list)|
93
+ items << segment(start...index, items.any?) if items.empty? || index > start
94
+ items.concat(list.recursive_items)
95
+ start = index
96
+ end
97
+ items << segment(start...runs.size, true) if runs.size > start
98
+ items
99
+ end
100
+
101
+
57
102
  #=============== SUB-METHODS ===========================
58
103
 
59
104
  # .ol
@@ -62,7 +107,7 @@ module Caracal
62
107
 
63
108
  model = Caracal::Core::Models::ListModel.new(options, &block)
64
109
  if model.valid?
65
- @nested_list = model
110
+ self.nested_list = model
66
111
  else
67
112
  raise Caracal::Errors::InvalidModelError, 'Ordered lists require at least one list item.'
68
113
  end
@@ -75,7 +120,7 @@ module Caracal
75
120
 
76
121
  model = Caracal::Core::Models::ListModel.new(options, &block)
77
122
  if model.valid?
78
- @nested_list = model
123
+ self.nested_list = model
79
124
  else
80
125
  raise Caracal::Errors::InvalidModelError, 'Unordered lists require at least one list item.'
81
126
  end
@@ -97,6 +142,15 @@ module Caracal
97
142
  #-------------------------------------------------------------
98
143
  private
99
144
 
145
+ # This method returns a copy of the item holding only the given runs.
146
+ def segment(range, continuation)
147
+ copy = dup
148
+ copy.instance_variable_set(:@runs, runs[range])
149
+ copy.instance_variable_set(:@nested_lists, [])
150
+ copy.continuation = continuation
151
+ copy
152
+ end
153
+
100
154
  def option_keys
101
155
  [:type, :level, :content, :style, :color, :size, :bold, :italic, :underline, :bgcolor]
102
156
  end
@@ -61,13 +61,7 @@ module Caracal
61
61
  # item within this list's tree.
62
62
  #
63
63
  def recursive_items
64
- items.map do |model|
65
- if model.nested_list.nil?
66
- model
67
- else
68
- [model, model.nested_list.recursive_items]
69
- end
70
- end.flatten
64
+ items.flat_map(&:recursive_items)
71
65
  end
72
66
 
73
67
 
@@ -108,7 +108,7 @@ module Caracal
108
108
  private
109
109
 
110
110
  def option_keys
111
- [:type, :level, :format, :value, :align, :left, :indent, :start]
111
+ [:type, :level, :format, :value, :align, :left, :indent, :start, :restart]
112
112
  end
113
113
 
114
114
  end
@@ -24,6 +24,8 @@ module Caracal
24
24
  attr_reader :page_number_label_size
25
25
  attr_reader :page_number_number_size
26
26
  attr_reader :page_number_show
27
+ attr_reader :page_number_first
28
+ attr_reader :page_number_start
27
29
 
28
30
  # initialization
29
31
  def initialize(options={}, &block)
@@ -32,6 +34,8 @@ module Caracal
32
34
  @page_number_label_size = nil
33
35
  @page_number_number_size = nil
34
36
  @page_number_show = DEFAULT_PAGE_NUMBER_SHOW
37
+ @page_number_first = true
38
+ @page_number_start = nil
35
39
 
36
40
  super options, &block
37
41
  end
@@ -65,6 +69,15 @@ module Caracal
65
69
  @page_number_show = !!value
66
70
  end
67
71
 
72
+ # false leaves the page number off the first page
73
+ def first_page(value)
74
+ @page_number_first = !!value
75
+ end
76
+
77
+ def start(value)
78
+ @page_number_start = value.nil? ? nil : value.to_i
79
+ end
80
+
68
81
  def size(value)
69
82
  v = value.to_i
70
83
  @page_number_label_size = (v == 0) ? nil : v
@@ -75,7 +88,9 @@ module Caracal
75
88
  #=============== VALIDATION ===========================
76
89
 
77
90
  def valid?
78
- (!page_number_show || [:left, :center, :right].include?(page_number_align))
91
+ valid_align = (!page_number_show || [:left, :center, :right].include?(page_number_align))
92
+ valid_start = (page_number_start.nil? || page_number_start >= 0)
93
+ valid_align && valid_start
79
94
  end
80
95
 
81
96
 
@@ -85,7 +100,7 @@ module Caracal
85
100
  private
86
101
 
87
102
  def option_keys
88
- [:align, :label, :label_size, :number_size, :show]
103
+ [:align, :label, :label_size, :number_size, :show, :size, :start, :first_page]
89
104
  end
90
105
 
91
106
  end
@@ -2,6 +2,7 @@ require 'caracal/core/models/base_model'
2
2
  require 'caracal/core/models/bookmark_model'
3
3
  require 'caracal/core/models/link_model'
4
4
  require 'caracal/core/models/text_model'
5
+ require 'caracal/core/models/field_model'
5
6
  require 'caracal/errors'
6
7
 
7
8
 
@@ -127,6 +128,20 @@ module Caracal
127
128
  model
128
129
  end
129
130
 
131
+ # .field
132
+ def field(*args, &block)
133
+ options = Caracal::Utilities.extract_options!(args)
134
+ options = options.merge({ type: args[0] })
135
+
136
+ model = Caracal::Core::Models::FieldModel.new(options, &block)
137
+ if model.valid?
138
+ runs << model
139
+ else
140
+ raise Caracal::Errors::InvalidModelError, ':field method must receive :page or :numpages.'
141
+ end
142
+ model
143
+ end
144
+
130
145
  # .link
131
146
  def link(*args, &block)
132
147
  options = Caracal::Utilities.extract_options!(args)
@@ -177,7 +192,7 @@ module Caracal
177
192
  private
178
193
 
179
194
  def option_keys
180
- [:content, :style, :align, :color, :size, :bold, :italic, :underline, :bgcolor]
195
+ [:content, :style, :align, :color, :size, :bold, :italic, :underline, :bgcolor, :keep_next]
181
196
  end
182
197
 
183
198
  end
@@ -18,6 +18,7 @@ module Caracal
18
18
  TYPE_MAP = {
19
19
  font: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable',
20
20
  footer: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer',
21
+ header: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/header',
21
22
  image: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/image',
22
23
  link: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink',
23
24
  numbering: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering',
@@ -232,7 +232,7 @@ module Caracal
232
232
  k << [:data, :align, :width]
233
233
  k << [:border_color, :border_line, :border_size, :border_spacing]
234
234
  k << [:border_bottom, :border_left, :border_right, :border_top, :border_horizontal, :border_vertical]
235
- k << [:column_widths]
235
+ k << [:column_widths, :header_rows]
236
236
  k.flatten
237
237
  end
238
238
 
@@ -25,6 +25,8 @@ module Caracal
25
25
  attr_reader :page_number_label_size
26
26
  attr_reader :page_number_number_size
27
27
  attr_reader :page_number_show
28
+ attr_reader :page_number_first
29
+ attr_reader :page_number_start
28
30
 
29
31
 
30
32
  #-------------------------------------------------------------
@@ -45,8 +47,10 @@ module Caracal
45
47
  @page_number_label_size = model.page_number_label_size
46
48
  @page_number_number_size = model.page_number_number_size
47
49
  @page_number_show = model.page_number_show
50
+ @page_number_first = model.page_number_first
51
+ @page_number_start = model.page_number_start
48
52
  else
49
- raise Caracal::Errors::InvalidModelError, 'page_numbers :align parameter must be :left, :center, or :right'
53
+ raise Caracal::Errors::InvalidModelError, 'page_numbers :align must be :left, :center, or :right, and :start must not be negative.'
50
54
  end
51
55
  end
52
56
 
@@ -4,6 +4,8 @@ require 'caracal/core/bookmarks'
4
4
  require 'caracal/core/custom_properties'
5
5
  require 'caracal/core/file_name'
6
6
  require 'caracal/core/fonts'
7
+ require 'caracal/core/footer'
8
+ require 'caracal/core/header'
7
9
  require 'caracal/core/iframes'
8
10
  require 'caracal/core/ignorables'
9
11
  require 'caracal/core/images'
@@ -26,6 +28,7 @@ require 'caracal/renderers/custom_renderer'
26
28
  require 'caracal/renderers/document_renderer'
27
29
  require 'caracal/renderers/fonts_renderer'
28
30
  require 'caracal/renderers/footer_renderer'
31
+ require 'caracal/renderers/header_renderer'
29
32
  require 'caracal/renderers/numbering_renderer'
30
33
  require 'caracal/renderers/package_relationships_renderer'
31
34
  require 'caracal/renderers/relationships_renderer'
@@ -62,6 +65,9 @@ module Caracal
62
65
  include Caracal::Core::Tables
63
66
  include Caracal::Core::Text
64
67
 
68
+ include Caracal::Core::Footer
69
+ include Caracal::Core::Header
70
+
65
71
 
66
72
  #------------------------------------------------------
67
73
  # Public Class Methods
@@ -128,6 +134,49 @@ module Caracal
128
134
  @contents ||= []
129
135
  end
130
136
 
137
+ # A header or footer part to write: which kind it is, which pages it
138
+ # covers ('default' or 'first'), the content to render, whether to
139
+ # add the page number, and whether the section refers to it.
140
+ #
141
+ HeaderFooterPart = Struct.new(:kind, :type, :target, :model, :page_number, :referenced)
142
+
143
+ # This method returns the header and footer parts the document needs.
144
+ # footer1.xml is always written, as it always has been, but is only
145
+ # referenced when it has something to show.
146
+ #
147
+ def header_footer_parts
148
+ parts = []
149
+ if header_content
150
+ parts << HeaderFooterPart.new(:header, 'default', 'header1.xml', header_content, false, true)
151
+ end
152
+ shown = page_number_show || !footer_content.nil?
153
+ parts << HeaderFooterPart.new(:footer, 'default', 'footer1.xml', footer_content, page_number_show || footer_content.nil?, shown)
154
+
155
+ if title_page?
156
+ # The first page uses only first-page parts, so fall back to the
157
+ # default content for whichever one wasn't customized.
158
+ first_header = first_header_content || header_content
159
+ if first_header && first_header.valid?
160
+ parts << HeaderFooterPart.new(:header, 'first', 'header2.xml', first_header, false, true)
161
+ end
162
+
163
+ first_footer = first_footer_content || footer_content
164
+ first_footer = nil unless first_footer && first_footer.valid?
165
+ first_number = page_number_show && page_number_first
166
+ if first_footer || first_number
167
+ parts << HeaderFooterPart.new(:footer, 'first', 'footer2.xml', first_footer, first_number, true)
168
+ end
169
+ end
170
+ parts
171
+ end
172
+
173
+ # This method returns whether the first page has its own header and
174
+ # footer (Word's "Different First Page").
175
+ #
176
+ def title_page?
177
+ !first_header_content.nil? || !first_footer_content.nil? || (page_number_show && !page_number_first)
178
+ end
179
+
131
180
 
132
181
  #============ RENDERING ===============================
133
182
 
@@ -136,6 +185,9 @@ module Caracal
136
185
  #
137
186
  def render
138
187
  register_nested_iframes(contents)
188
+ header_footer_parts.each do |part|
189
+ relationship({ target: part.target, type: part.kind })
190
+ end
139
191
 
140
192
  buffer = ::Zip::OutputStream.write_buffer do |zip|
141
193
  render_package_relationships(zip)
@@ -144,7 +196,7 @@ module Caracal
144
196
  render_core(zip)
145
197
  render_custom(zip)
146
198
  render_fonts(zip)
147
- render_footer(zip)
199
+ render_headers_and_footers(zip)
148
200
  render_settings(zip)
149
201
  render_styles(zip)
150
202
  render_document(zip)
@@ -193,53 +245,65 @@ module Caracal
193
245
 
194
246
  #============ RENDERERS ===============================
195
247
 
248
+ # This method adds a part to the package. Giving rubyzip the size up
249
+ # front stops rubyzip 3 from adding a zip64 extra field "just in case"
250
+ # to every entry, which LibreOffice 7.4 and older can't read.
251
+ #
252
+ def write_entry(zip, name, content)
253
+ content = content.to_s
254
+ entry = ::Zip::Entry.new('', name)
255
+ entry.size = content.bytesize
256
+
257
+ zip.put_next_entry(entry)
258
+ zip.write(content)
259
+ end
260
+
196
261
  def render_app(zip)
197
262
  content = ::Caracal::Renderers::AppRenderer.render(self)
198
263
 
199
- zip.put_next_entry('docProps/app.xml')
200
- zip.write(content)
264
+ write_entry(zip, 'docProps/app.xml', content)
201
265
  end
202
266
 
203
267
  def render_content_types(zip)
204
268
  content = ::Caracal::Renderers::ContentTypesRenderer.render(self)
205
269
 
206
- zip.put_next_entry('[Content_Types].xml')
207
- zip.write(content)
270
+ write_entry(zip, '[Content_Types].xml', content)
208
271
  end
209
272
 
210
273
  def render_core(zip)
211
274
  content = ::Caracal::Renderers::CoreRenderer.render(self)
212
275
 
213
- zip.put_next_entry('docProps/core.xml')
214
- zip.write(content)
276
+ write_entry(zip, 'docProps/core.xml', content)
215
277
  end
216
278
 
217
279
  def render_custom(zip)
218
280
  content = ::Caracal::Renderers::CustomRenderer.render(self)
219
281
 
220
- zip.put_next_entry('docProps/custom.xml')
221
- zip.write(content)
282
+ write_entry(zip, 'docProps/custom.xml', content)
222
283
  end
223
284
 
224
285
  def render_document(zip)
225
286
  content = ::Caracal::Renderers::DocumentRenderer.render(self)
226
287
 
227
- zip.put_next_entry('word/document.xml')
228
- zip.write(content)
288
+ write_entry(zip, 'word/document.xml', content)
229
289
  end
230
290
 
231
291
  def render_fonts(zip)
232
292
  content = ::Caracal::Renderers::FontsRenderer.render(self)
233
293
 
234
- zip.put_next_entry('word/fontTable.xml')
235
- zip.write(content)
294
+ write_entry(zip, 'word/fontTable.xml', content)
236
295
  end
237
296
 
238
- def render_footer(zip)
239
- content = ::Caracal::Renderers::FooterRenderer.render(self)
297
+ def render_headers_and_footers(zip)
298
+ header_footer_parts.each do |part|
299
+ renderer = (part.kind == :header) ? ::Caracal::Renderers::HeaderRenderer : ::Caracal::Renderers::FooterRenderer
300
+ content = renderer.render(self, part.model, part.page_number)
240
301
 
241
- zip.put_next_entry('word/footer1.xml')
242
- zip.write(content)
302
+ write_entry(zip, "word/#{ part.target }", content)
303
+
304
+ # rendering collects the relationships, so write them straight away
305
+ render_part_relationships(zip, part.target, part.model)
306
+ end
243
307
  end
244
308
 
245
309
  def render_media(zip)
@@ -251,44 +315,49 @@ module Caracal
251
315
  content = ::Caracal::Utilities.read_resource(rel.relationship_target)
252
316
  end
253
317
 
254
- zip.put_next_entry("word/#{ rel.formatted_target }")
255
- zip.write(content)
318
+ write_entry(zip, "word/#{ rel.formatted_target }", content)
256
319
  end
257
320
  end
258
321
 
259
322
  def render_numbering(zip)
260
323
  content = ::Caracal::Renderers::NumberingRenderer.render(self)
261
324
 
262
- zip.put_next_entry('word/numbering.xml')
263
- zip.write(content)
325
+ write_entry(zip, 'word/numbering.xml', content)
264
326
  end
265
327
 
266
328
  def render_package_relationships(zip)
267
329
  content = ::Caracal::Renderers::PackageRelationshipsRenderer.render(self)
268
330
 
269
- zip.put_next_entry('_rels/.rels')
270
- zip.write(content)
331
+ write_entry(zip, '_rels/.rels', content)
271
332
  end
272
333
 
273
334
  def render_relationships(zip)
274
335
  content = ::Caracal::Renderers::RelationshipsRenderer.render(self)
275
336
 
276
- zip.put_next_entry('word/_rels/document.xml.rels')
277
- zip.write(content)
337
+ write_entry(zip, 'word/_rels/document.xml.rels', content)
338
+ end
339
+
340
+ # Headers and footers resolve their images and links against their
341
+ # own relationships part, not the document's.
342
+ #
343
+ def render_part_relationships(zip, part, model)
344
+ return if model.nil? || model.relationships.empty?
345
+
346
+ content = ::Caracal::Renderers::RelationshipsRenderer.render(self, model.relationships)
347
+
348
+ write_entry(zip, "word/_rels/#{ part }.rels", content)
278
349
  end
279
350
 
280
351
  def render_settings(zip)
281
352
  content = ::Caracal::Renderers::SettingsRenderer.render(self)
282
353
 
283
- zip.put_next_entry('word/settings.xml')
284
- zip.write(content)
354
+ write_entry(zip, 'word/settings.xml', content)
285
355
  end
286
356
 
287
357
  def render_styles(zip)
288
358
  content = ::Caracal::Renderers::StylesRenderer.render(self)
289
359
 
290
- zip.put_next_entry('word/styles.xml')
291
- zip.write(content)
360
+ write_entry(zip, 'word/styles.xml', content)
292
361
  end
293
362
 
294
363
  end
@@ -31,7 +31,9 @@ module Caracal
31
31
  xml.send 'Override', { 'PartName' => '/docProps/core.xml', 'ContentType' => 'application/vnd.openxmlformats-package.core-properties+xml' }
32
32
  xml.send 'Override', { 'PartName' => '/docProps/custom.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.custom-properties+xml' }
33
33
  xml.send 'Override', { 'PartName' => '/word/document.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml' }
34
- xml.send 'Override', { 'PartName' => '/word/footer1.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml' }
34
+ document.header_footer_parts.each do |part|
35
+ xml.send 'Override', { 'PartName' => "/word/#{ part.target }", 'ContentType' => "application/vnd.openxmlformats-officedocument.wordprocessingml.#{ part.kind }+xml" }
36
+ end
35
37
  xml.send 'Override', { 'PartName' => '/word/fontTable.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml' }
36
38
  xml.send 'Override', { 'PartName' => '/word/numbering.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml' }
37
39
  xml.send 'Override', { 'PartName' => '/word/settings.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml' }
@@ -31,13 +31,17 @@ module Caracal
31
31
  #============= PAGE SETTINGS ==============================
32
32
 
33
33
  xml['w'].sectPr do
34
- if document.page_number_show
35
- if rel = document.find_relationship('footer1.xml')
36
- xml['w'].footerReference({ 'r:id' => rel.formatted_id, 'w:type' => 'default' })
34
+ document.header_footer_parts.select(&:referenced).each do |part|
35
+ if rel = document.find_relationship(part.target)
36
+ xml['w'].send("#{ part.kind }Reference", { 'r:id' => rel.formatted_id, 'w:type' => part.type })
37
37
  end
38
38
  end
39
39
  xml['w'].pgSz page_size_options
40
40
  xml['w'].pgMar page_margin_options
41
+ unless document.page_number_start.nil?
42
+ xml['w'].pgNumType({ 'w:start' => document.page_number_start })
43
+ end
44
+ xml['w'].titlePg if document.title_page?
41
45
  end
42
46
 
43
47
  end
@@ -63,6 +67,13 @@ module Caracal
63
67
  "render_#{ type }"
64
68
  end
65
69
 
70
+ # This method registers a relationship needed by the part being
71
+ # rendered. Images and links in the body belong to the document.
72
+ #
73
+ def relationship(options)
74
+ document.relationship(options)
75
+ end
76
+
66
77
  # This method renders a standard node of run properties based on the
67
78
  # model's attributes.
68
79
  #
@@ -113,7 +124,7 @@ module Caracal
113
124
  r_href = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
114
125
 
115
126
  entry = zip.glob('word/document.xml').first
116
- content = entry.get_input_stream.read
127
+ content = Caracal::Core::Models::IFrameModel.read_entry(entry)
117
128
  doc_xml = Nokogiri::XML(content)
118
129
 
119
130
  fragment = doc_xml.xpath('//w:body').first.children
@@ -145,7 +156,7 @@ module Caracal
145
156
  raise Caracal::Errors::NoDefaultStyleError 'Document must declare a default paragraph style.'
146
157
  end
147
158
 
148
- rel = document.relationship({ type: :image, target: model.image_url, data: model.image_data })
159
+ rel = relationship({ type: :image, target: model.image_url, data: model.image_data })
149
160
  rel_id = rel.relationship_id
150
161
  rel_name = rel.formatted_target
151
162
 
@@ -205,7 +216,7 @@ module Caracal
205
216
 
206
217
  def render_link(xml, model)
207
218
  if model.external?
208
- rel = document.relationship({ target: model.link_href, type: :link })
219
+ rel = relationship({ target: model.link_href, type: :link })
209
220
  hyperlink_options = { 'r:id' => rel.formatted_id }
210
221
  else
211
222
  hyperlink_options = { 'w:anchor' => model.link_href }
@@ -219,6 +230,27 @@ module Caracal
219
230
  end
220
231
  end
221
232
 
233
+ # A complex field is a sequence of runs: begin, instruction, separate
234
+ # and end. Viewers that don't evaluate fields show the instruction
235
+ # text when the field characters aren't in runs of their own.
236
+ #
237
+ def render_field(xml, model)
238
+ xml['w'].r run_options do
239
+ render_run_attributes(xml, model, false)
240
+ xml['w'].fldChar({ 'w:fldCharType' => 'begin' })
241
+ end
242
+ xml['w'].r run_options do
243
+ render_run_attributes(xml, model, false)
244
+ xml['w'].instrText({ 'xml:space' => 'preserve' }, " #{ model.formatted_type } ")
245
+ end
246
+ %w(separate end).each do |type|
247
+ xml['w'].r run_options do
248
+ render_run_attributes(xml, model, false)
249
+ xml['w'].fldChar({ 'w:fldCharType' => type })
250
+ end
251
+ end
252
+ end
253
+
222
254
  def render_list(xml, model)
223
255
  if model.list_level == 0
224
256
  document.toplevel_lists << model
@@ -236,11 +268,16 @@ module Caracal
236
268
 
237
269
  xml['w'].p paragraph_options do
238
270
  xml['w'].pPr do
239
- xml['w'].numPr do
240
- xml['w'].ilvl({ 'w:val' => model.list_item_level })
241
- xml['w'].numId({ 'w:val' => list_num })
271
+ if model.continuation?
272
+ # text after a nested list: no number, aligned with the item's text
273
+ xml['w'].ind({ 'w:left' => ls.style_left })
274
+ else
275
+ xml['w'].numPr do
276
+ xml['w'].ilvl({ 'w:val' => model.list_item_level })
277
+ xml['w'].numId({ 'w:val' => list_num })
278
+ end
279
+ xml['w'].ind({ 'w:left' => ls.style_left, 'w:hanging' => hanging })
242
280
  end
243
- xml['w'].ind({ 'w:left' => ls.style_left, 'w:hanging' => hanging })
244
281
  xml['w'].contextualSpacing({ 'w:val' => '1' })
245
282
  xml['w'].rPr do
246
283
  xml['w'].u({ 'w:val' => 'none' })