docx 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +72 -1
- 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/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.rb +2 -1
- data/lib/docx/errors.rb +7 -0
- data/lib/docx/helpers.rb +22 -0
- data/lib/docx/version.rb +1 -1
- metadata +10 -7
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
@@ -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
|
@@ -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.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
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,6 +103,7 @@ 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
|
@@ -113,13 +113,17 @@ files:
|
|
113
113
|
- lib/docx/elements.rb
|
114
114
|
- lib/docx/elements/bookmark.rb
|
115
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
|
116
119
|
- lib/docx/elements/text.rb
|
120
|
+
- lib/docx/errors.rb
|
121
|
+
- lib/docx/helpers.rb
|
117
122
|
- lib/docx/version.rb
|
118
123
|
homepage: https://github.com/chrahunt/docx
|
119
124
|
licenses:
|
120
125
|
- MIT
|
121
126
|
metadata: {}
|
122
|
-
post_install_message:
|
123
127
|
rdoc_options: []
|
124
128
|
require_paths:
|
125
129
|
- lib
|
@@ -127,15 +131,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
127
131
|
requirements:
|
128
132
|
- - ">="
|
129
133
|
- !ruby/object:Gem::Version
|
130
|
-
version: 2.
|
134
|
+
version: 2.7.0
|
131
135
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
136
|
requirements:
|
133
137
|
- - ">="
|
134
138
|
- !ruby/object:Gem::Version
|
135
139
|
version: '0'
|
136
140
|
requirements: []
|
137
|
-
rubygems_version: 3.
|
138
|
-
signing_key:
|
141
|
+
rubygems_version: 3.6.2
|
139
142
|
specification_version: 4
|
140
143
|
summary: a ruby library/gem for interacting with .docx files
|
141
144
|
test_files: []
|