avv2word 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 7933697b51e17a3323bce8bf811072d0dd12551c
4
+ data.tar.gz: 217e4cbcd2f30f77694cabdb9709a1e9d0cfb900
5
+ SHA512:
6
+ metadata.gz: 33d2afe64f13b6a10290b8610931d9f400218cc2fe510ad79b7db8e76723bc0af5123e029c9d5b30cd9a3f149735aefab09ae81e35755971777a9e91dd78aeab
7
+ data.tar.gz: bb69cb58ef3ca4293aa78b7a5e61030a8ccd7c9d1975f1a4066fff6f980be3e1f638e694aefd3dd105a0651da82e56845aaf75f17738163f9d620abc48c29418
data/README.md ADDED
@@ -0,0 +1,192 @@
1
+ # Ruby Avvoka Html to word Gem
2
+
3
+ This simple gem allows you to create MS Word docx documents from Avvoka html documents. This makes it easy to create dynamic reports and forms that can be downloaded by your users as simple MS Word docx files.
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ gem 'avv2word'
8
+
9
+ And then execute:
10
+
11
+ $ bundle
12
+
13
+ Or install it yourself as:
14
+
15
+ $ gem install avv2word
16
+
17
+
18
+ ** Note: ** Since version 0.4.0 the ```create``` method will return a string with the contents of the file. If you want to save the file please use ```create_and_save```. See the usage for more
19
+
20
+ ## Usage
21
+
22
+ ### Standalone
23
+
24
+ By default, the file will be saved at the specified location. In case you want to handle the contents of the file
25
+ as a string and do what suits you best, you can specify that when calling the create function.
26
+
27
+ Using the default word file as template
28
+ ```ruby
29
+ require 'avv2word'
30
+
31
+ my_html = '<html><head></head><body><p>Hello</p></body></html>'
32
+ document = Avv2word::Document.create(my_html)
33
+ file = Avv2word::Document.create_and_save(my_html, file_path)
34
+ ```
35
+
36
+ Using your custom word file as a template, where you can setup your own style for normal text, h1,h2, etc.
37
+ ```ruby
38
+ require 'avv2word'
39
+
40
+ # Configure the location of your custom templates
41
+ Avv2word.config.custom_templates_path = 'some_path'
42
+
43
+ my_html = '<html><head></head><body><p>Hello</p></body></html>'
44
+ document = Avv2word::Document.create(my_html, word_template_file_name)
45
+ file = Avv2word::Document.create_and_save(my_html, file_path, word_template_file_name)
46
+ ```
47
+
48
+ The ```create``` function will return a string with the file, so you can do with it what you consider best.
49
+ The ```create_and_save``` function will create the file in the specified file_path.
50
+
51
+ ### With Rails
52
+ **For avv2word version >= 0.2**
53
+ An action controller renderer has been defined, so there's no need to declare the mime-type and you can just respond to .docx format. It will look then for views with the extension ```.docx.erb``` which will provide the HTML that will be rendered in the Word file.
54
+
55
+ ```ruby
56
+ # On your controller.
57
+ respond_to :docx
58
+
59
+ # filename and word_template are optional. By default it will name the file as your action and use the default template provided by the gem. The use of the .docx in the filename and word_template is optional.
60
+ def my_action
61
+ # ...
62
+ respond_with(@object, filename: 'my_file.docx', word_template: 'my_template.docx')
63
+ # Alternatively, if you don't want to create the .docx.erb template you could
64
+ respond_with(@object, content: '<html><head></head><body><p>Hello</p></body></html>', filename: 'my_file.docx')
65
+ end
66
+
67
+ def my_action2
68
+ # ...
69
+ respond_to do |format|
70
+ format.docx do
71
+ render docx: 'my_view', filename: 'my_file.docx'
72
+ # Alternatively, if you don't want to create the .docx.erb template you could
73
+ render docx: 'my_file.docx', content: '<html><head></head><body><p>Hello</p></body></html>'
74
+ end
75
+ end
76
+ end
77
+ ```
78
+
79
+ Example of my_view.docx.erb
80
+ ```
81
+ <h1> My custom template </h1>
82
+ <%= render partial: 'my_partial', collection: @objects, as: :item %>
83
+ ```
84
+ Example of _my_partial.docx.erb
85
+ ```
86
+ <h3><%= item.title %></h3>
87
+ <p> My html for item <%= item.id %> goes here </p>
88
+ ```
89
+
90
+ **For avv2word version <= 0.1.8**
91
+ ```ruby
92
+ # Add mime-type in /config/initializers/mime_types.rb:
93
+ Mime::Type.register "application/vnd.openxmlformats-officedocument.wordprocessingml.document", :docx
94
+
95
+ # Add docx responder in your controller
96
+ def show
97
+ respond_to do |format|
98
+ format.docx do
99
+ file = Avv2word::Document.create params[:docx_html_source], "file_name.docx"
100
+ send_file file.path, :disposition => "attachment"
101
+ end
102
+ end
103
+ end
104
+ ```
105
+
106
+ ```javascript
107
+ // OPTIONAL: Use a jquery click handler to store the markup in a hidden form field before the form is submitted.
108
+ // Using this strategy makes it easy to allow users to dynamically edit the document that will be turned
109
+ // into a docx file, for example by toggling sections of a document.
110
+ $('#download-as-docx').on('click', function () {
111
+ $('input[name="docx_html_source"]').val('<!DOCTYPE html>\n' + $('.delivery').html());
112
+ });
113
+ ```
114
+
115
+ ### Configure templates and xslt paths
116
+
117
+ From version 2.0 you can configure the location of default and custom templates and xslt files. By default templates are defined under ```lib/avv2word/templates``` and xslt under ```lib/avv2word/xslt```
118
+
119
+ ```ruby
120
+ Avv2word.configure do |config|
121
+ config.custom_templates_path = 'path_for_custom_templates'
122
+ # If you modify this path, there should be a 'default.docx' file in there
123
+ config.default_templates_path = 'path_for_default_template'
124
+ # If you modify this path, there should be a 'html_to_wordml.xslt' file in there
125
+ config.default_xslt_path = 'some_path'
126
+ # The use of additional custom xslt will come soon
127
+ config.custom_xslt_path = 'some_path'
128
+ end
129
+ ```
130
+
131
+ ## Features
132
+
133
+ All standard html elements are supported and will create the closest equivalent in wordml. For example spans will create inline elements and divs will create block like elements.
134
+
135
+ ### Highlighting text
136
+
137
+ You can add highlighting to text by wrapping it in a span with class h and adding a data style with a color that wordml supports (http://www.schemacentral.com/sc/ooxml/t-w_ST_HighlightColor.html) ie:
138
+
139
+ ```html
140
+ <span class="h" data-style="green">This text will have a green highlight</span>
141
+ ```
142
+
143
+ ### Page breaks
144
+
145
+ To create page breaks simply add a div with class -page-break ie:
146
+
147
+ ```html
148
+ <div class="-page-break"></div>
149
+ ````
150
+
151
+ ### Images
152
+ Support for images is very basic and is only possible for external images(i.e accessed via URL). If the image doesn't
153
+ have correctly defined it's width and height it won't be included in the document
154
+
155
+ **Limitations:**
156
+ - Images are external i.e. pictures accessed via URL, not stored within document
157
+ - only sizing is customisable
158
+
159
+ Examples:
160
+ ```html
161
+ <img src="http://placehold.it/250x100.png" style="width: 250px; height: 100px">
162
+ <img src="http://placehold.it/250x100.png" data-width="250px" data-height="100px">
163
+ <img src="http://placehold.it/250x100.png" data-height="150px" style="width:250px; height:100px">
164
+ ```
165
+
166
+ ## Contributing / Extending
167
+
168
+ Word docx files are essentially just a zipped collection of xml files and resources.
169
+ This gem contains a standard empty MS Word docx file and a stylesheet to transform arbitrary html into wordml.
170
+ The basic functioning of this gem can be summarised as:
171
+
172
+ 1. Transform inputed html to wordml.
173
+ 2. Unzip empty word docx file bundled with gem and replace its document.xml content with the new transformed result of step 1.
174
+ 3. Zip up contents again into a resulting .docx file.
175
+
176
+ For more info about WordML: http://rep.oio.dk/microsoft.com/officeschemas/wordprocessingml_article.htm
177
+
178
+ Contributions would be very much appreciated.
179
+
180
+ 1. Fork it
181
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
182
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
183
+ 4. Push to the branch (`git push origin my-new-feature`)
184
+ 5. Create new Pull Request
185
+
186
+ ## License
187
+
188
+ (The MIT License)
189
+
190
+ Copyright © 2018:
191
+
192
+ * Marián Bilas
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ task :default => :spec
4
+ RSpec::Core::RakeTask.new
data/bin/avv2word ADDED
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env ruby
2
+ require 'methadone'
3
+ require 'rmultimarkdown'
4
+ require_relative '../lib/avv2word'
5
+
6
+ include Methadone::Main
7
+ include Methadone::CLILogging
8
+
9
+ main do |input, output|
10
+ puts "Converting #{input} to #{output}" if options[:verbose]
11
+ markup = File.read input
12
+ if options[:format] == 'markdown'
13
+ markup = markdown2html(markup)
14
+ end
15
+ Avv2word::Document.create_and_save(markup, output, options[:template_name], options[:extras])
16
+ puts "Done" if options[:verbose]
17
+ end
18
+
19
+ def markdown2html(text)
20
+ MultiMarkdown.new(text.to_s).to_html
21
+ end
22
+
23
+ version Avv2word::VERSION
24
+ description 'Convert simple html input (or markdown) to MS Word (docx)'
25
+ arg :input, :required
26
+ arg :output, :required
27
+
28
+ on('--verbose', '-v', 'Be verbose')
29
+ on('--extras', '-e', 'Use extra formatting features')
30
+ on('--template', '-t', 'Use custom word base template (.docx file)')
31
+ on('-f FORMAT', '--format', 'Format', /markdown|html/)
32
+
33
+ # options['ip-address'] = '127.0.0.1'
34
+ # on('-i IP_ADDRESS', '--ip-address', 'IP Address', /^\d+\.\d+\.\d+\.\d+$/)
35
+
36
+ go!
@@ -0,0 +1,12 @@
1
+ module Avv2word
2
+ class Configuration
3
+ attr_accessor :default_templates_path, :custom_templates_path, :default_xslt_path, :custom_xslt_path
4
+
5
+ def initialize
6
+ @default_templates_path = File.join(File.expand_path('../', __FILE__), 'templates')
7
+ @custom_templates_path = File.join(File.expand_path('../', __FILE__), 'templates')
8
+ @default_xslt_path = File.join(File.expand_path('../', __FILE__), 'xslt')
9
+ @custom_xslt_path = File.join(File.expand_path('../', __FILE__), 'xslt')
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,165 @@
1
+ module Avv2word
2
+ class Document
3
+ include XSLTHelper
4
+
5
+ class << self
6
+ include TemplatesHelper
7
+ def create(content, template_name = nil, extras = false)
8
+ template_name += extension if template_name && !template_name.end_with?(extension)
9
+ document = new(template_file(template_name))
10
+ document.replace_files(content, extras)
11
+ document.generate
12
+ end
13
+
14
+ def create_and_save(content, file_path, template_name = nil, extras = false)
15
+ File.open(file_path, 'wb') do |out|
16
+ out << create(content, template_name, extras)
17
+ end
18
+ end
19
+
20
+ def create_with_content(template, content, extras = false)
21
+ template += extension unless template.end_with?(extension)
22
+ document = new(template_file(template))
23
+ document.replace_files(content, extras)
24
+ document.generate
25
+ end
26
+
27
+ def extension
28
+ '.docx'
29
+ end
30
+
31
+ def doc_xml_file
32
+ 'word/document.xml'
33
+ end
34
+
35
+ def numbering_xml_file
36
+ 'word/numbering.xml'
37
+ end
38
+
39
+ def relations_xml_file
40
+ 'word/_rels/document.xml.rels'
41
+ end
42
+
43
+ def footer_xml_file
44
+ 'word/footer.xml'
45
+ end
46
+
47
+ def header_xml_file
48
+ 'word/header.xml'
49
+ end
50
+
51
+ def content_types_xml_file
52
+ '[Content_Types].xml'
53
+ end
54
+ end
55
+
56
+ def initialize(template_path)
57
+ @replaceable_files = {}
58
+ @template_path = template_path
59
+ @image_files = []
60
+ end
61
+
62
+ #
63
+ # Generate a string representing the contents of a docx file.
64
+ #
65
+ def generate
66
+ Zip::File.open(@template_path) do |template_zip|
67
+ buffer = Zip::OutputStream.write_buffer do |out|
68
+ template_zip.each do |entry|
69
+ out.put_next_entry entry.name
70
+ if @replaceable_files[entry.name] && entry.name == Document.doc_xml_file
71
+ source = entry.get_input_stream.read
72
+ # Change only the body of document. TODO: Improve this...
73
+ source = source.sub(/(<w:body>)((.|\n)*?)(<w:sectPr)/, "\\1#{@replaceable_files[entry.name]}\\4")
74
+ out.write(source)
75
+ elsif @replaceable_files[entry.name]
76
+ out.write(@replaceable_files[entry.name])
77
+ elsif entry.name == Document.content_types_xml_file
78
+ raw_file = entry.get_input_stream.read
79
+ content_types = @image_files.empty? ? raw_file : inject_image_content_types(raw_file)
80
+
81
+ out.write(content_types)
82
+ else
83
+ out.write(template_zip.read(entry.name))
84
+ end
85
+ end
86
+ unless @image_files.empty?
87
+ #stream the image files into the media folder using open-uri
88
+ @image_files.each do |hash|
89
+ out.put_next_entry("word/media/#{hash[:filename]}")
90
+ open(hash[:url], 'rb') do |f|
91
+ out.write(f.read)
92
+ end
93
+ end
94
+ end
95
+ end
96
+ buffer.string
97
+ end
98
+ end
99
+
100
+ def replace_files(html, extras = false)
101
+ html = '<body></body>' if html.nil? || html.empty?
102
+ original_source = Nokogiri::HTML(html.gsub(/>\s+</, '><'))
103
+ transform_and_replace(original_source, xslt_path('header'), Document.header_xml_file)
104
+ transform_and_replace(original_source, xslt_path('footer'), Document.footer_xml_file)
105
+ transform_and_replace(original_source, xslt_path('relations'), Document.relations_xml_file)
106
+ source = xslt(stylesheet_name: 'cleanup').transform(original_source)
107
+ transform_and_replace(source, xslt_path('numbering'), Document.numbering_xml_file)
108
+ transform_doc_xml(source, extras)
109
+ local_images(source)
110
+ end
111
+
112
+ def transform_doc_xml(source, extras = false)
113
+ transformed_source = xslt(stylesheet_name: 'cleanup').transform(source)
114
+ transformed_source = xslt(stylesheet_name: 'inline_elements').transform(transformed_source)
115
+ transform_and_replace(transformed_source, document_xslt(extras), Document.doc_xml_file, extras)
116
+ end
117
+
118
+ private
119
+
120
+ def transform_and_replace(source, stylesheet_path, file, remove_ns = false)
121
+ stylesheet = xslt(stylesheet_path: stylesheet_path)
122
+ content = stylesheet.apply_to(source)
123
+ content.gsub!(/\s*xmlns:(\w+)="(.*?)\s*"/, '') if remove_ns
124
+ @replaceable_files[file] = content
125
+ end
126
+
127
+ #generates an array of hashes with filename and full url
128
+ #for all images to be embeded in the word document
129
+ def local_images(source)
130
+ source.css('img').each_with_index do |image,i|
131
+ filename = image['data-filename'] ? image['data-filename'] : image['src'].split("/").last
132
+ ext = File.extname(filename).delete(".").downcase
133
+
134
+ @image_files << { filename: "image#{i+1}.#{ext}", url: image['src'], ext: ext }
135
+ end
136
+ end
137
+
138
+ #get extension from filename and clean to match content_types
139
+ def content_type_from_extension(ext)
140
+ ext == "jpg" ? "jpeg" : ext
141
+ end
142
+
143
+ #inject the required content_types into the [content_types].xml file...
144
+ def inject_image_content_types(source)
145
+ doc = Nokogiri::XML(source)
146
+
147
+ #get a list of all extensions currently in content_types file
148
+ existing_exts = doc.css("Default").map { |node| node.attribute("Extension").value }.compact
149
+
150
+ #get a list of extensions we need for our images
151
+ required_exts = @image_files.map{ |i| i[:ext] }
152
+
153
+ #workout which required extensions are missing from the content_types file
154
+ missing_exts = (required_exts - existing_exts).uniq
155
+
156
+ #inject missing extensions into document
157
+ missing_exts.each do |ext|
158
+ doc.at_css("Types").add_child( "<Default Extension='#{ext}' ContentType='image/#{content_type_from_extension(ext)}'/>")
159
+ end
160
+
161
+ #return the amended source to be saved into the zip
162
+ doc.to_s
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,9 @@
1
+ module Avv2word
2
+ module TemplatesHelper
3
+ def template_file(template_file_name = nil)
4
+ default_path = File.join(::Avv2word.config.default_templates_path, 'default.docx')
5
+ template_path = template_file_name.nil? ? '' : File.join(::Avv2word.config.custom_templates_path, template_file_name)
6
+ File.exist?(template_path) ? template_path : default_path
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ module Avv2word
2
+ module XSLTHelper
3
+ def document_xslt(extras = false)
4
+ file_name = extras ? 'avv2word' : 'base'
5
+ xslt_path(file_name)
6
+ end
7
+
8
+ def xslt_path(template_name)
9
+ File.join(Avv2word.config.default_xslt_path, "#{template_name}.xslt")
10
+ end
11
+
12
+ def xslt(stylesheet_name: nil, stylesheet_path: nil)
13
+ return Nokogiri::XSLT(File.open(stylesheet_path)) if stylesheet_path
14
+ Nokogiri::XSLT(File.open(xslt_path(stylesheet_name)))
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ module Avv2word
2
+ class Railtie < ::Rails::Railtie
3
+ initializer 'avv2word.setup' do
4
+ if defined?(Mime) and Mime[:docx].nil?
5
+ Mime::Type.register 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', :docx
6
+ end
7
+
8
+ ActionController::Renderers.add :docx do |file_name, options|
9
+ Avv2word::Renderer.send_file(self, file_name, options)
10
+ end
11
+
12
+ if defined? ActionController::Responder
13
+ ActionController::Responder.class_eval do
14
+ def to_docx
15
+ if @default_response
16
+ @default_response.call(options)
17
+ else
18
+ controller.render({ docx: controller.action_name }.merge(options))
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ module Avv2word
2
+ class Renderer
3
+ class << self
4
+ def send_file(context, filename, options = {})
5
+ new(context, filename, options).send_file
6
+ end
7
+ end
8
+
9
+ def initialize(context, filename, options)
10
+ @word_template = options[:word_template].presence
11
+ @disposition = options.fetch(:disposition, 'attachment')
12
+ @use_extras = options.fetch(:extras, false)
13
+ @file_name = file_name(filename, options)
14
+ @context = context
15
+ define_template(filename, options)
16
+ @content = options[:content] || @context.render_to_string(options)
17
+ end
18
+
19
+ def send_file
20
+ document = Avv2word::Document.create(@content, @word_template, @use_extras)
21
+ @context.send_data(document, filename: @file_name, type: Mime[:docx], disposition: @disposition)
22
+ end
23
+
24
+ private
25
+
26
+ def define_template(filename, options)
27
+ if options[:template] == @context.action_name
28
+ if filename =~ %r{^([^\/]+)/(.+)$}
29
+ options[:prefixes] ||= []
30
+ options[:prefixes].unshift $1
31
+ options[:template] = $2
32
+ else
33
+ options[:template] = filename
34
+ end
35
+ end
36
+ end
37
+
38
+ def file_name(filename, options)
39
+ name = options[:filename].presence || filename
40
+ name =~ /\.docx$/ ? name : "#{name}.docx"
41
+ end
42
+ end
43
+ end
Binary file
@@ -0,0 +1,3 @@
1
+ module Avv2word
2
+ VERSION = '1.0.0'
3
+ end