documatic 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: