bitops-docx 0.2.07
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 +7 -0
- data/LICENSE.md +21 -0
- data/README.md +114 -0
- data/lib/docx.rb +7 -0
- data/lib/docx/containers.rb +4 -0
- data/lib/docx/containers/container.rb +20 -0
- data/lib/docx/containers/paragraph.rb +96 -0
- data/lib/docx/containers/table.rb +51 -0
- data/lib/docx/containers/table_cell.rb +39 -0
- data/lib/docx/containers/table_column.rb +29 -0
- data/lib/docx/containers/table_row.rb +28 -0
- data/lib/docx/containers/text_run.rb +92 -0
- data/lib/docx/core_ext/module.rb +172 -0
- data/lib/docx/document.rb +148 -0
- data/lib/docx/elements.rb +3 -0
- data/lib/docx/elements/bookmark.rb +79 -0
- data/lib/docx/elements/element.rb +96 -0
- data/lib/docx/elements/text.rb +17 -0
- data/lib/docx/version.rb +3 -0
- metadata +107 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 746f86da717d8e88492061e60c5394f5ee54b38c
|
4
|
+
data.tar.gz: 5ca6b5f0cbc0bfc26f79e9c8efbddbf11878df70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 470948df1d7d617e731090a2336a4e2e3c8aa9789e699c66936d653d26c68d98b39ba61714f257601ea38ddde3d5a2bafa9096a3666abcd741b0bd88cf9891ea
|
7
|
+
data.tar.gz: a04e5def0dff4febe5311ca62b21f0887edeb2b9863c425c261e9cdab0efebce966f9929c0f770c76be7fe790dc9f0f497792dbacffcb013af4e1ee4d8674157
|
data/LICENSE.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License
|
2
|
+
|
3
|
+
Copyright (c) Marcus Ortiz, http://marcusortiz.com
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
# This is a fork!!!
|
2
|
+
|
3
|
+
This library stands on the shoulders of the original [docx](https://github.com/chrahunt/docx) -- it is my hope that [@chrahunt](https://github.com/chrahunt) will pull the changes I've incorporated into this library back into his original library.
|
4
|
+
|
5
|
+
This library depends on the latest version of rubyzip and incorporates changes from these two pull requests:
|
6
|
+
* [Paragraph alignment, text run font sizes, and HTML output](https://github.com/chrahunt/docx/pull/13) from [@higginsdragon](https://github.com/higginsdragon)
|
7
|
+
* [File replacement within document](https://github.com/chrahunt/docx/pull/18) from [@tmikoss](https://github.com/tmikoss)
|
8
|
+
|
9
|
+
# docx
|
10
|
+
|
11
|
+
a ruby library/gem for interacting with `.docx` files. currently capabilities include reading paragraphs/bookmarks, inserting text at bookmarks, reading tables/rows/columns/cells and saving the document.
|
12
|
+
|
13
|
+
## usage
|
14
|
+
|
15
|
+
### install
|
16
|
+
|
17
|
+
requires ruby (tested with 2.1.1)
|
18
|
+
|
19
|
+
gem install docx
|
20
|
+
|
21
|
+
### reading
|
22
|
+
|
23
|
+
``` ruby
|
24
|
+
require 'docx'
|
25
|
+
|
26
|
+
# Create a Docx::Document object for our existing docx file
|
27
|
+
doc = Docx::Document.open('example.docx')
|
28
|
+
|
29
|
+
# Retrieve and display paragraphs
|
30
|
+
doc.paragraphs.each do |p|
|
31
|
+
puts p
|
32
|
+
end
|
33
|
+
|
34
|
+
# Retrieve and display bookmarks, returned as hash with bookmark names as keys and objects as values
|
35
|
+
doc.bookmarks.each_pair do |bookmark_name, bookmark_object|
|
36
|
+
puts bookmark_name
|
37
|
+
end
|
38
|
+
```
|
39
|
+
|
40
|
+
### reading tables
|
41
|
+
|
42
|
+
``` ruby
|
43
|
+
require 'docx'
|
44
|
+
|
45
|
+
# Create a Docx::Document object for our existing docx file
|
46
|
+
doc = Docx::Document.open('tables.docx')
|
47
|
+
|
48
|
+
first_table = doc.tables[0]
|
49
|
+
puts first_table.row_count
|
50
|
+
puts first_table.column_count
|
51
|
+
puts first_table.rows[0].cells[0].text
|
52
|
+
puts first_table.columns[0].cells[0].text
|
53
|
+
|
54
|
+
# Iterate through tables
|
55
|
+
doc.tables.each do |table|
|
56
|
+
table.rows.each do |row| # Row-based iteration
|
57
|
+
row.cells.each do |cell|
|
58
|
+
puts cell.text
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
table.columns.each do |column| # Column-based iteration
|
63
|
+
column.cells.each do |cell|
|
64
|
+
puts cell.text
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
```
|
69
|
+
|
70
|
+
### writing
|
71
|
+
|
72
|
+
``` ruby
|
73
|
+
require 'docx'
|
74
|
+
|
75
|
+
# Create a Docx::Document object for our existing docx file
|
76
|
+
doc = Docx::Document.open('example.docx')
|
77
|
+
|
78
|
+
# Insert a single line of text after one of our bookmarks
|
79
|
+
doc.bookmarks['example_bookmark'].insert_text_after("Hello world.")
|
80
|
+
|
81
|
+
# Insert multiple lines of text at our bookmark
|
82
|
+
doc.bookmarks['example_bookmark_2'].insert_multiple_lines_after(['Hello', 'World', 'foo'])
|
83
|
+
|
84
|
+
# Save document to specified path
|
85
|
+
doc.save('example-edited.docx')
|
86
|
+
```
|
87
|
+
|
88
|
+
### advanced
|
89
|
+
|
90
|
+
``` ruby
|
91
|
+
require 'docx'
|
92
|
+
|
93
|
+
d = Docx::Document.open('example.docx')
|
94
|
+
|
95
|
+
# The Nokogiri::XML::Node on which an element is based can be accessed using #node
|
96
|
+
d.paragraphs.each do |p|
|
97
|
+
puts p.node.inspect
|
98
|
+
end
|
99
|
+
|
100
|
+
# The #xpath and #at_xpath methods are delegated to the node from the element, saving a step
|
101
|
+
p_element = d.paragraphs.first
|
102
|
+
p_children = p_element.xpath("//child::*") # selects all children
|
103
|
+
p_child = p_element.at_xpath("//child::*") # selects first child
|
104
|
+
```
|
105
|
+
|
106
|
+
## Development
|
107
|
+
|
108
|
+
### todo
|
109
|
+
|
110
|
+
* Calculate element formatting based on values present in element properties as well as properties inherited from parents
|
111
|
+
* Default formatting of inserted elements to inherited values
|
112
|
+
* Implement formattable elements.
|
113
|
+
* Implement styles.
|
114
|
+
* Easier multi-line text insertion at a single bookmark (inserting paragraph nodes after the one containing the bookmark)
|
data/lib/docx.rb
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'docx/elements'
|
2
|
+
|
3
|
+
module Docx
|
4
|
+
module Elements
|
5
|
+
module Containers
|
6
|
+
module Container
|
7
|
+
# Relation methods
|
8
|
+
# TODO: Create a properties object, include Element
|
9
|
+
def properties
|
10
|
+
@node.at_xpath("./#{@properties_tag}")
|
11
|
+
end
|
12
|
+
|
13
|
+
# Erase text within an element
|
14
|
+
def blank!
|
15
|
+
@node.xpath(".//w:t").each {|t| t.content = '' }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'docx/containers/text_run'
|
2
|
+
require 'docx/containers/container'
|
3
|
+
|
4
|
+
module Docx
|
5
|
+
module Elements
|
6
|
+
module Containers
|
7
|
+
class Paragraph
|
8
|
+
include Container
|
9
|
+
include Elements::Element
|
10
|
+
|
11
|
+
def self.tag
|
12
|
+
'p'
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
# Child elements: pPr, r, fldSimple, hlink, subDoc
|
17
|
+
# http://msdn.microsoft.com/en-us/library/office/ee364458(v=office.11).aspx
|
18
|
+
def initialize(node, document_properties = {})
|
19
|
+
@node = node
|
20
|
+
@properties_tag = 'pPr'
|
21
|
+
@document_properties = document_properties
|
22
|
+
@font_size = @document_properties[:font_size]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set text of paragraph
|
26
|
+
def text=(content)
|
27
|
+
if text_runs.size == 1
|
28
|
+
text_runs.first.text = content
|
29
|
+
elsif text_runs.size == 0
|
30
|
+
new_r = TextRun.create_within(self)
|
31
|
+
new_r.text = content
|
32
|
+
else
|
33
|
+
text_runs.each {|r| r.node.remove }
|
34
|
+
new_r = TextRun.create_within(self)
|
35
|
+
new_r.text = content
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Return text of paragraph
|
40
|
+
def to_s
|
41
|
+
text_runs.map(&:text).join('')
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return paragraph as a <p></p> HTML fragment with formatting based on properties.
|
45
|
+
def to_html
|
46
|
+
html = ''
|
47
|
+
text_runs.each do |text_run|
|
48
|
+
html << text_run.to_html
|
49
|
+
end
|
50
|
+
styles = { 'font-size' => "#{font_size}pt" }
|
51
|
+
styles['text-align'] = alignment if alignment
|
52
|
+
html_tag(:p, content: html, styles: styles)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
# Array of text runs contained within paragraph
|
57
|
+
def text_runs
|
58
|
+
@node.xpath('w:r|w:hyperlink/w:r').map { |r_node| Containers::TextRun.new(r_node, @document_properties) }
|
59
|
+
end
|
60
|
+
|
61
|
+
# Iterate over each text run within a paragraph
|
62
|
+
def each_text_run
|
63
|
+
text_runs.each { |tr| yield(tr) }
|
64
|
+
end
|
65
|
+
|
66
|
+
def aligned_left?
|
67
|
+
['left', nil].include?(alignment)
|
68
|
+
end
|
69
|
+
|
70
|
+
def aligned_right?
|
71
|
+
alignment == 'right'
|
72
|
+
end
|
73
|
+
|
74
|
+
def aligned_center?
|
75
|
+
alignment == 'center'
|
76
|
+
end
|
77
|
+
|
78
|
+
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
|
+
end
|
82
|
+
|
83
|
+
alias_method :text, :to_s
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
# Returns the alignment if any, or nil if left
|
88
|
+
def alignment
|
89
|
+
alignment_tag = @node.xpath('.//w:jc').first
|
90
|
+
alignment_tag ? alignment_tag.attributes['val'].value : nil
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'docx/containers/table_row'
|
2
|
+
require 'docx/containers/table_column'
|
3
|
+
require 'docx/containers/container'
|
4
|
+
|
5
|
+
module Docx
|
6
|
+
module Elements
|
7
|
+
module Containers
|
8
|
+
class Table
|
9
|
+
include Container
|
10
|
+
include Elements::Element
|
11
|
+
|
12
|
+
def self.tag
|
13
|
+
'tbl'
|
14
|
+
end
|
15
|
+
|
16
|
+
def initialize(node)
|
17
|
+
@node = node
|
18
|
+
@properties_tag = 'tblGrid'
|
19
|
+
end
|
20
|
+
|
21
|
+
# Array of row
|
22
|
+
def rows
|
23
|
+
@node.xpath('w:tr').map {|r_node| Containers::TableRow.new(r_node) }
|
24
|
+
end
|
25
|
+
|
26
|
+
def row_count
|
27
|
+
@node.xpath('w:tr').count
|
28
|
+
end
|
29
|
+
|
30
|
+
# Array of column
|
31
|
+
def columns
|
32
|
+
columns_containers = []
|
33
|
+
(0..(column_count-1)).each do |i|
|
34
|
+
columns_containers[i] = Containers::TableColumn.new @node.xpath("w:tr//w:tc[#{i+1}]")
|
35
|
+
end
|
36
|
+
columns_containers
|
37
|
+
end
|
38
|
+
|
39
|
+
def column_count
|
40
|
+
@node.xpath('w:tblGrid/w:gridCol').count
|
41
|
+
end
|
42
|
+
|
43
|
+
# Iterate over each row within a table
|
44
|
+
def each_rows
|
45
|
+
rows.each { |r| yield(r) }
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'docx/containers/text_run'
|
2
|
+
require 'docx/containers/container'
|
3
|
+
|
4
|
+
module Docx
|
5
|
+
module Elements
|
6
|
+
module Containers
|
7
|
+
class TableCell
|
8
|
+
include Container
|
9
|
+
include Elements::Element
|
10
|
+
|
11
|
+
def self.tag
|
12
|
+
'tc'
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(node)
|
16
|
+
@node = node
|
17
|
+
@properties_tag = 'tcPr'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Return text of paragraph's cell
|
21
|
+
def to_s
|
22
|
+
paragraphs.map(&:text).join('')
|
23
|
+
end
|
24
|
+
|
25
|
+
# Array of paragraphs contained within cell
|
26
|
+
def paragraphs
|
27
|
+
@node.xpath('w:p').map {|p_node| Containers::Paragraph.new(p_node) }
|
28
|
+
end
|
29
|
+
|
30
|
+
# Iterate over each text run within a paragraph's cell
|
31
|
+
def each_paragraph
|
32
|
+
paragraphs.each { |tr| yield(tr) }
|
33
|
+
end
|
34
|
+
|
35
|
+
alias_method :text, :to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'docx/containers/table_cell'
|
2
|
+
require 'docx/containers/container'
|
3
|
+
|
4
|
+
module Docx
|
5
|
+
module Elements
|
6
|
+
module Containers
|
7
|
+
class TableColumn
|
8
|
+
include Container
|
9
|
+
include Elements::Element
|
10
|
+
|
11
|
+
def self.tag
|
12
|
+
'w:gridCol'
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(cell_nodes)
|
16
|
+
@node = ''
|
17
|
+
@properties_tag = ''
|
18
|
+
@cells = cell_nodes.map { |c_node| Containers::TableCell.new(c_node) }
|
19
|
+
end
|
20
|
+
|
21
|
+
# Array of cells contained within row
|
22
|
+
def cells
|
23
|
+
@cells
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'docx/containers/table_cell'
|
2
|
+
require 'docx/containers/container'
|
3
|
+
|
4
|
+
module Docx
|
5
|
+
module Elements
|
6
|
+
module Containers
|
7
|
+
class TableRow
|
8
|
+
include Container
|
9
|
+
include Elements::Element
|
10
|
+
|
11
|
+
def self.tag
|
12
|
+
'tr'
|
13
|
+
end
|
14
|
+
|
15
|
+
def initialize(node)
|
16
|
+
@node = node
|
17
|
+
@properties_tag = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
# Array of cells contained within row
|
21
|
+
def cells
|
22
|
+
@node.xpath('w:tc').map {|c_node| Containers::TableCell.new(c_node) }
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'docx/containers/container'
|
2
|
+
|
3
|
+
module Docx
|
4
|
+
module Elements
|
5
|
+
module Containers
|
6
|
+
class TextRun
|
7
|
+
include Container
|
8
|
+
include Elements::Element
|
9
|
+
|
10
|
+
DEFAULT_FORMATTING = {
|
11
|
+
italic: false,
|
12
|
+
bold: false,
|
13
|
+
underline: false
|
14
|
+
}
|
15
|
+
|
16
|
+
def self.tag
|
17
|
+
'r'
|
18
|
+
end
|
19
|
+
|
20
|
+
attr_reader :text
|
21
|
+
attr_reader :formatting
|
22
|
+
|
23
|
+
def initialize(node, document_properties = {})
|
24
|
+
@node = node
|
25
|
+
@text_nodes = @node.xpath('w:t').map {|t_node| Elements::Text.new(t_node) }
|
26
|
+
@properties_tag = 'rPr'
|
27
|
+
@text = parse_text || ''
|
28
|
+
@formatting = parse_formatting || DEFAULT_FORMATTING
|
29
|
+
@document_properties = document_properties
|
30
|
+
@font_size = @document_properties[:font_size]
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set text of text run
|
34
|
+
def text=(content)
|
35
|
+
if @text_nodes.size == 1
|
36
|
+
@text_nodes.first.content = content
|
37
|
+
elsif @text_nodes.empty?
|
38
|
+
new_t = Elements::Text.create_within(self)
|
39
|
+
new_t.content = content
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns text contained within text run
|
44
|
+
def parse_text
|
45
|
+
@text_nodes.map(&:content).join('')
|
46
|
+
end
|
47
|
+
|
48
|
+
def parse_formatting
|
49
|
+
{
|
50
|
+
italic: !@node.xpath('.//w:i').empty?,
|
51
|
+
bold: !@node.xpath('.//w:b').empty?,
|
52
|
+
underline: !@node.xpath('.//w:u').empty?
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
def to_s
|
57
|
+
@text
|
58
|
+
end
|
59
|
+
|
60
|
+
# Return text as a HTML fragment with formatting based on properties.
|
61
|
+
def to_html
|
62
|
+
html = @text
|
63
|
+
html = html_tag(:em, content: html) if italicized?
|
64
|
+
html = html_tag(:strong, content: html) if bolded?
|
65
|
+
styles = {}
|
66
|
+
styles['text-decoration'] = 'underline' if underlined?
|
67
|
+
# No need to be granular with font size down to the span level if it doesn't vary.
|
68
|
+
styles['font-size'] = "#{font_size}pt" if font_size != @font_size
|
69
|
+
html = html_tag(:span, content: html, styles: styles) unless styles.empty?
|
70
|
+
return html
|
71
|
+
end
|
72
|
+
|
73
|
+
def italicized?
|
74
|
+
@formatting[:italic]
|
75
|
+
end
|
76
|
+
|
77
|
+
def bolded?
|
78
|
+
@formatting[:bold]
|
79
|
+
end
|
80
|
+
|
81
|
+
def underlined?
|
82
|
+
@formatting[:underline]
|
83
|
+
end
|
84
|
+
|
85
|
+
def font_size
|
86
|
+
size_tag = @node.xpath('w:rPr//w:sz').first
|
87
|
+
size_tag ? size_tag.attributes['val'].value.to_i / 2 : @font_size
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,172 @@
|
|
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
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'docx/containers'
|
2
|
+
require 'docx/elements'
|
3
|
+
require 'nokogiri'
|
4
|
+
require 'zip'
|
5
|
+
|
6
|
+
module Docx
|
7
|
+
# The Document class wraps around a docx file and provides methods to
|
8
|
+
# interface with it.
|
9
|
+
#
|
10
|
+
# # get a Docx::Document for a docx file in the local directory
|
11
|
+
# doc = Docx::Document.open("test.docx")
|
12
|
+
#
|
13
|
+
# # get the text from the document
|
14
|
+
# puts doc.text
|
15
|
+
#
|
16
|
+
# # do the same thing in a block
|
17
|
+
# Docx::Document.open("test.docx") do |d|
|
18
|
+
# puts d.text
|
19
|
+
# end
|
20
|
+
class Document
|
21
|
+
attr_reader :xml, :doc, :zip, :styles
|
22
|
+
|
23
|
+
def initialize(path, &block)
|
24
|
+
@replace = {}
|
25
|
+
@zip = Zip::File.open(path)
|
26
|
+
@document_xml = @zip.read('word/document.xml')
|
27
|
+
@doc = Nokogiri::XML(@document_xml)
|
28
|
+
@styles_xml = @zip.read('word/styles.xml')
|
29
|
+
@styles = Nokogiri::XML(@styles_xml)
|
30
|
+
if block_given?
|
31
|
+
yield self
|
32
|
+
@zip.close
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
# This stores the current global document properties, for now
|
38
|
+
def document_properties
|
39
|
+
{
|
40
|
+
font_size: font_size
|
41
|
+
}
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
# With no associated block, Docx::Document.open is a synonym for Docx::Document.new. If the optional code block is given, it will be passed the opened +docx+ file as an argument and the Docx::Document oject will automatically be closed when the block terminates. The values of the block will be returned from Docx::Document.open.
|
46
|
+
# call-seq:
|
47
|
+
# open(filepath) => file
|
48
|
+
# open(filepath) {|file| block } => obj
|
49
|
+
def self.open(path, &block)
|
50
|
+
self.new(path, &block)
|
51
|
+
end
|
52
|
+
|
53
|
+
def paragraphs
|
54
|
+
@doc.xpath('//w:document//w:body//w:p').map { |p_node| parse_paragraph_from p_node }
|
55
|
+
end
|
56
|
+
|
57
|
+
def bookmarks
|
58
|
+
bkmrks_hsh = Hash.new
|
59
|
+
bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
|
60
|
+
# auto-generated by office 2010
|
61
|
+
bkmrks_ary.reject! {|b| b.name == "_GoBack" }
|
62
|
+
bkmrks_ary.each {|b| bkmrks_hsh[b.name] = b }
|
63
|
+
bkmrks_hsh
|
64
|
+
end
|
65
|
+
|
66
|
+
def tables
|
67
|
+
@doc.xpath('//w:document//w:body//w:tbl').map { |t_node| parse_table_from t_node }
|
68
|
+
end
|
69
|
+
|
70
|
+
# Some documents have this set, others don't.
|
71
|
+
# Values are returned as half-points, so to get points, that's why it's divided by 2.
|
72
|
+
def font_size
|
73
|
+
size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
|
74
|
+
size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# *Deprecated*
|
79
|
+
#
|
80
|
+
# Iterates over paragraphs within document
|
81
|
+
# call-seq:
|
82
|
+
# each_paragraph => Enumerator
|
83
|
+
def each_paragraph
|
84
|
+
paragraphs.each { |p| yield(p) }
|
85
|
+
end
|
86
|
+
|
87
|
+
# call-seq:
|
88
|
+
# to_s -> string
|
89
|
+
def to_s
|
90
|
+
paragraphs.map(&:to_s).join("\n")
|
91
|
+
end
|
92
|
+
|
93
|
+
# Output entire document as a String HTML fragment
|
94
|
+
def to_html
|
95
|
+
paragraphs.map(&:to_html).join('\n')
|
96
|
+
end
|
97
|
+
|
98
|
+
# Save document to provided path
|
99
|
+
# call-seq:
|
100
|
+
# save(filepath) => void
|
101
|
+
def save(path)
|
102
|
+
update
|
103
|
+
Zip::OutputStream.open(path) do |out|
|
104
|
+
zip.each do |entry|
|
105
|
+
out.put_next_entry(entry.name)
|
106
|
+
|
107
|
+
if @replace[entry.name]
|
108
|
+
out.write(@replace[entry.name])
|
109
|
+
else
|
110
|
+
out.write(zip.read(entry.name))
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
zip.close
|
115
|
+
end
|
116
|
+
|
117
|
+
alias_method :text, :to_s
|
118
|
+
|
119
|
+
def replace_entry(entry_path, file_contents)
|
120
|
+
@replace[entry_path] = file_contents
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
#--
|
126
|
+
# TODO: Flesh this out to be compatible with other files
|
127
|
+
# TODO: Method to set flag on files that have been edited, probably by inserting something at the
|
128
|
+
# end of methods that make edits?
|
129
|
+
#++
|
130
|
+
def update
|
131
|
+
replace_entry "word/document.xml", doc.serialize(:save_with => 0)
|
132
|
+
end
|
133
|
+
|
134
|
+
# generate Elements::Containers::Paragraph from paragraph XML node
|
135
|
+
def parse_paragraph_from(p_node)
|
136
|
+
Elements::Containers::Paragraph.new(p_node, document_properties)
|
137
|
+
end
|
138
|
+
|
139
|
+
# generate Elements::Bookmark from bookmark XML node
|
140
|
+
def parse_bookmark_from(b_node)
|
141
|
+
Elements::Bookmark.new(b_node)
|
142
|
+
end
|
143
|
+
|
144
|
+
def parse_table_from(t_node)
|
145
|
+
Elements::Containers::Table.new(t_node)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'docx/elements/element'
|
2
|
+
|
3
|
+
module Docx
|
4
|
+
module Elements
|
5
|
+
class Bookmark
|
6
|
+
include Element
|
7
|
+
attr_accessor :name
|
8
|
+
|
9
|
+
def self.tag
|
10
|
+
'bookmarkStart'
|
11
|
+
end
|
12
|
+
|
13
|
+
def initialize(node)
|
14
|
+
@node = node
|
15
|
+
@name = @node['w:name']
|
16
|
+
end
|
17
|
+
|
18
|
+
# Insert text before bookmarkStart node
|
19
|
+
def insert_text_before(text)
|
20
|
+
text_run = get_run_after
|
21
|
+
text_run.text = "#{text}#{text_run.text}"
|
22
|
+
end
|
23
|
+
|
24
|
+
# Insert text after bookmarkStart node
|
25
|
+
def insert_text_after(text)
|
26
|
+
text_run = get_run_before
|
27
|
+
text_run.text = "#{text_run.text}#{text}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# insert multiple lines starting with paragraph containing bookmark node.
|
31
|
+
def insert_multiple_lines(text_array)
|
32
|
+
# Hold paragraphs to be inserted into, corresponding to the index of the strings in the text array
|
33
|
+
paragraphs = []
|
34
|
+
paragraph = self.parent_paragraph
|
35
|
+
# Remove text from paragraph
|
36
|
+
paragraph.blank!
|
37
|
+
paragraphs << paragraph
|
38
|
+
for i in 0...(text_array.size - 1)
|
39
|
+
# Copy previous paragraph
|
40
|
+
new_p = paragraphs[i].copy
|
41
|
+
# Insert as sibling of previous paragraph
|
42
|
+
new_p.insert_after(paragraphs[i])
|
43
|
+
paragraphs << new_p
|
44
|
+
end
|
45
|
+
|
46
|
+
# Insert text into corresponding newly created paragraphs
|
47
|
+
paragraphs.each_index do |index|
|
48
|
+
paragraphs[index].text = text_array[index]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
# Get text run immediately prior to bookmark node
|
53
|
+
def get_run_before
|
54
|
+
# at_xpath returns the first match found and preceding-sibling returns siblings in the
|
55
|
+
# order they appear in the document not the order as they appear when moving out from
|
56
|
+
# the starting node
|
57
|
+
if not (r_nodes = @node.xpath("./preceding-sibling::w:r")).empty?
|
58
|
+
r_node = r_nodes.last
|
59
|
+
Containers::TextRun.new(r_node)
|
60
|
+
else
|
61
|
+
new_r = Containers::TextRun.create_with(self)
|
62
|
+
new_r.insert_before(self)
|
63
|
+
new_r
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Get text run immediately after bookmark node
|
68
|
+
def get_run_after
|
69
|
+
if (r_node = @node.at_xpath("./following-sibling::w:r"))
|
70
|
+
Containers::TextRun.new(r_node)
|
71
|
+
else
|
72
|
+
new_r = Containers::TextRun.create_with(self)
|
73
|
+
new_r.insert_after(self)
|
74
|
+
new_r
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'nokogiri'
|
2
|
+
require 'docx/elements'
|
3
|
+
require 'docx/containers'
|
4
|
+
|
5
|
+
module Docx
|
6
|
+
module Elements
|
7
|
+
module Element
|
8
|
+
DEFAULT_TAG = ''
|
9
|
+
|
10
|
+
# Ensure that a 'tag' corresponding to the XML element that defines the element is defined
|
11
|
+
def self.included(base)
|
12
|
+
base.extend(ClassMethods)
|
13
|
+
base.const_set(:TAG, Element::DEFAULT_TAG) unless base.const_defined?(:TAG)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :node
|
17
|
+
delegate :at_xpath, :xpath, :to => :@node
|
18
|
+
|
19
|
+
# TODO: Should create a docx object from this
|
20
|
+
def parent(type = '*')
|
21
|
+
@node.at_xpath("./parent::#{type}")
|
22
|
+
end
|
23
|
+
|
24
|
+
# Get parent paragraph of element
|
25
|
+
def parent_paragraph
|
26
|
+
Elements::Containers::Paragraph.new(parent('w:p'))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Insertion methods
|
30
|
+
# Insert node as last child
|
31
|
+
def append_to(element)
|
32
|
+
@node = element.node.add_child(@node)
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
# Insert node as first child (after properties)
|
37
|
+
def prepend_to(element)
|
38
|
+
@node = element.node.properties.add_next_sibling(@node)
|
39
|
+
self
|
40
|
+
end
|
41
|
+
|
42
|
+
def insert_after(element)
|
43
|
+
# Returns newly re-parented node
|
44
|
+
@node = element.node.add_next_sibling(@node)
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def insert_before(element)
|
49
|
+
@node = element.node.add_previous_sibling(@node)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
# Creation/edit methods
|
54
|
+
def copy
|
55
|
+
self.class.new(@node.dup)
|
56
|
+
end
|
57
|
+
|
58
|
+
# A method to wrap content in an HTML tag.
|
59
|
+
# Currently used in paragraph and text_run for the to_html methods
|
60
|
+
#
|
61
|
+
# content:: The base text content for the tag.
|
62
|
+
# styles:: Hash of the inline CSS styles to be applied. e.g.
|
63
|
+
# { 'font-size' => '12pt', 'text-decoration' => 'underline' }
|
64
|
+
#
|
65
|
+
def html_tag(name, options = {})
|
66
|
+
content = options[:content]
|
67
|
+
styles = options[:styles]
|
68
|
+
|
69
|
+
html = "<#{name.to_s}"
|
70
|
+
unless styles.nil? || styles.empty?
|
71
|
+
styles_array = []
|
72
|
+
styles.each do |property, value|
|
73
|
+
styles_array << "#{property.to_s}:#{value};"
|
74
|
+
end
|
75
|
+
html << " style=\"#{styles_array.join('')}\""
|
76
|
+
end
|
77
|
+
html << ">"
|
78
|
+
html << content if content
|
79
|
+
html << "</#{name.to_s}>"
|
80
|
+
end
|
81
|
+
|
82
|
+
module ClassMethods
|
83
|
+
def create_with(element)
|
84
|
+
# Need to somehow get the xml document accessible here by default, but this is alright in the interim
|
85
|
+
self.new(Nokogiri::XML::Node.new("w:#{self.tag}", element.node))
|
86
|
+
end
|
87
|
+
|
88
|
+
def create_within(element)
|
89
|
+
new_element = create_with(element)
|
90
|
+
new_element.append_to(element)
|
91
|
+
new_element
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
data/lib/docx/version.rb
ADDED
metadata
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bitops-docx
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.07
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Christopher Hunt
|
8
|
+
- Marcus Ortiz
|
9
|
+
- Higgins Dragon
|
10
|
+
- Toms Mikoss
|
11
|
+
- Sebastian Wittenkamp
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
date: 2014-08-03 00:00:00.000000000 Z
|
16
|
+
dependencies:
|
17
|
+
- !ruby/object:Gem::Dependency
|
18
|
+
name: nokogiri
|
19
|
+
requirement: !ruby/object:Gem::Requirement
|
20
|
+
requirements:
|
21
|
+
- - "~>"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: '1.5'
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - "~>"
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '1.5'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: rubyzip
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - "~>"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 1.1.6
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - "~>"
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.1.6
|
45
|
+
- !ruby/object:Gem::Dependency
|
46
|
+
name: rspec
|
47
|
+
requirement: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
type: :development
|
53
|
+
prerelease: false
|
54
|
+
version_requirements: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
description: a ruby library/gem for interacting with .docx files
|
60
|
+
email:
|
61
|
+
- sebastian@bitops.io
|
62
|
+
executables: []
|
63
|
+
extensions: []
|
64
|
+
extra_rdoc_files: []
|
65
|
+
files:
|
66
|
+
- LICENSE.md
|
67
|
+
- README.md
|
68
|
+
- lib/docx.rb
|
69
|
+
- lib/docx/containers.rb
|
70
|
+
- lib/docx/containers/container.rb
|
71
|
+
- lib/docx/containers/paragraph.rb
|
72
|
+
- lib/docx/containers/table.rb
|
73
|
+
- lib/docx/containers/table_cell.rb
|
74
|
+
- lib/docx/containers/table_column.rb
|
75
|
+
- lib/docx/containers/table_row.rb
|
76
|
+
- lib/docx/containers/text_run.rb
|
77
|
+
- lib/docx/core_ext/module.rb
|
78
|
+
- lib/docx/document.rb
|
79
|
+
- lib/docx/elements.rb
|
80
|
+
- lib/docx/elements/bookmark.rb
|
81
|
+
- lib/docx/elements/element.rb
|
82
|
+
- lib/docx/elements/text.rb
|
83
|
+
- lib/docx/version.rb
|
84
|
+
homepage: https://github.com/bitops/docx
|
85
|
+
licenses: []
|
86
|
+
metadata: {}
|
87
|
+
post_install_message:
|
88
|
+
rdoc_options: []
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
97
|
+
requirements:
|
98
|
+
- - ">="
|
99
|
+
- !ruby/object:Gem::Version
|
100
|
+
version: '0'
|
101
|
+
requirements: []
|
102
|
+
rubyforge_project:
|
103
|
+
rubygems_version: 2.2.2
|
104
|
+
signing_key:
|
105
|
+
specification_version: 4
|
106
|
+
summary: a ruby library/gem for interacting with .docx files
|
107
|
+
test_files: []
|