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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -0
- data/CLAUDE.md +50 -0
- data/Gemfile +1 -1
- data/README.md +27 -14
- data/Rakefile +3 -3
- data/lib/odf-report/composable.rb +51 -0
- data/lib/odf-report/data_source.rb +30 -20
- data/lib/odf-report/field.rb +2 -6
- data/lib/odf-report/image.rb +10 -7
- data/lib/odf-report/nestable.rb +14 -46
- data/lib/odf-report/parser/default.rb +6 -17
- data/lib/odf-report/report.rb +25 -60
- data/lib/odf-report/section.rb +1 -5
- data/lib/odf-report/table.rb +11 -25
- data/lib/odf-report/template.rb +9 -11
- data/lib/odf-report/text.rb +7 -17
- data/lib/odf-report/version.rb +1 -1
- data/lib/odf-report.rb +1 -0
- data/odf-report.gemspec +10 -14
- data/spec/fields_spec.rb +3 -20
- data/spec/images/images_spec.rb +132 -0
- data/spec/{sections_spec.rb → sections/sections_spec.rb} +4 -10
- data/spec/sections/sub_sections_spec.rb +94 -0
- data/spec/spec_helper.rb +8 -11
- data/spec/tables/nested_tables_spec.rb +62 -0
- data/spec/tables/table_headers_spec.rb +54 -0
- data/spec/{tables_spec.rb → tables/tables_spec.rb} +2 -10
- data/spec/template_spec.rb +1 -9
- data/spec/templates/sections/sub_sections.odt +0 -0
- data/spec/templates/tables/nested_tables.odt +0 -0
- data/spec/templates/tables/table_headers.odt +0 -0
- data/spec/templates/texts/fields_inside_text.odt +0 -0
- data/spec/templates/texts/text.odt +0 -0
- data/spec/texts/fields_inside_text_spec.rb +50 -0
- data/spec/texts/texts_spec.rb +101 -0
- data/test/fields_inside_text_test.rb +14 -19
- data/test/images_test.rb +16 -19
- data/test/nested_tables_test.rb +28 -35
- data/test/sections_test.rb +29 -36
- data/test/sub_sections_test.rb +39 -47
- data/test/table_headers_test.rb +28 -35
- data/test/tables_test.rb +61 -67
- data/test/test.rb +203 -203
- data/test/text_test.rb +31 -39
- metadata +22 -50
- data/spec/images_spec.rb +0 -159
- data/spec/texts_spec.rb +0 -48
- /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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e779540093e247d2640705b1e099c7c651e2cef975468fa0970f69110223fa05
|
|
4
|
+
data.tar.gz: a14b575ef72783bd3f735db7824a8e2aeea122161d07535fa1ab632f5e98ad7e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
data/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
# ODF-REPORT
|
|
3
3
|
|
|
4
|
-
Gem for generating .odt files by making
|
|
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 *
|
|
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
|
|
28
|
+
It's just an uppercase sentence, surrounded by brackets. It will be replaced by the value you supply.
|
|
28
29
|
|
|
29
|
-
In the
|
|
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
|
|
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
|
|
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
|
|
62
|
+
As with **Field placeholders**, just insert a `[FIELD_NAME]` in each cell and let the magic take place.
|
|
50
63
|
|
|
51
|
-
Taking the
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
2
|
+
require "launchy"
|
|
3
3
|
task :test do
|
|
4
|
-
Dir.glob(
|
|
4
|
+
Dir.glob("./test/*_test.rb").each { |file| require file }
|
|
5
5
|
end
|
|
6
6
|
|
|
7
7
|
task :open do
|
|
8
|
-
Dir.glob(
|
|
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]
|
|
7
|
-
@
|
|
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
|
-
@
|
|
18
|
+
@record = record
|
|
13
19
|
end
|
|
14
20
|
|
|
15
21
|
def each(&block)
|
|
16
|
-
return unless
|
|
17
|
-
|
|
22
|
+
return unless value
|
|
23
|
+
value.each(&block)
|
|
18
24
|
end
|
|
19
25
|
|
|
20
26
|
def empty?
|
|
21
|
-
|
|
27
|
+
value.nil? || value.empty?
|
|
22
28
|
end
|
|
23
29
|
|
|
24
30
|
private
|
|
25
31
|
|
|
26
|
-
def
|
|
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
|
-
|
|
32
|
-
record[
|
|
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 @
|
|
35
|
-
|
|
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 @
|
|
38
|
-
|
|
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?(@
|
|
41
|
-
record.
|
|
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 [#{@
|
|
54
|
+
raise "Can't find [#{@field}] in this #{record.class}"
|
|
45
55
|
|
|
46
56
|
end
|
|
47
57
|
end
|
|
48
58
|
|
|
49
|
-
def
|
|
59
|
+
def execute_methods_on(record)
|
|
50
60
|
tmp = record.dup
|
|
51
|
-
@
|
|
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
|
data/lib/odf-report/field.rb
CHANGED
|
@@ -12,7 +12,7 @@ module ODFReport
|
|
|
12
12
|
self
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def replace!(content
|
|
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
|
-
|
|
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)
|
data/lib/odf-report/image.rb
CHANGED
|
@@ -9,16 +9,17 @@ module ODFReport
|
|
|
9
9
|
super
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
def replace!(doc
|
|
12
|
+
def replace!(doc)
|
|
13
13
|
frame = doc.xpath("//draw:frame[@draw:name='#{@name}']").first
|
|
14
|
-
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
data/lib/odf-report/nestable.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
if /
|
|
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
|
data/lib/odf-report/report.rb
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 ||=
|
|
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
|