caracal_the_curve 1.4.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.
Files changed (116) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +22 -0
  3. data/.github/workflows/publish_gem.yml +40 -0
  4. data/.gitignore +25 -0
  5. data/.travis.yml +20 -0
  6. data/CHANGELOG.md +141 -0
  7. data/Gemfile +3 -0
  8. data/LICENSE.txt +22 -0
  9. data/README.md +842 -0
  10. data/Rakefile +7 -0
  11. data/caracal.gemspec +28 -0
  12. data/lib/caracal/core/bookmarks.rb +52 -0
  13. data/lib/caracal/core/custom_properties.rb +49 -0
  14. data/lib/caracal/core/file_name.rb +43 -0
  15. data/lib/caracal/core/fonts.rb +75 -0
  16. data/lib/caracal/core/iframes.rb +42 -0
  17. data/lib/caracal/core/ignorables.rb +47 -0
  18. data/lib/caracal/core/images.rb +37 -0
  19. data/lib/caracal/core/list_styles.rb +93 -0
  20. data/lib/caracal/core/lists.rb +57 -0
  21. data/lib/caracal/core/models/base_model.rb +51 -0
  22. data/lib/caracal/core/models/bookmark_model.rb +86 -0
  23. data/lib/caracal/core/models/border_model.rb +120 -0
  24. data/lib/caracal/core/models/custom_property_model.rb +60 -0
  25. data/lib/caracal/core/models/font_model.rb +64 -0
  26. data/lib/caracal/core/models/iframe_model.rb +148 -0
  27. data/lib/caracal/core/models/image_model.rb +133 -0
  28. data/lib/caracal/core/models/line_break_model.rb +15 -0
  29. data/lib/caracal/core/models/link_model.rb +94 -0
  30. data/lib/caracal/core/models/list_item_model.rb +108 -0
  31. data/lib/caracal/core/models/list_model.rb +132 -0
  32. data/lib/caracal/core/models/list_style_model.rb +118 -0
  33. data/lib/caracal/core/models/margin_model.rb +76 -0
  34. data/lib/caracal/core/models/namespace_model.rb +65 -0
  35. data/lib/caracal/core/models/page_break_model.rb +61 -0
  36. data/lib/caracal/core/models/page_number_model.rb +95 -0
  37. data/lib/caracal/core/models/page_size_model.rb +79 -0
  38. data/lib/caracal/core/models/paragraph_model.rb +186 -0
  39. data/lib/caracal/core/models/relationship_model.rb +114 -0
  40. data/lib/caracal/core/models/rule_model.rb +27 -0
  41. data/lib/caracal/core/models/style_model.rb +165 -0
  42. data/lib/caracal/core/models/table_cell_model.rb +176 -0
  43. data/lib/caracal/core/models/table_model.rb +206 -0
  44. data/lib/caracal/core/models/text_model.rb +118 -0
  45. data/lib/caracal/core/namespaces.rb +89 -0
  46. data/lib/caracal/core/page_breaks.rb +29 -0
  47. data/lib/caracal/core/page_numbers.rb +58 -0
  48. data/lib/caracal/core/page_settings.rb +74 -0
  49. data/lib/caracal/core/relationships.rb +90 -0
  50. data/lib/caracal/core/rules.rb +35 -0
  51. data/lib/caracal/core/styles.rb +86 -0
  52. data/lib/caracal/core/tables.rb +42 -0
  53. data/lib/caracal/core/text.rb +75 -0
  54. data/lib/caracal/document.rb +272 -0
  55. data/lib/caracal/errors.rb +23 -0
  56. data/lib/caracal/renderers/app_renderer.rb +41 -0
  57. data/lib/caracal/renderers/content_types_renderer.rb +54 -0
  58. data/lib/caracal/renderers/core_renderer.rb +44 -0
  59. data/lib/caracal/renderers/custom_renderer.rb +64 -0
  60. data/lib/caracal/renderers/document_renderer.rb +427 -0
  61. data/lib/caracal/renderers/fonts_renderer.rb +56 -0
  62. data/lib/caracal/renderers/footer_renderer.rb +90 -0
  63. data/lib/caracal/renderers/numbering_renderer.rb +88 -0
  64. data/lib/caracal/renderers/package_relationships_renderer.rb +51 -0
  65. data/lib/caracal/renderers/relationships_renderer.rb +48 -0
  66. data/lib/caracal/renderers/settings_renderer.rb +58 -0
  67. data/lib/caracal/renderers/styles_renderer.rb +181 -0
  68. data/lib/caracal/renderers/xml_renderer.rb +83 -0
  69. data/lib/caracal/utilities.rb +23 -0
  70. data/lib/caracal/version.rb +3 -0
  71. data/lib/caracal.rb +40 -0
  72. data/lib/tilt/caracal.rb +21 -0
  73. data/spec/lib/caracal/core/bookmarks_spec.rb +35 -0
  74. data/spec/lib/caracal/core/file_name_spec.rb +54 -0
  75. data/spec/lib/caracal/core/fonts_spec.rb +119 -0
  76. data/spec/lib/caracal/core/iframes_spec.rb +29 -0
  77. data/spec/lib/caracal/core/ignorables_spec.rb +79 -0
  78. data/spec/lib/caracal/core/images_spec.rb +25 -0
  79. data/spec/lib/caracal/core/list_styles_spec.rb +121 -0
  80. data/spec/lib/caracal/core/lists_spec.rb +43 -0
  81. data/spec/lib/caracal/core/models/base_model_spec.rb +38 -0
  82. data/spec/lib/caracal/core/models/bookmark_model_spec.rb +135 -0
  83. data/spec/lib/caracal/core/models/border_model_spec.rb +159 -0
  84. data/spec/lib/caracal/core/models/font_model_spec.rb +92 -0
  85. data/spec/lib/caracal/core/models/iframe_model_spec.rb +83 -0
  86. data/spec/lib/caracal/core/models/image_model_spec.rb +225 -0
  87. data/spec/lib/caracal/core/models/line_break_model_spec.rb +21 -0
  88. data/spec/lib/caracal/core/models/link_model_spec.rb +179 -0
  89. data/spec/lib/caracal/core/models/list_item_model_spec.rb +197 -0
  90. data/spec/lib/caracal/core/models/list_model_spec.rb +178 -0
  91. data/spec/lib/caracal/core/models/list_style_model_spec.rb +198 -0
  92. data/spec/lib/caracal/core/models/margin_model_spec.rb +111 -0
  93. data/spec/lib/caracal/core/models/namespace_model_spec.rb +107 -0
  94. data/spec/lib/caracal/core/models/page_break_model_spec.rb +21 -0
  95. data/spec/lib/caracal/core/models/page_number_model_spec.rb +136 -0
  96. data/spec/lib/caracal/core/models/page_size_model_spec.rb +101 -0
  97. data/spec/lib/caracal/core/models/paragraph_model_spec.rb +196 -0
  98. data/spec/lib/caracal/core/models/relationship_model_spec.rb +193 -0
  99. data/spec/lib/caracal/core/models/rule_model_spec.rb +108 -0
  100. data/spec/lib/caracal/core/models/style_model_spec.rb +225 -0
  101. data/spec/lib/caracal/core/models/table_cell_model_spec.rb +230 -0
  102. data/spec/lib/caracal/core/models/table_model_spec.rb +222 -0
  103. data/spec/lib/caracal/core/models/text_model_spec.rb +154 -0
  104. data/spec/lib/caracal/core/namespaces_spec.rb +116 -0
  105. data/spec/lib/caracal/core/page_breaks_spec.rb +25 -0
  106. data/spec/lib/caracal/core/page_numbers_spec.rb +89 -0
  107. data/spec/lib/caracal/core/page_settings_spec.rb +151 -0
  108. data/spec/lib/caracal/core/relationships_spec.rb +119 -0
  109. data/spec/lib/caracal/core/rules_spec.rb +25 -0
  110. data/spec/lib/caracal/core/styles_spec.rb +129 -0
  111. data/spec/lib/caracal/core/tables_spec.rb +25 -0
  112. data/spec/lib/caracal/core/text_spec.rb +52 -0
  113. data/spec/lib/caracal/errors_spec.rb +10 -0
  114. data/spec/spec_helper.rb +8 -0
  115. data/spec/support/_fixtures/snippet.docx +0 -0
  116. metadata +292 -0
@@ -0,0 +1,41 @@
1
+ require 'nokogiri'
2
+
3
+ require 'caracal/renderers/xml_renderer'
4
+
5
+
6
+ module Caracal
7
+ module Renderers
8
+ class AppRenderer < XmlRenderer
9
+
10
+ #-------------------------------------------------------------
11
+ # Public Methods
12
+ #-------------------------------------------------------------
13
+
14
+ # This method produces the xml required for the `docProps/app.xml`
15
+ # sub-document.
16
+ #
17
+ def to_xml
18
+ builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
19
+ xml.send 'Properties', root_options do
20
+ xml.send 'Application', 'Caracal'
21
+ end
22
+ end
23
+ builder.to_xml(save_options)
24
+ end
25
+
26
+
27
+ #-------------------------------------------------------------
28
+ # Private Methods
29
+ #-------------------------------------------------------------
30
+ private
31
+
32
+ def root_options
33
+ {
34
+ 'xmlns' => 'http://schemas.openxmlformats.org/officeDocument/2006/extended-properties',
35
+ 'xmlns:vt' => 'http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes'
36
+ }
37
+ end
38
+
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,54 @@
1
+ require 'nokogiri'
2
+
3
+ require 'caracal/renderers/xml_renderer'
4
+
5
+
6
+ module Caracal
7
+ module Renderers
8
+ class ContentTypesRenderer < XmlRenderer
9
+
10
+ #-------------------------------------------------------------
11
+ # Public Methods
12
+ #-------------------------------------------------------------
13
+
14
+ # This method produces the xml required for the `_[ContentTypes].xml`
15
+ # sub-document.
16
+ #
17
+ def to_xml
18
+ builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
19
+ xml.send 'Types', root_options do
20
+ xml.send 'Default', { 'Extension' => 'gif', 'ContentType' => 'image/gif' }
21
+ xml.send 'Default', { 'Extension' => 'jpeg', 'ContentType' => 'image/jpeg' }
22
+ xml.send 'Default', { 'Extension' => 'jpg', 'ContentType' => 'image/jpeg' }
23
+ xml.send 'Default', { 'Extension' => 'png', 'ContentType' => 'image/png' }
24
+ xml.send 'Default', { 'Extension' => 'rels', 'ContentType' => 'application/vnd.openxmlformats-package.relationships+xml' }
25
+ xml.send 'Default', { 'Extension' => 'xml', 'ContentType' => 'application/xml' }
26
+ xml.send 'Override', { 'PartName' => '/docProps/app.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.extended-properties+xml' }
27
+ xml.send 'Override', { 'PartName' => '/docProps/core.xml', 'ContentType' => 'application/vnd.openxmlformats-package.core-properties+xml' }
28
+ xml.send 'Override', { 'PartName' => '/docProps/custom.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.custom-properties+xml' }
29
+ xml.send 'Override', { 'PartName' => '/word/document.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml' }
30
+ xml.send 'Override', { 'PartName' => '/word/footer1.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml' }
31
+ xml.send 'Override', { 'PartName' => '/word/fontTable.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml' }
32
+ xml.send 'Override', { 'PartName' => '/word/numbering.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml' }
33
+ xml.send 'Override', { 'PartName' => '/word/settings.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml' }
34
+ xml.send 'Override', { 'PartName' => '/word/styles.xml', 'ContentType' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml' }
35
+ end
36
+ end
37
+ builder.to_xml(save_options)
38
+ end
39
+
40
+
41
+ #-------------------------------------------------------------
42
+ # Private Methods
43
+ #-------------------------------------------------------------
44
+ private
45
+
46
+ def root_options
47
+ {
48
+ 'xmlns' => 'http://schemas.openxmlformats.org/package/2006/content-types'
49
+ }
50
+ end
51
+
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ require 'nokogiri'
2
+
3
+ require 'caracal/renderers/xml_renderer'
4
+
5
+
6
+ module Caracal
7
+ module Renderers
8
+ class CoreRenderer < XmlRenderer
9
+
10
+ #-------------------------------------------------------------
11
+ # Public Methods
12
+ #-------------------------------------------------------------
13
+
14
+ # This method produces the xml required for the `docProps/core.xml`
15
+ # sub-document.
16
+ #
17
+ def to_xml
18
+ builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
19
+ xml['cp'].coreProperties root_options do
20
+ xml['dc'].title document.name
21
+ end
22
+ end
23
+ builder.to_xml(save_options)
24
+ end
25
+
26
+
27
+ #-------------------------------------------------------------
28
+ # Private Methods
29
+ #-------------------------------------------------------------
30
+ private
31
+
32
+ def root_options
33
+ {
34
+ 'xmlns:cp' => 'http://schemas.openxmlformats.org/package/2006/metadata/core-properties',
35
+ 'xmlns:dc' => 'http://purl.org/dc/elements/1.1/',
36
+ 'xmlns:dcterms' => 'http://purl.org/dc/terms/',
37
+ 'xmlns:dcmitype' => 'http://purl.org/dc/dcmitype/',
38
+ 'xmlns:xsi' => 'http://www.w3.org/2001/XMLSchema-instance'
39
+ }
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,64 @@
1
+ require 'nokogiri'
2
+ require 'date'
3
+
4
+ require 'caracal/renderers/xml_renderer'
5
+
6
+
7
+ module Caracal
8
+ module Renderers
9
+ class CustomRenderer < XmlRenderer
10
+
11
+ #-------------------------------------------------------------
12
+ # Public Methods
13
+ #-------------------------------------------------------------
14
+
15
+ # This method produces the xml required for the `docProps/custom.xml`
16
+ # sub-document.
17
+ #
18
+ def to_xml
19
+ builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
20
+ xml.send 'Properties', root_options do
21
+ document.custom_props.each_with_index do |property, index|
22
+ xml.send 'property',
23
+ { fmtid: '{D5CDD505-2E9C-101B-9397-08002B2CF9AE}', pid: index + 2, name: property.custom_property_name } do
24
+ case property.custom_property_type.downcase
25
+ when 'text'
26
+ xml['vt'].lpwstr property.custom_property_value
27
+ when 'date'
28
+ xml['vt'].filetime property.custom_property_value.to_date
29
+ when 'number'
30
+ xml['vt'].i4 property.custom_property_value.to_f
31
+ when 'boolean'
32
+ if property.custom_property_value == 'true' || property.custom_property_value == 'false'
33
+ xml['vt'].bool property.custom_property_value
34
+ else
35
+ # Not a boolean sent, so reverting to string so docx will open
36
+ xml['vt'].lpwstr property.custom_property_value
37
+ end
38
+ else
39
+ # Fail to string type
40
+ xml['vt'].lpwstr property.custom_property_value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
46
+ builder.to_xml(save_options)
47
+ end
48
+
49
+
50
+ #-------------------------------------------------------------
51
+ # Private Methods
52
+ #-------------------------------------------------------------
53
+ private
54
+
55
+ def root_options
56
+ {
57
+ 'xmlns' => "http://schemas.openxmlformats.org/officeDocument/2006/custom-properties",
58
+ 'xmlns:vt' => "http://schemas.openxmlformats.org/officeDocument/2006/docPropsVTypes"
59
+ }
60
+ end
61
+
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,427 @@
1
+ require 'nokogiri'
2
+
3
+ require 'caracal/renderers/xml_renderer'
4
+ require 'caracal/errors'
5
+
6
+
7
+ module Caracal
8
+ module Renderers
9
+ class DocumentRenderer < XmlRenderer
10
+
11
+ #-------------------------------------------------------------
12
+ # Public Methods
13
+ #-------------------------------------------------------------
14
+
15
+ # This method produces the xml required for the `word/document.xml`
16
+ # sub-document.
17
+ #
18
+ def to_xml
19
+ builder = ::Nokogiri::XML::Builder.with(declaration_xml) do |xml|
20
+ xml['w'].document root_options do
21
+ xml['w'].background({ 'w:color' => 'FFFFFF' })
22
+ xml['w'].body do
23
+
24
+ #============= CONTENTS ===================================
25
+
26
+ document.contents.each do |model|
27
+ method = render_method_for_model(model)
28
+ send(method, xml, model)
29
+ end
30
+
31
+ #============= PAGE SETTINGS ==============================
32
+
33
+ xml['w'].sectPr do
34
+ if document.page_number_show
35
+ if rel = document.find_relationship('footer1.xml')
36
+ xml['w'].footerReference({ 'r:id' => rel.formatted_id, 'w:type' => 'default' })
37
+ end
38
+ end
39
+ xml['w'].pgSz page_size_options
40
+ xml['w'].pgMar page_margin_options
41
+ end
42
+
43
+ end
44
+ end
45
+ end
46
+ builder.to_xml(save_options)
47
+ end
48
+
49
+
50
+ #-------------------------------------------------------------
51
+ # Private Methods
52
+ #-------------------------------------------------------------
53
+ private
54
+
55
+ #============= COMMON RENDERERS ==========================
56
+
57
+ # This method converts a model class name to a rendering
58
+ # function on this class (e.g., Caracal::Core::Models::ParagraphModel
59
+ # translates to `render_paragraph`).
60
+ #
61
+ def render_method_for_model(model)
62
+ type = model.class.name.split('::').last.downcase.gsub('model', '')
63
+ "render_#{ type }"
64
+ end
65
+
66
+ # This method renders a standard node of run properties based on the
67
+ # model's attributes.
68
+ #
69
+ def render_run_attributes(xml, model, paragraph_level=false)
70
+ if model.respond_to? :run_attributes
71
+ attrs = model.run_attributes.delete_if { |k, v| v.nil? }
72
+
73
+ if paragraph_level && attrs.empty?
74
+ # skip
75
+ else
76
+ xml['w'].rPr do
77
+ unless attrs.empty?
78
+ xml['w'].rStyle( { 'w:val' => attrs[:style] }) unless attrs[:style].nil?
79
+ xml['w'].color( { 'w:val' => attrs[:color] }) unless attrs[:color].nil?
80
+ xml['w'].sz( { 'w:val' => attrs[:size] }) unless attrs[:size].nil?
81
+ xml['w'].b( { 'w:val' => (attrs[:bold] ? '1' : '0') }) unless attrs[:bold].nil?
82
+ xml['w'].i( { 'w:val' => (attrs[:italic] ? '1' : '0') }) unless attrs[:italic].nil?
83
+ xml['w'].u( { 'w:val' => (attrs[:underline] ? 'single' : 'none') }) unless attrs[:underline].nil?
84
+ xml['w'].shd( { 'w:fill' => attrs[:bgcolor], 'w:val' => 'clear' }) unless attrs[:bgcolor].nil?
85
+ xml['w'].highlight( { 'w:val' => attrs[:highlight_color] }) unless attrs[:highlight_color].nil?
86
+ xml['w'].vertAlign( { 'w:val' => attrs[:vertical_align] }) unless attrs[:vertical_align].nil?
87
+ unless attrs[:font].nil?
88
+ f = attrs[:font]
89
+ xml['w'].rFonts( { 'w:ascii' => f, 'w:hAnsi' => f, 'w:eastAsia' => f, 'w:cs' => f })
90
+ end
91
+ end
92
+ xml['w'].rtl({ 'w:val' => '0' })
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+
99
+ #============= MODEL RENDERERS ===========================
100
+
101
+ def render_bookmark(xml, model)
102
+ if model.start?
103
+ xml['w'].bookmarkStart({ 'w:id' => model.bookmark_id, 'w:name' => model.bookmark_name })
104
+ else
105
+ xml['w'].bookmarkEnd({ 'w:id' => model.bookmark_id })
106
+ end
107
+ end
108
+
109
+ def render_iframe(xml, model)
110
+ ::Zip::File.open(model.file) do |zip|
111
+ a_href = 'http://schemas.openxmlformats.org/drawingml/2006/main'
112
+ pic_href = 'http://schemas.openxmlformats.org/drawingml/2006/picture'
113
+ r_href = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
114
+
115
+ entry = zip.glob('word/document.xml').first
116
+ content = entry.get_input_stream.read
117
+ doc_xml = Nokogiri::XML(content)
118
+
119
+ fragment = doc_xml.xpath('//w:body').first.children
120
+ fragment.pop
121
+
122
+ model.relationships.each do |r_hash|
123
+ id = r_hash.delete(:id) # we can't update the fragment until
124
+ model = document.relationship(r_hash) # the parent document assigns the embedded
125
+ index = model.relationship_id # relationship an id.
126
+
127
+ r_node = fragment.at_xpath("//a:blip[@r:embed='#{ id }']", { a: a_href, r: r_href })
128
+ if r_attr = r_node.attributes['embed']
129
+ r_attr.value = "rId#{ index }"
130
+ end
131
+
132
+ p_parent = r_node.parent.parent
133
+ p_node = p_parent.children[0].children[0]
134
+ if p_attr = p_node.attributes['id']
135
+ p_attr.value = index.to_s
136
+ end
137
+ end
138
+
139
+ xml << fragment.to_s
140
+ end
141
+ end
142
+
143
+ def render_image(xml, model)
144
+ unless ds = document.default_style
145
+ raise Caracal::Errors::NoDefaultStyleError 'Document must declare a default paragraph style.'
146
+ end
147
+
148
+ rel = document.relationship({ type: :image, target: model.image_url, data: model.image_data })
149
+ rel_id = rel.relationship_id
150
+ rel_name = rel.formatted_target
151
+
152
+ xml['w'].p paragraph_options do
153
+ xml['w'].pPr do
154
+ xml['w'].spacing({ 'w:lineRule' => 'auto', 'w:line' => ds.style_line })
155
+ xml['w'].contextualSpacing({ 'w:val' => '0' })
156
+ xml['w'].jc({ 'w:val' => model.image_align.to_s })
157
+ xml['w'].rPr
158
+ end
159
+ xml['w'].r run_options do
160
+ xml['w'].drawing do
161
+ xml['wp'].inline({ distR: model.formatted_right, distT: model.formatted_top, distB: model.formatted_bottom, distL: model.formatted_left }) do
162
+ xml['wp'].extent({ cx: model.formatted_width, cy: model.formatted_height })
163
+ xml['wp'].effectExtent({ t: 0, b: 0, r: 0, l: 0 })
164
+ xml['wp'].docPr({ id: rel_id, name: rel_name })
165
+ xml['a'].graphic do
166
+ xml['a'].graphicData({ uri: 'http://schemas.openxmlformats.org/drawingml/2006/picture' }) do
167
+ xml['pic'].pic do
168
+ xml['pic'].nvPicPr do
169
+ xml['pic'].cNvPr({ id: rel_id, name: rel_name })
170
+ xml['pic'].cNvPicPr
171
+ end
172
+ xml['pic'].blipFill do
173
+ xml['a'].blip({ 'r:embed' => rel.formatted_id })
174
+ xml['a'].srcRect
175
+ xml['a'].stretch do
176
+ xml['a'].fillRect
177
+ end
178
+ end
179
+ xml['pic'].spPr do
180
+ xml['a'].xfrm do
181
+ xml['a'].ext({ cx: model.formatted_width, cy: model.formatted_height })
182
+ end
183
+ xml['a'].prstGeom({ prst: 'rect' })
184
+ xml['a'].ln
185
+ end
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
191
+ end
192
+ xml['w'].r run_options do
193
+ xml['w'].rPr do
194
+ xml['w'].rtl({ 'w:val' => '0' })
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ def render_linebreak(xml, model)
201
+ xml['w'].r do
202
+ xml['w'].br
203
+ end
204
+ end
205
+
206
+ def render_link(xml, model)
207
+ if model.external?
208
+ rel = document.relationship({ target: model.link_href, type: :link })
209
+ hyperlink_options = { 'r:id' => rel.formatted_id }
210
+ else
211
+ hyperlink_options = { 'w:anchor' => model.link_href }
212
+ end
213
+
214
+ xml['w'].hyperlink(hyperlink_options) do
215
+ xml['w'].r run_options do
216
+ render_run_attributes(xml, model, false)
217
+ xml['w'].t({ 'xml:space' => 'preserve' }, model.link_content)
218
+ end
219
+ end
220
+ end
221
+
222
+ def render_list(xml, model)
223
+ if model.list_level == 0
224
+ document.toplevel_lists << model
225
+ list_num = document.toplevel_lists.length # numbering uses 1-based index
226
+ end
227
+
228
+ model.recursive_items.each do |item|
229
+ render_listitem(xml, item, list_num)
230
+ end
231
+ end
232
+
233
+ def render_listitem(xml, model, list_num)
234
+ ls = document.find_list_style(model.list_item_type, model.list_item_level)
235
+ hanging = ls.style_left.to_i - ls.style_indent.to_i - 1
236
+
237
+ xml['w'].p paragraph_options do
238
+ xml['w'].pPr do
239
+ xml['w'].numPr do
240
+ xml['w'].ilvl({ 'w:val' => model.list_item_level })
241
+ xml['w'].numId({ 'w:val' => list_num })
242
+ end
243
+ xml['w'].ind({ 'w:left' => ls.style_left, 'w:hanging' => hanging })
244
+ xml['w'].contextualSpacing({ 'w:val' => '1' })
245
+ xml['w'].rPr do
246
+ xml['w'].u({ 'w:val' => 'none' })
247
+ end
248
+ end
249
+ model.runs.each do |run|
250
+ method = render_method_for_model(run)
251
+ send(method, xml, run)
252
+ end
253
+ end
254
+ end
255
+
256
+ def render_pagebreak(xml, model)
257
+ if model.page_break_wrap
258
+ xml['w'].p paragraph_options do
259
+ xml['w'].r run_options do
260
+ xml['w'].br({ 'w:type' => 'page' })
261
+ end
262
+ end
263
+ else
264
+ xml['w'].r run_options do
265
+ xml['w'].br({ 'w:type' => 'page' })
266
+ end
267
+ end
268
+ end
269
+
270
+ def render_paragraph(xml, model)
271
+ run_props = [:color, :size, :bold, :italic, :underline].map { |m| model.send("paragraph_#{ m }") }.compact
272
+
273
+ xml['w'].p paragraph_options do
274
+ xml['w'].pPr do
275
+ xml['w'].pStyle({ 'w:val' => model.paragraph_style }) unless model.paragraph_style.nil?
276
+ xml['w'].contextualSpacing({ 'w:val' => '0' })
277
+ xml['w'].jc({ 'w:val' => model.paragraph_align }) unless model.paragraph_align.nil?
278
+ render_run_attributes(xml, model, true)
279
+ end
280
+ model.runs.each do |run|
281
+ method = render_method_for_model(run)
282
+ send(method, xml, run)
283
+ end
284
+ end
285
+ end
286
+
287
+ def render_rule(xml, model)
288
+ options = { 'w:color' => model.rule_color, 'w:sz' => model.rule_size, 'w:val' => model.rule_line, 'w:space' => model.rule_spacing }
289
+
290
+ xml['w'].p paragraph_options do
291
+ xml['w'].pPr do
292
+ xml['w'].pBdr do
293
+ xml['w'].top options
294
+ end
295
+ end
296
+ end
297
+ end
298
+
299
+ def render_text(xml, model)
300
+ xml['w'].r run_options do
301
+ render_run_attributes(xml, model, false)
302
+ xml['w'].t({ 'xml:space' => 'preserve' }, model.text_content)
303
+ end
304
+ end
305
+
306
+ def render_table(xml, model)
307
+ borders = %w(top left bottom right horizontal vertical).select do |m|
308
+ model.send("table_border_#{ m }_size") > 0
309
+ end
310
+
311
+ xml['w'].tbl do
312
+ xml['w'].tblPr do
313
+ xml['w'].tblStyle({ 'w:val' => 'DefaultTable' })
314
+ xml['w'].bidiVisual({ 'w:val' => '0' })
315
+ xml['w'].tblW({ 'w:w' => model.table_width.to_f, 'w:type' => 'dxa' })
316
+ xml['w'].tblInd({ 'w:w' => '0.0', 'w:type' => 'dxa' })
317
+ xml['w'].jc({ 'w:val' => model.table_align })
318
+ unless borders.empty?
319
+ xml['w'].tblBorders do
320
+ borders.each do |m|
321
+ options = {
322
+ 'w:color' => model.send("table_border_#{ m }_color"),
323
+ 'w:val' => model.send("table_border_#{ m }_line"),
324
+ 'w:sz' => model.send("table_border_#{ m }_size"),
325
+ 'w:space' => model.send("table_border_#{ m }_spacing")
326
+ }
327
+ xml['w'].method_missing "#{ Caracal::Core::Models::BorderModel.formatted_type(m) }", options
328
+ end
329
+ end
330
+ end
331
+ xml['w'].tblLayout({ 'w:type' => 'fixed' })
332
+ xml['w'].tblLook({ 'w:val' => '0600' })
333
+ end
334
+ xml['w'].tblGrid do
335
+ model.rows.first.each do |tc|
336
+ (tc.cell_colspan || 1).times do
337
+ xml['w'].gridCol({ 'w:w' => tc.cell_width })
338
+ end
339
+ end
340
+ xml['w'].tblGridChange({ 'w:id' => '0' }) do
341
+ xml['w'].tblGrid do
342
+ model.rows.first.each do |tc|
343
+ (tc.cell_colspan || 1).times do
344
+ xml['w'].gridCol({ 'w:w' => tc.cell_width })
345
+ end
346
+ end
347
+ end
348
+ end
349
+ end
350
+
351
+ rowspan_hash = {}
352
+ model.rows.each do |row|
353
+ xml['w'].tr do
354
+ tc_index = 0
355
+ row.each do |tc|
356
+ xml['w'].tc do
357
+ xml['w'].tcPr do
358
+ xml['w'].shd({ 'w:fill' => tc.cell_background })
359
+ xml['w'].vAlign({ 'w:val' => tc.cell_vertical_align })
360
+
361
+ # applying rowspan
362
+ if tc.cell_rowspan && tc.cell_rowspan > 0
363
+ rowspan_hash[tc_index] = tc.cell_rowspan - 1
364
+ xml['w'].vMerge({ 'w:val' => 'restart' })
365
+ elsif rowspan_hash[tc_index] && rowspan_hash[tc_index] > 0
366
+ xml['w'].vMerge({ 'w:val' => 'continue' })
367
+ rowspan_hash[tc_index] -= 1
368
+ end
369
+
370
+ # applying colspan
371
+ xml['w'].gridSpan({ 'w:val' => tc.cell_colspan }) if tc.cell_colspan
372
+
373
+ xml['w'].tcMar do
374
+ %w(top left bottom right).each do |d|
375
+ xml['w'].method_missing "#{ d }", { 'w:w' => tc.send("cell_margin_#{ d }").to_f, 'w:type' => 'dxa' }
376
+ end
377
+ end
378
+ end
379
+ tc.contents.each do |m|
380
+ method = render_method_for_model(m)
381
+ send(method, xml, m)
382
+ end
383
+ end
384
+
385
+ # adjust tc_index for next cell taking into account current cell's colspan
386
+ tc_index += (tc.cell_colspan && tc.cell_colspan > 0) ? tc.cell_colspan : 1
387
+ end
388
+ end
389
+ end
390
+ end
391
+ end
392
+
393
+
394
+ #============= OPTIONS ===================================
395
+
396
+ def root_options
397
+ opts = {}
398
+ document.namespaces.each do |model|
399
+ opts[model.namespace_prefix] = model.namespace_href
400
+ end
401
+ unless document.ignorables.empty?
402
+ v = document.ignorables.join(' ')
403
+ opts['mc:Ignorable'] = v
404
+ end
405
+ opts
406
+ end
407
+
408
+ def page_margin_options
409
+ {
410
+ 'w:top' => document.page_margin_top,
411
+ 'w:bottom' => document.page_margin_bottom,
412
+ 'w:left' => document.page_margin_left,
413
+ 'w:right' => document.page_margin_right
414
+ }
415
+ end
416
+
417
+ def page_size_options
418
+ {
419
+ 'w:w' => document.page_width,
420
+ 'w:h' => document.page_height,
421
+ 'w:orient' => document.page_orientation
422
+ }
423
+ end
424
+
425
+ end
426
+ end
427
+ end