Skip to content

Commit 8744a21

Browse files
committed
Merge pull request #1 from chrahunt/pull-request-saving
Added several features
2 parents 6ec822c + a9f3b24 commit 8744a21

22 files changed

Lines changed: 596 additions & 87 deletions

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.gem
2+
.bundle
3+
Gemfile.lock
4+
pkg/*

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,15 @@ d.each_paragraph do |p|
3636
run.text
3737
end
3838
end
39+
```
40+
41+
## Development
42+
43+
### todo
44+
45+
* Add better formatting identification for specific nodes and other formatting indicators (text size, paragraph spacing)
46+
* Calculate element formatting based on values present in element properties as well as properties inherited from parents
47+
* Default formatting of inserted elements to inherited values
48+
* Implement formattable elements.
49+
* Implement styles.
50+
* Easier multi-line text insertion at a single bookmark (inserting paragraph nodes after the one containing the bookmark)

lib/docx.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
module Docx
44
autoload :Document, 'docx/document'
55
end
6+
7+
require 'docx/core_ext/module'

lib/docx/containers.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
require 'docx/containers/container'
12
require 'docx/containers/text_run'
2-
require 'docx/containers/paragraph'
3+
require 'docx/containers/paragraph'

lib/docx/containers/container.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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+
def blank!
14+
@node.xpath(".//w:t").each {|t| t.content = '' }
15+
end
16+
end
17+
end
18+
end
19+
end

lib/docx/containers/paragraph.rb

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,50 @@
11
require 'docx/containers/text_run'
2+
require 'docx/containers/container'
23

34
module Docx
4-
module Containers
5-
class Paragraph
6-
attr_accessor :text_runs
7-
8-
def initialize(txt_runs)
9-
@text_runs = txt_runs
10-
end
11-
12-
def to_s
13-
@text_runs.map(&:text).join('')
14-
end
15-
16-
def each_text_run
17-
@text_runs.each { |tr| yield(tr) }
5+
module Elements
6+
module Containers
7+
class Paragraph
8+
include Container
9+
include Elements::Element
10+
11+
TAG = 'p'
12+
13+
# Child elements: pPr, r, fldSimple, hlink, subDoc
14+
# http://msdn.microsoft.com/en-us/library/office/ee364458(v=office.11).aspx
15+
def initialize(node)
16+
@node = node
17+
@properties_tag = 'pPr'
18+
end
19+
20+
# Handle direct text insertion into paragraph on some conditions
21+
def text=(content)
22+
if text_runs.size == 1
23+
text_runs.first.text = content
24+
elsif text_runs.size == 0
25+
new_r = TextRun.create_within(self)
26+
new_r.text = content
27+
else
28+
text_runs.each {|r| r.node.remove }
29+
new_r = TextRun.create_within(self)
30+
new_r.text = content
31+
end
32+
end
33+
34+
def to_s
35+
text_runs.map(&:text).join('')
36+
end
37+
38+
def text_runs
39+
@node.xpath('w:r').map {|r_node| Containers::TextRun.new(r_node) }
40+
end
41+
42+
def each_text_run
43+
text_runs.each { |tr| yield(tr) }
44+
end
45+
46+
alias_method :text, :to_s
1847
end
19-
20-
alias_method :text, :to_s
2148
end
2249
end
2350
end

lib/docx/containers/text_run.rb

Lines changed: 63 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,67 @@
1+
require 'docx/containers/container'
2+
13
module Docx
2-
module Containers
3-
class TextRun
4-
DEFAULT_FORMATTING = {
5-
italic: false,
6-
bold: false,
7-
underline: false
8-
}
9-
10-
attr_reader :text
11-
attr_reader :formatting
12-
13-
def initialize(attrs)
14-
@text = attrs[:text] || ''
15-
@formatting = attrs[:formatting] || DEFAULT_FORMATTING
16-
end
17-
18-
def to_s
19-
@text
20-
end
21-
22-
def italicized?
23-
@formatting[:italic]
24-
end
25-
26-
def bolded?
27-
@formatting[:bold]
28-
end
29-
30-
def underlined?
31-
@formatting[:underline]
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+
TAG = 'r'
17+
18+
attr_reader :text
19+
attr_reader :formatting
20+
21+
def initialize(node)
22+
@node = node
23+
@text_nodes = @node.xpath('w:t').map {|t_node| Elements::Text.new(t_node) }
24+
@properties_tag = 'rPr'
25+
@text = parse_text || ''
26+
@formatting = parse_formatting || DEFAULT_FORMATTING
27+
end
28+
29+
def text=(content)
30+
if @text_nodes.size == 1
31+
@text_nodes.first.content = content
32+
elsif @text_nodes.empty?
33+
new_t = Elements::Text.create_within(self)
34+
new_t.content = content
35+
end
36+
end
37+
38+
def parse_text
39+
@text_nodes.map(&:content).join('')
40+
end
41+
42+
def parse_formatting
43+
{
44+
italic: !@node.xpath('.//w:i').empty?,
45+
bold: !@node.xpath('.//w:b').empty?,
46+
underline: !@node.xpath('.//w:u').empty?
47+
}
48+
end
49+
50+
def to_s
51+
@text
52+
end
53+
54+
def italicized?
55+
@formatting[:italic]
56+
end
57+
58+
def bolded?
59+
@formatting[:bold]
60+
end
61+
62+
def underlined?
63+
@formatting[:underline]
64+
end
3265
end
3366
end
3467
end

lib/docx/core_ext/module.rb

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)