Skip to content

Commit

Permalink
Merge pull request #145 from rickychilcott/feature/full-style-support
Browse files Browse the repository at this point in the history
Fully Support Styles
  • Loading branch information
satoryu authored Mar 10, 2024
2 parents 9ea9595 + 22094ca commit d62cfe8
Show file tree
Hide file tree
Showing 17 changed files with 828 additions and 37 deletions.
73 changes: 72 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,83 @@ p_children = p_element.xpath("//child::*") # selects all children
p_child = p_element.at_xpath("//child::*") # selects first child
```

### Writing and Manipulating Styles
``` ruby
require 'docx'

d = Docx::Document.open('example.docx')
existing_style = d.styles_configuration.style_of("Heading 1")
existing_style.font_color = "000000"

# see attributes below
new_style = d.styles_configuration.add_style("Red", name: "Red", font_color: "FF0000", font_size: 20)
new_style.bold = true

d.paragraphs.each do |p|
p.style = "Red"
end

d.paragraphs.each do |p|
p.style = "Heading 1"
end

d.styles_configuration.remove_style("Red")
```

#### Style Attributes

The following is a list of attributes and what they control within the style.

- **id**: The unique identifier of the style. (required)
- **name**: The human-readable name of the style. (required)
- **type**: Indicates the type of the style (e.g., paragraph, character).
- **keep_next**: Boolean value controlling whether to keep a paragraph and the next one on the same page. Valid values: `true`/`false`.
- **keep_lines**: Boolean value specifying whether to keep all lines of a paragraph together on one page. Valid values: `true`/`false`.
- **page_break_before**: Boolean value indicating whether to insert a page break before the paragraph. Valid values: `true`/`false`.
- **widow_control**: Boolean value controlling widow and orphan lines in a paragraph. Valid values: `true`/`false`.
- **shading_style**: Defines the shading pattern style.
- **shading_color**: Specifies the color of the shading pattern. Valid values: Hex color codes.
- **shading_fill**: Indicates the background fill color of shading.
- **suppress_auto_hyphens**: Boolean value controlling automatic hyphenation. Valid values: `true`/`false`.
- **bidirectional_text**: Boolean value indicating if the paragraph contains bidirectional text. Valid values: `true`/`false`.
- **spacing_before**: Defines the spacing before a paragraph.
- **spacing_after**: Specifies the spacing after a paragraph.
- **line_spacing**: Indicates the line spacing of a paragraph.
- **line_rule**: Defines how line spacing is calculated.
- **indent_left**: Sets the left indentation of a paragraph.
- **indent_right**: Specifies the right indentation of a paragraph.
- **indent_first_line**: Indicates the first line indentation of a paragraph.
- **align**: Controls the text alignment within a paragraph.
- **font**: Sets the font for different scripts (ASCII, complex script, East Asian, etc.).
- **font_ascii**: Specifies the font for ASCII characters.
- **font_cs**: Indicates the font for complex script characters.
- **font_hAnsi**: Sets the font for high ANSI characters.
- **font_eastAsia**: Specifies the font for East Asian characters.
- **bold**: Boolean value controlling bold formatting. Valid values: `true`/`false`.
- **italic**: Boolean value indicating italic formatting. Valid values: `true`/`false`.
- **caps**: Boolean value controlling capitalization. Valid values: `true`/`false`.
- **small_caps**: Boolean value specifying small capital letters. Valid values: `true`/`false`.
- **strike**: Boolean value indicating strikethrough formatting. Valid values: `true`/`false`.
- **double_strike**: Boolean value defining double strikethrough formatting. Valid values: `true`/`false`.
- **outline**: Boolean value specifying outline effects. Valid values: `true`/`false`.
- **outline_level**: Indicates the outline level in a document's hierarchy.
- **font_color**: Sets the text color. Valid values: Hex color codes.
- **font_size**: Controls the font size.
- **font_size_cs**: Specifies the font size for complex script characters.
- **underline_style**: Indicates the style of underlining.
- **underline_color**: Specifies the color of the underline. Valid values: Hex color codes.
- **spacing**: Controls character spacing.
- **kerning**: Sets the space between characters.
- **position**: Controls the position of characters (superscript/subscript).
- **text_fill_color**: Sets the fill color of text. Valid values: Hex color codes.
- **vertical_alignment**: Controls the vertical alignment of text within a line.
- **lang**: Specifies the language tag for the text.

## Development

### todo

* Calculate element formatting based on values present in element properties as well as properties inherited from parents
* Default formatting of inserted elements to inherited values
* Implement formattable elements.
* Implement styles.
* Easier multi-line text insertion at a single bookmark (inserting paragraph nodes after the one containing the bookmark)
1 change: 1 addition & 0 deletions lib/docx/containers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
require 'docx/containers/text_run'
require 'docx/containers/paragraph'
require 'docx/containers/table'
require 'docx/containers/styles_configuration'
28 changes: 19 additions & 9 deletions lib/docx/containers/paragraph.rb
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,11 @@ def aligned_center?
end

def font_size
size_tag = @node.xpath('w:pPr//w:sz').first
size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
size_attribute = @node.at_xpath('w:pPr//w:sz//@w:val')

return @font_size unless size_attribute

size_attribute.value.to_i / 2
end

def font_color
Expand All @@ -90,25 +93,32 @@ def font_color
def style
return nil unless @document

if style_property.nil?
@document.style_name_of(style_id) ||
@document.default_paragraph_style
else
@document.style_name(style_property.attributes['val'].value)
end
end

def style_id
style_property.get_attribute('w:val')
end

def style=(identifier)
id = @document.styles_configuration.style_of(identifier).id

style_property.set_attribute('w:val', id)
end

alias_method :style_id=, :style=
alias_method :text, :to_s

private

def style_property
properties&.at_xpath('w:pStyle')
properties&.at_xpath('w:pStyle') || properties&.add_child('<w:pStyle/>').first
end

# Returns the alignment if any, or nil if left
def alignment
alignment_tag = @node.xpath('.//w:jc').first
alignment_tag ? alignment_tag.attributes['val'].value : nil
@node.at_xpath('.//w:jc/@w:val')&.value
end
end
end
Expand Down
52 changes: 52 additions & 0 deletions lib/docx/containers/styles_configuration.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
require 'docx/containers/container'
require 'docx/elements/style'

module Docx
module Elements
module Containers
StyleNotFound = Class.new(StandardError)

class StylesConfiguration
def initialize(raw_styles)
@raw_styles = raw_styles
@styles_parent_node = raw_styles.root
end

attr_reader :styles, :styles_parent_node

def styles
styles_parent_node
.children
.filter_map do |style|
next unless style.get_attribute("w:styleId")

Elements::Style.new(self, style)
end
end

def style_of(id_or_name)
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")
end

def size
styles.size
end

def add_style(id, attributes = {})
Elements::Style.create(self, {id: id, name: id}.merge(attributes))
end

def remove_style(id)
style = styles.find { |style| style.id == id }

style.node.remove
styles.delete(style)
end

def serialize(**options)
@raw_styles.serialize(**options)
end
end
end
end
end
7 changes: 5 additions & 2 deletions lib/docx/containers/text_run.rb
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,11 @@ def hyperlink_id
end

def font_size
size_tag = @node.xpath('w:rPr//w:sz').first
size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
size_attribute = @node.at_xpath('w:rPr//w:sz//@w:val')

return @font_size unless size_attribute

size_attribute.value.to_i / 2
end

private
Expand Down
34 changes: 19 additions & 15 deletions lib/docx/document.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
require 'docx/containers'
require 'docx/elements'
require 'docx/errors'
require 'docx/helpers'
require 'nokogiri'
require 'zip'

Expand All @@ -18,6 +20,8 @@ module Docx
# puts d.text
# end
class Document
include Docx::SimpleInspect

attr_reader :xml, :doc, :zip, :styles

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

return nil unless size_value

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

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

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

if @replace[entry.name]
out.write(@replace[entry.name])
else
out.write(zip.read(entry.name))
end
out.write(value)
end

end
zip.close
end
Expand Down Expand Up @@ -169,15 +172,15 @@ def replace_entry(entry_path, file_contents)
end

def default_paragraph_style
s = @styles.at_xpath("w:styles/w:style[@w:type='paragraph' and @w:default='1']")
s = s.at_xpath('w:name')
s.attributes['val'].value
@styles.at_xpath("w:styles/w:style[@w:type='paragraph' and @w:default='1']/w:name/@w:val").value
end

def style_name_of(style_id)
styles_configuration.style_of(style_id).name
end

def style_name(style_id)
s = @styles.at_xpath("w:styles/w:style[@w:styleId='#{style_id}']")
s = s.at_xpath('w:name')
s.attributes['val'].value
def styles_configuration
@styles_configuration ||= Elements::Containers::StylesConfiguration.new(@styles.dup)
end

private
Expand Down Expand Up @@ -206,6 +209,7 @@ def load_rels
#++
def update
replace_entry 'word/document.xml', doc.serialize(save_with: 0)
replace_entry 'word/styles.xml', styles_configuration.serialize(save_with: 0)
end

# generate Elements::Containers::Paragraph from paragraph XML node
Expand Down
3 changes: 2 additions & 1 deletion lib/docx/elements.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'docx/elements/bookmark'
require 'docx/elements/element'
require 'docx/elements/text'
require 'docx/elements/text'
require 'docx/elements/style'
Loading

0 comments on commit d62cfe8

Please sign in to comment.