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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96b871b0d07fecde4e6c93c67463da727565f3acca519b08b9c554d987570e58
4
- data.tar.gz: 2f3b9362b91c33d02e6c1b4acf0b23fb723f71d26f9b2e800b930c91c3687eee
3
+ metadata.gz: 38d8b4c53502b022e87a0b00ac8b15c99c06d79c2a51e664364ddd52dcca52a7
4
+ data.tar.gz: e7a747b4ef58b53e279fdd0cfc516b06ab5c14820e622938bea4ec73858f637c
5
5
  SHA512:
6
- metadata.gz: 5e198cb74eb7a06b62bb63a3a0ff065b0c95cf26aafb53320bb5b14339dc386d975a1f57178316d1d5d7753081e07989b4498bcdfa3f0092f2fc14b1e4086b84
7
- data.tar.gz: d0626b7332fd3a3d95b6358a3fb6ea09b0faf58545db9bd948c9f97baa5457eacb43267cc40914df2fa54872b42cca5c1963f66111a8b0b4be33a3e6f99c4b38
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.5 or later
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("./#{@properties_tag}")
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(".//w:t").each {|t| t.content = '' }
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
- size_tag = @node.xpath('w:pPr//w:sz').first
80
- 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
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
- alignment_tag = @node.xpath('.//w:jc').first
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
- size_tag = @node.xpath('w:rPr//w:sz').first
111
- 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
112
126
  end
113
127
 
114
128
  private
@@ -2,3 +2,4 @@ require 'docx/containers/container'
2
2
  require 'docx/containers/text_run'
3
3
  require 'docx/containers/paragraph'
4
4
  require 'docx/containers/table'
5
+ require 'docx/containers/styles_configuration'
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
- 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
85
92
 
86
- size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
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
- if @replace[entry.name]
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
@@ -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
@@ -1,3 +1,4 @@
1
1
  require 'docx/elements/bookmark'
2
2
  require 'docx/elements/element'
3
- require 'docx/elements/text'
3
+ require 'docx/elements/text'
4
+ require 'docx/elements/style'
@@ -0,0 +1,7 @@
1
+ module Docx
2
+ module Errors
3
+ StyleNotFound = Class.new(StandardError)
4
+ StyleInvalidPropertyValue = Class.new(StandardError)
5
+ StyleRequiredPropertyValue = Class.new(StandardError)
6
+ end
7
+ end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Docx #:nodoc:
4
- VERSION = '0.7.0'
4
+ VERSION = '0.9.0'
5
5
  end
data/lib/docx.rb CHANGED
@@ -4,4 +4,3 @@ module Docx #:nodoc:
4
4
  autoload :Document, 'docx/document'
5
5
  end
6
6
 
7
- require 'docx/core_ext/module'
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.7.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: 2022-03-13 00:00:00.000000000 Z
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.6.0
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.3.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: []
@@ -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