Skip to content

Commit d62cfe8

Browse files
authored
Merge pull request #145 from rickychilcott/feature/full-style-support
Fully Support Styles
2 parents 9ea9595 + 22094ca commit d62cfe8

File tree

17 files changed

+828
-37
lines changed

17 files changed

+828
-37
lines changed

README.md

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,83 @@ p_children = p_element.xpath("//child::*") # selects all children
181181
p_child = p_element.at_xpath("//child::*") # selects first child
182182
```
183183

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+
184256
## Development
185257

186258
### todo
187259

188260
* Calculate element formatting based on values present in element properties as well as properties inherited from parents
189261
* Default formatting of inserted elements to inherited values
190262
* Implement formattable elements.
191-
* Implement styles.
192263
* Easier multi-line text insertion at a single bookmark (inserting paragraph nodes after the one containing the bookmark)

lib/docx/containers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@
22
require 'docx/containers/text_run'
33
require 'docx/containers/paragraph'
44
require 'docx/containers/table'
5+
require 'docx/containers/styles_configuration'

lib/docx/containers/paragraph.rb

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,11 @@ def aligned_center?
7878
end
7979

8080
def font_size
81-
size_tag = @node.xpath('w:pPr//w:sz').first
82-
size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
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
8386
end
8487

8588
def font_color
@@ -90,25 +93,32 @@ def font_color
9093
def style
9194
return nil unless @document
9295

93-
if style_property.nil?
96+
@document.style_name_of(style_id) ||
9497
@document.default_paragraph_style
95-
else
96-
@document.style_name(style_property.attributes['val'].value)
97-
end
9898
end
9999

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)
108+
end
109+
110+
alias_method :style_id=, :style=
100111
alias_method :text, :to_s
101112

102113
private
103114

104115
def style_property
105-
properties&.at_xpath('w:pStyle')
116+
properties&.at_xpath('w:pStyle') || properties&.add_child('<w:pStyle/>').first
106117
end
107118

108119
# Returns the alignment if any, or nil if left
109120
def alignment
110-
alignment_tag = @node.xpath('.//w:jc').first
111-
alignment_tag ? alignment_tag.attributes['val'].value : nil
121+
@node.at_xpath('.//w:jc/@w:val')&.value
112122
end
113123
end
114124
end
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/docx/containers/text_run.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,11 @@ def hyperlink_id
118118
end
119119

120120
def font_size
121-
size_tag = @node.xpath('w:rPr//w:sz').first
122-
size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
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
123126
end
124127

125128
private

lib/docx/document.rb

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
require 'docx/containers'
22
require 'docx/elements'
3+
require 'docx/errors'
4+
require 'docx/helpers'
35
require 'nokogiri'
46
require 'zip'
57

@@ -18,6 +20,8 @@ module Docx
1820
# puts d.text
1921
# end
2022
class Document
23+
include Docx::SimpleInspect
24+
2125
attr_reader :xml, :doc, :zip, :styles
2226

2327
def initialize(path_or_io, options = {})
@@ -82,10 +86,11 @@ def tables
8286
# Some documents have this set, others don't.
8387
# Values are returned as half-points, so to get points, that's why it's divided by 2.
8488
def font_size
85-
return nil unless @styles
89+
size_value = @styles&.at_xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz/@w:val')&.value
90+
91+
return nil unless size_value
8692

87-
size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
88-
size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
93+
size_value.to_i / 2
8994
end
9095

9196
# Hyperlink targets are extracted from the document.xml.rels file
@@ -130,13 +135,11 @@ def save(path)
130135
next unless entry.file?
131136

132137
out.put_next_entry(entry.name)
138+
value = @replace[entry.name] || zip.read(entry.name)
133139

134-
if @replace[entry.name]
135-
out.write(@replace[entry.name])
136-
else
137-
out.write(zip.read(entry.name))
138-
end
140+
out.write(value)
139141
end
142+
140143
end
141144
zip.close
142145
end
@@ -169,15 +172,15 @@ def replace_entry(entry_path, file_contents)
169172
end
170173

171174
def default_paragraph_style
172-
s = @styles.at_xpath("w:styles/w:style[@w:type='paragraph' and @w:default='1']")
173-
s = s.at_xpath('w:name')
174-
s.attributes['val'].value
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
175180
end
176181

177-
def style_name(style_id)
178-
s = @styles.at_xpath("w:styles/w:style[@w:styleId='#{style_id}']")
179-
s = s.at_xpath('w:name')
180-
s.attributes['val'].value
182+
def styles_configuration
183+
@styles_configuration ||= Elements::Containers::StylesConfiguration.new(@styles.dup)
181184
end
182185

183186
private
@@ -206,6 +209,7 @@ def load_rels
206209
#++
207210
def update
208211
replace_entry 'word/document.xml', doc.serialize(save_with: 0)
212+
replace_entry 'word/styles.xml', styles_configuration.serialize(save_with: 0)
209213
end
210214

211215
# generate Elements::Containers::Paragraph from paragraph XML node

lib/docx/elements.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
require 'docx/elements/bookmark'
22
require 'docx/elements/element'
3-
require 'docx/elements/text'
3+
require 'docx/elements/text'
4+
require 'docx/elements/style'

0 commit comments

Comments
 (0)