docx 0.7.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.
- checksums.yaml +4 -4
- data/README.md +73 -2
- data/lib/docx/containers/container.rb +2 -2
- data/lib/docx/containers/paragraph.rb +37 -7
- data/lib/docx/containers/styles_configuration.rb +52 -0
- data/lib/docx/containers/text_run.rb +19 -5
- data/lib/docx/containers.rb +1 -0
- data/lib/docx/document.rb +27 -10
- data/lib/docx/elements/element.rb +9 -2
- data/lib/docx/elements/style/converters.rb +37 -0
- data/lib/docx/elements/style/validators.rb +31 -0
- data/lib/docx/elements/style.rb +179 -0
- data/lib/docx/elements/text.rb +8 -2
- data/lib/docx/elements.rb +2 -1
- data/lib/docx/errors.rb +7 -0
- data/lib/docx/helpers.rb +22 -0
- data/lib/docx/version.rb +1 -1
- data/lib/docx.rb +0 -1
- metadata +10 -8
- data/lib/docx/core_ext/module.rb +0 -172
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38d8b4c53502b022e87a0b00ac8b15c99c06d79c2a51e664364ddd52dcca52a7
|
4
|
+
data.tar.gz: e7a747b4ef58b53e279fdd0cfc516b06ab5c14820e622938bea4ec73858f637c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f4db62333498540c5ff8ea488828d17c05629c0477fd917bd9106a061d987f368e02a16d4fe566b93c456a5b1920fe1aa5dfe3c39aa3d68682a99e83087c4f25
|
7
|
+
data.tar.gz: 12864372c581d4fb4cd7f4330b49b4aa115061dfef5b8bc9ff603adaea6a0766b6e1475ae402eef473ce7636352e8b68657be59bc18cd048d85e2314d523f416
|
data/README.md
CHANGED
@@ -11,7 +11,7 @@ A ruby library/gem for interacting with `.docx` files. currently capabilities in
|
|
11
11
|
|
12
12
|
### Prerequisites
|
13
13
|
|
14
|
-
- Ruby 2.
|
14
|
+
- Ruby 2.6 or later
|
15
15
|
|
16
16
|
### Install
|
17
17
|
|
@@ -181,6 +181,78 @@ p_children = p_element.xpath("//child::*") # selects all children
|
|
181
181
|
p_child = p_element.at_xpath("//child::*") # selects first child
|
182
182
|
```
|
183
183
|
|
184
|
+
### Writing and Manipulating Styles
|
185
|
+
``` ruby
|
186
|
+
require 'docx'
|
187
|
+
|
188
|
+
d = Docx::Document.open('example.docx')
|
189
|
+
existing_style = d.styles_configuration.style_of("Heading 1")
|
190
|
+
existing_style.font_color = "000000"
|
191
|
+
|
192
|
+
# see attributes below
|
193
|
+
new_style = d.styles_configuration.add_style("Red", name: "Red", font_color: "FF0000", font_size: 20)
|
194
|
+
new_style.bold = true
|
195
|
+
|
196
|
+
d.paragraphs.each do |p|
|
197
|
+
p.style = "Red"
|
198
|
+
end
|
199
|
+
|
200
|
+
d.paragraphs.each do |p|
|
201
|
+
p.style = "Heading 1"
|
202
|
+
end
|
203
|
+
|
204
|
+
d.styles_configuration.remove_style("Red")
|
205
|
+
```
|
206
|
+
|
207
|
+
#### Style Attributes
|
208
|
+
|
209
|
+
The following is a list of attributes and what they control within the style.
|
210
|
+
|
211
|
+
- **id**: The unique identifier of the style. (required)
|
212
|
+
- **name**: The human-readable name of the style. (required)
|
213
|
+
- **type**: Indicates the type of the style (e.g., paragraph, character).
|
214
|
+
- **keep_next**: Boolean value controlling whether to keep a paragraph and the next one on the same page. Valid values: `true`/`false`.
|
215
|
+
- **keep_lines**: Boolean value specifying whether to keep all lines of a paragraph together on one page. Valid values: `true`/`false`.
|
216
|
+
- **page_break_before**: Boolean value indicating whether to insert a page break before the paragraph. Valid values: `true`/`false`.
|
217
|
+
- **widow_control**: Boolean value controlling widow and orphan lines in a paragraph. Valid values: `true`/`false`.
|
218
|
+
- **shading_style**: Defines the shading pattern style.
|
219
|
+
- **shading_color**: Specifies the color of the shading pattern. Valid values: Hex color codes.
|
220
|
+
- **shading_fill**: Indicates the background fill color of shading.
|
221
|
+
- **suppress_auto_hyphens**: Boolean value controlling automatic hyphenation. Valid values: `true`/`false`.
|
222
|
+
- **bidirectional_text**: Boolean value indicating if the paragraph contains bidirectional text. Valid values: `true`/`false`.
|
223
|
+
- **spacing_before**: Defines the spacing before a paragraph.
|
224
|
+
- **spacing_after**: Specifies the spacing after a paragraph.
|
225
|
+
- **line_spacing**: Indicates the line spacing of a paragraph.
|
226
|
+
- **line_rule**: Defines how line spacing is calculated.
|
227
|
+
- **indent_left**: Sets the left indentation of a paragraph.
|
228
|
+
- **indent_right**: Specifies the right indentation of a paragraph.
|
229
|
+
- **indent_first_line**: Indicates the first line indentation of a paragraph.
|
230
|
+
- **align**: Controls the text alignment within a paragraph.
|
231
|
+
- **font**: Sets the font for different scripts (ASCII, complex script, East Asian, etc.).
|
232
|
+
- **font_ascii**: Specifies the font for ASCII characters.
|
233
|
+
- **font_cs**: Indicates the font for complex script characters.
|
234
|
+
- **font_hAnsi**: Sets the font for high ANSI characters.
|
235
|
+
- **font_eastAsia**: Specifies the font for East Asian characters.
|
236
|
+
- **bold**: Boolean value controlling bold formatting. Valid values: `true`/`false`.
|
237
|
+
- **italic**: Boolean value indicating italic formatting. Valid values: `true`/`false`.
|
238
|
+
- **caps**: Boolean value controlling capitalization. Valid values: `true`/`false`.
|
239
|
+
- **small_caps**: Boolean value specifying small capital letters. Valid values: `true`/`false`.
|
240
|
+
- **strike**: Boolean value indicating strikethrough formatting. Valid values: `true`/`false`.
|
241
|
+
- **double_strike**: Boolean value defining double strikethrough formatting. Valid values: `true`/`false`.
|
242
|
+
- **outline**: Boolean value specifying outline effects. Valid values: `true`/`false`.
|
243
|
+
- **outline_level**: Indicates the outline level in a document's hierarchy.
|
244
|
+
- **font_color**: Sets the text color. Valid values: Hex color codes.
|
245
|
+
- **font_size**: Controls the font size.
|
246
|
+
- **font_size_cs**: Specifies the font size for complex script characters.
|
247
|
+
- **underline_style**: Indicates the style of underlining.
|
248
|
+
- **underline_color**: Specifies the color of the underline. Valid values: Hex color codes.
|
249
|
+
- **spacing**: Controls character spacing.
|
250
|
+
- **kerning**: Sets the space between characters.
|
251
|
+
- **position**: Controls the position of characters (superscript/subscript).
|
252
|
+
- **text_fill_color**: Sets the fill color of text. Valid values: Hex color codes.
|
253
|
+
- **vertical_alignment**: Controls the vertical alignment of text within a line.
|
254
|
+
- **lang**: Specifies the language tag for the text.
|
255
|
+
|
184
256
|
## Development
|
185
257
|
|
186
258
|
### todo
|
@@ -188,5 +260,4 @@ p_child = p_element.at_xpath("//child::*") # selects first child
|
|
188
260
|
* Calculate element formatting based on values present in element properties as well as properties inherited from parents
|
189
261
|
* Default formatting of inserted elements to inherited values
|
190
262
|
* Implement formattable elements.
|
191
|
-
* Implement styles.
|
192
263
|
* Easier multi-line text insertion at a single bookmark (inserting paragraph nodes after the one containing the bookmark)
|
@@ -7,12 +7,12 @@ module Docx
|
|
7
7
|
# Relation methods
|
8
8
|
# TODO: Create a properties object, include Element
|
9
9
|
def properties
|
10
|
-
@node.at_xpath("
|
10
|
+
@node.at_xpath("./w:#{@properties_tag}")
|
11
11
|
end
|
12
12
|
|
13
13
|
# Erase text within an element
|
14
14
|
def blank!
|
15
|
-
@node.xpath(
|
15
|
+
@node.xpath('.//w:t').each { |t| t.content = '' }
|
16
16
|
end
|
17
17
|
|
18
18
|
def remove!
|
@@ -15,11 +15,12 @@ module Docx
|
|
15
15
|
|
16
16
|
# Child elements: pPr, r, fldSimple, hlink, subDoc
|
17
17
|
# http://msdn.microsoft.com/en-us/library/office/ee364458(v=office.11).aspx
|
18
|
-
def initialize(node, document_properties = {})
|
18
|
+
def initialize(node, document_properties = {}, doc = nil)
|
19
19
|
@node = node
|
20
20
|
@properties_tag = 'pPr'
|
21
21
|
@document_properties = document_properties
|
22
22
|
@font_size = @document_properties[:font_size]
|
23
|
+
@document = doc
|
23
24
|
end
|
24
25
|
|
25
26
|
# Set text of paragraph
|
@@ -48,6 +49,7 @@ module Docx
|
|
48
49
|
html << text_run.to_html
|
49
50
|
end
|
50
51
|
styles = { 'font-size' => "#{font_size}pt" }
|
52
|
+
styles['color'] = "##{font_color}" if font_color
|
51
53
|
styles['text-align'] = alignment if alignment
|
52
54
|
html_tag(:p, content: html, styles: styles)
|
53
55
|
end
|
@@ -76,20 +78,48 @@ module Docx
|
|
76
78
|
end
|
77
79
|
|
78
80
|
def font_size
|
79
|
-
|
80
|
-
|
81
|
+
size_attribute = @node.at_xpath('w:pPr//w:sz//@w:val')
|
82
|
+
|
83
|
+
return @font_size unless size_attribute
|
84
|
+
|
85
|
+
size_attribute.value.to_i / 2
|
86
|
+
end
|
87
|
+
|
88
|
+
def font_color
|
89
|
+
color_tag = @node.xpath('w:r//w:rPr//w:color').first
|
90
|
+
color_tag ? color_tag.attributes['val'].value : nil
|
91
|
+
end
|
92
|
+
|
93
|
+
def style
|
94
|
+
return nil unless @document
|
95
|
+
|
96
|
+
@document.style_name_of(style_id) ||
|
97
|
+
@document.default_paragraph_style
|
98
|
+
end
|
99
|
+
|
100
|
+
def style_id
|
101
|
+
style_property.get_attribute('w:val')
|
102
|
+
end
|
103
|
+
|
104
|
+
def style=(identifier)
|
105
|
+
id = @document.styles_configuration.style_of(identifier).id
|
106
|
+
|
107
|
+
style_property.set_attribute('w:val', id)
|
81
108
|
end
|
82
|
-
|
109
|
+
|
110
|
+
alias_method :style_id=, :style=
|
83
111
|
alias_method :text, :to_s
|
84
112
|
|
85
113
|
private
|
86
114
|
|
115
|
+
def style_property
|
116
|
+
properties&.at_xpath('w:pStyle') || properties&.add_child('<w:pStyle/>').first
|
117
|
+
end
|
118
|
+
|
87
119
|
# Returns the alignment if any, or nil if left
|
88
120
|
def alignment
|
89
|
-
|
90
|
-
alignment_tag ? alignment_tag.attributes['val'].value : nil
|
121
|
+
@node.at_xpath('.//w:jc/@w:val')&.value
|
91
122
|
end
|
92
|
-
|
93
123
|
end
|
94
124
|
end
|
95
125
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'docx/containers/container'
|
2
|
+
require 'docx/elements/style'
|
3
|
+
|
4
|
+
module Docx
|
5
|
+
module Elements
|
6
|
+
module Containers
|
7
|
+
StyleNotFound = Class.new(StandardError)
|
8
|
+
|
9
|
+
class StylesConfiguration
|
10
|
+
def initialize(raw_styles)
|
11
|
+
@raw_styles = raw_styles
|
12
|
+
@styles_parent_node = raw_styles.root
|
13
|
+
end
|
14
|
+
|
15
|
+
attr_reader :styles, :styles_parent_node
|
16
|
+
|
17
|
+
def styles
|
18
|
+
styles_parent_node
|
19
|
+
.children
|
20
|
+
.filter_map do |style|
|
21
|
+
next unless style.get_attribute("w:styleId")
|
22
|
+
|
23
|
+
Elements::Style.new(self, style)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def style_of(id_or_name)
|
28
|
+
styles.find { |style| style.id == id_or_name || style.name == id_or_name } || raise(Errors::StyleNotFound, "Style name or id '#{id_or_name}' not found")
|
29
|
+
end
|
30
|
+
|
31
|
+
def size
|
32
|
+
styles.size
|
33
|
+
end
|
34
|
+
|
35
|
+
def add_style(id, attributes = {})
|
36
|
+
Elements::Style.create(self, {id: id, name: id}.merge(attributes))
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_style(id)
|
40
|
+
style = styles.find { |style| style.id == id }
|
41
|
+
|
42
|
+
style.node.remove
|
43
|
+
styles.delete(style)
|
44
|
+
end
|
45
|
+
|
46
|
+
def serialize(**options)
|
47
|
+
@raw_styles.serialize(**options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -10,7 +10,8 @@ module Docx
|
|
10
10
|
DEFAULT_FORMATTING = {
|
11
11
|
italic: false,
|
12
12
|
bold: false,
|
13
|
-
underline: false
|
13
|
+
underline: false,
|
14
|
+
strike: false
|
14
15
|
}
|
15
16
|
|
16
17
|
def self.tag
|
@@ -60,7 +61,8 @@ module Docx
|
|
60
61
|
{
|
61
62
|
italic: !@node.xpath('.//w:i').empty?,
|
62
63
|
bold: !@node.xpath('.//w:b').empty?,
|
63
|
-
underline: !@node.xpath('.//w:u').empty
|
64
|
+
underline: !@node.xpath('.//w:u').empty?,
|
65
|
+
strike: !@node.xpath('.//w:strike').empty?
|
64
66
|
}
|
65
67
|
end
|
66
68
|
|
@@ -73,6 +75,7 @@ module Docx
|
|
73
75
|
html = @text
|
74
76
|
html = html_tag(:em, content: html) if italicized?
|
75
77
|
html = html_tag(:strong, content: html) if bolded?
|
78
|
+
html = html_tag(:s, content: html) if striked?
|
76
79
|
styles = {}
|
77
80
|
styles['text-decoration'] = 'underline' if underlined?
|
78
81
|
# No need to be granular with font size down to the span level if it doesn't vary.
|
@@ -90,12 +93,20 @@ module Docx
|
|
90
93
|
@formatting[:bold]
|
91
94
|
end
|
92
95
|
|
96
|
+
def striked?
|
97
|
+
@formatting[:strike]
|
98
|
+
end
|
99
|
+
|
93
100
|
def underlined?
|
94
101
|
@formatting[:underline]
|
95
102
|
end
|
96
103
|
|
97
104
|
def hyperlink?
|
98
|
-
@node.name == 'hyperlink'
|
105
|
+
@node.name == 'hyperlink' && external_link?
|
106
|
+
end
|
107
|
+
|
108
|
+
def external_link?
|
109
|
+
!@node.attributes['id'].nil?
|
99
110
|
end
|
100
111
|
|
101
112
|
def href
|
@@ -107,8 +118,11 @@ module Docx
|
|
107
118
|
end
|
108
119
|
|
109
120
|
def font_size
|
110
|
-
|
111
|
-
|
121
|
+
size_attribute = @node.at_xpath('w:rPr//w:sz//@w:val')
|
122
|
+
|
123
|
+
return @font_size unless size_attribute
|
124
|
+
|
125
|
+
size_attribute.value.to_i / 2
|
112
126
|
end
|
113
127
|
|
114
128
|
private
|
data/lib/docx/containers.rb
CHANGED
data/lib/docx/document.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
require 'docx/containers'
|
2
2
|
require 'docx/elements'
|
3
|
+
require 'docx/errors'
|
4
|
+
require 'docx/helpers'
|
3
5
|
require 'nokogiri'
|
4
6
|
require 'zip'
|
5
7
|
|
@@ -18,6 +20,8 @@ module Docx
|
|
18
20
|
# puts d.text
|
19
21
|
# end
|
20
22
|
class Document
|
23
|
+
include Docx::SimpleInspect
|
24
|
+
|
21
25
|
attr_reader :xml, :doc, :zip, :styles
|
22
26
|
|
23
27
|
def initialize(path_or_io, options = {})
|
@@ -25,6 +29,7 @@ module Docx
|
|
25
29
|
|
26
30
|
# if path-or_io is string && does not contain a null byte
|
27
31
|
if (path_or_io.instance_of?(String) && !/\u0000/.match?(path_or_io))
|
32
|
+
raise Errno::EIO.new('Invalid file format') if !File.extname(path_or_io).eql?('.docx')
|
28
33
|
@zip = Zip::File.open(path_or_io)
|
29
34
|
else
|
30
35
|
@zip = Zip::File.open_buffer(path_or_io)
|
@@ -38,7 +43,7 @@ module Docx
|
|
38
43
|
load_styles
|
39
44
|
yield(self) if block_given?
|
40
45
|
ensure
|
41
|
-
@zip.close
|
46
|
+
@zip.close unless @zip.nil?
|
42
47
|
end
|
43
48
|
|
44
49
|
# This stores the current global document properties, for now
|
@@ -81,10 +86,11 @@ module Docx
|
|
81
86
|
# Some documents have this set, others don't.
|
82
87
|
# Values are returned as half-points, so to get points, that's why it's divided by 2.
|
83
88
|
def font_size
|
84
|
-
|
89
|
+
size_value = @styles&.at_xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz/@w:val')&.value
|
90
|
+
|
91
|
+
return nil unless size_value
|
85
92
|
|
86
|
-
|
87
|
-
size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
|
93
|
+
size_value.to_i / 2
|
88
94
|
end
|
89
95
|
|
90
96
|
# Hyperlink targets are extracted from the document.xml.rels file
|
@@ -129,13 +135,11 @@ module Docx
|
|
129
135
|
next unless entry.file?
|
130
136
|
|
131
137
|
out.put_next_entry(entry.name)
|
138
|
+
value = @replace[entry.name] || zip.read(entry.name)
|
132
139
|
|
133
|
-
|
134
|
-
out.write(@replace[entry.name])
|
135
|
-
else
|
136
|
-
out.write(zip.read(entry.name))
|
137
|
-
end
|
140
|
+
out.write(value)
|
138
141
|
end
|
142
|
+
|
139
143
|
end
|
140
144
|
zip.close
|
141
145
|
end
|
@@ -167,6 +171,18 @@ module Docx
|
|
167
171
|
@replace[entry_path] = file_contents
|
168
172
|
end
|
169
173
|
|
174
|
+
def default_paragraph_style
|
175
|
+
@styles.at_xpath("w:styles/w:style[@w:type='paragraph' and @w:default='1']/w:name/@w:val").value
|
176
|
+
end
|
177
|
+
|
178
|
+
def style_name_of(style_id)
|
179
|
+
styles_configuration.style_of(style_id).name
|
180
|
+
end
|
181
|
+
|
182
|
+
def styles_configuration
|
183
|
+
@styles_configuration ||= Elements::Containers::StylesConfiguration.new(@styles.dup)
|
184
|
+
end
|
185
|
+
|
170
186
|
private
|
171
187
|
|
172
188
|
def load_styles
|
@@ -193,11 +209,12 @@ module Docx
|
|
193
209
|
#++
|
194
210
|
def update
|
195
211
|
replace_entry 'word/document.xml', doc.serialize(save_with: 0)
|
212
|
+
replace_entry 'word/styles.xml', styles_configuration.serialize(save_with: 0)
|
196
213
|
end
|
197
214
|
|
198
215
|
# generate Elements::Containers::Paragraph from paragraph XML node
|
199
216
|
def parse_paragraph_from(p_node)
|
200
|
-
Elements::Containers::Paragraph.new(p_node, document_properties)
|
217
|
+
Elements::Containers::Paragraph.new(p_node, document_properties, self)
|
201
218
|
end
|
202
219
|
|
203
220
|
# generate Elements::Bookmark from bookmark XML node
|
@@ -14,13 +14,20 @@ module Docx
|
|
14
14
|
end
|
15
15
|
|
16
16
|
attr_accessor :node
|
17
|
-
delegate :at_xpath, :xpath, :to => :@node
|
18
17
|
|
19
18
|
# TODO: Should create a docx object from this
|
20
19
|
def parent(type = '*')
|
21
20
|
@node.at_xpath("./parent::#{type}")
|
22
21
|
end
|
23
22
|
|
23
|
+
def at_xpath(*args)
|
24
|
+
@node.at_xpath(*args)
|
25
|
+
end
|
26
|
+
|
27
|
+
def xpath(*args)
|
28
|
+
@node.xpath(*args)
|
29
|
+
end
|
30
|
+
|
24
31
|
# Get parent paragraph of element
|
25
32
|
def parent_paragraph
|
26
33
|
Elements::Containers::Paragraph.new(parent('w:p'))
|
@@ -102,4 +109,4 @@ module Docx
|
|
102
109
|
end
|
103
110
|
end
|
104
111
|
end
|
105
|
-
end
|
112
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Docx
|
2
|
+
module Elements
|
3
|
+
class Style
|
4
|
+
module Converters
|
5
|
+
class DefaultValueConverter
|
6
|
+
def self.encode(value)
|
7
|
+
value
|
8
|
+
end
|
9
|
+
|
10
|
+
def self.decode(value)
|
11
|
+
value
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class FontSizeConverter
|
16
|
+
def self.encode(value)
|
17
|
+
value.to_i * 2
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.decode(value)
|
21
|
+
value.to_i / 2
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class BooleanConverter
|
26
|
+
def self.encode(value)
|
27
|
+
value ? "1" : "0"
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.decode(value)
|
31
|
+
value == "1"
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Docx
|
2
|
+
module Elements
|
3
|
+
class Style
|
4
|
+
module Validators
|
5
|
+
class DefaultValidator
|
6
|
+
def self.validate(value)
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
class ColorValidator
|
12
|
+
COLOR_REGEX = /^([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/
|
13
|
+
|
14
|
+
def self.validate(value)
|
15
|
+
value =~ COLOR_REGEX
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ValueValidator
|
20
|
+
def initialize(*values)
|
21
|
+
@values = values
|
22
|
+
end
|
23
|
+
|
24
|
+
def validate(value)
|
25
|
+
@values.include?(value)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,179 @@
|
|
1
|
+
require 'docx/helpers'
|
2
|
+
require 'docx/elements'
|
3
|
+
require 'docx/elements/style/converters'
|
4
|
+
require 'docx/elements/style/validators'
|
5
|
+
|
6
|
+
module Docx
|
7
|
+
module Elements
|
8
|
+
class Style
|
9
|
+
include Docx::SimpleInspect
|
10
|
+
|
11
|
+
class Attribute
|
12
|
+
attr_reader :name, :selectors, :required, :converter, :validator
|
13
|
+
|
14
|
+
def initialize(name, selectors, required: false, converter:, validator:)
|
15
|
+
@name = name
|
16
|
+
@selectors = selectors
|
17
|
+
@required = required
|
18
|
+
@converter = converter || Converters::DefaultValueConverter
|
19
|
+
@validator = validator || Validators::DefaultValidator
|
20
|
+
end
|
21
|
+
|
22
|
+
def required?
|
23
|
+
required
|
24
|
+
end
|
25
|
+
|
26
|
+
def retrieve_from(style)
|
27
|
+
selectors
|
28
|
+
.lazy
|
29
|
+
.filter_map { |node_xpath| style.node.at_xpath(node_xpath)&.value }
|
30
|
+
.map { |value| converter.decode(value) }
|
31
|
+
.first
|
32
|
+
end
|
33
|
+
|
34
|
+
def assign_to(style, value)
|
35
|
+
(required && value.nil?) &&
|
36
|
+
raise(Errors::StyleRequiredPropertyValue, "Required value #{name}")
|
37
|
+
|
38
|
+
validator.validate(value) ||
|
39
|
+
raise(Errors::StyleInvalidPropertyValue, "Invalid value for #{name}: '#{value.nil? ? "nil" : value}'")
|
40
|
+
|
41
|
+
encoded_value = converter.encode(value)
|
42
|
+
|
43
|
+
selectors.map do |attribute_xpath|
|
44
|
+
if (existing_attribute = style.node.at_xpath(attribute_xpath))
|
45
|
+
if encoded_value.nil?
|
46
|
+
existing_attribute.remove
|
47
|
+
else
|
48
|
+
existing_attribute.value = encoded_value.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
next encoded_value
|
52
|
+
end
|
53
|
+
|
54
|
+
next encoded_value if encoded_value.nil?
|
55
|
+
|
56
|
+
node_xpath, attribute = attribute_xpath.split("/@")
|
57
|
+
|
58
|
+
created_node =
|
59
|
+
node_xpath
|
60
|
+
.split("/")
|
61
|
+
.reduce(style.node) do |parent_node, child_xpath|
|
62
|
+
# find the child node
|
63
|
+
parent_node.at_xpath(child_xpath) ||
|
64
|
+
# or create the child node
|
65
|
+
Nokogiri::XML::Node.new(child_xpath, parent_node)
|
66
|
+
.tap { |created_child_node| parent_node << created_child_node }
|
67
|
+
end
|
68
|
+
|
69
|
+
created_node.set_attribute(attribute, encoded_value)
|
70
|
+
end
|
71
|
+
.first
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
@attributes = []
|
76
|
+
|
77
|
+
class << self
|
78
|
+
attr_accessor :attributes
|
79
|
+
|
80
|
+
def required_attributes
|
81
|
+
attributes.select(&:required?)
|
82
|
+
end
|
83
|
+
|
84
|
+
def attribute(name, *selectors, required: false, converter: nil, validator: nil)
|
85
|
+
new_attribute = Attribute.new(name, selectors, required: required, converter: converter, validator: validator)
|
86
|
+
attributes << new_attribute
|
87
|
+
|
88
|
+
define_method(name) do
|
89
|
+
new_attribute.retrieve_from(self)
|
90
|
+
end
|
91
|
+
|
92
|
+
define_method("#{name}=") do |value|
|
93
|
+
new_attribute.assign_to(self, value)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def create(configuration, attributes = {})
|
98
|
+
node = Nokogiri::XML::Node.new("w:style", configuration.styles_parent_node)
|
99
|
+
configuration.styles_parent_node.add_child(node)
|
100
|
+
|
101
|
+
Elements::Style.new(configuration, node, **attributes)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def initialize(configuration, node, **attributes)
|
106
|
+
@configuration = configuration
|
107
|
+
@node = node
|
108
|
+
|
109
|
+
attributes.each do |name, value|
|
110
|
+
self.send("#{name}=", value)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
attr_accessor :node
|
115
|
+
|
116
|
+
attribute :id, "./@w:styleId", required: true
|
117
|
+
attribute :name, "./w:name/@w:val", "./w:next/@w:val", required: true
|
118
|
+
attribute :type, ".//@w:type", required: true, validator: Validators::ValueValidator.new("paragraph", "character", "table", "numbering")
|
119
|
+
attribute :keep_next, "./w:pPr/w:keepNext/@w:val", converter: Converters::BooleanConverter
|
120
|
+
attribute :keep_lines, "./w:pPr/w:keepLines/@w:val", converter: Converters::BooleanConverter
|
121
|
+
attribute :page_break_before, "./w:pPr/w:pageBreakBefore/@w:val", converter: Converters::BooleanConverter
|
122
|
+
attribute :widow_control, "./w:pPr/w:widowControl/@w:val", converter: Converters::BooleanConverter
|
123
|
+
attribute :shading_style, "./w:pPr/w:shd/@w:val", "./w:rPr/w:shd/@w:val"
|
124
|
+
attribute :shading_color, "./w:pPr/w:shd/@w:color", "./w:rPr/w:shd/@w:color", validator: Validators::ColorValidator
|
125
|
+
attribute :shading_fill, "./w:pPr/w:shd/@w:fill", "./w:rPr/w:shd/@w:fill"
|
126
|
+
attribute :suppress_auto_hyphens, "./w:pPr/w:suppressAutoHyphens/@w:val", converter: Converters::BooleanConverter
|
127
|
+
attribute :bidirectional_text, "./w:pPr/w:bidi/@w:val", converter: Converters::BooleanConverter
|
128
|
+
attribute :spacing_before, "./w:pPr/w:spacing/@w:before"
|
129
|
+
attribute :spacing_after, "./w:pPr/w:spacing/@w:after"
|
130
|
+
attribute :line_spacing, "./w:pPr/w:spacing/@w:line"
|
131
|
+
attribute :line_rule, "./w:pPr/w:spacing/@w:lineRule"
|
132
|
+
attribute :indent_left, "./w:pPr/w:ind/@w:left"
|
133
|
+
attribute :indent_right, "./w:pPr/w:ind/@w:right"
|
134
|
+
attribute :indent_first_line, "./w:pPr/w:ind/@w:firstLine"
|
135
|
+
attribute :align, "./w:pPr/w:jc/@w:val"
|
136
|
+
attribute :font, "./w:rPr/w:rFonts/@w:ascii", "./w:rPr/w:rFonts/@w:cs", "./w:rPr/w:rFonts/@w:hAnsi", "./w:rPr/w:rFonts/@w:eastAsia" # setting :font, will set all other fonts
|
137
|
+
attribute :font_ascii, "./w:rPr/w:rFonts/@w:ascii"
|
138
|
+
attribute :font_cs, "./w:rPr/w:rFonts/@w:cs"
|
139
|
+
attribute :font_hAnsi, "./w:rPr/w:rFonts/@w:hAnsi"
|
140
|
+
attribute :font_eastAsia, "./w:rPr/w:rFonts/@w:eastAsia"
|
141
|
+
attribute :bold, "./w:rPr/w:b/@w:val", "./w:rPr/w:bCs/@w:val", converter: Converters::BooleanConverter
|
142
|
+
attribute :italic, "./w:rPr/w:i/@w:val", "./w:rPr/w:iCs/@w:val", converter: Converters::BooleanConverter
|
143
|
+
attribute :caps, "./w:rPr/w:caps/@w:val", converter: Converters::BooleanConverter
|
144
|
+
attribute :small_caps, "./w:rPr/w:smallCaps/@w:val", converter: Converters::BooleanConverter
|
145
|
+
attribute :strike, "./w:rPr/w:strike/@w:val", converter: Converters::BooleanConverter
|
146
|
+
attribute :double_strike, "./w:rPr/w:dstrike/@w:val", converter: Converters::BooleanConverter
|
147
|
+
attribute :outline, "./w:rPr/w:outline/@w:val", converter: Converters::BooleanConverter
|
148
|
+
attribute :outline_level, "./w:pPr/w:outlineLvl/@w:val"
|
149
|
+
attribute :font_color, "./w:rPr/w:color/@w:val", validator: Validators::ColorValidator
|
150
|
+
attribute :font_size, "./w:rPr/w:sz/@w:val", "./w:rPr/w:szCs/@w:val", converter: Converters::FontSizeConverter
|
151
|
+
attribute :font_size_cs, "./w:rPr/w:szCs/@w:val", converter: Converters::FontSizeConverter
|
152
|
+
attribute :underline_style, "./w:rPr/w:u/@w:val"
|
153
|
+
attribute :underline_color, "./w:rPr/w:u/@w:color", validator: Validators::ColorValidator
|
154
|
+
attribute :spacing, "./w:rPr/w:spacing/@w:val"
|
155
|
+
attribute :kerning, "./w:rPr/w:kern/@w:val"
|
156
|
+
attribute :position, "./w:rPr/w:position/@w:val"
|
157
|
+
attribute :text_fill_color, "./w:rPr/w14:textFill/w14:solidFill/w14:srgbClr/@w14:val", validator: Validators::ColorValidator
|
158
|
+
attribute :vertical_alignment, "./w:rPr/w:vertAlign/@w:val"
|
159
|
+
attribute :lang, "./w:rPr/w:lang/@w:val"
|
160
|
+
|
161
|
+
def valid?
|
162
|
+
self.class.required_attributes.all? do |a|
|
163
|
+
attribute_value = a.retrieve_from(self)
|
164
|
+
|
165
|
+
a.validator&.validate(attribute_value)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def to_xml
|
170
|
+
node.to_xml
|
171
|
+
end
|
172
|
+
|
173
|
+
def remove
|
174
|
+
node.remove
|
175
|
+
@configuration.styles.delete(self)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
end
|
data/lib/docx/elements/text.rb
CHANGED
@@ -2,16 +2,22 @@ module Docx
|
|
2
2
|
module Elements
|
3
3
|
class Text
|
4
4
|
include Element
|
5
|
-
delegate :content, :content=, :to => :@node
|
6
5
|
|
7
6
|
def self.tag
|
8
7
|
't'
|
9
8
|
end
|
10
9
|
|
10
|
+
def content
|
11
|
+
@node.content
|
12
|
+
end
|
13
|
+
|
14
|
+
def content=(args)
|
15
|
+
@node.content = args
|
16
|
+
end
|
11
17
|
|
12
18
|
def initialize(node)
|
13
19
|
@node = node
|
14
20
|
end
|
15
21
|
end
|
16
22
|
end
|
17
|
-
end
|
23
|
+
end
|
data/lib/docx/elements.rb
CHANGED
data/lib/docx/errors.rb
ADDED
data/lib/docx/helpers.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Docx
|
2
|
+
module SimpleInspect
|
3
|
+
# Returns a string representation of the document that is far more readable and understandable
|
4
|
+
# than the default inspect method. But you can still get the default inspect method by passing
|
5
|
+
# true as the first argument.
|
6
|
+
def inspect(full = false)
|
7
|
+
return(super) if full
|
8
|
+
|
9
|
+
variable_values =
|
10
|
+
instance_variables.map do |var|
|
11
|
+
value = v = instance_variable_get(var).inspect
|
12
|
+
|
13
|
+
[
|
14
|
+
var,
|
15
|
+
value.length > 100 ? "#{value[0..100]}..." : value
|
16
|
+
].join('=')
|
17
|
+
end
|
18
|
+
|
19
|
+
"#<#{self.class}:0x#{(object_id << 1).to_s(16)} #{variable_values.join(' ')}>"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/docx/version.rb
CHANGED
data/lib/docx.rb
CHANGED
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: docx
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Hunt
|
@@ -9,10 +9,9 @@ authors:
|
|
9
9
|
- Higgins Dragon
|
10
10
|
- Toms Mikoss
|
11
11
|
- Sebastian Wittenkamp
|
12
|
-
autorequire:
|
13
12
|
bindir: bin
|
14
13
|
cert_chain: []
|
15
|
-
date:
|
14
|
+
date: 2025-04-20 00:00:00.000000000 Z
|
16
15
|
dependencies:
|
17
16
|
- !ruby/object:Gem::Dependency
|
18
17
|
name: nokogiri
|
@@ -104,23 +103,27 @@ files:
|
|
104
103
|
- lib/docx/containers.rb
|
105
104
|
- lib/docx/containers/container.rb
|
106
105
|
- lib/docx/containers/paragraph.rb
|
106
|
+
- lib/docx/containers/styles_configuration.rb
|
107
107
|
- lib/docx/containers/table.rb
|
108
108
|
- lib/docx/containers/table_cell.rb
|
109
109
|
- lib/docx/containers/table_column.rb
|
110
110
|
- lib/docx/containers/table_row.rb
|
111
111
|
- lib/docx/containers/text_run.rb
|
112
|
-
- lib/docx/core_ext/module.rb
|
113
112
|
- lib/docx/document.rb
|
114
113
|
- lib/docx/elements.rb
|
115
114
|
- lib/docx/elements/bookmark.rb
|
116
115
|
- lib/docx/elements/element.rb
|
116
|
+
- lib/docx/elements/style.rb
|
117
|
+
- lib/docx/elements/style/converters.rb
|
118
|
+
- lib/docx/elements/style/validators.rb
|
117
119
|
- lib/docx/elements/text.rb
|
120
|
+
- lib/docx/errors.rb
|
121
|
+
- lib/docx/helpers.rb
|
118
122
|
- lib/docx/version.rb
|
119
123
|
homepage: https://github.com/chrahunt/docx
|
120
124
|
licenses:
|
121
125
|
- MIT
|
122
126
|
metadata: {}
|
123
|
-
post_install_message:
|
124
127
|
rdoc_options: []
|
125
128
|
require_paths:
|
126
129
|
- lib
|
@@ -128,15 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
128
131
|
requirements:
|
129
132
|
- - ">="
|
130
133
|
- !ruby/object:Gem::Version
|
131
|
-
version: 2.
|
134
|
+
version: 2.7.0
|
132
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
133
136
|
requirements:
|
134
137
|
- - ">="
|
135
138
|
- !ruby/object:Gem::Version
|
136
139
|
version: '0'
|
137
140
|
requirements: []
|
138
|
-
rubygems_version: 3.
|
139
|
-
signing_key:
|
141
|
+
rubygems_version: 3.6.2
|
140
142
|
specification_version: 4
|
141
143
|
summary: a ruby library/gem for interacting with .docx files
|
142
144
|
test_files: []
|
data/lib/docx/core_ext/module.rb
DELETED
@@ -1,172 +0,0 @@
|
|
1
|
-
unless Object.const_defined?("ActiveSupport")
|
2
|
-
class Module
|
3
|
-
# Provides a delegate class method to easily expose contained objects' public methods
|
4
|
-
# as your own. Pass one or more methods (specified as symbols or strings)
|
5
|
-
# and the name of the target object via the <tt>:to</tt> option (also a symbol
|
6
|
-
# or string). At least one method and the <tt>:to</tt> option are required.
|
7
|
-
#
|
8
|
-
# Delegation is particularly useful with Active Record associations:
|
9
|
-
#
|
10
|
-
# class Greeter < ActiveRecord::Base
|
11
|
-
# def hello
|
12
|
-
# 'hello'
|
13
|
-
# end
|
14
|
-
#
|
15
|
-
# def goodbye
|
16
|
-
# 'goodbye'
|
17
|
-
# end
|
18
|
-
# end
|
19
|
-
#
|
20
|
-
# class Foo < ActiveRecord::Base
|
21
|
-
# belongs_to :greeter
|
22
|
-
# delegate :hello, to: :greeter
|
23
|
-
# end
|
24
|
-
#
|
25
|
-
# Foo.new.hello # => "hello"
|
26
|
-
# Foo.new.goodbye # => NoMethodError: undefined method `goodbye' for #<Foo:0x1af30c>
|
27
|
-
#
|
28
|
-
# Multiple delegates to the same target are allowed:
|
29
|
-
#
|
30
|
-
# class Foo < ActiveRecord::Base
|
31
|
-
# belongs_to :greeter
|
32
|
-
# delegate :hello, :goodbye, to: :greeter
|
33
|
-
# end
|
34
|
-
#
|
35
|
-
# Foo.new.goodbye # => "goodbye"
|
36
|
-
#
|
37
|
-
# Methods can be delegated to instance variables, class variables, or constants
|
38
|
-
# by providing them as a symbols:
|
39
|
-
#
|
40
|
-
# class Foo
|
41
|
-
# CONSTANT_ARRAY = [0,1,2,3]
|
42
|
-
# @@class_array = [4,5,6,7]
|
43
|
-
#
|
44
|
-
# def initialize
|
45
|
-
# @instance_array = [8,9,10,11]
|
46
|
-
# end
|
47
|
-
# delegate :sum, to: :CONSTANT_ARRAY
|
48
|
-
# delegate :min, to: :@@class_array
|
49
|
-
# delegate :max, to: :@instance_array
|
50
|
-
# end
|
51
|
-
#
|
52
|
-
# Foo.new.sum # => 6
|
53
|
-
# Foo.new.min # => 4
|
54
|
-
# Foo.new.max # => 11
|
55
|
-
#
|
56
|
-
# It's also possible to delegate a method to the class by using +:class+:
|
57
|
-
#
|
58
|
-
# class Foo
|
59
|
-
# def self.hello
|
60
|
-
# "world"
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# delegate :hello, to: :class
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# Foo.new.hello # => "world"
|
67
|
-
#
|
68
|
-
# Delegates can optionally be prefixed using the <tt>:prefix</tt> option. If the value
|
69
|
-
# is <tt>true</tt>, the delegate methods are prefixed with the name of the object being
|
70
|
-
# delegated to.
|
71
|
-
#
|
72
|
-
# Person = Struct.new(:name, :address)
|
73
|
-
#
|
74
|
-
# class Invoice < Struct.new(:client)
|
75
|
-
# delegate :name, :address, to: :client, prefix: true
|
76
|
-
# end
|
77
|
-
#
|
78
|
-
# john_doe = Person.new('John Doe', 'Vimmersvej 13')
|
79
|
-
# invoice = Invoice.new(john_doe)
|
80
|
-
# invoice.client_name # => "John Doe"
|
81
|
-
# invoice.client_address # => "Vimmersvej 13"
|
82
|
-
#
|
83
|
-
# It is also possible to supply a custom prefix.
|
84
|
-
#
|
85
|
-
# class Invoice < Struct.new(:client)
|
86
|
-
# delegate :name, :address, to: :client, prefix: :customer
|
87
|
-
# end
|
88
|
-
#
|
89
|
-
# invoice = Invoice.new(john_doe)
|
90
|
-
# invoice.customer_name # => 'John Doe'
|
91
|
-
# invoice.customer_address # => 'Vimmersvej 13'
|
92
|
-
#
|
93
|
-
# If the delegate object is +nil+ an exception is raised, and that happens
|
94
|
-
# no matter whether +nil+ responds to the delegated method. You can get a
|
95
|
-
# +nil+ instead with the +:allow_nil+ option.
|
96
|
-
#
|
97
|
-
# class Foo
|
98
|
-
# attr_accessor :bar
|
99
|
-
# def initialize(bar = nil)
|
100
|
-
# @bar = bar
|
101
|
-
# end
|
102
|
-
# delegate :zoo, to: :bar
|
103
|
-
# end
|
104
|
-
#
|
105
|
-
# Foo.new.zoo # raises NoMethodError exception (you called nil.zoo)
|
106
|
-
#
|
107
|
-
# class Foo
|
108
|
-
# attr_accessor :bar
|
109
|
-
# def initialize(bar = nil)
|
110
|
-
# @bar = bar
|
111
|
-
# end
|
112
|
-
# delegate :zoo, to: :bar, allow_nil: true
|
113
|
-
# end
|
114
|
-
#
|
115
|
-
# Foo.new.zoo # returns nil
|
116
|
-
def delegate(*methods)
|
117
|
-
options = methods.pop
|
118
|
-
unless options.is_a?(Hash) && to = options[:to]
|
119
|
-
raise ArgumentError, 'Delegation needs a target. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
|
120
|
-
end
|
121
|
-
|
122
|
-
prefix, allow_nil = options.values_at(:prefix, :allow_nil)
|
123
|
-
|
124
|
-
if prefix == true && to =~ /^[^a-z_]/
|
125
|
-
raise ArgumentError, 'Can only automatically set the delegation prefix when delegating to a method.'
|
126
|
-
end
|
127
|
-
|
128
|
-
method_prefix = \
|
129
|
-
if prefix
|
130
|
-
"#{prefix == true ? to : prefix}_"
|
131
|
-
else
|
132
|
-
''
|
133
|
-
end
|
134
|
-
|
135
|
-
file, line = caller.first.split(':', 2)
|
136
|
-
line = line.to_i
|
137
|
-
|
138
|
-
to = to.to_s
|
139
|
-
to = 'self.class' if to == 'class'
|
140
|
-
|
141
|
-
methods.each do |method|
|
142
|
-
# Attribute writer methods only accept one argument. Makes sure []=
|
143
|
-
# methods still accept two arguments.
|
144
|
-
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
|
145
|
-
|
146
|
-
if allow_nil
|
147
|
-
module_eval(<<-EOS, file, line - 2)
|
148
|
-
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
|
149
|
-
if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
|
150
|
-
#{to}.#{method}(#{definition}) # client.name(*args, &block)
|
151
|
-
end # end
|
152
|
-
end # end
|
153
|
-
EOS
|
154
|
-
else
|
155
|
-
exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
|
156
|
-
|
157
|
-
module_eval(<<-EOS, file, line - 1)
|
158
|
-
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
|
159
|
-
#{to}.#{method}(#{definition}) # client.name(*args, &block)
|
160
|
-
rescue NoMethodError # rescue NoMethodError
|
161
|
-
if #{to}.nil? # if client.nil?
|
162
|
-
#{exception} # # add helpful message to the exception
|
163
|
-
else # else
|
164
|
-
raise # raise
|
165
|
-
end # end
|
166
|
-
end # end
|
167
|
-
EOS
|
168
|
-
end
|
169
|
-
end
|
170
|
-
end
|
171
|
-
end
|
172
|
-
end
|