documatic 0.0.1

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.
data/README ADDED
@@ -0,0 +1,29 @@
1
+ = Documatic
2
+
3
+ Documatic is an OpenDocument processor for Ruby. It can be used to
4
+ produce attractive printable documents such as database reports,
5
+ invoices, letters, faxes and more. Both the data inputs and the
6
+ printable output are very flexible and easy to configure.
7
+
8
+
9
+ == Installation
10
+
11
+ Documatic can be installed via Rubygems.
12
+
13
+ % gem install documatic
14
+
15
+
16
+ == Licence
17
+
18
+ Documatic is (p) Public Domain 2007, no rights reserved.
19
+
20
+ Documatic comes with no warranty whatsoever.
21
+
22
+
23
+ == Support
24
+
25
+ Further information is available on the Documatic homepage at
26
+ http://documatic.240gl.org.
27
+
28
+ The Documatic project page (including tracker) is at
29
+ http://rubyforge.org/projects/documatic.
@@ -0,0 +1,78 @@
1
+ require 'erb'
2
+
3
+ module Documatic
4
+ class Component
5
+ include ERB::Util
6
+ include Documatic::Helper
7
+
8
+ attr_accessor :erb
9
+
10
+ def initialize(erb_text)
11
+ @erb = ERB.new(erb_text)
12
+ end
13
+
14
+ # Injects the provided assigns into this component and sends it through ERB.
15
+ def process(local_assigns)
16
+ if local_assigns.is_a? Binding
17
+ context = local_assigns
18
+ else # Hash
19
+ local_assigns.each do |key, val|
20
+ self.define_singleton_method(key) do val end
21
+ end
22
+ context = binding
23
+ end
24
+
25
+ @xml = nil ; @text = self.erb.result(context)
26
+ end
27
+
28
+ # Returns a REXML::Document constructed from the text of this
29
+ # component. Note that this flushes self.text: subsequently if
30
+ # self.text is called it will be reconstructed from this XML
31
+ # document and self.xml will be flushed. Therefore the content of
32
+ # this partial is always stored either as XML or text, never both.
33
+ def xml
34
+ @xml ||= REXML::Document.new( remove_instance_variable(:@text) )
35
+ end
36
+
37
+ # Returns the text of this component. Note that this flushes
38
+ # self.xml: subsequently if self.xml is called it will be
39
+ # reconstructed from this text and self.text will be flushed.
40
+ def text
41
+ @text ||= ( remove_instance_variable(:@xml) ).to_s
42
+ end
43
+
44
+ # Merge the auto-styles from the cached partials into
45
+ # <office:automatic-styles> of this component. This method isn't
46
+ # intended to be called directly by client applications: it is
47
+ # called automatically by Documatic::Template after processing.
48
+ def merge_partial_styles
49
+ cache = Documatic::Partial.cache_by_prefix
50
+ if cache.length > 0
51
+ styles = self.xml.root.elements['office:automatic-styles']
52
+ if styles
53
+ cache.each_value do |partial|
54
+ partial.styles.each_element do |e|
55
+ styles << e
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # to_s() is a synonym for text()
63
+ alias_method :to_s, :text
64
+
65
+
66
+ protected
67
+
68
+ # Adds methods to the singleton class representing the current
69
+ # instance. Useful for injecting faux local variables into an
70
+ # instance object.
71
+ # Courtesy John Hume,
72
+ # http://practicalruby.blogspot.com/2007/02/ruby-metaprogramming-introduction.html
73
+ def define_singleton_method(name, &body)
74
+ (class << self ; self ; end).send(:define_method, name, &body)
75
+ end
76
+
77
+ end
78
+ end
@@ -0,0 +1,53 @@
1
+ require 'erb'
2
+
3
+ module Documatic
4
+ module Helper
5
+
6
+ include ERB::Util
7
+
8
+
9
+ # Inserts a paragraph (<text:p>) containing the provided content;
10
+ # or nothing if no content is provided. This helper should be
11
+ # invoked from within "Ruby Block" because paragraphs are
12
+ # block-level elements.
13
+ #
14
+ # Note that the content is not escaped by default because you
15
+ # might want to include other tags in the content. You should use
16
+ # ERB::Util.h() to escape any content that could possibly contain
17
+ # XML characters.
18
+ def para(stylename, content = nil)
19
+ end_element = ( content ? ">#{content}</text:p>" : "/>" )
20
+ %Q(<text:p text:style-name="#{stylename}"#{end_element})
21
+ end
22
+
23
+ # Inserts a text span (<text:span>) containing the provided
24
+ # content. This helper should be invoked from within "Ruby
25
+ # Literal" because the tag is a text-level element that shouldn't
26
+ # be escaped. However the content is escaped.
27
+ def span(stylename, content)
28
+ %Q(<text:span text:style-name="#{stylename}">#{ERB::Util.h(content)}</text:span>)
29
+ end
30
+
31
+
32
+ # Inserts a partial into the document at the chosen position.
33
+ # This helper should be invoked from within "Ruby Block" because
34
+ # it inserts unescaped block-level material in the current
35
+ # template.
36
+ #
37
+ # The +assigns+ hash is passed through to the partial for binding.
38
+ #
39
+ # This method will add the provided partial to the
40
+ # Documatic::Partial cache if it hasn't yet been loaded; or if it
41
+ # has been loaded then the existing partial will be re-used.
42
+ def partial(filename, *assigns)
43
+ if Documatic::Partial.cache.has_key?(filename)
44
+ p = Documatic::Partial.cache[filename]
45
+ else
46
+ p = Documatic::Partial.new(filename)
47
+ Documatic::Partial.add_partial(filename, p)
48
+ end
49
+ p.process(*assigns)
50
+ end
51
+
52
+ end
53
+ end
@@ -0,0 +1,124 @@
1
+ require 'rexml/document'
2
+
3
+ module Documatic
4
+ class Partial < Documatic::Template
5
+
6
+ attr_accessor :content
7
+ attr_accessor :filename
8
+
9
+ class << self
10
+ def add_partial(name, partial)
11
+ self.cache[name] = partial
12
+ end
13
+
14
+ def cache
15
+ @cache ||= Hash.new
16
+ end
17
+
18
+ def cache_by_prefix
19
+ by_prefix = Hash.new
20
+ self.cache.each_value do |partial|
21
+ by_prefix[partial.prefix] = partial
22
+ end
23
+ by_prefix
24
+ end
25
+
26
+ def flush_cache
27
+ @cache = Hash.new
28
+ end
29
+
30
+ end # class << self
31
+
32
+ def initialize(filename, prefix_name = nil)
33
+ super filename
34
+ @prefix = prefix_name || self.prefix
35
+ end
36
+
37
+ def process(local_assigns = {})
38
+ self.zip_file.find_entry('documatic/partial') || self.compile
39
+ @content = Documatic::Component.new( self.content_erb )
40
+ @content.process(local_assigns)
41
+ end
42
+
43
+ def compile
44
+ doc = REXML::Document.new( self.zip_file.read('content.xml') )
45
+ style_names = Hash.new
46
+
47
+ # Gather all auto style names from <office:automatic-styles>
48
+ doc.root.each_element('office:automatic-styles/*') do |e|
49
+ attr = e.attributes.get_attribute('style:name')
50
+ attr && style_names[attr.value] = attr
51
+ end
52
+
53
+ # Replace all auto styles in the document's attributes with the prefixed form.
54
+ doc.each_element('//*') do |e|
55
+ e.attributes.each_attribute do |attr|
56
+ if style_names.has_key? attr.value
57
+ e.add_attribute(attr.expanded_name, "#{self.prefix}_#{attr.value}")
58
+ end
59
+ end
60
+ end
61
+
62
+ # Create 'documatic/partial/' in zip file
63
+ self.zip_file.find_entry('documatic/partial') || self.zip_file.mkdir('documatic/partial')
64
+
65
+ # Save the prefix in documatic/partial.txt
66
+ self.zip_file.get_output_stream('documatic/partial/partial.txt') do |f|
67
+ f.write self.prefix
68
+ end
69
+
70
+ # Set @styles, & save it in documatic/styles.xml
71
+ @styles = doc.root.elements['office:automatic-styles']
72
+ self.zip_file.get_output_stream('documatic/partial/styles.xml') do |f|
73
+ f.write @styles.to_s
74
+ end
75
+
76
+ # Get body text, erbify it, keep it in @content and save it in documatic/content.erb
77
+ body_text = doc.root.elements['office:body/office:text']
78
+ body_text.elements.delete('text:sequence-decls')
79
+ body_text.elements.delete('office:forms')
80
+ @content_erb = self.erbify( (body_text.elements.to_a.collect do |e| e.to_s ; end ).join )
81
+ self.zip_file.get_output_stream('documatic/partial/content.erb') do |f|
82
+ f.write @content_erb
83
+ end
84
+
85
+ end
86
+
87
+ # Partials aren't saved in the same way that templates are: this is a no-op.
88
+ def save ; end
89
+
90
+ def prefix
91
+ if not @prefix
92
+ if self.zip_file.find_entry('documatic/partial/partial.txt')
93
+ @prefix = self.zip_file.read('documatic/partial/partial.txt')
94
+ else
95
+ @prefix = File.basename(self.filename, '.odt').gsub(/[^A-Za-z0-9_]/, '_')
96
+ end
97
+ end
98
+ return @prefix
99
+ end
100
+
101
+ def content_erb
102
+ if not @content_erb
103
+ if self.zip_file.find_entry('documatic/partial/content.erb')
104
+ self.zip_file.read('documatic/partial/content.erb')
105
+ else
106
+ self.compile
107
+ end
108
+ end
109
+ @content_erb
110
+ end
111
+
112
+ def styles
113
+ if not @styles
114
+ if self.zip_file.find_entry('documatic/partial/styles.xml')
115
+ @styles = REXML::Document.new( self.zip_file.read('documatic/partial/styles.xml') ).root
116
+ else
117
+ self.compile
118
+ end
119
+ end
120
+ @styles
121
+ end
122
+
123
+ end
124
+ end
@@ -0,0 +1,303 @@
1
+ require 'rexml/text'
2
+ require 'rexml/attribute'
3
+ require 'zip/zip'
4
+ require 'erb'
5
+
6
+ module Documatic
7
+ class Template
8
+ include ERB::Util
9
+
10
+ attr_accessor :content
11
+ attr_accessor :styles
12
+ attr_accessor :zip_file
13
+ # The raw contents of 'content.xml'.
14
+ attr_accessor :content_raw
15
+ # Compiled text, to be written to 'content.erb'
16
+ attr_accessor :content_erb
17
+ # The raw contents of 'styles.xml'
18
+ attr_accessor :styles_raw
19
+ # Compiled text, to be written to 'styles.erb'
20
+ attr_accessor :styles_erb
21
+
22
+ # RE_STYLES match positions
23
+ STYLE_NAME = 1
24
+ STYLE_TYPE = 2
25
+
26
+ # RE_ERB match positions
27
+ TYPE = 5
28
+ ERB_CODE = 6
29
+
30
+ ROW_START = 1
31
+ ROW_END = 11
32
+
33
+ ITEM_START = 2
34
+ ITEM_END = 10
35
+
36
+ PARA_START = 3
37
+ PARA_END = 9
38
+
39
+ SPAN_END = 4
40
+ SPAN_START = 8
41
+
42
+ # Match types:
43
+ TABLE_ROW = 1
44
+ PARAGRAPH = 2
45
+ INLINE_CODE = 3
46
+ VALUE = 4
47
+
48
+ class << self
49
+
50
+ # Includes the number and text helpers from Rails' ActionPack.
51
+ # Requires that the Rails gems be installed.
52
+ def include_rails_helpers
53
+ require 'action_pack'
54
+ require 'action_controller'
55
+ require 'action_view'
56
+
57
+ require 'action_view/helpers/number_helper'
58
+ require 'action_view/helpers/text_helper'
59
+
60
+ [self, Documatic::Partial].each do |klass|
61
+ klass.class_eval do
62
+ include ActionView::Helpers::NumberHelper
63
+ include ActionView::Helpers::TextHelper
64
+ end
65
+ end
66
+ end
67
+
68
+ end # class << self
69
+
70
+ def initialize(filename)
71
+ @filename = filename
72
+ @zip_file = Zip::ZipFile.open(@filename)
73
+ return true
74
+ end
75
+
76
+ def process(local_assigns = {})
77
+ # Compile this template, if not compiled already.
78
+ self.zip_file.find_entry('documatic/master') || self.compile
79
+ # Process the styles (incl. headers and footers).
80
+ # This is conditional because partials don't need styles.erb.
81
+ @styles = Documatic::Component.new( self.zip_file.read('documatic/master/styles.erb') )
82
+ @styles.process(local_assigns)
83
+ # Process the main (body) content.
84
+ @content = Documatic::Component.new( self.zip_file.read('documatic/master/content.erb') )
85
+ @content.process(local_assigns)
86
+ @content.merge_partial_styles
87
+ end
88
+
89
+ def save
90
+ # Gather all the styles from the partials, add them to the master's styles.
91
+ # Put the body into the document.
92
+ self.zip_file.get_output_stream('content.xml') do |f|
93
+ f.write self.content.to_s
94
+ end
95
+
96
+ if self.styles
97
+ self.zip_file.get_output_stream('styles.xml') do |f|
98
+ f.write self.styles.to_s
99
+ end
100
+ end
101
+ end
102
+
103
+ def close
104
+ self.zip_file.close
105
+ end
106
+
107
+ def compile
108
+ # Read the raw files
109
+ @content_raw = self.zip_file.read('content.xml')
110
+ @styles_raw = self.zip_file.read('styles.xml')
111
+
112
+ @content_erb = self.erbify(@content_raw)
113
+ @styles_erb = self.erbify(@styles_raw)
114
+
115
+ # Create 'documatic/master/' in zip file
116
+ self.zip_file.find_entry('documatic/master') || self.zip_file.mkdir('documatic/master')
117
+
118
+ self.zip_file.get_output_stream('documatic/master/content.erb') do |f|
119
+ f.write @content_erb
120
+ end
121
+ self.zip_file.get_output_stream('documatic/master/styles.erb') do |f|
122
+ f.write @styles_erb
123
+ end
124
+ end
125
+
126
+
127
+ protected
128
+
129
+ # Change OpenDocument line breaks and tabs in the ERb code to regular characters.
130
+ def unnormalize(code)
131
+ code = code.gsub(/<text:line-break\/>/, "\n")
132
+ code = code.gsub(/<text:tab\/>/, "\t")
133
+ return REXML::Text.unnormalize(code)
134
+ end
135
+
136
+ # Massage OpenDocument XML into ERb. (This is the heart of the compiler.)
137
+ def erbify(code, escape = true)
138
+ # First gather all the ERb-related derived styles
139
+ remaining = code
140
+ styles = {'Ruby_20_Code' => 'Code', 'Ruby_20_Value' => 'Value',
141
+ 'Ruby_20_Block' => 'Block', 'Ruby_20_Literal' => 'Literal'}
142
+ re_styles = /<style:style style:name="([^"]+)" style:family="text" style:parent-style-name="Ruby_20_(Code|Value|Block|Literal)">/
143
+
144
+ while remaining.length > 0
145
+ md = re_styles.match remaining
146
+ if md
147
+ styles[md[STYLE_NAME]] = md[STYLE_TYPE]
148
+ remaining = md.post_match
149
+ else
150
+ remaining = ""
151
+ end
152
+ end
153
+
154
+ remaining = code
155
+ result = String.new
156
+
157
+ # Then make a RE that includes the ERb-related styles.
158
+ # Match positions:
159
+ #
160
+ # 1. ROW_START Begin table row ?
161
+ # 2. ITEM_START Begin list item ?
162
+ # 3. PARA_START Begin paragraph ?
163
+ # 4. SPAN_END Another text span ends immediately before ERb ?
164
+ # 5. TYPE ERb text style type
165
+ # 6. ERB_CODE ERb code
166
+ # 7. (ERb inner brackets)
167
+ # 8. SPAN_START Another text span begins immediately after ERb ?
168
+ # 9. PARA_END End paragraph ?
169
+ # 10. ITEM_END End list item ?
170
+ # 11. ROW_END End table row (incl. covered rows) ?
171
+ #
172
+ # "?": optional, might not occur every time
173
+ re_erb = /(<table:table-row><table:table-cell [^>]+>)?(<text:list-item>)?(<text:p [^>]+>)?(<\/text:span>)?<text:span text:style-name="(#{styles.keys.join '|'})">(([^<]*|<text:line-break\/>|<text:tab\/>)+)<\/text:span>(<text:span [^>]+>)?(<\/text:p>)?(<\/text:list-item>)?(<\/table:table-cell>(<table:covered-table-cell\/>)*<\/table:table-row>)?/
174
+
175
+ # Then search for all text using those styles
176
+ while remaining.length > 0
177
+
178
+ md = re_erb.match remaining
179
+
180
+ if md
181
+
182
+ ### TESTING ONLY
183
+ # puts "#{md[ERB_CODE]}:"
184
+ # for i in 1 ... md.size do
185
+ # puts "\t#{i}: #{md[i]}"
186
+ # end
187
+ # puts ""
188
+
189
+ result += md.pre_match
190
+
191
+ match_code = false
192
+ match_row = false
193
+ match_item = false
194
+ match_para = false
195
+ match_span = false
196
+
197
+ if styles[md[TYPE]] == 'Code'
198
+ match_code = true
199
+ delim_start = '<% ' ; delim_end = ' %>'
200
+ if md[PARA_START] and md[PARA_END]
201
+ match_para = true
202
+ if md[ITEM_START] and md[ITEM_END]
203
+ match_item = true
204
+ end
205
+ if md[ROW_START] and md[ROW_END]
206
+ match_row = true
207
+ end
208
+ end
209
+ elsif styles[md[TYPE]] == 'Block'
210
+ delim_start = '<%= ' ; delim_end = ' %>'
211
+ if md[PARA_START] and md[PARA_END]
212
+ match_para = true
213
+ end
214
+ else # style is Value or Literal
215
+ if (not escape) || (styles[md[TYPE]] == 'Literal')
216
+ delim_start = '<%= ' ; delim_end = ' %>'
217
+ else
218
+ delim_start = '<%= ERB::Util.h(' ; delim_end = ') %>'
219
+ end
220
+
221
+ if md[SPAN_END] and md[SPAN_START]
222
+ match_span = true
223
+ end
224
+ end
225
+
226
+ if md[ROW_START] and not match_row
227
+ result += md[ROW_START]
228
+ end
229
+
230
+ if md[ITEM_START] and not match_item
231
+ result += md[ITEM_START]
232
+ end
233
+
234
+ if md[PARA_START] and not match_para
235
+ result += md[PARA_START]
236
+ end
237
+
238
+ # Text formatting before ERb
239
+ if match_code
240
+ if md[SPAN_END]
241
+ result += md[SPAN_END]
242
+ end
243
+ else
244
+ if md[SPAN_START] and not md[SPAN_END]
245
+ result += md[SPAN_START]
246
+ end
247
+ end
248
+ # if md[SPAN_START] and not match_span
249
+ # result += md[SPAN_START]
250
+ # end
251
+
252
+ result += "#{delim_start}#{self.unnormalize md[ERB_CODE]}#{delim_end}"
253
+
254
+ # Text formatting after ERb
255
+ if match_code
256
+ if md[SPAN_START]
257
+ result += md[SPAN_START]
258
+ end
259
+ else
260
+ if md[SPAN_END]
261
+ result += md[SPAN_END]
262
+ if md[SPAN_START]
263
+ result += md[SPAN_START]
264
+ end
265
+ end
266
+ end
267
+
268
+ # if md[SPAN_END] and not match_span
269
+ # result += md[SPAN_END]
270
+ # end
271
+
272
+ if md[PARA_END] and not match_para
273
+ result += md[PARA_END]
274
+ end
275
+
276
+ if md[ITEM_END] and not match_item
277
+ result += md[ITEM_END]
278
+ end
279
+
280
+ if md[ROW_END] and not match_row
281
+ result += md[ROW_END]
282
+ end
283
+
284
+ remaining = md.post_match
285
+
286
+ else # no further matches
287
+ result += remaining
288
+ remaining = ""
289
+ end
290
+ end
291
+
292
+ return result
293
+ end
294
+
295
+ end
296
+ end
297
+
298
+ # Force REXML to use double-quotes (consistent with OOo).
299
+ REXML::Attribute.class_eval do
300
+ def to_string
301
+ %Q[#@expanded_name="#{to_s().gsub(/"/, '&quot;')}"]
302
+ end
303
+ end
data/lib/documatic.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'documatic/helper'
2
+ require 'documatic/template'
3
+ require 'documatic/component'
4
+ require 'documatic/partial'
5
+
6
+ module Documatic
7
+ end
metadata ADDED
@@ -0,0 +1,62 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.0
3
+ specification_version: 1
4
+ name: documatic
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2007-05-16 00:00:00 +10:00
8
+ summary: Documatic is a Ruby OpenDocument processor for preparing documents and reports.
9
+ require_paths:
10
+ - lib
11
+ email: urbanus@240gl.org
12
+ homepage: http://documatic.240gl.org
13
+ rubyforge_project: documatic
14
+ description:
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors: []
30
+
31
+ files:
32
+ - lib
33
+ - lib/documatic
34
+ - lib/documatic.rb
35
+ - lib/documatic/template.rb
36
+ - lib/documatic/component.rb
37
+ - lib/documatic/helper.rb
38
+ - lib/documatic/partial.rb
39
+ - tests
40
+ - README
41
+ test_files: []
42
+
43
+ rdoc_options: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ requirements: []
52
+
53
+ dependencies:
54
+ - !ruby/object:Gem::Dependency
55
+ name: rubyzip
56
+ version_requirement:
57
+ version_requirements: !ruby/object:Gem::Version::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 0.9.1
62
+ version: