Skip to content

Commit 089ab6f

Browse files
authored
Merge branch 'master' into fix-newlines
2 parents 9d62f32 + b137f71 commit 089ab6f

16 files changed

Lines changed: 384 additions & 92 deletions

File tree

.github/FUNDING.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# These are supported funding model platforms
2+
3+
github: [satoryu] # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4+
patreon: # Replace with a single Patreon username
5+
open_collective: # Replace with a single Open Collective username
6+
ko_fi: # Replace with a single Ko-fi username
7+
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8+
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9+
liberapay: # Replace with a single Liberapay username
10+
issuehunt: # Replace with a single IssueHunt username
11+
otechie: # Replace with a single Otechie username
12+
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
---
2+
name: Bug report
3+
about: Create a report to help us improve
4+
title: ''
5+
labels: 'bug'
6+
assignees: ''
7+
8+
---
9+
10+
## Describe the bug
11+
12+
A clear and concise description of what the bug is.
13+
14+
## To Reproduce
15+
16+
Steps to reproduce the behavior or put a short code to reproduce the bug.
17+
18+
### example
19+
20+
```rb
21+
require 'docx'
22+
23+
doc = Docx::Document.new('/path/to/your/docx/file.docx')
24+
25+
## Something to reproduce the bug here
26+
```
27+
28+
## Sample docx file
29+
30+
Put a sample docx file to reproduce the bug reported here to this issue.
31+
32+
## Expected behavior
33+
34+
A clear and concise description of what you expected to happen.
35+
36+
## Environment
37+
38+
- Ruby version: [e.g 2.7.1]
39+
- `docx` gem version: [e.g 0.5.0]
40+
- OS: [e.g. iOS]
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
name: Feature request
3+
about: Suggest an idea for this project
4+
title: ''
5+
labels: 'enhancement'
6+
assignees: ''
7+
8+
---
9+
10+
## Problem
11+
12+
Please share a clear and concise description of what the problem is.
13+
14+
> Ex. I'm always frustrated when [...]
15+
16+
## Solution
17+
18+
Please describe the solution you'd like.
19+
20+
## Alternative solutions
21+
22+
A clear and concise description of any alternative solutions or features you've considered.

.travis.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,18 @@ language: ruby
22
dist: trusty
33
sudo: false
44
rvm:
5-
- 2.4
6-
- 2.5
5+
- 2.4
6+
- 2.5
7+
- 2.6
8+
- 2.7
9+
- ruby-head
10+
11+
matrix:
12+
allow_failures:
13+
- rvm: ruby-head
14+
15+
cache: bundler
716
bundler_args: --jobs=2
17+
818
script:
9-
- bundle exec rake spec
19+
- bundle exec rake spec

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Changelog
2+
3+
## v0.5.0
4+
5+
### Enhancements
6+
7+
- Added opening streams and outputting to a stream [#66](https://github.com/ruby-docx/docx/pull/66)
8+
- Added supports for Office 365 files [#85](https://github.com/ruby-docx/docx/pull/85)
9+
10+
### Bug fixes
11+
12+
- `Docx::Document` handles a docx file without styles.xml [#81](https://github.com/ruby-docx/docx/pull/81)
13+
- Fixes insert text before after were switched [#84](https://github.com/ruby-docx/docx/pull/84)
14+
15+
## v0.4.0
16+
17+
### Enhancements
18+
19+
- Implement substitute method on TextRun class. [#75](https://github.com/ruby-docx/docx/pull/75)
20+
21+
### Improvements
22+
23+
- Updates dependencies. [#72](https://github.com/ruby-docx/docx/pull/72), [#77](https://github.com/ruby-docx/docx/pull/77)
24+
- Fix: #paragraphs grabs paragraphs in tables. [#76](https://github.com/ruby-docx/docx/pull/76)
25+
- Updates supported ruby versions. [#78](https://github.com/ruby-docx/docx/pull/78)

README.md

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,36 @@
11
# docx
22

3+
[![Gem Version](https://badge.fury.io/rb/docx.svg)](https://badge.fury.io/rb/docx)
4+
[![Build Status](https://travis-ci.org/ruby-docx/docx.svg?branch=master)](https://travis-ci.org/ruby-docx/docx)
5+
[![Gitter](https://badges.gitter.im/ruby-docx/community.svg)](https://gitter.im/ruby-docx/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
6+
37
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.
48

59
## Usage
610

11+
### Prerequisites
12+
13+
- Ruby 2.4 or later
14+
715
### Install
816

9-
Requires ruby (tested with 2.1.1)
17+
Add the following line to your application's Gemfile:
1018

11-
gem 'docx', '~> 0.2.07', :require => ["docx"]
19+
```ruby
20+
gem 'docx'
21+
```
22+
23+
And then execute:
24+
25+
```shell
26+
bundle install
27+
```
28+
29+
Or install it yourself as:
30+
31+
```shell
32+
gem install docx
33+
```
1234

1335
### Reading
1436

@@ -29,6 +51,17 @@ doc.bookmarks.each_pair do |bookmark_name, bookmark_object|
2951
end
3052
```
3153

54+
Don't have a local file but a buffer? Docx handles those to:
55+
56+
```ruby
57+
require 'docx'
58+
59+
# Create a Docx::Document object from a remote file
60+
doc = Docx::Document.open(buffer)
61+
62+
# Everything about reading is the same as shown above
63+
```
64+
3265
### Rendering html
3366
``` ruby
3467
require 'docx'
@@ -61,7 +94,7 @@ doc.tables.each do |table|
6194
puts cell.text
6295
end
6396
end
64-
97+
6598
table.columns.each do |column| # Column-based iteration
6699
column.cells.each do |cell|
67100
puts cell.text
@@ -89,6 +122,13 @@ doc.paragraphs.each do |p|
89122
p.remove! if p.to_s =~ /TODO/
90123
end
91124

125+
# Substitute text, preserving formatting
126+
doc.paragraphs.each do |p|
127+
p.each_text_run do |tr|
128+
tr.substitute('_placeholder_', 'replacement value')
129+
end
130+
end
131+
92132
# Save document to specified path
93133
doc.save('example-edited.docx')
94134
```

docx.gemspec

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ Gem::Specification.new do |s|
1111
s.email = ['chrahunt@gmail.com']
1212
s.homepage = 'https://github.com/chrahunt/docx'
1313
s.files = Dir["README.md", "LICENSE.md", "lib/**/*.rb"]
14+
s.required_ruby_version = '>= 2.4.0'
1415

15-
s.add_dependency 'nokogiri', '~> 1.8', '>= 1.8.1'
16-
s.add_dependency 'rubyzip', '~> 1.2', '>= 1.2.1'
16+
s.add_dependency 'nokogiri', '~> 1.10', '>= 1.10.4'
17+
s.add_dependency 'rubyzip', '~> 2.0'
1718

1819
s.add_development_dependency 'rspec', '~> 3.7'
19-
s.add_development_dependency 'rake', '~> 12.3'
20+
s.add_development_dependency 'rake', '~> 13.0'
2021
end

lib/docx/containers/text_run.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ def parse_text
4545
@text_nodes.map(&:content).join('')
4646
end
4747

48+
# Substitute text in text @text_nodes
49+
def substitute(match, replacement)
50+
@text_nodes.each do |text_node|
51+
text_node.content = text_node.content.gsub(match, replacement)
52+
end
53+
end
54+
4855
def parse_formatting
4956
{
5057
italic: !@node.xpath('.//w:i').empty?,

lib/docx/document.rb

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,20 +19,28 @@ module Docx
1919
# end
2020
class Document
2121
attr_reader :xml, :doc, :zip, :styles
22-
23-
def initialize(path, &block)
22+
23+
def initialize(path_or_io, options = {})
2424
@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
25+
26+
# if path-or_io is string && does not contain a null byte
27+
if (path_or_io.instance_of?(String) && !/\u0000/.match?(path_or_io))
28+
@zip = Zip::File.open(path_or_io)
29+
else
30+
@zip = Zip::File.open_buffer(path_or_io)
3331
end
34-
end
3532

33+
document = @zip.find_entry('word/document.xml')
34+
document ||= @zip.find_entry('word/document2.xml')
35+
raise Errno::ENOENT if document.nil?
36+
37+
@document_xml = document.get_input_stream.read
38+
@doc = Nokogiri::XML(@document_xml)
39+
load_styles
40+
yield(self) if block_given?
41+
ensure
42+
@zip.close
43+
end
3644

3745
# This stores the current global document properties, for now
3846
def document_properties
@@ -41,25 +49,24 @@ def document_properties
4149
}
4250
end
4351

44-
4552
# 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.
4653
# call-seq:
4754
# open(filepath) => file
4855
# open(filepath) {|file| block } => obj
4956
def self.open(path, &block)
50-
self.new(path, &block)
57+
new(path, &block)
5158
end
5259

5360
def paragraphs
54-
@doc.xpath('//w:document//w:body//w:p').map { |p_node| parse_paragraph_from p_node }
61+
@doc.xpath('//w:document//w:body/w:p').map { |p_node| parse_paragraph_from p_node }
5562
end
5663

5764
def bookmarks
58-
bkmrks_hsh = Hash.new
65+
bkmrks_hsh = {}
5966
bkmrks_ary = @doc.xpath('//w:bookmarkStart').map { |b_node| parse_bookmark_from b_node }
6067
# auto-generated by office 2010
61-
bkmrks_ary.reject! {|b| b.name == "_GoBack" }
62-
bkmrks_ary.each {|b| bkmrks_hsh[b.name] = b }
68+
bkmrks_ary.reject! { |b| b.name == '_GoBack' }
69+
bkmrks_ary.each { |b| bkmrks_hsh[b.name] = b }
6370
bkmrks_hsh
6471
end
6572

@@ -70,6 +77,8 @@ def tables
7077
# Some documents have this set, others don't.
7178
# Values are returned as half-points, so to get points, that's why it's divided by 2.
7279
def font_size
80+
return nil unless @styles
81+
7382
size_tag = @styles.xpath('//w:docDefaults//w:rPrDefault//w:rPr//w:sz').first
7483
size_tag ? size_tag.attributes['val'].value.to_i / 2 : nil
7584
end
@@ -102,6 +111,8 @@ def save(path)
102111
update
103112
Zip::OutputStream.open(path) do |out|
104113
zip.each do |entry|
114+
next unless entry.file?
115+
105116
out.put_next_entry(entry.name)
106117

107118
if @replace[entry.name]
@@ -114,21 +125,50 @@ def save(path)
114125
zip.close
115126
end
116127

117-
alias_method :text, :to_s
128+
# Output entire document as a StringIO object
129+
def stream
130+
update
131+
stream = Zip::OutputStream.write_buffer do |out|
132+
zip.each do |entry|
133+
next unless entry.file?
134+
135+
out.put_next_entry(entry.name)
136+
137+
if @replace[entry.name]
138+
out.write(@replace[entry.name])
139+
else
140+
out.write(zip.read(entry.name))
141+
end
142+
end
143+
end
144+
145+
stream.rewind
146+
stream
147+
end
148+
149+
alias text to_s
118150

119151
def replace_entry(entry_path, file_contents)
120152
@replace[entry_path] = file_contents
121153
end
122154

123155
private
124156

157+
def load_styles
158+
@styles_xml = @zip.read('word/styles.xml')
159+
@styles = Nokogiri::XML(@styles_xml)
160+
rescue Errno::ENOENT => e
161+
warn e.message
162+
nil
163+
end
164+
125165
#--
126166
# TODO: Flesh this out to be compatible with other files
127167
# TODO: Method to set flag on files that have been edited, probably by inserting something at the
128168
# end of methods that make edits?
129169
#++
130170
def update
131-
replace_entry "word/document.xml", doc.serialize(:save_with => 0)
171+
replace_entry 'word/document.xml', doc.serialize(save_with: 0)
132172
end
133173

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

0 commit comments

Comments
 (0)