markdown2docx 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'markdown2docx'
4
+
5
+ puts 'Running'
6
+
7
+ m = Markdown2Docx.open ARGV[0]
8
+ m.merge_yaml ARGV[1]
9
+ m.save ARGV[2]
@@ -0,0 +1,416 @@
1
+ require 'rubygems'
2
+ require 'zip' # 'zip/zip' # rubyzip gem
3
+ require 'nokogiri'
4
+ require 'yaml'
5
+ require 'dimensions'
6
+ require 'kramdown'
7
+
8
+ YAML::ENGINE.yamler = 'syck'
9
+
10
+ class Markdown2Docx
11
+ def self.open(path, &block)
12
+ self.new(path, &block)
13
+ end
14
+
15
+ def initialize(path, &block)
16
+ @replace = {}
17
+ @media = {}
18
+ if block_given?
19
+ @zip = Zip::File.open(path)
20
+ yield(self)
21
+ @zip.close
22
+ else
23
+ @zip = Zip::File.open(path)
24
+ end
25
+ end
26
+
27
+ def get_node(run, search)
28
+ node_to_find = (run.xpath(".//#{search}")).first
29
+ if node_to_find.nil?
30
+ node_to_find = Nokogiri::XML::Node.new search, @doc
31
+ run.add_child node_to_find
32
+ end
33
+ node_to_find
34
+ end
35
+
36
+ def get_emus_dimensions(image_file)
37
+ dimensions = Dimensions.dimensions(image_file)
38
+ px_width = dimensions[0]
39
+ px_height = dimensions[1]
40
+ dpi = 96.0
41
+ emus_per_inch = 914400.0
42
+ emus_per_cm = 360000.0
43
+ max_width_cm = 15.0
44
+ emus_width = px_width / dpi * emus_per_inch
45
+ emus_height = px_height / dpi * emus_per_inch
46
+ max_width = max_width_cm * emus_per_cm
47
+ if (emus_width > max_width)
48
+ ratio = emus_height / emus_width
49
+ emus_width = max_width
50
+ emus_height = emus_width * ratio
51
+ end
52
+
53
+ return emus_width.round, emus_height.round
54
+ end
55
+
56
+ def create_image_node(image_file, description)
57
+ image_name = File.basename image_file
58
+ docx_image_path = File.join 'media', image_name
59
+ @media[File.join 'word',docx_image_path] = File.binread image_file
60
+ rel_id = add_rels_link(docx_image_path, :image)
61
+ check_for_jpeg_content_type
62
+ drawing = Nokogiri::XML::Node.new 'w:drawing', @doc
63
+ inline = Nokogiri::XML::Node.new 'wp:inline', @doc
64
+ drawing.add_child inline
65
+ inline['distT'] = '0'
66
+ inline['distB'] = '0'
67
+ inline['distL'] = '0'
68
+ inline['distR'] = '0'
69
+
70
+ extent = Nokogiri::XML::Node.new 'wp:extent', @doc
71
+ inline.add_child extent
72
+ emu_dimensions = get_emus_dimensions image_file
73
+ extent['cx'] = emu_dimensions[0]
74
+ extent['cy'] = emu_dimensions[1]
75
+
76
+ effect_extent = Nokogiri::XML::Node.new 'wp:effectExtent', @doc
77
+ inline.add_child effect_extent
78
+ effect_extent['l'] = 0 # '19050'
79
+ effect_extent['t'] = 0
80
+ effect_extent['r'] = 0 #2099'
81
+ effect_extent['b'] = 0
82
+
83
+ docPr = Nokogiri::XML::Node.new 'wp:docPr', @doc
84
+ inline.add_child docPr
85
+ docPr['id'] = '1'
86
+ docPr['name'] = image_name
87
+ docPr['descr'] = description
88
+
89
+ non_visual_graphic_props = Nokogiri::XML::Node.new 'wp:cNvGraphicFramePr', @doc
90
+ inline.add_child non_visual_graphic_props
91
+ frame_locks = Nokogiri::XML::Node.new 'a:graphicFrameLocks', @doc
92
+ frame_locks.add_namespace_definition 'a', 'http://schemas.openxmlformats.org/drawingml/2006/main'
93
+ frame_locks['noChangeAspect'] = '1'
94
+ non_visual_graphic_props.add_child frame_locks
95
+
96
+ graphic = Nokogiri::XML::Node.new 'a:graphic', @doc
97
+ inline.add_child graphic
98
+ graphic.add_namespace_definition 'a', 'http://schemas.openxmlformats.org/drawingml/2006/main'
99
+
100
+ graphic_data = Nokogiri::XML::Node.new 'a:graphicData', @doc
101
+ graphic.add_child graphic_data
102
+ graphic_data['uri'] = 'http://schemas.openxmlformats.org/drawingml/2006/picture'
103
+
104
+ pic = Nokogiri::XML::Node.new 'pic:pic', @doc
105
+ pic.add_namespace_definition 'pic', 'http://schemas.openxmlformats.org/drawingml/2006/picture'
106
+ graphic_data.add_child pic
107
+
108
+ nv_picture_properties = Nokogiri::XML::Node.new 'pic:nvPicPr', @doc
109
+ pic.add_child nv_picture_properties
110
+ nv_drawing_properties = Nokogiri::XML::Node.new 'pic:cNvPr', @doc
111
+ nv_drawing_properties['id'] = '0'
112
+ nv_drawing_properties['name'] = image_name
113
+ nv_drawing_properties['descr'] = description
114
+ nv_picture_properties.add_child nv_drawing_properties
115
+
116
+ nv_picture_drawing_properties = Nokogiri::XML::Node.new 'pic:cNvPicPr', @doc
117
+ nv_picture_properties.add_child nv_picture_drawing_properties
118
+ pic_locks = Nokogiri::XML::Node.new 'a:picLocks', @doc
119
+ pic_locks['noChangeAspect'] = '1'
120
+ nv_picture_drawing_properties.add_child pic_locks
121
+
122
+ blip_fill = Nokogiri::XML::Node.new 'pic:blipFill', @doc
123
+ pic.add_child blip_fill
124
+
125
+ blip = Nokogiri::XML::Node.new 'a:blip', @doc
126
+ blip_fill.add_child blip
127
+ blip['r:embed'] = "rId#{rel_id}"
128
+ blip['cstate'] = 'print'
129
+
130
+ src_rect = Nokogiri::XML::Node.new 'a:srcRect', @doc
131
+ blip_fill.add_child src_rect
132
+
133
+ stretch = Nokogiri::XML::Node.new 'a:stretch', @doc
134
+ fill_rect = Nokogiri::XML::Node.new 'a:fillRect', @doc
135
+ stretch.add_child fill_rect
136
+ blip_fill.add_child stretch
137
+
138
+ shape_properties = Nokogiri::XML::Node.new 'pic:spPr', @doc
139
+ shape_properties['bwMode'] = 'auto'
140
+ transform = Nokogiri::XML::Node.new 'a:xfrm', @doc
141
+ offset = Nokogiri::XML::Node.new 'a:off', @doc
142
+ offset['x'] = '0'
143
+ offset['y'] = '0'
144
+ transform.add_child offset
145
+ extents = Nokogiri::XML::Node.new 'a:ext', @doc
146
+
147
+ extents['cx'] = (emu_dimensions[0] * 1.0000762).round
148
+ extents['cy'] = (emu_dimensions[1] * 1.0000762).round
149
+ transform.add_child extents
150
+ shape_properties.add_child transform
151
+
152
+ preset_geometry = Nokogiri::XML::Node.new 'a:prstGeom', @doc
153
+ preset_geometry['prst'] = 'rect'
154
+ adjust_value_list = Nokogiri::XML::Node.new 'a:avLst', @doc
155
+ preset_geometry.add_child adjust_value_list
156
+ shape_properties.add_child preset_geometry
157
+
158
+ no_fill = Nokogiri::XML::Node.new 'a:noFill', @doc
159
+ shape_properties.add_child no_fill
160
+
161
+ line = Nokogiri::XML::Node.new 'a:ln', @doc
162
+ line['w'] = 0 # '9525'
163
+ line.add_child no_fill.dup
164
+ miter = Nokogiri::XML::Node.new 'a:miter', @doc
165
+ miter['lim'] = '800000'
166
+ line.add_child miter
167
+ line.add_child Nokogiri::XML::Node.new 'a:headEnd', @doc
168
+ line.add_child Nokogiri::XML::Node.new 'a:tailEnd', @doc
169
+ shape_properties.add_child line
170
+
171
+ pic.add_child shape_properties
172
+
173
+ drawing
174
+ end
175
+
176
+ def process_runs(source_node, cur_run)
177
+ old_run = cur_run
178
+
179
+ source_node.children.each do |run|
180
+ next_run = cur_run.dup
181
+ if cur_run.parent.nil?
182
+ old_run.add_next_sibling(cur_run)
183
+ end
184
+ if run.name == 'strong'
185
+ bold = Nokogiri::XML::Node.new 'w:b', @doc
186
+ get_node(cur_run, 'w:rPr').add_child bold
187
+ elsif run.name == 'br'
188
+ br = Nokogiri::XML::Node.new 'w:br', @doc
189
+ get_node(cur_run, 'w:rPr').add_child br
190
+ elsif run.name == 'a'
191
+ rel_id = add_rels_link(run['href'], :hyperlink)
192
+ r_id = "rId#{rel_id}"
193
+ hyperlink = Nokogiri::XML::Node.new 'w:hyperlink', @doc
194
+ hyperlink['r:id'] = r_id
195
+ hyperlink['w:history'] = "1"
196
+ cur_run.add_next_sibling(hyperlink)
197
+ hyperlink.add_child(cur_run)
198
+ style = Nokogiri::XML::Node.new 'w:rStyle', @doc
199
+ style['w:val'] = 'Hyperlink'
200
+ get_node(cur_run, 'w:rPr').add_child style
201
+ elsif run.name == 'img'
202
+ image_node = create_image_node run['src'], run['alt']
203
+ run_properties = get_node(cur_run, 'w:rPr')
204
+ run_properties.add_child Nokogiri::XML::Node.new('w:noProof', @doc)
205
+ #(cur_run.parent/'.//w:ind').remove
206
+ lang = Nokogiri::XML::Node.new 'w:lang', @doc
207
+ lang['w:val'] = 'en-GB'
208
+ lang['w:eastAsia'] = 'en-GB'
209
+ run_properties.add_child lang
210
+ cur_run.add_child image_node
211
+ end
212
+ if run.name != 'img'
213
+ if run.text[0] == ' '
214
+ new_text = Nokogiri::XML::Node.new "w:t", @doc
215
+ cur_run.add_child new_text
216
+ new_text['xml:space'] = "preserve"
217
+ new_text.content = ' '
218
+ old_run = cur_run
219
+ cur_run = next_run
220
+ next_run = cur_run.dup
221
+ old_run.add_next_sibling(cur_run)
222
+ end
223
+
224
+ new_text = Nokogiri::XML::Node.new "w:t", @doc
225
+ cur_run.add_child new_text
226
+ new_text.content = run.text
227
+
228
+ if run.text[run.text.length-1] == ' '
229
+ cur_run.add_next_sibling(next_run)
230
+ cur_run = next_run
231
+ next_run = cur_run.dup
232
+ new_text = Nokogiri::XML::Node.new "w:t", @doc
233
+ cur_run.add_child new_text
234
+ new_text['xml:space'] = "preserve"
235
+ new_text.content = ' '
236
+ end
237
+ end
238
+
239
+ if run.name == 'a'
240
+ old_run = hyperlink
241
+ else
242
+ old_run = cur_run
243
+ end
244
+ cur_run = next_run
245
+ end
246
+ end
247
+
248
+ def add_paragraph(text_elements, cur_node)
249
+ first_run = (cur_node.xpath('.//w:r')).first
250
+
251
+ process_runs text_elements, first_run
252
+ end
253
+
254
+ def add_bullet(text_elements, cur_node)
255
+ bullet_node = (cur_node.xpath('.//w:pPr')).first
256
+ (bullet_node/'.//w:ind').remove
257
+ numpr = Nokogiri::XML::Node.new "w:numPr", @doc
258
+ ilvl = Nokogiri::XML::Node.new "w:ilvl", @doc
259
+ ilvl['w:val'] = '0'
260
+ numpr.add_child ilvl
261
+ numId = Nokogiri::XML::Node.new 'w:numId', @doc
262
+ numId['w:val'] = '22'
263
+ numpr.add_child numId
264
+ bullet_node.add_child numpr
265
+
266
+ first_run = (cur_node.xpath('.//w:r')).first
267
+ process_runs text_elements, first_run
268
+ end
269
+
270
+ def process_elements(elements, cur_node)
271
+ old_node = cur_node
272
+ elements.each do |element|
273
+ dup_p = cur_node.dup
274
+ if cur_node.parent.nil?
275
+ old_node.add_next_sibling(cur_node)
276
+ end
277
+ if element.name == 'p'
278
+ add_paragraph element, cur_node
279
+ elsif element.name == 'h3'
280
+ properties = get_node(cur_node, 'w:pPr')
281
+ style = get_node(properties, 'w:pStyle')
282
+ style['w:val'] = 'Heading3'
283
+ add_paragraph element, cur_node
284
+ elsif element.name == 'li'
285
+ add_bullet element, cur_node
286
+ elsif element.name == 'ul'
287
+ cur_node = process_elements element.elements, cur_node
288
+ end
289
+
290
+ old_node = cur_node
291
+ cur_node = dup_p
292
+ end
293
+ old_node
294
+ end
295
+
296
+ def merge_yaml(yaml_file)
297
+ records = YAML::parse_file yaml_file
298
+ merge(records)
299
+ end
300
+
301
+ def merge(rec)
302
+ xml = @zip.read("word/document.xml")
303
+ @doc = Nokogiri::XML(xml) {|x| x.noent}
304
+ doc_values = (@doc/"//w:p") #.select { |element| element.text =~ /^\$(.*)\$$/ }
305
+ doc_values.each do |element|
306
+ #puts element.text
307
+ if element.text =~ /^\$(.*)\$$/
308
+ value = (rec.select $1)[0]
309
+ if value.nil?
310
+ value = ''
311
+ else
312
+ value = value.value
313
+ end
314
+
315
+ # Ensure there is only one w:t node
316
+ (element/'.//w:t').remove
317
+
318
+ value_html = Kramdown::Document.new(value).to_html
319
+
320
+ html_doc = Nokogiri::HTML(value_html)
321
+ if html_doc.elements.count > 0
322
+ markdown_elements = html_doc.elements[0].elements[0].elements
323
+ process_elements(markdown_elements, element)
324
+ end
325
+ end
326
+ end
327
+ @replace["word/document.xml"] = @doc.serialize :save_with => 0
328
+ end
329
+
330
+ def check_for_jpeg_content_type()
331
+ xml_file = '[Content_Types].xml'
332
+ xml = @replace[xml_file] || @zip.read(xml_file)
333
+ types = Nokogiri::XML(xml) {|x| x.noent}
334
+
335
+ default_types = types.elements[0]/"Default"
336
+
337
+ if default_types.select { |element| element['Extension'] == 'jpeg'}.count == 0
338
+ jpeg_type = Nokogiri::XML::Node.new 'Default', types
339
+ jpeg_type['Extension'] = 'jpeg'
340
+ jpeg_type['ContentType'] = 'image/jeg'
341
+ #default_types.last.add_next_sibling jpeg_type
342
+ types.children[0].add_child jpeg_type
343
+ @replace[xml_file] = types.serialize :save_with => 0
344
+ end
345
+
346
+
347
+ end
348
+
349
+ def add_rels_link(url, type)
350
+ xml = @replace["word/_rels/document.xml.rels"] || @zip.read("word/_rels/document.xml.rels")
351
+ rels = Nokogiri::XML(xml) {|x| x.noent}
352
+
353
+ cur_id = 1
354
+ have_unused_id = false
355
+ until have_unused_id
356
+ id = "rId#{cur_id}"
357
+ have_unused_id = true
358
+ rels.elements[0].elements.each do |element|
359
+ if element['Id'] == id
360
+ cur_id += 1
361
+ id = "rId#{cur_id}"
362
+ have_unused_id = false
363
+ end
364
+ end
365
+ end
366
+
367
+ relationship = Nokogiri::XML::Node.new "Relationship", rels
368
+ relationship['Id'] = id
369
+ if type == :hyperlink
370
+ relationship['Type'] = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
371
+ relationship['TargetMode'] = 'External'
372
+ elsif type == :image
373
+ relationship['Type'] = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
374
+ end
375
+ relationship['Target'] = url
376
+
377
+ rels.elements[0].add_child relationship
378
+
379
+ @replace["word/_rels/document.xml.rels"] = rels.serialize :save_with => 0
380
+
381
+ cur_id
382
+ end
383
+
384
+ def save(path)
385
+ Zip::File.open(path, Zip::File::CREATE) do |out|
386
+ @zip.each do |entry|
387
+ out.get_output_stream(entry.name) do |o|
388
+ if @replace[entry.name]
389
+ o.write(@replace[entry.name])
390
+ else
391
+ o.write(@zip.read(entry.name))
392
+ end
393
+ end
394
+ end
395
+ @media.keys.each do |key|
396
+ out.get_output_stream(key) { |o| o.write @media[key] }
397
+ end
398
+ end
399
+ @zip.close
400
+
401
+ # this is to ensure the zip can actually be opened by word. RubyZip doesn't quite do it
402
+ # from the build server
403
+ require 'rbconfig'
404
+ is_windows = (RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/)
405
+ if is_windows
406
+ if Dir.exist?('releasePack')
407
+ FileUtils.rmtree 'releasePack'
408
+ end
409
+ `"C:\\program files\\7-zip\\7z.exe" x -oreleasePack release.docx`
410
+ `cd releasePack && "C:\\program files\\7-zip\\7z.exe" a release.docx *`
411
+ FileUtils.move File.join('releasePack', 'release.docx'), 'release.docx'
412
+ FileUtils.rmtree 'releasePack'
413
+ end
414
+ end
415
+ end
416
+
Binary file
@@ -0,0 +1,34 @@
1
+ # Release pack
2
+
3
+ # Title page
4
+
5
+ author: Dave Arkell
6
+ project: Markdown to docx
7
+ system_area: Ruby
8
+
9
+ version_history:
10
+ - author: Dave Arkell
11
+ description: Initial version
12
+ version: 1
13
+ date: 26/09/2013
14
+ - author: Daoud Clarke
15
+ description: Remove company content
16
+ date: 22/10/2013
17
+
18
+ # Overview
19
+
20
+ release_overview: |+
21
+ This is a test paragraph.
22
+
23
+ \\
24
+ This is another paragraph.
25
+ * **Something bold** - nice
26
+ * **Something else** - also good
27
+
28
+ \\
29
+ Here is a link to [DuckDuckGo](https://duckduckgo.com)
30
+
31
+ Here is a wombat:
32
+ \\
33
+ ![printdata](samples/wombat.jpeg)
34
+
metadata ADDED
@@ -0,0 +1,130 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: markdown2docx
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dave Arkell
9
+ - Daoud Clarke
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2013-10-07 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: dimensions
17
+ requirement: !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ! '>='
21
+ - !ruby/object:Gem::Version
22
+ version: '0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ! '>='
29
+ - !ruby/object:Gem::Version
30
+ version: '0'
31
+ - !ruby/object:Gem::Dependency
32
+ name: rubyzip
33
+ requirement: !ruby/object:Gem::Requirement
34
+ none: false
35
+ requirements:
36
+ - - ~>
37
+ - !ruby/object:Gem::Version
38
+ version: 1.0.0
39
+ type: :runtime
40
+ prerelease: false
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ~>
45
+ - !ruby/object:Gem::Version
46
+ version: 1.0.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: nokogiri
49
+ requirement: !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: !ruby/object:Gem::Requirement
58
+ none: false
59
+ requirements:
60
+ - - ! '>='
61
+ - !ruby/object:Gem::Version
62
+ version: '0'
63
+ - !ruby/object:Gem::Dependency
64
+ name: dimensions
65
+ requirement: !ruby/object:Gem::Requirement
66
+ none: false
67
+ requirements:
68
+ - - ! '>='
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ none: false
75
+ requirements:
76
+ - - ! '>='
77
+ - !ruby/object:Gem::Version
78
+ version: '0'
79
+ - !ruby/object:Gem::Dependency
80
+ name: kramdown
81
+ requirement: !ruby/object:Gem::Requirement
82
+ none: false
83
+ requirements:
84
+ - - ! '>='
85
+ - !ruby/object:Gem::Version
86
+ version: '0'
87
+ type: :runtime
88
+ prerelease: false
89
+ version_requirements: !ruby/object:Gem::Requirement
90
+ none: false
91
+ requirements:
92
+ - - ! '>='
93
+ - !ruby/object:Gem::Version
94
+ version: '0'
95
+ description: Combines markdown in a yaml file with a docx template
96
+ email: daoud.clarke@gmail.com
97
+ executables:
98
+ - md2docx
99
+ extensions: []
100
+ extra_rdoc_files: []
101
+ files:
102
+ - lib/markdown2docx.rb
103
+ - bin/md2docx
104
+ - samples/release.yaml
105
+ - samples/release.docx
106
+ homepage:
107
+ licenses: []
108
+ post_install_message:
109
+ rdoc_options: []
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ required_rubygems_version: !ruby/object:Gem::Requirement
119
+ none: false
120
+ requirements:
121
+ - - ! '>='
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
124
+ requirements: []
125
+ rubyforge_project:
126
+ rubygems_version: 1.8.23
127
+ signing_key:
128
+ specification_version: 3
129
+ summary: markdown2docx
130
+ test_files: []