odf-report 0.8.0 → 0.9.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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +29 -1
  3. data/CLAUDE.md +50 -0
  4. data/Gemfile +1 -1
  5. data/README.md +27 -14
  6. data/Rakefile +3 -3
  7. data/lib/odf-report/composable.rb +51 -0
  8. data/lib/odf-report/data_source.rb +34 -28
  9. data/lib/odf-report/field.rb +5 -14
  10. data/lib/odf-report/image.rb +14 -16
  11. data/lib/odf-report/nestable.rb +13 -44
  12. data/lib/odf-report/parser/default.rb +56 -75
  13. data/lib/odf-report/report.rb +29 -72
  14. data/lib/odf-report/section.rb +3 -14
  15. data/lib/odf-report/table.rb +15 -34
  16. data/lib/odf-report/template.rb +10 -24
  17. data/lib/odf-report/text.rb +8 -24
  18. data/lib/odf-report/version.rb +1 -1
  19. data/lib/odf-report.rb +17 -16
  20. data/odf-report.gemspec +10 -14
  21. data/spec/fields_spec.rb +3 -20
  22. data/spec/images/images_spec.rb +132 -0
  23. data/spec/{sections_spec.rb → sections/sections_spec.rb} +4 -10
  24. data/spec/sections/sub_sections_spec.rb +94 -0
  25. data/spec/spec_helper.rb +8 -11
  26. data/spec/tables/nested_tables_spec.rb +62 -0
  27. data/spec/tables/table_headers_spec.rb +54 -0
  28. data/spec/{tables_spec.rb → tables/tables_spec.rb} +2 -10
  29. data/spec/template_spec.rb +1 -9
  30. data/spec/templates/sections/sub_sections.odt +0 -0
  31. data/spec/templates/tables/nested_tables.odt +0 -0
  32. data/spec/templates/tables/table_headers.odt +0 -0
  33. data/spec/templates/texts/fields_inside_text.odt +0 -0
  34. data/spec/templates/texts/text.odt +0 -0
  35. data/spec/texts/fields_inside_text_spec.rb +50 -0
  36. data/spec/texts/texts_spec.rb +101 -0
  37. data/test/fields_inside_text_test.rb +14 -19
  38. data/test/images_test.rb +16 -19
  39. data/test/nested_tables_test.rb +28 -35
  40. data/test/sections_test.rb +29 -36
  41. data/test/sub_sections_test.rb +39 -47
  42. data/test/table_headers_test.rb +28 -35
  43. data/test/tables_test.rb +61 -67
  44. data/test/test.rb +203 -203
  45. data/test/text_test.rb +31 -39
  46. metadata +22 -50
  47. data/spec/images_spec.rb +0 -159
  48. data/spec/texts_spec.rb +0 -48
  49. /data/spec/templates/{images.odt → images/images.odt} +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 14183fe9ba54b5f98cd72cd48aa1277aa9d5b576c7980ae2bbfeef2346c9c49d
4
- data.tar.gz: dcf77b2934897f3b490effdd62a1ee1effc2d13898523249b81b4be62be1eea9
3
+ metadata.gz: e779540093e247d2640705b1e099c7c651e2cef975468fa0970f69110223fa05
4
+ data.tar.gz: a14b575ef72783bd3f735db7824a8e2aeea122161d07535fa1ab632f5e98ad7e
5
5
  SHA512:
6
- metadata.gz: 7eb665c1fe1f414dac1b5e3ef3714b398d207125b00589573f2e2e9a2d4632355f26bf968fe3eec53c7bdcdff92c3fe24f5216fe18a1e160ddd4c8947804390a
7
- data.tar.gz: 237f77a79aa11754aff00d0973c696dc9f62a8ce65b6b57c1c101293b632ee0399025f13b04f9649feaeda1d63310b58ac83ac67d5565cc602c9b2bcd5be127c
6
+ metadata.gz: 050a25739018f420a6985a30633c889a9e9ac599b3eed318034690d1533e9d4208d5e48e3160fcd1375ea7c9677a13519f1036f7366ceca518d062c8cb2e7ae5
7
+ data.tar.gz: 6116ecfce938d03174b814d9ea27b72f0462c0cb8a19e105dc49440033b163e1365a6cad6f2bd292459ba409db60e1013d4b78d47486dcb60fd11f333ff781bb
data/CHANGELOG.md CHANGED
@@ -18,10 +18,38 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
18
18
 
19
19
  - None
20
20
 
21
+ ## 0.9.0
22
+
23
+ ### Refactored
24
+
25
+ - Extracted shared DSL (`add_field`, `add_text`, `add_image`, `add_table`, `add_section`) into `Composable` module, included by both `Report` and `Nestable`
26
+ - Unified `DataSource` to use lazy evaluation — `set_source` stores the record, `value` decides extraction strategy at read time
27
+ - Broke up `Report#generate` into focused private methods (`replace_placeholders!`, `include_images`)
28
+ - Simplified `Table` internals: renamed methods (`get_next_row` → `next_row`), removed redundant code
29
+ - Cleaned up `Image`: single XPath lookup, `image_href` helper, removed unused parameters
30
+ - Cleaned up `Template`: cached ZIP entries, renamed methods for clarity
31
+ - Cleaned up `Text`: removed `attr_accessor`, simplified `find_text_node`
32
+ - Cleaned up `Field`: removed dead code in `to_placeholder`
33
+ - Cleaned up `Parser::Default`: tightened visibility, simplified `check_style`
34
+ - Lazy-initialized all replacer arrays (no more `init_replacers`)
35
+
36
+ ### Testing
37
+
38
+ - Added RSpec specs for nested tables, sub-sections, table headers, fields inside text, and rich text in sections/tables (19 → 38 examples)
39
+ - Organized specs and templates into grouped subfolders (`tables/`, `sections/`, `texts/`, `images/`)
40
+ - Decoupled spec templates from legacy test directory
41
+
42
+ ## 0.8.1
43
+
44
+ ### Fixed
45
+ - \<br\> replacement in default text parser #130
46
+
21
47
  ## 0.8.0
22
48
 
23
- ### Dependencies
49
+ ### Fixed
50
+ - Use Nokogiri HTML5 parser for text nodes parsing #129
24
51
 
52
+ ### Dependencies
25
53
  - nokogiri >= 1.12.0 (was >= 1.10.0)
26
54
 
27
55
  ## 0.7.3
data/CLAUDE.md ADDED
@@ -0,0 +1,50 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ Ruby gem that generates `.odt` (OpenDocument Text) files by taking a template and replacing placeholders with data. ODF files are ZIP archives containing XML; the gem uses `rubyzip` for ZIP manipulation and `nokogiri` for XML parsing.
8
+
9
+ ## Common Commands
10
+
11
+ ```bash
12
+ bundle install # Install dependencies
13
+ bundle exec rspec # Run RSpec test suite
14
+ bundle exec rake test # Run legacy test suite (test/ directory)
15
+ bundle exec rspec spec/fields_spec.rb # Run single spec file
16
+ bundle exec rspec spec/fields_spec.rb -e "some description" # Run specific example
17
+ standardrb # Lint check
18
+ standardrb --fix # Auto-fix lint issues
19
+ bundle exec rake open # Open generated test result .odt files
20
+ ```
21
+
22
+ ## Architecture
23
+
24
+ **Processing pipeline:** `Report` collects replacement definitions via its block DSL (`add_field`, `add_table`, `add_section`, `add_image`, `add_text`). On `generate`, `Template` extracts `content.xml` and `styles.xml` from the ZIP, then each replacer modifies the Nokogiri XML in-place. Order: sections → tables → texts → fields → images. The result is written back to a new ZIP.
25
+
26
+ **Key classes (all under `ODFReport` module in `lib/odf-report/`):**
27
+
28
+ - `Report` — Main entry point and public API. Includes `Composable` for the DSL. `generate` delegates to `replace_placeholders!` (XML substitution) and `include_images` (ZIP + manifest updates).
29
+ - `Template` — Handles .odt file I/O (ZIP read/write). Accepts file path or `io:` buffer. Caches the ZIP handle via `template_entries`.
30
+ - `Composable` — Module included by both `Report` and `Nestable`. Provides `add_field`, `add_text`, `add_image`, `add_table`, `add_section`, `all_images`, and lazy-initialized arrays (`fields`, `texts`, `tables`, `sections`, `images`).
31
+ - `Nestable` — Base class for `Table` and `Section`. Includes `Composable`. Adds `@name`, `@data_source`, `set_source`, `replace_with!`, and `wrap_with_ns`.
32
+ - `Table` — Finds a named ODF table, clones its template row for each collection item.
33
+ - `Section` — Finds a named ODF section, clones it for each collection item. Supports nesting.
34
+ - `Field` — Replaces `[PLACEHOLDER]` text nodes in XML. Names are uppercased automatically.
35
+ - `Text` — Extends Field; parses HTML content and inserts ODF paragraphs via `Parser::Default`.
36
+ - `Image` — Replaces placeholder images (matched by draw frame name) with actual image files. Uses `image_href` class method for path building. Updates the ZIP manifest.
37
+ - `DataSource` — Unified value/extraction model. When `set_source` is not called (Report path), `value` returns the literal. When `set_source` is called (Nestable path), `value` extracts from the record using `@field` as a lookup key. Supports hashes, method calls, method chains, methods with arguments, and block transforms.
38
+ - `Parser::Default` — Converts HTML tags (`<br>`, `<p>`, `<strong>`, `<em>`, etc.) to ODF XML elements.
39
+
40
+ ## Testing
41
+
42
+ Tests use RSpec 3.0. The `Inspector` helper class (defined in `spec/spec_helper.rb`) opens a generated .odt result file and provides:
43
+ - `@data.text` — all text content as a string
44
+ - `@data.xml` — parsed Nokogiri document for XPath queries
45
+
46
+ Test pattern: `before(:context)` generates an .odt file into `spec/result/`, then `it` blocks verify content using `Inspector`. Legacy tests in `test/` follow a similar pattern with output to `test/result/`.
47
+
48
+ ## Code Style
49
+
50
+ Uses **standardrb** (Ruby Standard). No custom rubocop config.
data/Gemfile CHANGED
@@ -1,4 +1,4 @@
1
- source 'https://rubygems.org'
1
+ source "https://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in odf-report.gemspec
4
4
  gemspec
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
 
2
2
  # ODF-REPORT
3
3
 
4
- Gem for generating .odt files by making strings, images, tables and sections replacements in a previously created .odt file.
4
+ Gem for generating .odt files by making string, image, table and section replacements in a previously created .odt file.
5
5
 
6
6
  ## INSTALL
7
7
 
@@ -16,17 +16,18 @@ gem 'odf-report'
16
16
 
17
17
  First of all, you need a `.odt` file to serve as a template.
18
18
  Templates are normal .odt files with `[PLACEHOLDERS]` for *substitutions*.
19
- There are *four* kinds of substitutions available:
19
+ There are *five* kinds of substitutions available:
20
20
  * fields
21
+ * texts
21
22
  * tables
22
23
  * images
23
24
  * sections
24
25
 
25
26
  #### Fields
26
27
 
27
- It's just an upcase sentence, surrounded by brackets. It will be replaced by the value you supply.
28
+ It's just an uppercase sentence, surrounded by brackets. It will be replaced by the value you supply.
28
29
 
29
- In the folowing example:
30
+ In the following example:
30
31
 
31
32
  ```ruby
32
33
  report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
@@ -35,20 +36,32 @@ report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
35
36
  end
36
37
  ```
37
38
 
38
- All occurences of `[USER_NAME]` found in the file will be replaced by the value of `@user.name` whereas all `[ADDRESS]` 'es will contains `My new address`
39
+ All occurrences of `[USER_NAME]` found in the file will be replaced by the value of `@user.name` whereas all `[ADDRESS]` 'es will contain `My new address`
39
40
 
40
41
 
42
+ #### Texts (HTML formatted content)
43
+
44
+ You can also use `add_text` to replace a placeholder with rich HTML content, which gets converted into ODF-formatted paragraphs with support for inline formatting (`<strong>`, `<em>`, `<u>`), headings, blockquotes, and line breaks.
45
+
46
+ ```ruby
47
+ report = ODFReport::Report.new("my_template.odt") do |r|
48
+ r.add_text :description, '<p>A paragraph with <strong>bold</strong> and <em>italic</em> text.</p>'
49
+ end
50
+ ```
51
+
52
+ For full details on supported tags, required template styles, and examples, see the [Using add_text with HTML formatting](https://github.com/sandrods/odf-report/wiki/Using-add_text-with-HTML-formatting) wiki page.
53
+
41
54
  #### Tables
42
55
 
43
56
  To use table placeholders, you should create a Table in your document and give it a name. In OpenOffice, it's just a matter of right-clicking the table you just created, choose `Table Properties...` and type a name in the Name field.
44
57
 
45
58
  If you inform `header: true`, the first row will be treated as a *header* and left untouched. The remaining rows will be used as the template for the table.
46
59
 
47
- If you have more than one template row, they will be cycled. This is usefull for making zebra tables.
60
+ If you have more than one template row, they will be cycled. This is useful for making zebra tables.
48
61
 
49
- As with **Field placeholders**, just insert a `[FIELD_NAME]` in each cell and let the magic takes place.
62
+ As with **Field placeholders**, just insert a `[FIELD_NAME]` in each cell and let the magic take place.
50
63
 
51
- Taking the folowing example:
64
+ Taking the following example:
52
65
 
53
66
  ```ruby
54
67
  report = ODFReport::Report.new("Users/john/my_template.odt") do |r|
@@ -77,9 +90,9 @@ Any format applied to the fields in the template will be preserved.
77
90
 
78
91
  #### Sections
79
92
 
80
- Sometimes, you have to repeat a whole chunk of a document, in a structure a lot more complex than a table. You can make a Section in your template and use it in this situations. Creating a Section in OpenOffice is as easy as select menu *Insert* and then *Section...*, and then choose a name for it.
93
+ Sometimes, you have to repeat a whole chunk of a document, in a structure a lot more complex than a table. You can make a Section in your template and use it in these situations. Creating a Section in OpenOffice is as easy as selecting menu *Insert* and then *Section...*, and then choose a name for it.
81
94
 
82
- Sections are lot like Tables, in the sense that you can pass a collection and have that section repeated for each member of the collection. *But*, Sections can have anything inside it, even Tables *and nested Sections*, as long as you provide the appropriate data structure.
95
+ Sections are a lot like Tables, in the sense that you can pass a collection and have that section repeated for each member of the collection. *But*, Sections can have anything inside them, even Tables *and nested Sections*, as long as you provide the appropriate data structure.
83
96
 
84
97
  Let's see an example:
85
98
 
@@ -107,7 +120,7 @@ Let's see an example:
107
120
  if invoice.status == 'CLOSED'
108
121
  invoice.total
109
122
  else
110
- invoice.items.sum('product_value')}
123
+ invoice.items.sum('product_value')
111
124
  end
112
125
  end
113
126
 
@@ -217,15 +230,15 @@ report = ODFReport::Report.new(io: @template.attachment.read) do |r|
217
230
 
218
231
  **rubyzip**: manipulating the contents of the odt file, since it's actually a zip file.
219
232
  **nokogiri**: parsing and manipulating the document xml files.
220
- **mime-types**: identify images mime types
233
+ **mime-types**: identify image MIME types
221
234
 
222
235
  #### TROUBLESHOOTING
223
236
 
224
237
  ##### Placeholder not replaced
225
238
 
226
- If your placeholder is not being replaced, the problem might come from OpenOffice/LibreOffice which, when a placeholder is edited, add some markup that prevents odf-report from identifying the placeholder.
239
+ If your placeholder is not being replaced, the problem might come from OpenOffice/LibreOffice which, when a placeholder is edited, adds some markup that prevents odf-report from identifying the placeholder.
227
240
 
228
- The golden rule is: NEVER edit the placeholders. If you want to change one, delete it an write again, including the []
241
+ The golden rule is: NEVER edit the placeholders. If you want to change one, delete it and write again, including the []
229
242
  Example: if you have, say, [USER] in your template and you want to change to [USERNAME], you should not edit and type NAME.
230
243
  Delete the PLACEHOLDER [USER] and type [USERNAME].
231
244
 
data/Rakefile CHANGED
@@ -1,9 +1,9 @@
1
1
  require "bundler/gem_tasks"
2
- require 'launchy'
2
+ require "launchy"
3
3
  task :test do
4
- Dir.glob('./test/*_test.rb').each { |file| require file}
4
+ Dir.glob("./test/*_test.rb").each { |file| require file }
5
5
  end
6
6
 
7
7
  task :open do
8
- Dir.glob('./test/result/*.odt').each { |file| Launchy.open(file) }
8
+ Dir.glob("./test/result/*.odt").each { |file| Launchy.open(file) }
9
9
  end
@@ -0,0 +1,51 @@
1
+ module ODFReport
2
+ module Composable
3
+ def add_field(name, value = nil, &block)
4
+ fields << Field.new({name: name, value: value}, &block)
5
+ end
6
+
7
+ def add_text(name, value = nil, &block)
8
+ texts << Text.new({name: name, value: value}, &block)
9
+ end
10
+
11
+ def add_image(name, value = nil, &block)
12
+ images << Image.new({name: name, value: value}, &block)
13
+ end
14
+
15
+ def add_table(table_name, collection, opts = {})
16
+ opts[:name] = table_name
17
+ opts[:value] = collection
18
+
19
+ tab = Table.new(opts)
20
+ tables << tab
21
+
22
+ yield(tab)
23
+ end
24
+
25
+ def add_section(section_name, collection, opts = {})
26
+ opts[:name] = section_name
27
+ opts[:value] = collection
28
+
29
+ sec = Section.new(opts)
30
+ sections << sec
31
+
32
+ yield(sec)
33
+ end
34
+
35
+ def all_images
36
+ (images.map(&:files) + sections.map(&:all_images) + tables.map(&:all_images)).flatten
37
+ end
38
+
39
+ private
40
+
41
+ def fields = @fields ||= []
42
+
43
+ def texts = @texts ||= []
44
+
45
+ def tables = @tables ||= []
46
+
47
+ def sections = @sections ||= []
48
+
49
+ def images = @images ||= []
50
+ end
51
+ end
@@ -1,65 +1,71 @@
1
1
  module ODFReport
2
2
  class DataSource
3
-
4
- attr_reader :value
5
-
6
3
  def initialize(opts, &block)
7
- @value = opts[:value] || opts[:collection]
8
- @data_field = opts[:data_field] || opts[:collection_field] || opts[:name]
9
- @block = block
4
+ @value = opts[:value]
5
+ @field = opts[:value] || opts[:name]
6
+ @block = block
7
+ end
8
+
9
+ def value
10
+ if @record
11
+ extract_value_from(@record)
12
+ else
13
+ @value
14
+ end
10
15
  end
11
16
 
12
17
  def set_source(record)
13
- @value = extract_value_from_item(record)
18
+ @record = record
14
19
  end
15
20
 
16
21
  def each(&block)
17
- return unless @value
18
- @value.each(&block)
22
+ return unless value
23
+ value.each(&block)
19
24
  end
20
25
 
21
26
  def empty?
22
- @value.nil? || @value.empty?
27
+ value.nil? || value.empty?
23
28
  end
24
29
 
25
30
  private
26
31
 
27
- def extract_value_from_item(record)
28
-
32
+ def extract_value_from(record)
29
33
  if @block
34
+ # Block transform: add_field(:name) { |item| item.name.upcase }
30
35
  @block.call(record)
31
36
 
32
37
  elsif record.is_a?(Hash)
33
- key = @data_field
34
- record[key] || record[key.to_s.downcase] || record[key.to_s.upcase] || record[key.to_s.downcase.to_sym]
38
+ # Hash lookup: tries symbol, lowercase string, uppercase string, lowercase symbol
39
+ record[@field] || record[@field.to_s.downcase] || record[@field.to_s.upcase] || record[@field.to_s.downcase.to_sym]
35
40
 
36
- elsif @data_field.is_a?(Array)
37
- execute_methods_on_item(record)
41
+ elsif @field.is_a?(Array)
42
+ # Method chain: add_field(:name, [:company, :name]) calls record.company.name
43
+ execute_methods_on(record)
38
44
 
39
- elsif @data_field.is_a?(Hash) && record.respond_to?(@data_field.keys[0])
40
- record.send(@data_field.keys[0], @data_field.values[0])
45
+ elsif @field.is_a?(Hash) && record.respond_to?(@field.keys[0])
46
+ # Method with argument: add_field(:name, {full_name: :upcase}) calls record.full_name(:upcase)
47
+ record.send(@field.keys[0], @field.values[0])
41
48
 
42
- elsif record.respond_to?(@data_field)
43
- record.send(@data_field)
49
+ elsif record.respond_to?(@field)
50
+ # Simple method call: add_field(:name, :email) calls record.email
51
+ record.send(@field)
44
52
 
45
53
  else
46
- raise "Can't find [#{@data_field.to_s}] in this #{record.class}"
54
+ raise "Can't find [#{@field}] in this #{record.class}"
47
55
 
48
56
  end
49
-
50
57
  end
51
58
 
52
- def execute_methods_on_item(record)
59
+ def execute_methods_on(record)
53
60
  tmp = record.dup
54
- @data_field.each do |f|
55
- if f.is_a?(Hash)
56
- tmp = tmp.send(f.keys[0], f.values[0])
61
+ @field.each do |f|
62
+ tmp = if f.is_a?(Hash)
63
+ tmp.send(f.keys[0], f.values[0])
57
64
  else
58
- tmp = tmp.send(f)
65
+ tmp.send(f)
59
66
  end
60
67
  end
61
68
  tmp
62
69
  end
63
-
64
70
  end
65
71
  end
@@ -1,6 +1,5 @@
1
1
  module ODFReport
2
2
  class Field
3
-
4
3
  DELIMITERS = %w([ ])
5
4
 
6
5
  def initialize(opts, &block)
@@ -13,33 +12,26 @@ module ODFReport
13
12
  self
14
13
  end
15
14
 
16
- def replace!(content, data_item = nil)
17
-
15
+ def replace!(content)
18
16
  txt = content.inner_html
19
17
 
20
18
  if txt.gsub!(to_placeholder, sanitize(@data_source.value))
21
19
  content.inner_html = txt
22
20
  end
23
-
24
21
  end
25
22
 
26
- private
23
+ private
27
24
 
28
25
  def to_placeholder
29
- if DELIMITERS.is_a?(Array)
30
- "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
31
- else
32
- "#{DELIMITERS}#{@name.to_s.upcase}#{DELIMITERS}"
33
- end
26
+ "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
34
27
  end
35
28
 
36
29
  def sanitize(txt)
37
30
  txt = html_escape(txt)
38
- txt = odf_linebreak(txt)
39
- txt
31
+ odf_linebreak(txt)
40
32
  end
41
33
 
42
- HTML_ESCAPE = { '&' => '&amp;', '>' => '&gt;', '<' => '&lt;', '"' => '&quot;' }
34
+ HTML_ESCAPE = {"&" => "&amp;", ">" => "&gt;", "<" => "&lt;", '"' => "&quot;"}
43
35
 
44
36
  def html_escape(s)
45
37
  return "" unless s
@@ -50,6 +42,5 @@ module ODFReport
50
42
  return "" unless s
51
43
  s.to_s.gsub("\n", "<text:line-break/>")
52
44
  end
53
-
54
45
  end
55
46
  end
@@ -1,6 +1,5 @@
1
1
  module ODFReport
2
2
  class Image < Field
3
-
4
3
  IMAGE_DIR_NAME = "Pictures"
5
4
 
6
5
  attr_reader :files
@@ -10,48 +9,47 @@ module ODFReport
10
9
  super
11
10
  end
12
11
 
13
- def replace!(doc, data_item = nil)
14
-
12
+ def replace!(doc)
15
13
  frame = doc.xpath("//draw:frame[@draw:name='#{@name}']").first
16
- image = doc.xpath("//draw:frame[@draw:name='#{@name}']/draw:image").first
14
+ return unless frame
17
15
 
16
+ image = frame.at_xpath("draw:image")
18
17
  return unless image
19
18
 
20
19
  file = @data_source.value
21
20
 
22
21
  if file
23
- image.attribute('href').content = File.join(IMAGE_DIR_NAME, File.basename(file))
24
- frame.attribute('name').content = SecureRandom.uuid
22
+ image.attribute("href").content = self.class.image_href(file)
23
+ frame.attribute("name").content = SecureRandom.uuid
25
24
 
26
25
  @files << file
27
26
  else
28
27
  frame.remove
29
28
  end
30
-
31
29
  end
32
30
 
33
31
  def self.include_image_file(zip_file, image_file)
34
32
  return unless image_file
35
33
 
36
- href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
37
-
38
- zip_file.update_file(href, File.read(image_file))
34
+ zip_file.update_file(image_href(image_file), File.read(image_file))
39
35
  end
40
36
 
41
37
  def self.include_manifest_entry(content, image_file)
42
38
  return unless image_file
43
39
 
44
- return unless root_node = content.at("//manifest:manifest")
40
+ return unless (root_node = content.at("//manifest:manifest"))
45
41
 
46
- href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
42
+ href = image_href(image_file)
47
43
 
48
- entry = content.create_element('manifest:file-entry')
49
- entry['manifest:full-path'] = href
50
- entry['manifest:media-type'] = MIME::Types.type_for(href)[0].content_type
44
+ entry = content.create_element("manifest:file-entry")
45
+ entry["manifest:full-path"] = href
46
+ entry["manifest:media-type"] = MIME::Types.type_for(href)[0].content_type
51
47
 
52
48
  root_node.add_child entry
53
-
54
49
  end
55
50
 
51
+ def self.image_href(file)
52
+ File.join(IMAGE_DIR_NAME, File.basename(file))
53
+ end
56
54
  end
57
55
  end
@@ -1,65 +1,34 @@
1
1
  module ODFReport
2
2
  class Nestable
3
+ include Composable
3
4
 
4
5
  def initialize(opts)
5
6
  @name = opts[:name]
6
7
 
7
8
  @data_source = DataSource.new(opts)
8
-
9
- @fields = []
10
- @texts = []
11
- @tables = []
12
- @sections = []
13
- @images = []
14
-
15
9
  end
16
10
 
11
+ alias_method :add_column, :add_field
12
+
17
13
  def set_source(data_item)
18
14
  @data_source.set_source(data_item)
19
15
  self
20
16
  end
21
17
 
22
- def add_field(name, data_field=nil, &block)
23
- opts = { name: name, data_field: data_field }
24
- @fields << Field.new(opts, &block)
25
- end
26
- alias_method :add_column, :add_field
27
-
28
- def add_text(name, data_field=nil, &block)
29
- opts = {name: name, data_field: data_field}
30
- @texts << Text.new(opts, &block)
31
- end
32
-
33
- def add_image(name, data_field=nil, &block)
34
- opts = {name: name, data_field: data_field}
35
- @images << Image.new(opts, &block)
36
- end
37
-
38
- def add_table(table_name, collection_field, opts={})
39
- opts.merge!(name: table_name, collection_field: collection_field)
40
- tab = Table.new(opts)
41
- @tables << tab
42
-
43
- yield(tab)
44
- end
45
-
46
- def add_section(section_name, collection_field, opts={})
47
- opts.merge!(name: section_name, collection_field: collection_field)
48
- sec = Section.new(opts)
49
- @sections << sec
50
-
51
- yield(sec)
52
- end
53
-
54
- def all_images
55
- (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten
56
- end
57
-
58
18
  def wrap_with_ns(node)
59
19
  <<-XML
60
20
  <root xmlns:draw="a" xmlns:xlink="b" xmlns:text="c" xmlns:table="d">#{node.to_xml}</root>
61
21
  XML
62
22
  end
63
-
23
+
24
+ def replace_with!(record, node)
25
+ tables.each { |t| t.set_source(record).replace!(node) }
26
+ sections.each { |s| s.set_source(record).replace!(node) }
27
+ texts.each { |t| t.set_source(record).replace!(node) }
28
+ fields.each { |f| f.set_source(record).replace!(node) }
29
+ images.each { |i| i.set_source(record).replace!(node) }
30
+ end
31
+
32
+ private
64
33
  end
65
34
  end