caracal 1.4.1 → 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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +28 -0
- data/.github/workflows/push_gem.yml +29 -0
- data/CHANGELOG.md +50 -1
- data/Gemfile +3 -0
- data/LICENSE.txt +1 -1
- data/README.md +138 -15
- data/caracal.gemspec +15 -7
- data/lib/caracal/core/footer.rb +50 -0
- data/lib/caracal/core/header.rb +49 -0
- data/lib/caracal/core/iframes.rb +10 -5
- data/lib/caracal/core/models/border_model.rb +1 -1
- data/lib/caracal/core/models/field_model.rb +131 -0
- data/lib/caracal/core/models/footer_model.rb +43 -0
- data/lib/caracal/core/models/header_model.rb +43 -0
- data/lib/caracal/core/models/iframe_model.rb +45 -11
- data/lib/caracal/core/models/image_model.rb +1 -1
- data/lib/caracal/core/models/list_item_model.rb +57 -3
- data/lib/caracal/core/models/list_model.rb +1 -7
- data/lib/caracal/core/models/list_style_model.rb +1 -1
- data/lib/caracal/core/models/page_number_model.rb +17 -2
- data/lib/caracal/core/models/paragraph_model.rb +18 -2
- data/lib/caracal/core/models/relationship_model.rb +29 -5
- data/lib/caracal/core/models/table_model.rb +44 -7
- data/lib/caracal/core/page_numbers.rb +5 -1
- data/lib/caracal/document.rb +123 -31
- data/lib/caracal/renderers/content_types_renderer.rb +7 -1
- data/lib/caracal/renderers/custom_renderer.rb +3 -3
- data/lib/caracal/renderers/document_renderer.rb +69 -21
- data/lib/caracal/renderers/footer_renderer.rb +57 -50
- data/lib/caracal/renderers/header_footer_renderer.rb +90 -0
- data/lib/caracal/renderers/header_renderer.rb +28 -0
- data/lib/caracal/renderers/relationships_renderer.rb +22 -4
- data/lib/caracal/renderers/xml_renderer.rb +8 -0
- data/lib/caracal/utilities.rb +16 -1
- data/lib/caracal/version.rb +1 -1
- data/lib/caracal.rb +17 -0
- data/spec/lib/caracal/core/footer_spec.rb +37 -0
- data/spec/lib/caracal/core/header_spec.rb +48 -0
- data/spec/lib/caracal/core/images_spec.rb +7 -0
- data/spec/lib/caracal/core/models/footer_model_spec.rb +41 -0
- data/spec/lib/caracal/core/models/header_model_spec.rb +41 -0
- data/spec/lib/caracal/core/models/image_model_spec.rb +1 -1
- data/spec/lib/caracal/core/models/list_item_model_spec.rb +18 -0
- data/spec/lib/caracal/core/models/list_style_model_spec.rb +1 -1
- data/spec/lib/caracal/core/models/option_keys_spec.rb +47 -0
- data/spec/lib/caracal/core/models/page_number_model_spec.rb +16 -1
- data/spec/lib/caracal/core/models/paragraph_model_spec.rb +29 -1
- data/spec/lib/caracal/core/models/table_model_spec.rb +27 -2
- data/spec/lib/caracal/core/relationships_spec.rb +1 -1
- data/spec/lib/caracal/core/tables_spec.rb +8 -1
- data/spec/lib/caracal/document_output_spec.rb +715 -0
- metadata +57 -26
- data/.travis.yml +0 -20
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require 'caracal/core/models/base_model'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Caracal
|
|
5
|
+
module Core
|
|
6
|
+
module Models
|
|
7
|
+
|
|
8
|
+
# This class encapsulates the logic needed to store and render a
|
|
9
|
+
# field, such as the current page number, inside a paragraph.
|
|
10
|
+
#
|
|
11
|
+
class FieldModel < BaseModel
|
|
12
|
+
|
|
13
|
+
#--------------------------------------------------
|
|
14
|
+
# Configuration
|
|
15
|
+
#--------------------------------------------------
|
|
16
|
+
|
|
17
|
+
# constants
|
|
18
|
+
const_set(:TYPE_MAP, { page: 'PAGE', numpages: 'NUMPAGES' })
|
|
19
|
+
|
|
20
|
+
# accessors
|
|
21
|
+
attr_reader :field_type
|
|
22
|
+
attr_reader :field_style
|
|
23
|
+
attr_reader :field_font
|
|
24
|
+
attr_reader :field_color
|
|
25
|
+
attr_reader :field_size
|
|
26
|
+
attr_reader :field_bold
|
|
27
|
+
attr_reader :field_italic
|
|
28
|
+
attr_reader :field_underline
|
|
29
|
+
attr_reader :field_bgcolor
|
|
30
|
+
attr_reader :field_highlight_color
|
|
31
|
+
attr_reader :field_vertical_align
|
|
32
|
+
|
|
33
|
+
#-------------------------------------------------------------
|
|
34
|
+
# Public Class Methods
|
|
35
|
+
#-------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
def self.formatted_type(type)
|
|
38
|
+
TYPE_MAP.fetch(type.to_s.to_sym)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
#-------------------------------------------------------------
|
|
43
|
+
# Public Instance Methods
|
|
44
|
+
#-------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
#========== GETTERS ===============================
|
|
47
|
+
|
|
48
|
+
def formatted_type
|
|
49
|
+
self.class.formatted_type(field_type)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# .run_attributes
|
|
53
|
+
def run_attributes
|
|
54
|
+
{
|
|
55
|
+
style: field_style,
|
|
56
|
+
font: field_font,
|
|
57
|
+
color: field_color,
|
|
58
|
+
size: field_size,
|
|
59
|
+
bold: field_bold,
|
|
60
|
+
italic: field_italic,
|
|
61
|
+
underline: field_underline,
|
|
62
|
+
bgcolor: field_bgcolor,
|
|
63
|
+
highlight_color: field_highlight_color,
|
|
64
|
+
vertical_align: field_vertical_align
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
#========== SETTERS ===============================
|
|
70
|
+
|
|
71
|
+
# booleans
|
|
72
|
+
[:bold, :italic, :underline].each do |m|
|
|
73
|
+
define_method "#{ m }" do |value|
|
|
74
|
+
instance_variable_set("@field_#{ m }", !!value)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# integers
|
|
79
|
+
[:size].each do |m|
|
|
80
|
+
define_method "#{ m }" do |value|
|
|
81
|
+
instance_variable_set("@field_#{ m }", value.to_i)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# strings
|
|
86
|
+
[:bgcolor, :color, :font, :highlight_color, :style, :type].each do |m|
|
|
87
|
+
define_method "#{ m }" do |value|
|
|
88
|
+
instance_variable_set("@field_#{ m }", value.to_s)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# symbols
|
|
93
|
+
[:vertical_align].each do |m|
|
|
94
|
+
define_method "#{ m }" do |value|
|
|
95
|
+
instance_variable_set("@field_#{ m }", value.to_s.to_sym)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
#========== VALIDATION ============================
|
|
101
|
+
|
|
102
|
+
def valid?
|
|
103
|
+
TYPE_MAP.key?(field_type.to_s.to_sym)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
#--------------------------------------------------
|
|
108
|
+
# Private Methods
|
|
109
|
+
#--------------------------------------------------
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def option_keys
|
|
113
|
+
[:type, :style, :font, :color, :size, :bold, :italic, :underline, :bgcolor, :highlight_color, :vertical_align]
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def method_missing(method, *args, &block)
|
|
117
|
+
# I'm on the fence with respect to this implementation. We're ignoring
|
|
118
|
+
# :method_missing errors to allow syntax flexibility for paragraph-type
|
|
119
|
+
# models. The issue is the syntax format of those models--the way we pass
|
|
120
|
+
# the content value as a special argument--coupled with the model's
|
|
121
|
+
# ability to accept nested instructions.
|
|
122
|
+
#
|
|
123
|
+
# By ignoring method missing errors here, we can pass the entire paragraph
|
|
124
|
+
# block in the initial, built-in call to :text.
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'caracal/core/models/base_model'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Caracal
|
|
5
|
+
module Core
|
|
6
|
+
module Models
|
|
7
|
+
|
|
8
|
+
# This class holds the contents of the footer that appears on
|
|
9
|
+
# every page of the document.
|
|
10
|
+
#
|
|
11
|
+
class FooterModel < BaseModel
|
|
12
|
+
|
|
13
|
+
#-------------------------------------------------------------
|
|
14
|
+
# Public Methods
|
|
15
|
+
#-------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
#=============== DATA ACCESSORS =======================
|
|
18
|
+
|
|
19
|
+
def contents
|
|
20
|
+
@contents ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Images and links in the footer are registered with the document,
|
|
24
|
+
# which numbers them and writes the media, but Word resolves them
|
|
25
|
+
# against the footer part's own relationships. The renderer collects
|
|
26
|
+
# them here so they can be written to that part.
|
|
27
|
+
#
|
|
28
|
+
def relationships
|
|
29
|
+
@relationships ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#=============== VALIDATION ===========================
|
|
34
|
+
|
|
35
|
+
def valid?
|
|
36
|
+
contents.size > 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
require 'caracal/core/models/base_model'
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
module Caracal
|
|
5
|
+
module Core
|
|
6
|
+
module Models
|
|
7
|
+
|
|
8
|
+
# This class holds the contents of the header that appears on
|
|
9
|
+
# every page of the document.
|
|
10
|
+
#
|
|
11
|
+
class HeaderModel < BaseModel
|
|
12
|
+
|
|
13
|
+
#-------------------------------------------------------------
|
|
14
|
+
# Public Methods
|
|
15
|
+
#-------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
#=============== DATA ACCESSORS =======================
|
|
18
|
+
|
|
19
|
+
def contents
|
|
20
|
+
@contents ||= []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Images and links in the header are registered with the document,
|
|
24
|
+
# which numbers them and writes the media, but Word resolves them
|
|
25
|
+
# against the header part's own relationships. The renderer collects
|
|
26
|
+
# them here so they can be written to that part.
|
|
27
|
+
#
|
|
28
|
+
def relationships
|
|
29
|
+
@relationships ||= []
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
#=============== VALIDATION ===========================
|
|
34
|
+
|
|
35
|
+
def valid?
|
|
36
|
+
contents.size > 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -1,10 +1,12 @@
|
|
|
1
|
+
require 'tempfile'
|
|
1
2
|
require 'caracal/core/models/base_model'
|
|
3
|
+
require 'caracal/errors'
|
|
2
4
|
|
|
3
5
|
module Caracal
|
|
4
6
|
module Core
|
|
5
7
|
module Models
|
|
6
8
|
|
|
7
|
-
# This class handles block options passed to the
|
|
9
|
+
# This class handles block options passed to the iframe method.
|
|
8
10
|
#
|
|
9
11
|
class IFrameModel < BaseModel
|
|
10
12
|
|
|
@@ -12,6 +14,9 @@ module Caracal
|
|
|
12
14
|
# Configuration
|
|
13
15
|
#--------------------------------------------------
|
|
14
16
|
|
|
17
|
+
# constants
|
|
18
|
+
const_set(:DEFAULT_MAX_ENTRY_SIZE, 50 * 1024 * 1024) # 50MB
|
|
19
|
+
|
|
15
20
|
# accessors
|
|
16
21
|
attr_reader :iframe_url
|
|
17
22
|
attr_reader :iframe_data
|
|
@@ -25,6 +30,37 @@ module Caracal
|
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
|
|
33
|
+
#--------------------------------------------------
|
|
34
|
+
# Class Methods
|
|
35
|
+
#--------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# The largest single entry Caracal will read out of an embedded
|
|
38
|
+
# document. Zip entries expand to many times their stored size, so
|
|
39
|
+
# without a limit a small file can exhaust memory. Set to nil to
|
|
40
|
+
# disable the check.
|
|
41
|
+
#
|
|
42
|
+
def self.max_entry_size
|
|
43
|
+
defined?(@max_entry_size) ? @max_entry_size : DEFAULT_MAX_ENTRY_SIZE
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.max_entry_size=(value)
|
|
47
|
+
@max_entry_size = value
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# This method reads a single entry from an embedded document,
|
|
51
|
+
# refusing anything larger than :max_entry_size.
|
|
52
|
+
#
|
|
53
|
+
def self.read_entry(entry)
|
|
54
|
+
limit = max_entry_size
|
|
55
|
+
|
|
56
|
+
if limit && entry.size > limit
|
|
57
|
+
raise Caracal::Errors::InvalidModelError, "iframe entry #{ entry.name } is #{ entry.size } bytes, over the #{ limit } byte limit."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
entry.get_input_stream.read
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
|
|
28
64
|
#--------------------------------------------------
|
|
29
65
|
# Public Methods
|
|
30
66
|
#--------------------------------------------------
|
|
@@ -35,12 +71,12 @@ module Caracal
|
|
|
35
71
|
::Zip::File.open(file) do |zip|
|
|
36
72
|
# locate relationships xml
|
|
37
73
|
entry = zip.glob('word/_rels/document.xml.rels').first
|
|
38
|
-
content =
|
|
74
|
+
content = self.class.read_entry(entry)
|
|
39
75
|
rel_xml = Nokogiri::XML(content)
|
|
40
76
|
|
|
41
77
|
# locate document xml
|
|
42
78
|
entry = zip.glob('word/document.xml').first
|
|
43
|
-
content =
|
|
79
|
+
content = self.class.read_entry(entry)
|
|
44
80
|
doc_xml = Nokogiri::XML(content)
|
|
45
81
|
|
|
46
82
|
# master nodesets
|
|
@@ -74,7 +110,7 @@ module Caracal
|
|
|
74
110
|
p_node = node.children[0].children[0]
|
|
75
111
|
p_id = p_node.attributes['id'].to_s.to_i
|
|
76
112
|
p_name = p_node.attributes['name'].to_s
|
|
77
|
-
p_data = zip.glob(r_media).first
|
|
113
|
+
p_data = self.class.read_entry(zip.glob(r_media).first)
|
|
78
114
|
|
|
79
115
|
# register relationship
|
|
80
116
|
array << { id: r_id, type: 'image', target: p_name, data: p_data }
|
|
@@ -88,13 +124,11 @@ module Caracal
|
|
|
88
124
|
|
|
89
125
|
def file
|
|
90
126
|
@file ||= begin
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
file = open(iframe_url)
|
|
97
|
-
end
|
|
127
|
+
content = iframe_url.to_s.empty? ? iframe_data : ::Caracal::Utilities.read_resource(iframe_url)
|
|
128
|
+
file = Tempfile.new(['iframe', '.docx'])
|
|
129
|
+
file.binmode
|
|
130
|
+
file.write content
|
|
131
|
+
file.close # flushes the write; rubyzip reopens by path
|
|
98
132
|
file
|
|
99
133
|
end
|
|
100
134
|
end
|
|
@@ -17,7 +17,7 @@ module Caracal
|
|
|
17
17
|
#-------------------------------------------------------------
|
|
18
18
|
|
|
19
19
|
# accessors
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
|
@@ -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
|
|
|
@@ -27,6 +28,7 @@ module Caracal
|
|
|
27
28
|
attr_reader :paragraph_italic
|
|
28
29
|
attr_reader :paragraph_underline
|
|
29
30
|
attr_reader :paragraph_bgcolor
|
|
31
|
+
attr_reader :paragraph_keep_next
|
|
30
32
|
|
|
31
33
|
# initialization
|
|
32
34
|
def initialize(options={}, &block)
|
|
@@ -63,7 +65,7 @@ module Caracal
|
|
|
63
65
|
#========== SETTERS ===============================
|
|
64
66
|
|
|
65
67
|
# booleans
|
|
66
|
-
[:bold, :italic, :underline].each do |m|
|
|
68
|
+
[:bold, :italic, :keep_next, :underline].each do |m|
|
|
67
69
|
define_method "#{ m }" do |value|
|
|
68
70
|
instance_variable_set("@paragraph_#{ m }", !!value)
|
|
69
71
|
end
|
|
@@ -126,6 +128,20 @@ module Caracal
|
|
|
126
128
|
model
|
|
127
129
|
end
|
|
128
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
|
+
|
|
129
145
|
# .link
|
|
130
146
|
def link(*args, &block)
|
|
131
147
|
options = Caracal::Utilities.extract_options!(args)
|
|
@@ -176,7 +192,7 @@ module Caracal
|
|
|
176
192
|
private
|
|
177
193
|
|
|
178
194
|
def option_keys
|
|
179
|
-
[:content, :style, :align, :color, :size, :bold, :italic, :underline, :bgcolor]
|
|
195
|
+
[:content, :style, :align, :color, :size, :bold, :italic, :underline, :bgcolor, :keep_next]
|
|
180
196
|
end
|
|
181
197
|
|
|
182
198
|
end
|
|
@@ -18,13 +18,24 @@ 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',
|
|
24
25
|
setting: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings',
|
|
25
26
|
style: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles'
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
+
|
|
29
|
+
IMAGE_SIGNATURES = {
|
|
30
|
+
"\x89PNG\r\n\x1A\n".b => 'png',
|
|
31
|
+
"\xFF\xD8\xFF".b => 'jpeg',
|
|
32
|
+
'GIF8'.b => 'gif',
|
|
33
|
+
'BM'.b => 'bmp',
|
|
34
|
+
"II*\x00".b => 'tiff',
|
|
35
|
+
"MM\x00*".b => 'tiff'
|
|
36
|
+
}
|
|
37
|
+
IMAGE_EXTENSIONS = %w(png jpeg jpg gif bmp tif tiff svg)
|
|
38
|
+
|
|
28
39
|
# accessors
|
|
29
40
|
attr_reader :relationship_id
|
|
30
41
|
attr_reader :relationship_key
|
|
@@ -46,14 +57,27 @@ module Caracal
|
|
|
46
57
|
|
|
47
58
|
def formatted_target
|
|
48
59
|
if relationship_type == :image
|
|
49
|
-
|
|
50
|
-
ext = ext.split('?').first
|
|
51
|
-
"media/image#{ relationship_id }.#{ ext }"
|
|
60
|
+
"media/image#{ relationship_id }.#{ image_extension }"
|
|
52
61
|
else
|
|
53
62
|
relationship_target
|
|
54
63
|
end
|
|
55
64
|
end
|
|
56
|
-
|
|
65
|
+
|
|
66
|
+
# Prefers the type detected from the image bytes, then the
|
|
67
|
+
# extension of the target's path (ignoring any query string),
|
|
68
|
+
# and falls back to png so the part name is always valid.
|
|
69
|
+
#
|
|
70
|
+
def image_extension
|
|
71
|
+
data = relationship_data.to_s.b
|
|
72
|
+
if (match = IMAGE_SIGNATURES.find { |sig, _| data.start_with?(sig) })
|
|
73
|
+
return match.last
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
path = relationship_target.to_s.split(/[?#]/).first.to_s
|
|
77
|
+
ext = File.extname(path).delete('.').downcase
|
|
78
|
+
IMAGE_EXTENSIONS.include?(ext) ? ext : 'png'
|
|
79
|
+
end
|
|
80
|
+
|
|
57
81
|
def formatted_type
|
|
58
82
|
TYPE_MAP.fetch(relationship_type)
|
|
59
83
|
end
|