markdown2docx 0.1.3

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.
@@ -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: []