caracal 1.0.9 → 1.0.10
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/README.md +20 -0
- data/lib/caracal.rb +11 -2
- data/lib/caracal/core/custom_properties.rb +8 -6
- data/lib/caracal/core/iframes.rb +42 -0
- data/lib/caracal/core/ignorables.rb +47 -0
- data/lib/caracal/core/models/iframe_model.rb +148 -0
- data/lib/caracal/core/models/namespace_model.rb +65 -0
- data/lib/caracal/core/namespaces.rb +89 -0
- data/lib/caracal/document.rb +10 -4
- data/lib/caracal/renderers/document_renderer.rb +61 -18
- data/lib/caracal/version.rb +1 -1
- data/spec/lib/caracal/core/iframes_spec.rb +29 -0
- data/spec/lib/caracal/core/ignorables_spec.rb +79 -0
- data/spec/lib/caracal/core/models/iframe_model_spec.rb +83 -0
- data/spec/lib/caracal/core/models/image_model_spec.rb +60 -62
- data/spec/lib/caracal/core/models/namespace_model_spec.rb +107 -0
- data/spec/lib/caracal/core/namespaces_spec.rb +116 -0
- data/spec/lib/caracal/core/relationships_spec.rb +47 -47
- data/spec/support/_fixtures/snippet.docx +0 -0
- data/tmp_caracal +0 -0
- metadata +20 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8ba1b2ffe0fd04fdee4a28a8697b6862ab972630
|
4
|
+
data.tar.gz: 8a1c979775800f0eddef47ea163c5e6baf8b2116
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0575246a9d772fb51aa053d1a4539b3e28bc74b52f279fb99a2eab242a044d5422996a6ad1ff8068ecafc7ec340b16a0f3b71985599c1bfda026a40b7bb31815
|
7
|
+
data.tar.gz: 7e3f5804fad3bdd679079c01986a39b24a23e84a3b2e81a426be38b0b80837a2c3da94743dab2e9f04adbe49ad1e7aed0c2f7b1da272a57baea8e3c0d3a1b045
|
data/.gitignore
CHANGED
data/README.md
CHANGED
@@ -678,6 +678,23 @@ end
|
|
678
678
|
```
|
679
679
|
|
680
680
|
|
681
|
+
## Experimental Features
|
682
|
+
|
683
|
+
### IFrames
|
684
|
+
|
685
|
+
You can include an external Word document into your working Caracal document by specifying a URL or by supplying the data directly.
|
686
|
+
|
687
|
+
*It should be noted that the metaphor here is imperfect. Caracal fully includes the external file at the time of insertion. Further changes to the external file will not be reflected in your Caracal output.*
|
688
|
+
|
689
|
+
```ruby
|
690
|
+
# this example loads the file from the internet
|
691
|
+
docx.iframe url: 'http://www.some-website.org/snippet.docx'
|
692
|
+
|
693
|
+
# this example loads the data directly
|
694
|
+
docx.iframe data: File.read('my/path/to/snippet.docx')
|
695
|
+
|
696
|
+
```
|
697
|
+
|
681
698
|
## Template Rendering
|
682
699
|
|
683
700
|
Caracal includes [Tilt](https://github.com/rtomayko/tilt) integration to facilitate its inclusion in other frameworks.
|
@@ -690,11 +707,13 @@ Rails integration can be added via the [Caracal-Rails](https://github.com/trade-
|
|
690
707
|
Caracal was written for and tested against Word 2010, 2013, and Office365. It should also open in LibreOffice
|
691
708
|
with high fidelity.
|
692
709
|
|
710
|
+
|
693
711
|
### Older Versions
|
694
712
|
If you are using a version of Word that predates 2010, Caracal may or may not work for you. (Probably it won't.)
|
695
713
|
We don't ever plan to support versions before 2010, but if you choose to embark on that endeavor, we'd be
|
696
714
|
happy to answer questions and provide what guidance we can. We just won't write any code in that direction.
|
697
715
|
|
716
|
+
|
698
717
|
### Newer Versions
|
699
718
|
|
700
719
|
For those using reasonably current versions of Word, please consider the following:
|
@@ -709,6 +728,7 @@ made a mistake in your document's syntax or have an environment-specific, non-ca
|
|
709
728
|
- If you do see the same behavior in the example project, you've probably uncovered a variance in the way
|
710
729
|
your particular permutation of Windows/Word interprets the OOXML.
|
711
730
|
|
731
|
+
|
712
732
|
### How to Work on a Problem
|
713
733
|
|
714
734
|
Caracal is essentially an exercise in reverse engineering OOXML output. When developing features, we
|
data/lib/caracal.rb
CHANGED
@@ -18,8 +18,17 @@ require 'caracal/document'
|
|
18
18
|
# Extra Setup
|
19
19
|
#------------------------------------------------
|
20
20
|
|
21
|
-
#
|
22
|
-
#
|
21
|
+
# Convenience method for finding root directory.
|
22
|
+
#
|
23
|
+
module Caracal
|
24
|
+
def self.root
|
25
|
+
File.dirname __dir__
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
# Add functions to table cell model. we do this here to
|
31
|
+
# avoid a circular require between Caracal::Core::Tables
|
23
32
|
# and Caracal::Core::Models::TableCellModel.
|
24
33
|
#
|
25
34
|
Caracal::Core::Models::TableCellModel.class_eval do
|
@@ -4,8 +4,8 @@ require 'caracal/errors'
|
|
4
4
|
|
5
5
|
module Caracal
|
6
6
|
module Core
|
7
|
-
|
8
|
-
# This module encapsulates all the functionality related to setting the
|
7
|
+
|
8
|
+
# This module encapsulates all the functionality related to setting the
|
9
9
|
# document's custom properties.
|
10
10
|
#
|
11
11
|
module CustomProperties
|
@@ -26,14 +26,16 @@ module Caracal
|
|
26
26
|
model
|
27
27
|
end
|
28
28
|
|
29
|
+
|
29
30
|
#============== GETTERS =============================
|
30
|
-
|
31
|
+
|
31
32
|
def custom_props
|
32
33
|
@custom_props ||= []
|
33
34
|
end
|
34
35
|
|
36
|
+
|
35
37
|
#============== REGISTRATION ========================
|
36
|
-
|
38
|
+
|
37
39
|
def register_property(model)
|
38
40
|
custom_props << model
|
39
41
|
model
|
@@ -42,6 +44,6 @@ module Caracal
|
|
42
44
|
end
|
43
45
|
end
|
44
46
|
end
|
45
|
-
|
47
|
+
|
46
48
|
end
|
47
|
-
end
|
49
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'caracal/core/models/iframe_model'
|
2
|
+
require 'caracal/errors'
|
3
|
+
|
4
|
+
|
5
|
+
module Caracal
|
6
|
+
module Core
|
7
|
+
|
8
|
+
# This module encapsulates all the functionality related to inserting
|
9
|
+
# word document snippets into the document.
|
10
|
+
#
|
11
|
+
module IFrames
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
|
15
|
+
#-------------------------------------------------------------
|
16
|
+
# Public Methods
|
17
|
+
#-------------------------------------------------------------
|
18
|
+
|
19
|
+
def iframe(options={}, &block)
|
20
|
+
model = Caracal::Core::Models::IFrameModel.new(options, &block)
|
21
|
+
if model.valid?
|
22
|
+
model.preprocess!
|
23
|
+
model.namespaces.each do |(prefix, href)|
|
24
|
+
namespace({ prefix: prefix, href: href })
|
25
|
+
end
|
26
|
+
model.ignorables.each do |prefix|
|
27
|
+
ignorable(prefix)
|
28
|
+
end
|
29
|
+
|
30
|
+
contents << model
|
31
|
+
else
|
32
|
+
raise Caracal::Errors::InvalidModelError, 'IFrameModel requires either the :url or :data argument.'
|
33
|
+
end
|
34
|
+
model
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
module Caracal
|
2
|
+
module Core
|
3
|
+
|
4
|
+
# This module encapsulates all the functionality related to registering and
|
5
|
+
# retrieving ignorable namespaces.
|
6
|
+
#
|
7
|
+
module Ignorables
|
8
|
+
def self.included(base)
|
9
|
+
base.class_eval do
|
10
|
+
|
11
|
+
#-------------------------------------------------------------
|
12
|
+
# Public Methods
|
13
|
+
#-------------------------------------------------------------
|
14
|
+
|
15
|
+
#============== ATTRIBUTES ==========================
|
16
|
+
|
17
|
+
def ignorable(prefix)
|
18
|
+
register_ignorable(prefix)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
#============== GETTERS =============================
|
23
|
+
|
24
|
+
def ignorables
|
25
|
+
@ignorables ||= []
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
#============== REGISTRATION ========================
|
30
|
+
|
31
|
+
def register_ignorable(prefix)
|
32
|
+
unless ignorables.include?(prefix)
|
33
|
+
ignorables << prefix
|
34
|
+
prefix
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def unregister_ignorable(prefix)
|
39
|
+
ignorables.delete(prefix)
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
require 'caracal/core/models/base_model'
|
2
|
+
|
3
|
+
module Caracal
|
4
|
+
module Core
|
5
|
+
module Models
|
6
|
+
|
7
|
+
# This class handles block options passed to the img method.
|
8
|
+
#
|
9
|
+
class IFrameModel < BaseModel
|
10
|
+
|
11
|
+
#--------------------------------------------------
|
12
|
+
# Configuration
|
13
|
+
#--------------------------------------------------
|
14
|
+
|
15
|
+
# accessors
|
16
|
+
attr_reader :iframe_url
|
17
|
+
attr_reader :iframe_data
|
18
|
+
attr_reader :iframe_ignorables
|
19
|
+
attr_reader :iframe_namespaces
|
20
|
+
attr_reader :iframe_relationships
|
21
|
+
|
22
|
+
# initialization
|
23
|
+
def initialize(options={}, &block)
|
24
|
+
super options, &block
|
25
|
+
end
|
26
|
+
|
27
|
+
|
28
|
+
#--------------------------------------------------
|
29
|
+
# Public Methods
|
30
|
+
#--------------------------------------------------
|
31
|
+
|
32
|
+
#=============== PROCESSING =======================
|
33
|
+
|
34
|
+
def preprocess!
|
35
|
+
::Zip::File.open(file) do |zip|
|
36
|
+
# locate relationships xml
|
37
|
+
entry = zip.glob('word/_rels/document.xml.rels').first
|
38
|
+
content = entry.get_input_stream.read
|
39
|
+
rel_xml = Nokogiri::XML(content)
|
40
|
+
|
41
|
+
# locate document xml
|
42
|
+
entry = zip.glob('word/document.xml').first
|
43
|
+
content = entry.get_input_stream.read
|
44
|
+
doc_xml = Nokogiri::XML(content)
|
45
|
+
|
46
|
+
# master nodesets
|
47
|
+
rel_nodes = rel_xml.children.first.children
|
48
|
+
doc_root = doc_xml.at_xpath('//w:document')
|
49
|
+
pic_nodes = doc_xml.xpath('//pic:pic', { pic: 'http://schemas.openxmlformats.org/drawingml/2006/picture' })
|
50
|
+
|
51
|
+
# namespaces
|
52
|
+
@iframe_namespaces = doc_root.namespaces
|
53
|
+
|
54
|
+
# ignorable namespaces
|
55
|
+
if a = doc_root.attributes['Ignorable']
|
56
|
+
@iframe_ignorables = a.value.split(/\s+/)
|
57
|
+
end
|
58
|
+
|
59
|
+
# relationships
|
60
|
+
media_map = rel_nodes.reduce({}) do |hash, node|
|
61
|
+
type = node.at_xpath('@Type').value
|
62
|
+
if type.slice(-5, 5) == 'image'
|
63
|
+
id = node.at_xpath('@Id').value
|
64
|
+
path = "word/#{ node.at_xpath('@Target').value }"
|
65
|
+
hash[id] = path
|
66
|
+
end
|
67
|
+
hash
|
68
|
+
end
|
69
|
+
@iframe_relationships = pic_nodes.reduce([]) do |array, node|
|
70
|
+
r_node = node.children[1].children[0]
|
71
|
+
r_id = r_node.attributes['embed'].value.to_s
|
72
|
+
r_media = media_map[r_id]
|
73
|
+
|
74
|
+
p_node = node.children[0].children[0]
|
75
|
+
p_id = p_node.attributes['id'].to_s.to_i
|
76
|
+
p_name = p_node.attributes['name'].to_s
|
77
|
+
p_data = zip.glob(r_media).first.get_input_stream.read
|
78
|
+
|
79
|
+
# register relationship
|
80
|
+
array << { id: r_id, type: 'image', target: p_name, data: p_data }
|
81
|
+
array
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
|
87
|
+
#=============== GETTERS ==========================
|
88
|
+
|
89
|
+
def file
|
90
|
+
@file ||= begin
|
91
|
+
if iframe_url.nil?
|
92
|
+
file = File.new('tmp_caracal', 'w+')
|
93
|
+
file.print iframe_data
|
94
|
+
file.rewind
|
95
|
+
else
|
96
|
+
file = open(iframe_url)
|
97
|
+
end
|
98
|
+
file
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def ignorables
|
103
|
+
@iframe_ignorables || []
|
104
|
+
end
|
105
|
+
|
106
|
+
def namespaces
|
107
|
+
@iframe_namespaces || {}
|
108
|
+
end
|
109
|
+
|
110
|
+
def relationships
|
111
|
+
@iframe_relationships || []
|
112
|
+
end
|
113
|
+
|
114
|
+
|
115
|
+
#=============== SETTERS ==========================
|
116
|
+
|
117
|
+
# strings
|
118
|
+
[:data, :url].each do |m|
|
119
|
+
define_method "#{ m }" do |value|
|
120
|
+
instance_variable_set("@iframe_#{ m }", value.to_s)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
#=============== VALIDATION =======================
|
126
|
+
|
127
|
+
def valid?
|
128
|
+
vals = option_keys.map { |m| send("iframe_#{ m }") }.compact
|
129
|
+
vals = vals.reject { |v| v.size == 0 }
|
130
|
+
vals.size > 0
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
#--------------------------------------------------
|
136
|
+
# Private Methods
|
137
|
+
#--------------------------------------------------
|
138
|
+
private
|
139
|
+
|
140
|
+
def option_keys
|
141
|
+
[:url, :data]
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
require 'caracal/core/models/base_model'
|
2
|
+
|
3
|
+
|
4
|
+
module Caracal
|
5
|
+
module Core
|
6
|
+
module Models
|
7
|
+
|
8
|
+
# This class encapsulates the logic needed to store and manipulate
|
9
|
+
# namespace data.
|
10
|
+
#
|
11
|
+
class NamespaceModel < BaseModel
|
12
|
+
|
13
|
+
#-------------------------------------------------------------
|
14
|
+
# Configuration
|
15
|
+
#-------------------------------------------------------------
|
16
|
+
|
17
|
+
# accessors
|
18
|
+
attr_reader :namespace_prefix
|
19
|
+
attr_reader :namespace_href
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
#-------------------------------------------------------------
|
24
|
+
# Public Instance Methods
|
25
|
+
#-------------------------------------------------------------
|
26
|
+
|
27
|
+
#=================== SETTERS =============================
|
28
|
+
|
29
|
+
# strings
|
30
|
+
[:href, :prefix].each do |m|
|
31
|
+
define_method "#{ m }" do |value|
|
32
|
+
instance_variable_set("@namespace_#{ m }", value.to_s)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
|
37
|
+
#=================== STATE ===============================
|
38
|
+
|
39
|
+
def matches?(str)
|
40
|
+
namespace_prefix == str.to_s
|
41
|
+
end
|
42
|
+
|
43
|
+
|
44
|
+
#=============== VALIDATION ===========================
|
45
|
+
|
46
|
+
def valid?
|
47
|
+
required = [:href, :prefix]
|
48
|
+
required.all? { |m| !send("namespace_#{ m }").nil? }
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
#-------------------------------------------------------------
|
53
|
+
# Private Instance Methods
|
54
|
+
#-------------------------------------------------------------
|
55
|
+
private
|
56
|
+
|
57
|
+
def option_keys
|
58
|
+
[:prefix, :href]
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'caracal/core/models/namespace_model'
|
2
|
+
require 'caracal/errors'
|
3
|
+
|
4
|
+
|
5
|
+
module Caracal
|
6
|
+
module Core
|
7
|
+
|
8
|
+
# This module encapsulates all the functionality related to registering and
|
9
|
+
# retrieving namespaces.
|
10
|
+
#
|
11
|
+
module Namespaces
|
12
|
+
def self.included(base)
|
13
|
+
base.class_eval do
|
14
|
+
|
15
|
+
#-------------------------------------------------------------
|
16
|
+
# Class Methods
|
17
|
+
#-------------------------------------------------------------
|
18
|
+
|
19
|
+
def self.default_namespaces
|
20
|
+
[
|
21
|
+
{ prefix: 'xmlns:mc', href: 'http://schemas.openxmlformats.org/markup-compatibility/2006' },
|
22
|
+
{ prefix: 'xmlns:o', href: 'urn:schemas-microsoft-com:office:office' },
|
23
|
+
{ prefix: 'xmlns:r', href: 'http://schemas.openxmlformats.org/officeDocument/2006/relationships' },
|
24
|
+
{ prefix: 'xmlns:m', href: 'http://schemas.openxmlformats.org/officeDocument/2006/math' },
|
25
|
+
{ prefix: 'xmlns:v', href: 'urn:schemas-microsoft-com:vml' },
|
26
|
+
{ prefix: 'xmlns:wp', href: 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing' },
|
27
|
+
{ prefix: 'xmlns:w10', href: 'urn:schemas-microsoft-com:office:word' },
|
28
|
+
{ prefix: 'xmlns:w', href: 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' },
|
29
|
+
{ prefix: 'xmlns:wne', href: 'http://schemas.microsoft.com/office/word/2006/wordml' },
|
30
|
+
{ prefix: 'xmlns:sl', href: 'http://schemas.openxmlformats.org/schemaLibrary/2006/main' },
|
31
|
+
{ prefix: 'xmlns:a', href: 'http://schemas.openxmlformats.org/drawingml/2006/main' },
|
32
|
+
{ prefix: 'xmlns:pic', href: 'http://schemas.openxmlformats.org/drawingml/2006/picture' },
|
33
|
+
{ prefix: 'xmlns:c', href: 'http://schemas.openxmlformats.org/drawingml/2006/chart' },
|
34
|
+
{ prefix: 'xmlns:lc', href: 'http://schemas.openxmlformats.org/drawingml/2006/lockedCanvas' },
|
35
|
+
{ prefix: 'xmlns:dgm', href: 'http://schemas.openxmlformats.org/drawingml/2006/diagram' }
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
#-------------------------------------------------------------
|
41
|
+
# Public Methods
|
42
|
+
#-------------------------------------------------------------
|
43
|
+
|
44
|
+
#============== ATTRIBUTES ==========================
|
45
|
+
|
46
|
+
def namespace(options={}, &block)
|
47
|
+
model = Caracal::Core::Models::NamespaceModel.new(options, &block)
|
48
|
+
if model.valid?
|
49
|
+
ns = register_namespace(model)
|
50
|
+
else
|
51
|
+
raise Caracal::Errors::InvalidModelError, 'namespace must specify the :prefix and :href attributes.'
|
52
|
+
end
|
53
|
+
ns
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
#============== GETTERS =============================
|
58
|
+
|
59
|
+
def namespaces
|
60
|
+
@namespaces ||= []
|
61
|
+
end
|
62
|
+
|
63
|
+
def find_namespace(prefix)
|
64
|
+
namespaces.find { |ns| ns.matches?(prefix) }
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
#============== REGISTRATION ========================
|
69
|
+
|
70
|
+
def register_namespace(model)
|
71
|
+
unless ns = find_namespace(model.namespace_prefix)
|
72
|
+
namespaces << model
|
73
|
+
ns = model
|
74
|
+
end
|
75
|
+
ns
|
76
|
+
end
|
77
|
+
|
78
|
+
def unregister_namespace(prefix)
|
79
|
+
if ns = find_namespace(prefix)
|
80
|
+
namespaces.delete(ns)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
89
|
+
end
|