odf-report 0.8.1 → 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 +21 -0
  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 +30 -20
  9. data/lib/odf-report/field.rb +2 -6
  10. data/lib/odf-report/image.rb +10 -7
  11. data/lib/odf-report/nestable.rb +14 -46
  12. data/lib/odf-report/parser/default.rb +6 -17
  13. data/lib/odf-report/report.rb +25 -60
  14. data/lib/odf-report/section.rb +1 -5
  15. data/lib/odf-report/table.rb +11 -25
  16. data/lib/odf-report/template.rb +9 -11
  17. data/lib/odf-report/text.rb +7 -17
  18. data/lib/odf-report/version.rb +1 -1
  19. data/lib/odf-report.rb +1 -0
  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: 65db96f6bec66147072afafb0d4193df34d188f4c0479361c7dfb34b36300930
4
- data.tar.gz: c7f903e4fa5cb6a5fa811da28339fb3a27d0f9a7d9d2910107efb9cc941823e0
3
+ metadata.gz: e779540093e247d2640705b1e099c7c651e2cef975468fa0970f69110223fa05
4
+ data.tar.gz: a14b575ef72783bd3f735db7824a8e2aeea122161d07535fa1ab632f5e98ad7e
5
5
  SHA512:
6
- metadata.gz: 82b5cf956e054ed3eec7d93e3b1bcb2244a3922df1faa3d7145a00fbca4a79d7c0d0a7cbb0dd05916dcd020f38305aa33083564812eb1d2417050c26010dc4cd
7
- data.tar.gz: f0850ee3d3fa433cc55d38136f78c269796159feb07307a39c996af80da6f2597fe6928ec0cdcb38174044ba3899c399e73e6f0fd714df518f316a618ca7b912
6
+ metadata.gz: 050a25739018f420a6985a30633c889a9e9ac599b3eed318034690d1533e9d4208d5e48e3160fcd1375ea7c9677a13519f1036f7366ceca518d062c8cb2e7ae5
7
+ data.tar.gz: 6116ecfce938d03174b814d9ea27b72f0462c0cb8a19e105dc49440033b163e1365a6cad6f2bd292459ba409db60e1013d4b78d47486dcb60fd11f333ff781bb
data/CHANGELOG.md CHANGED
@@ -18,6 +18,27 @@ 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
+
21
42
  ## 0.8.1
22
43
 
23
44
  ### Fixed
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,54 +1,64 @@
1
1
  module ODFReport
2
2
  class DataSource
3
- attr_reader :value
4
-
5
3
  def initialize(opts, &block)
6
- @value = opts[:value] || opts[:collection]
7
- @data_field = opts[:data_field] || opts[:collection_field] || opts[:name]
4
+ @value = opts[:value]
5
+ @field = opts[:value] || opts[:name]
8
6
  @block = block
9
7
  end
10
8
 
9
+ def value
10
+ if @record
11
+ extract_value_from(@record)
12
+ else
13
+ @value
14
+ end
15
+ end
16
+
11
17
  def set_source(record)
12
- @value = extract_value_from_item(record)
18
+ @record = record
13
19
  end
14
20
 
15
21
  def each(&block)
16
- return unless @value
17
- @value.each(&block)
22
+ return unless value
23
+ value.each(&block)
18
24
  end
19
25
 
20
26
  def empty?
21
- @value.nil? || @value.empty?
27
+ value.nil? || value.empty?
22
28
  end
23
29
 
24
30
  private
25
31
 
26
- def extract_value_from_item(record)
32
+ def extract_value_from(record)
27
33
  if @block
34
+ # Block transform: add_field(:name) { |item| item.name.upcase }
28
35
  @block.call(record)
29
36
 
30
37
  elsif record.is_a?(Hash)
31
- key = @data_field
32
- 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]
33
40
 
34
- elsif @data_field.is_a?(Array)
35
- 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)
36
44
 
37
- elsif @data_field.is_a?(Hash) && record.respond_to?(@data_field.keys[0])
38
- 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])
39
48
 
40
- elsif record.respond_to?(@data_field)
41
- 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)
42
52
 
43
53
  else
44
- raise "Can't find [#{@data_field}] in this #{record.class}"
54
+ raise "Can't find [#{@field}] in this #{record.class}"
45
55
 
46
56
  end
47
57
  end
48
58
 
49
- def execute_methods_on_item(record)
59
+ def execute_methods_on(record)
50
60
  tmp = record.dup
51
- @data_field.each do |f|
61
+ @field.each do |f|
52
62
  tmp = if f.is_a?(Hash)
53
63
  tmp.send(f.keys[0], f.values[0])
54
64
  else
@@ -12,7 +12,7 @@ module ODFReport
12
12
  self
13
13
  end
14
14
 
15
- def replace!(content, data_item = nil)
15
+ def replace!(content)
16
16
  txt = content.inner_html
17
17
 
18
18
  if txt.gsub!(to_placeholder, sanitize(@data_source.value))
@@ -23,11 +23,7 @@ module ODFReport
23
23
  private
24
24
 
25
25
  def to_placeholder
26
- if DELIMITERS.is_a?(Array)
27
- "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
28
- else
29
- "#{DELIMITERS}#{@name.to_s.upcase}#{DELIMITERS}"
30
- end
26
+ "#{DELIMITERS[0]}#{@name.to_s.upcase}#{DELIMITERS[1]}"
31
27
  end
32
28
 
33
29
  def sanitize(txt)
@@ -9,16 +9,17 @@ module ODFReport
9
9
  super
10
10
  end
11
11
 
12
- def replace!(doc, data_item = nil)
12
+ def replace!(doc)
13
13
  frame = doc.xpath("//draw:frame[@draw:name='#{@name}']").first
14
- image = doc.xpath("//draw:frame[@draw:name='#{@name}']/draw:image").first
14
+ return unless frame
15
15
 
16
+ image = frame.at_xpath("draw:image")
16
17
  return unless image
17
18
 
18
19
  file = @data_source.value
19
20
 
20
21
  if file
21
- image.attribute("href").content = File.join(IMAGE_DIR_NAME, File.basename(file))
22
+ image.attribute("href").content = self.class.image_href(file)
22
23
  frame.attribute("name").content = SecureRandom.uuid
23
24
 
24
25
  @files << file
@@ -30,9 +31,7 @@ module ODFReport
30
31
  def self.include_image_file(zip_file, image_file)
31
32
  return unless image_file
32
33
 
33
- href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
34
-
35
- zip_file.update_file(href, File.read(image_file))
34
+ zip_file.update_file(image_href(image_file), File.read(image_file))
36
35
  end
37
36
 
38
37
  def self.include_manifest_entry(content, image_file)
@@ -40,7 +39,7 @@ module ODFReport
40
39
 
41
40
  return unless (root_node = content.at("//manifest:manifest"))
42
41
 
43
- href = File.join(IMAGE_DIR_NAME, File.basename(image_file))
42
+ href = image_href(image_file)
44
43
 
45
44
  entry = content.create_element("manifest:file-entry")
46
45
  entry["manifest:full-path"] = href
@@ -48,5 +47,9 @@ module ODFReport
48
47
 
49
48
  root_node.add_child entry
50
49
  end
50
+
51
+ def self.image_href(file)
52
+ File.join(IMAGE_DIR_NAME, File.basename(file))
53
+ end
51
54
  end
52
55
  end
@@ -1,66 +1,34 @@
1
1
  module ODFReport
2
2
  class Nestable
3
+ include Composable
4
+
3
5
  def initialize(opts)
4
6
  @name = opts[:name]
5
7
 
6
8
  @data_source = DataSource.new(opts)
7
-
8
- @fields = []
9
- @texts = []
10
- @tables = []
11
- @sections = []
12
- @images = []
13
9
  end
14
10
 
11
+ alias_method :add_column, :add_field
12
+
15
13
  def set_source(data_item)
16
14
  @data_source.set_source(data_item)
17
15
  self
18
16
  end
19
17
 
20
- def add_field(name, data_field = nil, &block)
21
- opts = {name: name, data_field: data_field}
22
- @fields << Field.new(opts, &block)
23
- end
24
- alias_method :add_column, :add_field
25
-
26
- def add_text(name, data_field = nil, &block)
27
- opts = {name: name, data_field: data_field}
28
- @texts << Text.new(opts, &block)
29
- end
30
-
31
- def add_image(name, data_field = nil, &block)
32
- opts = {name: name, data_field: data_field}
33
- @images << Image.new(opts, &block)
34
- end
35
-
36
- def add_table(table_name, collection_field, opts = {})
37
- opts[:name] = table_name
38
- opts[:collection_field] = collection_field
39
-
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[:name] = section_name
48
- opts[:collection_field] = collection_field
49
-
50
- sec = Section.new(opts)
51
- @sections << sec
52
-
53
- yield(sec)
54
- end
55
-
56
- def all_images
57
- (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten
58
- end
59
-
60
18
  def wrap_with_ns(node)
61
19
  <<-XML
62
20
  <root xmlns:draw="a" xmlns:xlink="b" xmlns:text="c" xmlns:table="d">#{node.to_xml}</root>
63
21
  XML
64
22
  end
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
65
33
  end
66
34
  end
@@ -19,7 +19,7 @@ module ODFReport
19
19
  #
20
20
 
21
21
  class Default
22
- attr_accessor :paragraphs
22
+ attr_reader :paragraphs
23
23
 
24
24
  def initialize(text, template_node)
25
25
  @text = text
@@ -29,6 +29,8 @@ module ODFReport
29
29
  parse
30
30
  end
31
31
 
32
+ private
33
+
32
34
  def parse
33
35
  html = Nokogiri::HTML5.fragment(@text)
34
36
 
@@ -49,8 +51,6 @@ module ODFReport
49
51
  @paragraphs << node
50
52
  end
51
53
 
52
- private
53
-
54
54
  def parse_formatting(text)
55
55
  text.strip!
56
56
  text.gsub!(/<strong.*?>(.+?)<\/strong>/) { "<text:span text:style-name=\"bold\">#{$1}</text:span>" }
@@ -62,20 +62,9 @@ module ODFReport
62
62
  end
63
63
 
64
64
  def check_style(node)
65
- style = nil
66
-
67
- if /h\d/i.match?(node.name)
68
- style = "title"
69
-
70
- elsif node.parent && node.parent.name == "blockquote"
71
- style = "quote"
72
-
73
- elsif /margin/.match?(node["style"])
74
- style = "quote"
75
-
76
- end
77
-
78
- style
65
+ return "title" if /h\d/i.match?(node.name)
66
+ return "quote" if node.parent&.name == "blockquote"
67
+ "quote" if /margin/.match?(node["style"])
79
68
  end
80
69
  end
81
70
  end
@@ -1,73 +1,18 @@
1
1
  module ODFReport
2
2
  class Report
3
+ include Composable
4
+
3
5
  def initialize(template_name = nil, io: nil)
4
6
  @template = ODFReport::Template.new(template_name, io: io)
5
7
 
6
- @texts = []
7
- @fields = []
8
- @tables = []
9
- @sections = []
10
-
11
- @images = []
12
-
13
8
  yield(self) if block_given?
14
9
  end
15
10
 
16
- def add_field(field_tag, value = "")
17
- opts = {name: field_tag, value: value}
18
- field = Field.new(opts)
19
- @fields << field
20
- end
21
-
22
- def add_text(field_tag, value = "")
23
- opts = {name: field_tag, value: value}
24
- text = Text.new(opts)
25
- @texts << text
26
- end
27
-
28
- def add_table(table_name, collection, opts = {})
29
- opts[:name] = table_name
30
- opts[:collection] = collection
31
-
32
- tab = Table.new(opts)
33
- @tables << tab
34
-
35
- yield(tab)
36
- end
37
-
38
- def add_section(section_name, collection, opts = {})
39
- opts[:name] = section_name
40
- opts[:collection] = collection
41
-
42
- sec = Section.new(opts)
43
- @sections << sec
44
-
45
- yield(sec)
46
- end
47
-
48
- def add_image(image_name, value = nil)
49
- opts = {name: image_name, value: value}
50
- image = Image.new(opts)
51
- @images << image
52
- end
53
-
54
11
  def generate(dest = nil)
55
12
  @template.update_content do |file|
56
- file.update_files do |doc|
57
- @sections.each { |c| c.replace!(doc) }
58
- @tables.each { |c| c.replace!(doc) }
59
-
60
- @texts.each { |c| c.replace!(doc) }
61
- @fields.each { |c| c.replace!(doc) }
62
-
63
- @images.each { |c| c.replace!(doc) }
64
- end
13
+ file.update_files { |doc| replace_placeholders!(doc) }
65
14
 
66
- all_images.each { |i| Image.include_image_file(file, i) }
67
-
68
- file.update_manifest do |content|
69
- all_images.each { |i| Image.include_manifest_entry(content, i) }
70
- end
15
+ include_images(file)
71
16
  end
72
17
 
73
18
  if dest
@@ -78,7 +23,27 @@ module ODFReport
78
23
  end
79
24
 
80
25
  def all_images
81
- @all_images ||= (@images.map(&:files) + @sections.map(&:all_images) + @tables.map(&:all_images)).flatten.uniq
26
+ @all_images ||= super.uniq
27
+ end
28
+
29
+ private
30
+
31
+ def replace_placeholders!(doc)
32
+ sections.each { |c| c.replace!(doc) }
33
+ tables.each { |c| c.replace!(doc) }
34
+
35
+ texts.each { |c| c.replace!(doc) }
36
+ fields.each { |c| c.replace!(doc) }
37
+
38
+ images.each { |c| c.replace!(doc) }
39
+ end
40
+
41
+ def include_images(file)
42
+ all_images.each { |i| Image.include_image_file(file, i) }
43
+
44
+ file.update_manifest do |content|
45
+ all_images.each { |i| Image.include_manifest_entry(content, i) }
46
+ end
82
47
  end
83
48
  end
84
49
  end