├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── lib
├── openxml-package.rb
├── openxml-package
│ └── version.rb
└── openxml
│ ├── builder.rb
│ ├── builder
│ └── element.rb
│ ├── contains_properties.rb
│ ├── content_types_presets.rb
│ ├── element.rb
│ ├── errors.rb
│ ├── has_attributes.rb
│ ├── has_children.rb
│ ├── has_properties.rb
│ ├── package.rb
│ ├── part.rb
│ ├── parts.rb
│ ├── parts
│ ├── content_types.rb
│ ├── rels.rb
│ └── unparsed_part.rb
│ ├── properties.rb
│ ├── properties
│ ├── base_property.rb
│ ├── boolean_property.rb
│ ├── complex_property.rb
│ ├── container_property.rb
│ ├── integer_property.rb
│ ├── on_off_property.rb
│ ├── positive_integer_property.rb
│ ├── string_property.rb
│ ├── toggle_property.rb
│ ├── transparent_container_property.rb
│ └── value_property.rb
│ ├── relationship.rb
│ ├── render_when_empty.rb
│ ├── types.rb
│ └── unmet_requirement.rb
├── openxml-package.gemspec
├── test
├── contains_properties_test.rb
├── content_types_test.rb
├── element_test.rb
├── has_attributes_test.rb
├── has_children_test.rb
├── has_properties_test.rb
├── package_test.rb
├── part_test.rb
├── rels_test.rb
├── support
│ ├── external_hyperlink.docx
│ └── sample.docx
└── test_helper.rb
└── tmp
└── .keep
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: Tests
2 | on: [push]
3 |
4 | jobs:
5 | ruby:
6 | name: Ruby Tests
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | ruby-version: [ "3.0", "3.1", "3.2", "3.3" ]
11 |
12 | steps:
13 | - name: Checkout
14 | uses: actions/checkout@v2
15 | - name: Setup Ruby
16 | uses: ruby/setup-ruby@v1
17 | with:
18 | bundler-cache: true
19 | ruby-version: ${{ matrix.ruby-version }}
20 | - name: Run Tests
21 | run: bundle exec rake test
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .ruby-version
2 | *.gem
3 | *.rbc
4 | .bundle
5 | .config
6 | .yardoc
7 | Gemfile.lock
8 | InstalledFiles
9 | _yardoc
10 | coverage
11 | doc/
12 | lib/bundler/man
13 | pkg
14 | rdoc
15 | spec/reports
16 | test/tmp
17 | test/version_tmp
18 | /tmp/*
19 | !/tmp/.keep
20 | *.bundle
21 | *.so
22 | *.o
23 | *.a
24 | mkmf.log
25 | .DS_Store
26 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | ## 0.3.5 - Oct 7, 2024
2 |
3 | - Ruby 3.2 support
4 |
5 | ## 0.3.4 - Oct 16, 2019
6 |
7 | - Removed rubyzip lock
8 |
9 |
10 | ## 0.3.3 - Oct 2, 2018
11 |
12 | - Optimized setting attributes
13 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in openxml-package.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 Bob Lail
2 |
3 | MIT License
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining
6 | a copy of this software and associated documentation files (the
7 | "Software"), to deal in the Software without restriction, including
8 | without limitation the rights to use, copy, modify, merge, publish,
9 | distribute, sublicense, and/or sell copies of the Software, and to
10 | permit persons to whom the Software is furnished to do so, subject to
11 | the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be
14 | included in all copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # OpenXml::Package
2 |
3 | [](https://rubygems.org/gems/openxml-package)
4 | [](https://codeclimate.com/github/openxml/openxml-package)
5 | [](https://travis-ci.org/openxml/openxml-package)
6 |
7 | A Ruby implementation of [DocumentFormat.OpenXml.Packaging.OpenXmlPackage](https://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpackage_members.aspx) from Microsoft's Open XML SDK.
8 |
9 | The base class for [OpenXml::Docx::Package](https://github.com/openxml/openxml-docx/blob/master/lib/openxml/docx/package.rb), [OpenXml::Xlsx::Package](https://github.com/openxml/openxml-xlsx/blob/master/lib/openxml/xlsx/package.rb), and [OpenXml::Pptx::Package](https://github.com/openxml/openxml-pptx/blob/master/lib/openxml/xlsx/package.rb).
10 |
11 |
12 | ## Installation
13 |
14 | Add this line to your application's Gemfile:
15 |
16 | gem 'openxml-package'
17 |
18 | And then execute:
19 |
20 | $ bundle
21 |
22 | Or install it yourself as:
23 |
24 | $ gem install openxml-package
25 |
26 |
27 |
28 | ## Usage
29 |
30 | #### Writing
31 |
32 | You can assemble an Open XML Package in-memory and then write it to disk:
33 |
34 | ```ruby
35 | package = OpenXml::Package.new
36 | package.add_part "content/document.xml", OpenXml::Parts::UnparsedPart.new("")
37 | package.add_part "media/image.png", OpenXml::Parts::UnparsedPart.new(File.open(image_path, "rb", &:read))
38 | package.write_to "~/Desktop/output.zip"
39 | ```
40 |
41 |
42 | #### Reading
43 |
44 | You can read the contents of an Open XML Package:
45 |
46 | ```ruby
47 | OpenXml::Package.open("~/Desktop/output.zip") do |package|
48 | package.parts.keys # => ["content/document.xml", "media/image.png"]
49 | end
50 | ```
51 |
52 |
53 | #### Subclassing
54 |
55 | `OpenXml::Package` is intended to be the base class for libraries that implement Open XML formats for Microsoft Office products.
56 |
57 | For example, a very simple Microsoft Word document can be defined as follows:
58 |
59 | ```ruby
60 | require "openxml/package"
61 |
62 | module Rocx
63 | class Package < OpenXml::Package
64 | attr_reader :document,
65 | :doc_rels,
66 | :settings,
67 | :styles
68 |
69 | content_types do
70 | default "png", TYPE_PNG
71 | override "/word/styles.xml", TYPE_STYLES
72 | override "/word/settings.xml", TYPE_SETTINGS
73 | end
74 |
75 | def initialize
76 | super
77 |
78 | rels.add_relationship REL_DOCUMENT, "/word/document.xml"
79 | @doc_rels = OpenXml::Parts::Rels.new([
80 | { type: REL_STYLES, target: "/word/styles.xml"},
81 | { type: REL_SETTINGS, target: "/word/settings.xml"}
82 | ])
83 | @settings = Rocx::Parts::Settings.new
84 | @styles = Rocx::Parts::Styles.new
85 | @document = Rocx::Parts::Document.new
86 |
87 | add_part "word/_rels/document.xml.rels", doc_rels
88 | add_part "word/document.xml", document
89 | add_part "word/settings.xml", settings
90 | add_part "word/styles.xml", styles
91 | end
92 |
93 | end
94 | end
95 | ```
96 |
97 | This gem also defines two "Parts" that are commonly used in Open XML packages.
98 |
99 | ##### OpenXml::Parts::ContentTypes
100 |
101 | Is used to identify the ContentType of all of the files in the package. There are two ways of identifying content types:
102 |
103 | 1. **Default**: declares the default content type for a file with a given extension
104 | 2. **Override**: declares the content type for a specific file with the given path inside the package
105 |
106 | ##### OpenXml::Parts::Rels
107 |
108 | Is used to identify links within the package
109 |
110 |
111 |
112 | ## Contributing
113 |
114 | 1. Fork it ( https://github.com/openxml/openxml-package/fork )
115 | 2. Create your feature branch (`git checkout -b my-new-feature`)
116 | 3. Commit your changes (`git commit -am 'Add some feature'`)
117 | 4. Push to the branch (`git push origin my-new-feature`)
118 | 5. Create a new Pull Request
119 |
120 | #### Reference
121 |
122 | - [DocumentFormat.OpenXml.Packaging.OpenXmlPackage](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpackage_members(v=office.14).aspx)
123 | - [DocumentFormat.OpenXml.Packaging.OpenXmlPartContainer](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpartcontainer_members(v=office.14).aspx)
124 | - [DocumentFormat.OpenXml.Packaging.OpenXmlPart](http://msdn.microsoft.com/en-us/library/documentformat.openxml.packaging.openxmlpart_members(v=office.14).aspx)
125 | - [System.IO.Packaging.Package](http://msdn.microsoft.com/en-us/library/system.io.packaging.package.aspx)
126 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new(:test) do |t|
5 | t.libs << "lib"
6 | t.libs << "test"
7 | t.pattern = "test/**/*_test.rb"
8 | t.verbose = false
9 | end
10 |
--------------------------------------------------------------------------------
/lib/openxml-package.rb:
--------------------------------------------------------------------------------
1 | require "openxml/package"
2 |
--------------------------------------------------------------------------------
/lib/openxml-package/version.rb:
--------------------------------------------------------------------------------
1 | module OpenXmlPackage
2 | VERSION = "0.3.5"
3 | end
4 |
--------------------------------------------------------------------------------
/lib/openxml/builder.rb:
--------------------------------------------------------------------------------
1 | # Constructing a large XML document (5MB) with the Ox
2 | # gem is about 4x faster than with Nokogiri and about
3 | # 5x fater than with Builder.
4 | #
5 | # This class mimics the XML Builder DSL.
6 | require "ox"
7 | require "openxml/builder/element"
8 |
9 | module OpenXml
10 | class Builder
11 | attr_reader :parent
12 |
13 | def initialize(options={})
14 | @to_s_options = { with_xml: true }
15 |
16 | @ns = nil
17 | @document = Ox::Document.new(
18 | encoding: "UTF-8",
19 | version: "1.0",
20 | standalone: options[:standalone])
21 | @parent = @document
22 | yield self if block_given?
23 | end
24 |
25 | def to_s(args={})
26 | options = @to_s_options
27 |
28 | # Unless we would like to debug the files,
29 | # don't add whitespace during generation.
30 | options = options.merge(indent: -1) unless args[:debug]
31 |
32 | Ox.dump(@document, options).strip
33 | end
34 | alias :to_xml :to_s
35 |
36 | def [](ns)
37 | @ns = ns.to_sym if ns
38 | return self
39 | end
40 |
41 | def method_missing(tag_name, *args)
42 | new_element = OpenXml::Builder::Element.new(tag_name)
43 | attributes = extract_options!(args)
44 | attributes.each do |key, value|
45 | new_element[key] = value
46 | end
47 |
48 | new_element.namespace = @ns
49 | @ns = nil
50 |
51 | if block_given?
52 | begin
53 | was_current = @parent
54 | @parent = new_element
55 | yield self
56 | ensure
57 | @parent = was_current
58 | end
59 | elsif value = args.first
60 | new_element << value.to_s
61 | end
62 |
63 | @parent << new_element.__getobj__
64 | end
65 |
66 | private
67 |
68 | def extract_options!(args)
69 | if args.last.is_a?(Hash) && args.last.instance_of?(Hash)
70 | args.pop
71 | else
72 | {}
73 | end
74 | end
75 |
76 | end
77 | end
78 |
--------------------------------------------------------------------------------
/lib/openxml/builder/element.rb:
--------------------------------------------------------------------------------
1 | require "delegate"
2 |
3 | module OpenXml
4 | class Builder
5 | class Element < SimpleDelegator
6 | attr_reader :namespace
7 |
8 | def initialize(*args)
9 | super Ox::Element.new(*args)
10 | end
11 |
12 | def []=(attribute, value)
13 | namespace_def = attribute.downcase.to_s.match(/^xmlns(?:\:(?.*))?$/)
14 | namespaces << namespace_def[:prefix].to_sym if namespace_def && namespace_def[:prefix]
15 | super
16 | end
17 |
18 | def namespace=(ns)
19 | @namespace = ns.to_sym if ns
20 | tag = name.match(/^(?:\w*?\:)?(?\w*)$/i)[:tag]
21 | self.value = "#{namespace}:#{tag}" if namespace
22 | end
23 |
24 | def namespaces=(ns)
25 | @namespaces = Array(ns)
26 | end
27 |
28 | def namespaces
29 | @namespaces ||= []
30 | end
31 | alias :namespace_definitions :namespaces
32 |
33 | end
34 | end
35 | end
36 |
--------------------------------------------------------------------------------
/lib/openxml/contains_properties.rb:
--------------------------------------------------------------------------------
1 | require "openxml/has_properties"
2 |
3 | module OpenXml
4 | module ContainsProperties
5 |
6 | def self.included(base)
7 | base.class_eval do
8 | include HasProperties
9 | include InstanceMethods
10 | end
11 | end
12 |
13 | module InstanceMethods
14 |
15 | def property_xml(xml)
16 | ensure_required_choices
17 | props = active_properties
18 | return unless render_properties? props
19 | props.each { |prop| prop.to_xml(xml) }
20 | end
21 |
22 | def properties_attributes
23 | {}
24 | end
25 |
26 | end
27 |
28 | end
29 | end
30 |
--------------------------------------------------------------------------------
/lib/openxml/content_types_presets.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | class ContentTypesPresets
3 | attr_reader :defaults, :overrides
4 |
5 | def initialize
6 | @defaults, @overrides = {}, {}
7 | end
8 |
9 | def default(extension, content_type)
10 | defaults[extension] = content_type
11 | end
12 |
13 | def override(part_name, content_type)
14 | overrides[part_name] = content_type
15 | end
16 |
17 | end
18 | end
19 |
--------------------------------------------------------------------------------
/lib/openxml/element.rb:
--------------------------------------------------------------------------------
1 | require "openxml/has_attributes"
2 |
3 | module OpenXml
4 | class Element
5 | include HasAttributes
6 |
7 | class << self
8 | attr_reader :property_name
9 |
10 | def tag(*args)
11 | @tag = args.first if args.any?
12 | @tag ||= nil
13 | end
14 |
15 | def name(*args)
16 | @property_name = args.first if args.any?
17 | @property_name ||= nil
18 | end
19 |
20 | def namespace(*args)
21 | @namespace = args.first if args.any?
22 | @namespace ||= nil
23 | end
24 |
25 | end
26 |
27 | def tag
28 | self.class.tag || default_tag
29 | end
30 |
31 | def name
32 | self.class.property_name || default_name
33 | end
34 |
35 | def namespace
36 | ([self.class] + self.class.ancestors).select { |klass| klass.respond_to?(:namespace) }.map(&:namespace).find(&:itself)
37 | end
38 |
39 | def to_xml(xml)
40 | raise UndefinedNamespaceError, self.class unless namespace
41 |
42 | xml[namespace].public_send(tag, xml_attributes) do
43 | yield xml if block_given?
44 | end
45 | end
46 |
47 | private
48 |
49 | def default_tag
50 | (class_name[0, 1].downcase + class_name[1..-1]).to_sym
51 | end
52 |
53 | def default_name
54 | class_name.gsub(/(.)([A-Z])/, '\1_\2').downcase
55 | end
56 |
57 | def class_name
58 | self.class.to_s.split(/::/).last
59 | end
60 |
61 | end
62 |
63 | class UndefinedNamespaceError < RuntimeError
64 | def initialize(klass)
65 | super "#{klass} does not define its namespace"
66 | end
67 | end
68 | end
69 |
--------------------------------------------------------------------------------
/lib/openxml/errors.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Errors
3 |
4 | class MissingContentTypesPart < StandardError
5 | end
6 |
7 | end
8 | end
9 |
--------------------------------------------------------------------------------
/lib/openxml/has_attributes.rb:
--------------------------------------------------------------------------------
1 | require "openxml/unmet_requirement"
2 |
3 | module OpenXml
4 | module HasAttributes
5 |
6 | def self.included(base)
7 | base.extend(ClassMethods)
8 | end
9 |
10 | module ClassMethods
11 |
12 | RESERVED_NAMES = %w{ tag name namespace properties_tag }.freeze
13 |
14 | def attribute(name, expects: nil, one_of: nil, in_range: nil, displays_as: nil, namespace: nil, matches: nil, validation: nil, required: false, deprecated: false)
15 | raise ArgumentError if RESERVED_NAMES.member? name.to_s
16 |
17 | required_attributes.push(name) if required
18 |
19 | attr_reader name
20 |
21 | define_method "#{name}=" do |value|
22 | valid_in?(value, one_of) unless one_of.nil?
23 | send(expects, value) unless expects.nil?
24 | matches?(value, matches) unless matches.nil?
25 | in_range?(value, in_range) unless in_range.nil?
26 | validation.call(value) if validation.respond_to? :call
27 | instance_variable_set "@#{name}", value
28 | end
29 |
30 | camelized_name = name.to_s.gsub(/_([a-z])/i) { $1.upcase }.to_sym
31 | attributes[name] = [displays_as || camelized_name, namespace || attribute_namespace]
32 | end
33 |
34 | def attributes
35 | @attributes ||= {}.tap do |attrs|
36 | if superclass.respond_to?(:attributes)
37 | superclass.attributes.each do |key, value|
38 | attrs[key] = value.dup
39 | end
40 | end
41 | end
42 | end
43 |
44 | def required_attributes
45 | @required_attributes ||= [].tap do |attrs|
46 | attrs.push(*superclass.required_attributes) if superclass.respond_to?(:required_attributes)
47 | end
48 | end
49 |
50 | def with_namespace(namespace, &block)
51 | @attribute_namespace = namespace
52 | instance_eval(&block)
53 | end
54 |
55 | def attribute_namespace
56 | @attribute_namespace ||= nil
57 | end
58 |
59 | end
60 |
61 | def render?
62 | attributes.keys.map(&method(:send)).any?
63 | end
64 |
65 | def attributes
66 | self.class.attributes
67 | end
68 |
69 | def required_attributes
70 | self.class.required_attributes
71 | end
72 |
73 | private
74 |
75 | def xml_attributes
76 | ensure_required_attributes_set
77 | attributes.each_with_object({}) do |(name, options), attrs|
78 | display, namespace = options
79 | value = send(name)
80 | attr_name = "#{namespace}:#{display}"
81 | attr_name = display.to_s if namespace.nil?
82 | attrs[attr_name] = value unless value.nil?
83 | end
84 | end
85 |
86 | def ensure_required_attributes_set
87 | unset_attributes = required_attributes.reject do |attr|
88 | instance_variable_defined?("@#{attr}")
89 | end
90 | return if unset_attributes.empty?
91 |
92 | raise OpenXml::UnmetRequirementError, "Required attribute(s) #{unset_attributes.join(", ")} have not been set"
93 | end
94 |
95 | def boolean(value)
96 | return if [true, false].member? value
97 | raise ArgumentError, "Invalid #{name}: frame must be true or false"
98 | end
99 |
100 | def hex_color(value)
101 | return if value == :auto || value =~ /^[0-9A-F]{6}$/
102 | raise ArgumentError, "Invalid #{name}: must be :auto or a hex color, e.g. 4F1B8C"
103 | end
104 |
105 | def hex_digit(value)
106 | return if value =~ /^[0-9A-F]{2}$/
107 | raise ArgumentError, "Invalid #{name}: must be a two-digit hex number, e.g. BF"
108 | end
109 |
110 | def hex_digit_4(value)
111 | return if value =~ /^[0-9A-F]{4}$/
112 | raise ArgumentError, "Invalid #{name}: must be a four-digit hex number, e.g. BF12"
113 | end
114 |
115 | def long_hex_number(value)
116 | return if value =~ /^[0-9A-F]{8}$/
117 | raise ArgumentError, "Invalid #{name}: must be an eight-digit hex number, e.g., FFAC0013"
118 | end
119 |
120 | def hex_string(value)
121 | return if value =~ /^[0-9A-F]+$/
122 | raise ArgumentError, "Invalid #{name}: must be a string of hexadecimal numbers, e.g. FFA23C6E"
123 | end
124 |
125 | def integer(value)
126 | return if value.is_a?(Integer)
127 | raise ArgumentError, "Invalid #{name}: must be an integer"
128 | end
129 |
130 | def positive_integer(value)
131 | return if value.is_a?(Integer) && value >= 0
132 | raise ArgumentError, "Invalid #{name}: must be a positive integer"
133 | end
134 |
135 | def string(value)
136 | return if value.is_a?(String) && value.length > 0
137 | raise ArgumentError, "Invalid #{name}: must be a string"
138 | end
139 |
140 | def string_or_blank(value)
141 | return if value.is_a?(String)
142 | raise ArgumentError, "Invalid #{name}: must be a string, even if the string is empty"
143 | end
144 |
145 | def in_range?(value, range)
146 | return if range.include?(value.to_i)
147 | raise ArgumentError, "Invalid #{name}: must be a number between #{range.begin} and #{range.end}"
148 | end
149 |
150 | def percentage(value)
151 | return if value.is_a?(String) && value =~ /-?[0-9]+(\.[0-9]+)?%/ # Regex supplied in sec. 22.9.2.9 of Office Open XML docs
152 | raise ArgumentError, "Invalid #{name}: must be a percentage"
153 | end
154 |
155 | def on_or_off(value)
156 | valid_in? value, %i{ on off }
157 | end
158 |
159 | def valid_in?(value, list)
160 | return if list.member?(value)
161 | raise ArgumentError, "Invalid #{name}: must be one of #{list} (was #{value.inspect})"
162 | end
163 |
164 | def matches?(value, regexp)
165 | return if value.is_a?(String) && value =~ regexp
166 | raise ArgumentError, "Value does not match #{regexp}"
167 | end
168 |
169 | end
170 | end
171 |
--------------------------------------------------------------------------------
/lib/openxml/has_children.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module HasChildren
3 | attr_reader :children
4 |
5 | def initialize(*)
6 | super
7 | @children = []
8 | end
9 |
10 | def <<(child)
11 | children << child
12 | end
13 |
14 | def push(child)
15 | children.push(child)
16 | end
17 |
18 | def to_xml(xml)
19 | super(xml) do
20 | yield xml if block_given?
21 | children.each do |child|
22 | child.to_xml(xml)
23 | end
24 | end
25 | end
26 |
27 | def render?
28 | super || children.any?
29 | end
30 |
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/lib/openxml/has_properties.rb:
--------------------------------------------------------------------------------
1 | require "openxml/unmet_requirement"
2 |
3 | module OpenXml
4 | module HasProperties
5 |
6 | class ChoiceGroupUniqueError < RuntimeError; end
7 |
8 | def self.included(base)
9 | base.extend ClassMethods
10 | end
11 |
12 | module ClassMethods
13 |
14 | def properties_tag(*args)
15 | @properties_tag = args.first if args.any?
16 | @properties_tag ||= nil
17 | end
18 |
19 | def value_property(name, as: nil, klass: nil, required: false, default_value: nil)
20 | attr_reader name
21 |
22 | properties[name] = (as || name).to_s
23 | required_properties[name] = default_value if required
24 | classified_name = properties[name].split("_").map(&:capitalize).join
25 | class_name = klass.to_s unless klass.nil?
26 | class_name ||= (to_s.split("::")[0...-2] + ["Properties", classified_name]).join("::")
27 |
28 | (choice_groups[current_group] ||= []).push(name) unless current_group.nil?
29 |
30 | class_eval <<-CODE, __FILE__, __LINE__ + 1
31 | def #{name}=(value)
32 | group_index = #{@current_group.inspect}
33 | ensure_unique_in_group(:#{name}, group_index) unless group_index.nil?
34 | instance_variable_set "@#{name}", #{class_name}.new(value)
35 | end
36 | CODE
37 | end
38 |
39 | def property(name, as: nil, klass: nil, required: false)
40 | properties[name] = (as || name).to_s
41 | required_properties[name] = true if required
42 | classified_name = properties[name].split("_").map(&:capitalize).join
43 | class_name = klass.to_s unless klass.nil?
44 | class_name ||= (to_s.split("::")[0...-2] + ["Properties", classified_name]).join("::")
45 |
46 | (choice_groups[current_group] ||= []).push(name) unless current_group.nil?
47 |
48 | class_eval <<-CODE, __FILE__, __LINE__ + 1
49 | def #{name}(*args)
50 | unless instance_variable_defined?("@#{name}")
51 | group_index = #{@current_group.inspect}
52 | ensure_unique_in_group(:#{name}, group_index) unless group_index.nil?
53 | instance_variable_set "@#{name}", #{class_name}.new(*args)
54 | end
55 |
56 | instance_variable_get "@#{name}"
57 | end
58 | CODE
59 | end
60 |
61 | def property_choice(required: false)
62 | @current_group = choice_groups.length
63 | required_choices << @current_group if required
64 | yield
65 | @current_group = nil
66 | end
67 |
68 | def current_group
69 | @current_group ||= nil
70 | end
71 |
72 | def properties
73 | @properties ||= {}.tap do |props|
74 | props.merge!(superclass.properties) if superclass.respond_to?(:properties)
75 | end
76 | end
77 |
78 | def choice_groups
79 | @choice_groups ||= [].tap do |choices|
80 | choices.push(*superclass.choice_groups.map(&:dup)) if superclass.respond_to?(:choice_groups)
81 | end
82 | end
83 |
84 | def required_properties
85 | @required_properties ||= {}.tap do |props|
86 | props.merge!(superclass.required_properties) if superclass.respond_to?(:required_properties)
87 | end
88 | end
89 |
90 | def required_choices
91 | @required_choices ||= [].tap do |choices|
92 | choices.push(*superclass.required_choices) if superclass.respond_to?(:required_choices)
93 | end
94 | end
95 |
96 | def properties_attribute(name, **args)
97 | properties_element.attribute name, **args
98 | class_eval <<~RUBY, __FILE__, __LINE__ + 1
99 | def #{name}=(value)
100 | properties_element.#{name} = value
101 | end
102 |
103 | def #{name}
104 | properties_element.#{name}
105 | end
106 | RUBY
107 | end
108 |
109 | def properties_element
110 | this = self
111 | parent_klass = superclass.respond_to?(:properties_element) ? superclass.properties_element : OpenXml::Element
112 | @properties_element ||= Class.new(parent_klass) do
113 | tag :"#{this.properties_tag || this.default_properties_tag}"
114 | namespace :"#{this.namespace}"
115 | end
116 | end
117 |
118 | def default_properties_tag
119 | :"#{tag}Pr"
120 | end
121 |
122 | end
123 |
124 | def initialize(*_args)
125 | super
126 | build_required_properties
127 | end
128 |
129 | def properties_element
130 | @properties_element ||= self.class.properties_element.new
131 | end
132 |
133 | def properties_attributes
134 | properties_element.attributes
135 | end
136 |
137 | def render?
138 | return true unless defined?(super)
139 | render_properties? || super
140 | end
141 |
142 | def to_xml(xml)
143 | super(xml) do
144 | property_xml(xml)
145 | yield xml if block_given?
146 | end
147 | end
148 |
149 | def property_xml(xml)
150 | ensure_required_choices
151 | props = active_properties
152 | return unless render_properties? props
153 |
154 | properties_element.to_xml(xml) do
155 | props.each { |prop| prop.to_xml(xml) }
156 | end
157 | end
158 |
159 | def build_required_properties
160 | required_properties.each do |prop, default_value|
161 | public_send(:"#{prop}=", default_value) if respond_to? :"#{prop}="
162 | public_send(:"#{prop}")
163 | end
164 | end
165 |
166 | private
167 |
168 | def properties
169 | self.class.properties
170 | end
171 |
172 | def active_properties
173 | properties.keys.map { |property| instance_variable_get("@#{property}") }.compact
174 | end
175 |
176 | def render_properties?(properties=active_properties)
177 | properties.any?(&:render?) || properties_attributes.keys.any? do |key|
178 | properties_element.instance_variable_defined?("@#{key}")
179 | end
180 | end
181 |
182 | def properties_tag
183 | self.class.properties_tag || default_properties_tag
184 | end
185 |
186 | def default_properties_tag
187 | :"#{tag}Pr"
188 | end
189 |
190 | def choice_groups
191 | self.class.choice_groups
192 | end
193 |
194 | def required_properties
195 | self.class.required_properties
196 | end
197 |
198 | def required_choices
199 | self.class.required_choices
200 | end
201 |
202 | def ensure_unique_in_group(name, group_index)
203 | other_names = (choice_groups[group_index] - [name])
204 | return if other_names.none? { |other_name| instance_variable_defined?("@#{other_name}") }
205 | raise ChoiceGroupUniqueError, "Property #{name} cannot also be set with #{other_names.join(", ")}."
206 | end
207 |
208 | def unmet_choices
209 | required_choices.reject do |choice_index|
210 | choice_groups[choice_index].one? do |prop_name|
211 | instance_variable_defined?("@#{prop_name}")
212 | end
213 | end
214 | end
215 |
216 | def ensure_required_choices
217 | unmet_choice_groups = unmet_choices.map { |index| choice_groups[index].join(", ") }
218 | return if unmet_choice_groups.empty?
219 | raise OpenXml::UnmetRequirementError, "Required choice from among group(s) (#{unmet_choice_groups.join("), (")}) not made"
220 | end
221 |
222 | end
223 | end
224 |
--------------------------------------------------------------------------------
/lib/openxml/package.rb:
--------------------------------------------------------------------------------
1 | require "openxml-package/version"
2 | require "openxml/content_types_presets"
3 | require "openxml/errors"
4 | require "openxml/types"
5 | require "openxml/parts"
6 | require "zip"
7 |
8 | module OpenXml
9 | class Package
10 | attr_reader :parts, :content_types, :rels
11 |
12 |
13 |
14 | class << self
15 | def content_types_presets
16 | @content_types_presets ||= OpenXml::ContentTypesPresets.new
17 | end
18 |
19 | def content_types(&block)
20 | content_types_presets.instance_eval(&block)
21 | end
22 |
23 | def open(path)
24 | if block_given?
25 | Zip::File.open(path) do |zipfile|
26 | yield new(zipfile)
27 | end
28 | else
29 | new Zip::File.open(path)
30 | end
31 | end
32 |
33 | def from_stream(stream)
34 | stream = StringIO.new(stream) if stream.is_a?(String)
35 |
36 | # Hack: Zip::Entry.read_c_dir_entry initializes
37 | # a new Zip::Entry by calling `io.path`. Zip::Entry
38 | # uses this to open the original zipfile; but in
39 | # this case, the StringIO _is_ the original.
40 | def stream.path
41 | self
42 | end
43 |
44 | zipfile = ::Zip::File.new("", true, true)
45 | zipfile.read_from_stream(stream)
46 | new(zipfile)
47 | end
48 | end
49 |
50 |
51 |
52 | def initialize(zipfile=nil)
53 | @zipfile = zipfile
54 | @parts = {}
55 |
56 | set_defaults
57 | read_zipfile! if zipfile
58 | end
59 |
60 |
61 |
62 | def add_part(path, part)
63 | @parts[path] = part
64 | end
65 |
66 | def get_part(path)
67 | @parts.fetch(path)
68 | end
69 |
70 | def type_of(path)
71 | raise Errors::MissingContentTypesPart, "We haven't yet read [ContentTypes].xml; but are reading #{path.inspect}" unless content_types
72 | content_types.of(path)
73 | end
74 |
75 |
76 |
77 | def close
78 | zipfile.close if zipfile
79 | end
80 |
81 | def write_to(path)
82 | File.open(path, "wb") do |file|
83 | file.write to_stream.string
84 | end
85 | end
86 | alias :save :write_to
87 |
88 | def to_stream
89 | Zip::OutputStream.write_buffer do |io|
90 | parts.each do |path, part|
91 | io.put_next_entry path
92 | io.write part.content
93 | end
94 | end
95 | end
96 |
97 | private
98 |
99 | attr_reader :zipfile
100 |
101 | def read_zipfile!
102 | zipfile.entries.each do |entry|
103 | next if entry.directory?
104 | path, part = entry.name, Parts::UnparsedPart.new(entry)
105 | add_part path, case path
106 | when "[Content_Types].xml" then @content_types = Parts::ContentTypes.parse(part.content)
107 | when "_rels/.rels" then @rels = Parts::Rels.parse(part.content)
108 | else part_for(path, type_of(path), part)
109 | end
110 | end
111 | end
112 |
113 | protected
114 |
115 | def set_defaults
116 | presets = self.class.content_types_presets
117 | @content_types = Parts::ContentTypes.new(presets.defaults.dup, presets.overrides.dup)
118 | add_part "[Content_Types].xml", content_types
119 |
120 | @rels = Parts::Rels.new
121 | add_part "_rels/.rels", rels
122 | end
123 |
124 | def part_for(path, content_type, part)
125 | case content_type
126 | when Types::RELATIONSHIPS then Parts::Rels.parse(part.content)
127 | else part
128 | end
129 | end
130 |
131 | end
132 | end
133 |
--------------------------------------------------------------------------------
/lib/openxml/part.rb:
--------------------------------------------------------------------------------
1 | require "openxml/builder"
2 |
3 | module OpenXml
4 | class Part
5 |
6 | def build_xml(options={})
7 | OpenXml::Builder.new(options) { |xml| yield xml }
8 | end
9 |
10 | def build_standalone_xml(&block)
11 | build_xml(standalone: :yes, &block)
12 | end
13 |
14 | def read
15 | to_xml.to_s
16 | end
17 | alias :content :read
18 |
19 | def to_xml
20 | raise NotImplementedError, "#{self.class} needs to implements to_xml"
21 | end
22 |
23 | end
24 | end
25 |
--------------------------------------------------------------------------------
/lib/openxml/parts.rb:
--------------------------------------------------------------------------------
1 | require "openxml/part"
2 |
3 | module OpenXml
4 | module Parts
5 | end
6 | end
7 |
8 | Dir.glob("#{File.join(File.dirname(__FILE__), "parts", "*.rb")}").each do |file|
9 | require file
10 | end
11 |
--------------------------------------------------------------------------------
/lib/openxml/parts/content_types.rb:
--------------------------------------------------------------------------------
1 | require "nokogiri"
2 |
3 | module OpenXml
4 | module Parts
5 | class ContentTypes < OpenXml::Part
6 | attr_reader :defaults, :overrides
7 |
8 | REQUIRED_DEFAULTS = {
9 | "xml" => Types::XML,
10 | "rels" => Types::RELATIONSHIPS
11 | }.freeze
12 |
13 | def self.parse(xml)
14 | document = Nokogiri::XML(xml)
15 | self.new.tap do |part|
16 | document.css("Default").each do |default|
17 | part.add_default default["Extension"], default["ContentType"]
18 | end
19 | document.css("Override").each do |default|
20 | part.add_override default["PartName"], default["ContentType"]
21 | end
22 | end
23 | end
24 |
25 | def initialize(defaults={}, overrides={})
26 | @defaults = REQUIRED_DEFAULTS.merge(defaults)
27 | @overrides = overrides
28 | end
29 |
30 | def add_default(extension, content_type)
31 | defaults[extension] = content_type
32 | end
33 |
34 | def add_override(part_name, content_type)
35 | overrides[part_name] = content_type
36 | end
37 |
38 | def of(path)
39 | overrides.fetch(path, defaults[File.extname(path)[1..-1]])
40 | end
41 |
42 | def to_xml
43 | build_standalone_xml do |xml|
44 | xml.Types(xmlns: "http://schemas.openxmlformats.org/package/2006/content-types") {
45 | defaults.each { |extension, content_type| xml.Default("Extension" => extension, "ContentType" => content_type) }
46 | overrides.each { |part_name, content_type| xml.Override("PartName" => part_name, "ContentType" => content_type) }
47 | }
48 | end
49 | end
50 |
51 | end
52 | end
53 | end
54 |
--------------------------------------------------------------------------------
/lib/openxml/parts/rels.rb:
--------------------------------------------------------------------------------
1 | require "securerandom"
2 | require "openxml/relationship"
3 | require "nokogiri"
4 |
5 | module OpenXml
6 | module Parts
7 | class Rels < OpenXml::Part
8 | include Enumerable
9 |
10 | def self.parse(xml)
11 | document = Nokogiri::XML(xml)
12 | self.new.tap do |part|
13 | document.css("Relationship").each do |rel|
14 | part.add_relationship rel["Type"], rel["Target"], rel["Id"], rel["TargetMode"]
15 | end
16 | end
17 | end
18 |
19 | def initialize(defaults=[])
20 | @relationships = []
21 | Array(defaults).each do |default|
22 | add_relationship(*default.values_at("Type", "Target", "Id", "TargetMode"))
23 | end
24 | end
25 |
26 | def add_relationship(type, target, id=next_id, target_mode=nil)
27 | OpenXml::Elements::Relationship.new(type, target, id, target_mode).tap do |relationship|
28 | relationships.push relationship
29 | end
30 | end
31 |
32 | def push(relationship)
33 | relationships.push relationship
34 | end
35 |
36 | def each(&block)
37 | relationships.each(&block)
38 | end
39 |
40 | def empty?
41 | relationships.empty?
42 | end
43 |
44 | def to_xml
45 | build_standalone_xml do |xml|
46 | xml.Relationships(xmlns: "http://schemas.openxmlformats.org/package/2006/relationships") do
47 | relationships.each do |rel|
48 | attributes = { "Id" => rel.id, "Type" => rel.type, "Target" => rel.target }
49 | attributes["TargetMode"] = rel.target_mode if rel.target_mode
50 | xml.Relationship(attributes)
51 | end
52 | end
53 | end
54 | end
55 |
56 | private
57 | attr_reader :relationships
58 |
59 | def next_id
60 | @current_id = (@current_id || 0) + 1
61 | "rId#{@current_id}"
62 | end
63 |
64 | end
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/lib/openxml/parts/unparsed_part.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Parts
3 | class UnparsedPart
4 |
5 | def initialize(content)
6 | @content = content
7 | end
8 |
9 | def content
10 | @content = @content.get_input_stream.read if promise?
11 | @content
12 | end
13 |
14 | def promise?
15 | @content.respond_to? :get_input_stream
16 | end
17 |
18 | end
19 | end
20 | end
21 |
--------------------------------------------------------------------------------
/lib/openxml/properties.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | end
4 | end
5 |
6 | require "openxml/properties/base_property"
7 | require "openxml/properties/complex_property"
8 | require "openxml/properties/value_property"
9 |
10 | require "openxml/properties/boolean_property"
11 | require "openxml/properties/integer_property"
12 | require "openxml/properties/positive_integer_property"
13 | require "openxml/properties/string_property"
14 | require "openxml/properties/on_off_property"
15 | require "openxml/properties/toggle_property"
16 |
17 | require "openxml/properties/container_property"
18 | require "openxml/properties/transparent_container_property"
19 |
20 | Dir.glob(File.join(File.dirname(__FILE__), "properties", "*.rb").to_s).each do |file|
21 | require file
22 | end
23 |
--------------------------------------------------------------------------------
/lib/openxml/properties/base_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class BaseProperty
4 | attr_reader :value
5 |
6 | class << self
7 | attr_reader :property_name
8 | attr_reader :allowed_tags
9 |
10 | def tag_is_one_of(tags)
11 | attr_accessor :tag
12 | @allowed_tags = tags
13 | end
14 |
15 | def tag(*args)
16 | @tag = args.first if args.any?
17 | @tag ||= nil
18 | end
19 |
20 | def name(*args)
21 | @property_name = args.first if args.any?
22 | @property_name ||= nil
23 | end
24 |
25 | def namespace(*args)
26 | @namespace = args.first if args.any?
27 | @namespace ||= nil
28 | end
29 |
30 | end
31 |
32 | def initialize(tag=nil, *_args)
33 | return unless self.class.allowed_tags
34 | validate_tag tag
35 | @tag = tag
36 | end
37 |
38 | def validate_tag(tag)
39 | return if self.class.allowed_tags.include?(tag)
40 | allowed = self.class.allowed_tags.join(", ")
41 | raise ArgumentError, "Invalid tag name for #{name}: #{tag.inspect}. It should be one of #{allowed}."
42 | end
43 |
44 | def render?
45 | !value.nil?
46 | end
47 |
48 | def name
49 | self.class.property_name || default_name
50 | end
51 |
52 | def default_name
53 | class_name.gsub(/(.)([A-Z])/, '\1_\2').downcase
54 | end
55 |
56 | def tag
57 | self.class.tag || default_tag
58 | end
59 |
60 | def default_tag
61 | (class_name[0, 1].downcase + class_name[1..-1]).to_sym
62 | end
63 |
64 | def namespace
65 | self.class.namespace
66 | end
67 |
68 | private
69 |
70 | def apply_namespace(xml)
71 | namespace.nil? ? xml : xml[namespace]
72 | end
73 |
74 | def class_name
75 | self.class.to_s.split(/::/).last
76 | end
77 |
78 | end
79 | end
80 | end
81 |
--------------------------------------------------------------------------------
/lib/openxml/properties/boolean_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class BooleanProperty < ValueProperty
4 |
5 | def ok_values
6 | [nil, true, false]
7 | end
8 |
9 | def to_xml(xml)
10 | super if value
11 | end
12 |
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/openxml/properties/complex_property.rb:
--------------------------------------------------------------------------------
1 | require "openxml/has_attributes"
2 |
3 | module OpenXml
4 | module Properties
5 | class ComplexProperty < BaseProperty
6 | include HasAttributes
7 |
8 | def to_xml(xml)
9 | return unless render?
10 | apply_namespace(xml).public_send(tag, xml_attributes) do
11 | yield xml if block_given?
12 | end
13 | end
14 |
15 | def render?
16 | !xml_attributes.empty?
17 | end
18 |
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/openxml/properties/container_property.rb:
--------------------------------------------------------------------------------
1 | require "openxml/has_attributes"
2 |
3 | module OpenXml
4 | module Properties
5 | class ContainerProperty < BaseProperty
6 | include Enumerable
7 | include HasAttributes
8 |
9 | class << self
10 |
11 | def child_class(*args)
12 | unless args.empty?
13 | @child_classes = args.map { |arg|
14 | prop_name = arg.to_s.split(/_/).map(&:capitalize).join # LazyCamelCase
15 | const_name = (self.to_s.split(/::/)[0...-1] + [prop_name]).join("::")
16 | Object.const_get const_name
17 | }
18 | end
19 |
20 | @child_classes
21 | end
22 | alias child_classes child_class
23 |
24 | end
25 |
26 | def initialize
27 | @children = []
28 | end
29 |
30 | def <<(child)
31 | raise ArgumentError, invalid_child_message unless valid_child?(child)
32 | children << child
33 | end
34 |
35 | def each(*args, &block)
36 | children.each(*args, &block)
37 | end
38 |
39 | def render?
40 | !children.length.zero?
41 | end
42 |
43 | def to_xml(xml)
44 | return unless render?
45 |
46 | apply_namespace(xml).public_send(tag, xml_attributes) {
47 | each { |child| child.to_xml(xml) }
48 | }
49 | end
50 |
51 | private
52 |
53 | attr_reader :children
54 |
55 | def invalid_child_message
56 | class_name = self.class.to_s.split(/::/).last
57 | "#{class_name} must be instances of one of the following: #{child_classes}"
58 | end
59 |
60 | def valid_child?(child)
61 | child_classes.any? { |child_class| child.is_a?(child_class) }
62 | end
63 |
64 | def child_classes
65 | self.class.child_classes
66 | end
67 |
68 | end
69 | end
70 | end
71 |
--------------------------------------------------------------------------------
/lib/openxml/properties/integer_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class IntegerProperty < ValueProperty
4 |
5 | def valid?
6 | value.is_a? Integer
7 | end
8 |
9 | def invalid_message
10 | "Invalid #{name}: must be an integer"
11 | end
12 |
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/openxml/properties/on_off_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class OnOffProperty < ValueProperty
4 |
5 | def ok_values
6 | [true, false, :on, :off] # :on and :off are from the Transitional Spec
7 | end
8 |
9 | def to_xml(xml)
10 | if value == true
11 | apply_namespace(xml).public_send(tag) do
12 | yield xml if block_given?
13 | end
14 | else
15 | super
16 | end
17 | end
18 |
19 | end
20 | end
21 | end
22 |
--------------------------------------------------------------------------------
/lib/openxml/properties/positive_integer_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class PositiveIntegerProperty < IntegerProperty
4 |
5 | def valid?
6 | super && value >= 0
7 | end
8 |
9 | def invalid_message
10 | "Invalid #{name}: must be a positive integer"
11 | end
12 |
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/openxml/properties/string_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class StringProperty < ValueProperty
4 |
5 | def valid?
6 | value.is_a?(String) && !value.length.zero?
7 | end
8 |
9 | def invalid_message
10 | "Invalid value for #{name}; string expected (provided: #{value.inspect})"
11 | end
12 |
13 | end
14 | end
15 | end
16 |
--------------------------------------------------------------------------------
/lib/openxml/properties/toggle_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class ToggleProperty < OnOffProperty
4 | # Toggle properties are no different in representation than on/off properties;
5 | # rather, the difference is in how they compose with one another (cf.
6 | # Section 17.7.3). It's helpful, then, to retain the concept, but entirely
7 | # unnecessary to duplicate implementation.
8 | # cf. Section A.6.9 of the spec, and Section A.7.9 of the transitional spec.
9 | end
10 | end
11 | end
12 |
--------------------------------------------------------------------------------
/lib/openxml/properties/transparent_container_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class TransparentContainerProperty < ContainerProperty
4 |
5 | def to_xml(xml)
6 | return unless render?
7 | each { |child| child.to_xml(xml) }
8 | end
9 |
10 | end
11 | end
12 | end
13 |
--------------------------------------------------------------------------------
/lib/openxml/properties/value_property.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Properties
3 | class ValueProperty < BaseProperty
4 | attr_reader :value
5 |
6 | def initialize(value)
7 | @value = value
8 | raise ArgumentError, invalid_message unless valid?
9 | end
10 |
11 | def valid?
12 | ok_values.member? value
13 | end
14 |
15 | def invalid_message
16 | "#{value.inspect} is an invalid value for #{name}; acceptable: #{ok_values.join(", ")}"
17 | end
18 |
19 | def render?
20 | !value.nil?
21 | end
22 |
23 | def to_xml(xml)
24 | apply_namespace(xml).public_send(tag, :"#{value_attribute}" => value) do
25 | yield xml if block_given?
26 | end
27 | end
28 |
29 | private
30 |
31 | def value_attribute
32 | namespace.nil? ? "val" : "#{namespace}:val"
33 | end
34 |
35 | end
36 | end
37 | end
38 |
--------------------------------------------------------------------------------
/lib/openxml/relationship.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Elements
3 | class Relationship < Struct.new(:type, :target, :id, :target_mode)
4 | def initialize(type, target, id=nil, target_mode=nil)
5 | super type, target, id || "R#{SecureRandom.hex}", target_mode
6 | end
7 | end
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/openxml/render_when_empty.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module RenderWhenEmpty
3 |
4 | def render?
5 | true
6 | end
7 |
8 | end
9 | end
10 |
--------------------------------------------------------------------------------
/lib/openxml/types.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | module Types
3 | XML = "application/xml".freeze
4 | RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml".freeze
5 | end
6 | end
7 |
--------------------------------------------------------------------------------
/lib/openxml/unmet_requirement.rb:
--------------------------------------------------------------------------------
1 | module OpenXml
2 | class UnmetRequirementError < RuntimeError; end
3 | end
4 |
--------------------------------------------------------------------------------
/openxml-package.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path("../lib", __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require "openxml-package/version"
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "openxml-package"
8 | spec.version = OpenXmlPackage::VERSION
9 | spec.authors = ["Bob Lail"]
10 | spec.email = ["bob.lailfamily@gmail.com"]
11 |
12 | spec.summary = %q{A Ruby implementation of OpenXmlPackage from Microsoft's Open XML SDK}
13 | spec.description = %q{A Ruby implementation of OpenXmlPackage from Microsoft's Open XML SDK}
14 | spec.homepage = "https://github.com/openxml/openxml-package"
15 | spec.license = "MIT"
16 |
17 | spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
18 | spec.bindir = "exe"
19 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
20 | spec.require_paths = ["lib"]
21 |
22 | spec.add_dependency "rubyzip", "~> 2.3"
23 | spec.add_dependency "nokogiri"
24 | spec.add_dependency "ox"
25 |
26 | spec.add_development_dependency "bundler"
27 | spec.add_development_dependency "rake"
28 | spec.add_development_dependency "minitest"
29 | spec.add_development_dependency "minitest-reporters"
30 | spec.add_development_dependency "minitest-reporters-turn_reporter"
31 | spec.add_development_dependency "pry"
32 | spec.add_development_dependency "rr"
33 | spec.add_development_dependency "simplecov"
34 | spec.add_development_dependency "shoulda-context"
35 |
36 | end
37 |
--------------------------------------------------------------------------------
/test/contains_properties_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "openxml/element"
3 | require "openxml/properties"
4 | require "openxml/contains_properties"
5 |
6 | class ContainsPropertiesTest < Minitest::Test
7 | should "allow properties to be rendered as direct children" do
8 | element = Class.new(OpenXml::Element) do
9 | include OpenXml::ContainsProperties
10 | tag :bodyPr
11 | namespace :a
12 |
13 | value_property :string_property
14 | end.new
15 | element.string_property = "A Value"
16 |
17 | rendered_xml = build(element)
18 | refute_match(/a:bodyPrPr/, rendered_xml)
19 | assert_match(/a:stringProperty val="A Value"/, rendered_xml)
20 | end
21 |
22 | private
23 |
24 | def build(element)
25 | builder = Nokogiri::XML::Builder.new
26 | builder.document("xmlns:a" => "http://microsoft.com") do |xml|
27 | element.to_xml(xml)
28 | end
29 | builder.to_xml
30 | end
31 |
32 | end
33 |
--------------------------------------------------------------------------------
/test/content_types_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ContentTypesTest < Minitest::Test
4 | attr_reader :content_types
5 |
6 | WORDPROCESSING_DOCUMENT_TYPE = "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"
7 |
8 | def setup
9 | @content_types = OpenXml::Parts::ContentTypes.new(
10 | {"xml" => OpenXml::Types::XML, "rels" => OpenXml::Types::RELATIONSHIPS},
11 | {"word/document.xml" => WORDPROCESSING_DOCUMENT_TYPE})
12 | end
13 |
14 |
15 | context "Given a path without an override" do
16 | should "identify the content type from its extension" do
17 | assert_equal OpenXml::Types::XML, content_types.of("content/some.xml")
18 | end
19 | end
20 |
21 | context "Given a path with an override" do
22 | should "identify the content type from its path" do
23 | assert_equal WORDPROCESSING_DOCUMENT_TYPE, content_types.of("word/document.xml")
24 | end
25 | end
26 |
27 | context "Given a path with an unrecognized extension" do
28 | should "be nil" do
29 | assert_nil content_types.of("img/screenshot.jpg")
30 | end
31 | end
32 |
33 |
34 | end
35 |
--------------------------------------------------------------------------------
/test/element_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class ElementTest < Minitest::Test
4 | attr_reader :element
5 |
6 | def setup
7 | @element = OpenXml::Builder::Element.new("tag")
8 | end
9 |
10 | context "The element" do
11 | should "correctly assign attributes" do
12 | element[:attribute] = "value"
13 | element["namespaced:attribute"] = "value"
14 | dump = Ox.dump(element.__getobj__)
15 | assert_equal "\n\n", dump
16 | end
17 |
18 | should "parse out available namespace prefixes from namespace definition attributes" do
19 | nsdef_prefix = "xmlns:namespace"
20 | nsdef_uri = "http://schema.somenamespace.org/"
21 | element[nsdef_prefix] = nsdef_uri
22 | assert_equal [:namespace], element.namespaces
23 | end
24 |
25 | should "only parse namespace definition attributes with a prefix" do
26 | element["xmlns"] = "http://schema.somenamespace.org/"
27 | assert element.namespaces.empty?
28 | end
29 |
30 | should "correctly use a namespace prefix" do
31 | element.namespace = "namespace"
32 | dump = Ox.dump(element.__getobj__)
33 | assert_equal "\n\n", dump
34 | end
35 | end
36 |
37 |
38 | end
39 |
--------------------------------------------------------------------------------
/test/has_attributes_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "openxml/has_attributes"
3 |
4 | class HasAttributesTest < Minitest::Test
5 | context "ClassMethod.attribute" do
6 | should "define a reader method for the attribute" do
7 | element = class_with_attribute :an_attribute
8 | assert element.new.respond_to? :an_attribute
9 | end
10 |
11 | should "define a writer method for the attribute" do
12 | element = class_with_attribute :an_attribute
13 | assert element.new.respond_to? :an_attribute=
14 | end
15 |
16 | should "be named by its camelCased version if no displays_as given" do
17 | element = class_with_attribute :an_attribute
18 | assert_equal :anAttribute, element.attributes[:an_attribute][0]
19 | end
20 |
21 | should "be named by its displays_as property if given" do
22 | element = class_with_attribute :an_attribute, displays_as: :theAttr
23 | assert_equal :theAttr, element.attributes[:an_attribute][0]
24 | end
25 |
26 | context "in a with_namespace block" do
27 | should "use the provided namespace" do
28 | element = Class.new do
29 | include OpenXml::HasAttributes
30 | with_namespace :a_namespace do
31 | attribute :an_attribute
32 | end
33 | end
34 | assert_equal :a_namespace, element.attributes[:an_attribute][1]
35 | end
36 | end
37 | end
38 |
39 | context "validations" do
40 | should "raise an ArgumentError if the name is disallowed" do
41 | assert_raises ArgumentError do
42 | class_with_attribute :name
43 | end
44 | end
45 |
46 | should "raise an ArgumentError if the value does not match the expects parameter" do
47 | assert_raises ArgumentError do
48 | element = class_with_attribute :an_attribute, expects: :integer
49 | element.new.an_attribute = "A String"
50 | end
51 | end
52 |
53 | should "raise an ArgumentError if the value is not one of the enumerated values" do
54 | assert_raises ArgumentError do
55 | element = class_with_attribute :an_attribute, one_of: %i{ left right }
56 | element.new.an_attribute = :start
57 | end
58 | end
59 |
60 | should "raise an ArgumentError if the value is not in the given range" do
61 | assert_raises ArgumentError do
62 | element = class_with_attribute :an_attribute, in_range: 1...9000
63 | element.new.an_attribute = 9001
64 | end
65 | end
66 |
67 | should "raise an ArgumentError if the value fails the regex match" do
68 | assert_raises ArgumentError do
69 | element = class_with_attribute :an_attribute, matches: /^[a-f0-9]+$/i
70 | element.new.an_attribute = "AGGPPTAAGGTT"
71 | end
72 | end
73 | end
74 |
75 | context "a subclass of a class with attributes" do
76 | should "inherit its superclass' attributes" do
77 | element = Class.new(class_with_attribute(:an_attribute)).new
78 | assert_equal %i{ an_attribute }, element.attributes.keys
79 | end
80 |
81 | should "not modify the attributes of its superclass" do
82 | parent = class_with_attribute(:an_attribute)
83 | element = Class.new(parent) do
84 | attribute :another_attribute
85 | end
86 |
87 | assert_equal %i{ an_attribute another_attribute }, element.new.attributes.keys
88 | assert_equal %i{ an_attribute }, parent.new.attributes.keys
89 | end
90 |
91 | should "inherit the accessors of its superclass" do
92 | element = Class.new(class_with_attribute(:an_attribute)).new
93 |
94 | assert element.respond_to? :an_attribute, "Should respond to read accessor"
95 | assert element.respond_to? :an_attribute=, "Should respond to write accessor"
96 | end
97 |
98 | should "inherit the required attributes of its superclass" do
99 | element = Class.new(class_with_attribute(:an_attribute, required: true)).new
100 |
101 | assert_equal %i{ an_attribute }, element.required_attributes
102 | end
103 |
104 | should "not modify the required attributes of its superclass" do
105 | parent = class_with_attribute(:an_attribute, required: true)
106 | element = Class.new(parent) do
107 | attribute :another_attribute, required: true
108 | end
109 |
110 | assert_equal %i{ an_attribute another_attribute }, element.new.required_attributes
111 | assert_equal %i{ an_attribute }, parent.new.required_attributes
112 | end
113 | end
114 |
115 | context "a class with a required attribute" do
116 | should "raise an exception if the attribute is not set when generating xml" do
117 | element = class_with_attribute(:an_attribute, required: true).new
118 | assert_raises OpenXml::UnmetRequirementError do
119 | element.send(:xml_attributes)
120 | end
121 | end
122 | end
123 |
124 | private
125 |
126 | def class_with_attribute(attr_name, **args)
127 | Class.new do
128 | include OpenXml::HasAttributes
129 | attribute attr_name, **args
130 |
131 | def name
132 | "element"
133 | end
134 | end
135 | end
136 |
137 | end
138 |
--------------------------------------------------------------------------------
/test/has_children_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "openxml/has_children"
3 |
4 | class HasChildrenTest < Minitest::Test
5 | attr_reader :element
6 |
7 | context "with HasChildren included" do
8 | setup do
9 | base_class = Class.new do
10 | def to_xml(xml)
11 | yield xml if block_given?
12 | xml
13 | end
14 |
15 | def render?
16 | false
17 | end
18 | end
19 |
20 | @element = Class.new(base_class) do
21 | include OpenXml::HasChildren
22 | end.new
23 | end
24 |
25 | should "append children using the shovel operator" do
26 | assert_equal 0, element.children.count
27 | element << :child_placeholder
28 | assert_equal 1, element.children.count
29 | end
30 |
31 | should "enable rendering if there are any children" do
32 | refute element.render?
33 | element.push :child_placeholder
34 | assert element.render?
35 | end
36 |
37 | should "call to_xml on all of its children" do
38 | child = Minitest::Mock.new
39 | child.expect :to_xml, "xml", %w{ xml }
40 | element << child
41 | element.to_xml "xml"
42 | child.verify
43 | end
44 | end
45 |
46 | end
47 |
--------------------------------------------------------------------------------
/test/has_properties_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "openxml/element"
3 | require "openxml/properties"
4 | require "openxml/has_properties"
5 |
6 | class HasPropertiesTest < Minitest::Test
7 | attr_reader :element
8 |
9 | context "When HasProperties is included," do
10 | context ".value_property" do
11 | setup do
12 | @element = Class.new do
13 | include OpenXml::HasProperties
14 |
15 | value_property :string_property
16 | end
17 | end
18 |
19 | should "generate accessor methods for the property" do
20 | an_element = element.new
21 | assert an_element.respond_to? :string_property
22 | assert an_element.respond_to? :string_property=
23 | end
24 |
25 | should "instantiate the property on assignment of a value" do
26 | an_element = element.new
27 | an_element.string_property = "A Value"
28 | an_element.string_property.is_a?(OpenXml::Properties::StringProperty)
29 | end
30 | end
31 |
32 | context ".property" do
33 | setup do
34 | @element = Class.new do
35 | include OpenXml::HasProperties
36 |
37 | property :complex_property
38 | property :polymorphic_property
39 | end
40 | end
41 |
42 | should "generate a reader method only for the property" do
43 | an_element = element.new
44 | assert an_element.respond_to? :complex_property
45 | end
46 |
47 | should "instantiate the property on first access" do
48 | an_element = element.new
49 | refute an_element.instance_variable_defined?("@complex_property")
50 | assert an_element.complex_property.is_a?(OpenXml::Properties::ComplexProperty)
51 | end
52 |
53 | should "allow a parameter to be passed in to initialize on access" do
54 | an_element = element.new
55 | an_element.polymorphic_property(:tagTwo)
56 | assert_equal an_element.polymorphic_property.tag, :tagTwo
57 | end
58 | end
59 |
60 | context ".property_choice" do
61 | setup do
62 | @element = Class.new do
63 | include OpenXml::HasProperties
64 |
65 | property_choice required: true do
66 | value_property :property_one, as: :boolean_property
67 | property :property_two, as: :complex_property
68 | end
69 | end
70 | end
71 |
72 | should "raise an exception when attempting to use more than one property in the group" do
73 | an_element = element.new
74 | assert_raises OpenXml::HasProperties::ChoiceGroupUniqueError do
75 | an_element.property_one = true
76 | an_element.property_two
77 | end
78 |
79 | another_element = element.new
80 | assert_raises OpenXml::HasProperties::ChoiceGroupUniqueError do
81 | another_element.property_two
82 | another_element.property_one = true
83 | end
84 | end
85 |
86 | should "raise an exception when a required choice has not been made" do
87 | assert_raises OpenXml::UnmetRequirementError do
88 | element.new.property_xml(Nokogiri::XML::Builder.new)
89 | end
90 | end
91 | end
92 |
93 | context "#to_xml" do
94 | setup do
95 | base_class = Class.new do
96 | def self.namespace
97 | :w
98 | end
99 |
100 | def namespace
101 | self.class.namespace
102 | end
103 |
104 | def to_xml(xml)
105 | xml.public_send(tag, "xmlns:w" => "http://microsoft.com") do
106 | yield xml if block_given?
107 | end
108 | end
109 | end
110 |
111 | @element = Class.new(base_class) do
112 | include OpenXml::HasProperties
113 | value_property :boolean_property
114 |
115 | def self.tag
116 | "p"
117 | end
118 |
119 | def tag
120 | self.class.tag
121 | end
122 | end
123 | end
124 |
125 | should "generate the property tag as part of to_xml" do
126 | an_element = element.new
127 | an_element.boolean_property = true
128 |
129 | assert_match(//, xml(an_element))
130 | end
131 |
132 | should "call to_xml on each property" do
133 | builder = OpenXml::Builder.new
134 | mock = Minitest::Mock.new
135 | mock.expect(:render?, true)
136 | mock.expect(:to_xml, nil, [ builder ])
137 |
138 | OpenXml::Properties::BooleanProperty.stub :new, mock do
139 | an_element = element.new
140 | an_element.boolean_property = true
141 |
142 | an_element.to_xml(builder)
143 | mock.verify
144 | end
145 | end
146 | end
147 |
148 | should "allow attributes to be set on the properties tag" do
149 | element = Class.new(OpenXml::Element) do
150 | include OpenXml::HasProperties
151 | tag :p
152 | namespace :w
153 |
154 | properties_attribute :bold, displays_as: :b, expects: :boolean
155 | end.new
156 | element.bold = true
157 |
158 | assert_match(/w:pPr b="true"/, xml(element))
159 | end
160 | end
161 |
162 | context "a subclass of a class that has included HasProperties" do
163 | should "inherit the properties of its superclass" do
164 | parent = Class.new do
165 | include OpenXml::HasProperties
166 | value_property :boolean_property
167 | end
168 | child = Class.new(parent)
169 |
170 | assert_equal %i{ boolean_property }, child.new.send(:properties).keys
171 | end
172 |
173 | should "not modify the properties of its superclass" do
174 | parent = Class.new do
175 | include OpenXml::HasProperties
176 | value_property :boolean_property
177 | end
178 | child = Class.new(parent) do
179 | value_property :another_bool, as: :boolean_property
180 | end
181 |
182 | assert_equal %i{ boolean_property }, parent.properties.keys
183 | assert_equal %i{ another_bool boolean_property }, child.new.send(:properties).keys.sort
184 | end
185 |
186 | should "inherit the required properties of its superclass" do
187 | parent = Class.new do
188 | include OpenXml::HasProperties
189 | property :complex_property, required: true
190 | end
191 | child = Class.new(parent)
192 |
193 | assert_equal %i{ complex_property }, child.new.send(:required_properties).keys
194 | end
195 |
196 | should "not modify the required properties of its superclass" do
197 | parent = Class.new do
198 | include OpenXml::HasProperties
199 | property :complex_property, required: true
200 | end
201 | child = Class.new(parent) do
202 | property :another_one, as: :complex_property, required: true
203 | end
204 |
205 | assert_equal %i{ complex_property }, parent.required_properties.keys
206 | assert_equal %i{ another_one complex_property }, child.new.send(:required_properties).keys.sort
207 | end
208 |
209 | should "inherit the required choice groups of its superclass" do
210 | parent = Class.new do
211 | include OpenXml::HasProperties
212 | property_choice required: true do
213 | property :boolean_property
214 | property :complex_property
215 | end
216 | end
217 | child = Class.new(parent)
218 |
219 | assert_equal [0], child.required_choices
220 | end
221 |
222 | should "not modify the required choice groups of its superclass" do
223 | parent = Class.new do
224 | include OpenXml::HasProperties
225 | property_choice required: true do
226 | property :boolean_property
227 | property :complex_property
228 | end
229 | end
230 | child = Class.new(parent) do
231 | property_choice required: true do
232 | property :another_boolean, as: :boolean_property
233 | property :another_complex, as: :complex_property
234 | end
235 | end
236 |
237 | assert_equal [0], parent.required_choices
238 | assert_equal [0, 1], child.required_choices
239 | end
240 |
241 | should "inherit the accessors of its superclass" do
242 | parent = Class.new do
243 | include OpenXml::HasProperties
244 | value_property :boolean_property
245 | end
246 | child = Class.new(parent).new
247 |
248 | assert child.respond_to?(:boolean_property=), "Should respond to property assignment"
249 | assert child.respond_to?(:boolean_property), "Should respond to property accessor"
250 | end
251 |
252 | should "inherit the choice groups of its superclass" do
253 | parent = Class.new do
254 | include OpenXml::HasProperties
255 | property_choice do
256 | value_property :boolean_property
257 | end
258 | end
259 | child = Class.new(parent).new
260 |
261 | assert_equal 1, child.send(:choice_groups).count
262 | assert_equal %i{ boolean_property }, child.send(:choice_groups).first
263 | end
264 |
265 | should "not modify the choice groups of its superclass" do
266 | parent = Class.new do
267 | include OpenXml::HasProperties
268 | property_choice do
269 | value_property :boolean_property
270 | end
271 | end
272 | child = Class.new(parent) do
273 | property_choice do
274 | value_property :another_boolean, as: :boolean_property
275 | end
276 | end
277 |
278 | assert_equal 1, parent.choice_groups.count
279 | assert_equal %i{ boolean_property }, parent.choice_groups.first
280 | assert_equal 2, child.choice_groups.count
281 | assert_equal %i{ boolean_property }, child.choice_groups.first
282 | assert_equal %i{ another_boolean }, child.choice_groups.last
283 | end
284 |
285 | should "inherit the attributes of the properties tag of its superclass" do
286 | parent = Class.new(OpenXml::Element) do
287 | include OpenXml::HasProperties
288 | properties_attribute :an_attribute
289 | end
290 | child = Class.new(parent) do
291 | tag :p
292 | end
293 |
294 | assert_equal %i{ an_attribute }, child.new.properties_attributes.keys
295 | end
296 |
297 | should "not modify the attributes of the properties tag of its superclass" do
298 | parent = Class.new(OpenXml::Element) do
299 | include OpenXml::HasProperties
300 | tag :q
301 | properties_attribute :an_attribute
302 | end
303 | child = Class.new(parent) do
304 | tag :p
305 | properties_attribute :another_attribute
306 | end
307 |
308 | assert_equal %i{ an_attribute }, parent.new.properties_attributes.keys
309 | assert_equal %i{ an_attribute another_attribute }, child.new.properties_attributes.keys.sort
310 | end
311 | end
312 |
313 | context "#build_required_properties" do
314 | setup do
315 | @element = Class.new do
316 | include OpenXml::HasProperties
317 | property :property_haver_property, required: true
318 | value_property :boolean_property, required: true, default_value: false
319 |
320 | end
321 | end
322 |
323 | should "instantiate each required property" do
324 | an_element = element.new
325 | assert an_element.instance_variable_defined?(:"@property_haver_property")
326 | assert an_element.instance_variable_defined?(:"@boolean_property")
327 | end
328 |
329 | should "set the specified default for value properties" do
330 | an_element = element.new
331 | assert_equal false, an_element.boolean_property.value
332 | end
333 | end
334 |
335 | private
336 |
337 | def xml(element)
338 | builder = Nokogiri::XML::Builder.new
339 | builder.document("xmlns:w" => "http://microsoft.com") do |xml|
340 | element.to_xml(xml)
341 | end
342 | builder.to_xml
343 | end
344 |
345 | end
346 |
347 | module OpenXml
348 | module Properties
349 |
350 | class PolymorphicProperty < BaseProperty
351 | tag_is_one_of %i{ tagOne tagTwo }
352 |
353 | end
354 |
355 | class PropertyHaverProperty < ComplexProperty
356 | include OpenXml::HasProperties
357 |
358 | end
359 |
360 | end
361 | end
362 |
--------------------------------------------------------------------------------
/test/package_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "fileutils"
3 | require "set"
4 |
5 | class OpenXmlPackageTest < Minitest::Test
6 | attr_reader :package, :temp_file
7 |
8 |
9 |
10 | context "#add_part" do
11 | should "accept a path and a part" do
12 | package = OpenXml::Package.new
13 | parts = package.parts.count
14 | package.add_part "PATH", OpenXml::Part.new
15 | assert_equal parts + 1, package.parts.count
16 | end
17 | end
18 |
19 |
20 |
21 | context "Writing" do
22 | setup do
23 | @temp_file = expand_path "../tmp/test.zip"
24 | FileUtils.rm temp_file, force: true
25 | end
26 |
27 | context "Given a simple part" do
28 | setup do
29 | @package = OpenXml::Package.new
30 | package.add_part "content/document.xml", OpenXml::Parts::UnparsedPart.new(document_content)
31 | end
32 |
33 | should "write a valid zip file with the expected parts" do
34 | package.write_to temp_file
35 | assert File.exist?(temp_file), "Expected the file #{temp_file.inspect} to have been created"
36 | assert_equal %w{[Content_Types].xml _rels/.rels content/document.xml},
37 | Zip::File.open(temp_file).entries.map(&:name)
38 | end
39 | end
40 | end
41 |
42 |
43 |
44 | context "Reading" do
45 | context "Given a sample Word document" do
46 | setup do
47 | @temp_file = expand_path "./support/sample.docx"
48 | @expected_contents = Set[
49 | "[Content_Types].xml",
50 | "_rels/.rels",
51 | "docProps/app.xml",
52 | "docProps/core.xml",
53 | "docProps/thumbnail.jpeg",
54 | "word/_rels/document.xml.rels",
55 | "word/document.xml",
56 | "word/fontTable.xml",
57 | "word/media/image1.png",
58 | "word/settings.xml",
59 | "word/styles.xml",
60 | "word/stylesWithEffects.xml",
61 | "word/theme/theme1.xml",
62 | "word/webSettings.xml" ]
63 | end
64 |
65 | context ".open" do
66 | setup do
67 | @package = OpenXml::Package.open(temp_file)
68 | end
69 |
70 | teardown do
71 | package.close
72 | end
73 |
74 | should "discover the expected parts" do
75 | assert_equal @expected_contents, package.parts.keys.to_set
76 | end
77 |
78 | should "read their content on-demand" do
79 | assert_equal web_settings_content, package.get_part("word/webSettings.xml").content
80 | end
81 | end
82 |
83 | context ".from_stream" do
84 | setup do
85 | @package = OpenXml::Package.from_stream(File.open(temp_file, "rb", &:read))
86 | end
87 |
88 | should "also discover the expected parts" do
89 | assert_equal @expected_contents, package.parts.keys.to_set
90 | end
91 |
92 | should "read their content" do
93 | assert_equal web_settings_content, package.get_part("word/webSettings.xml").content
94 | end
95 | end
96 |
97 | context "ContentTypes" do
98 | setup do
99 | @package = OpenXml::Package.open(temp_file)
100 | end
101 |
102 | teardown do
103 | package.close
104 | end
105 |
106 | should "be parsed" do
107 | assert_equal %w{jpeg png rels xml}, package.content_types.defaults.keys.sort
108 | assert_equal "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml",
109 | package.content_types.overrides["/word/document.xml"]
110 | end
111 | end
112 |
113 | context "Rels" do
114 | setup do
115 | @package = OpenXml::Package.open(temp_file)
116 | end
117 |
118 | teardown do
119 | package.close
120 | end
121 |
122 | should "be parsed" do
123 | assert_equal %w{docProps/core.xml docProps/app.xml word/document.xml docProps/thumbnail.jpeg},
124 | package.rels.map(&:target)
125 | end
126 | end
127 | end
128 |
129 | context "Given a document with an external hyperlink" do
130 | context ".open" do
131 | should "parse the TargetMode attribute of the Relationship element" do
132 | path = expand_path "support/external_hyperlink.docx"
133 | OpenXml::Package.open(path) do |package|
134 | part = package.parts["word/_rels/document.xml.rels"]
135 | relationship = part.find { |r| r.target == "http://example.com" }
136 | assert_equal relationship.target_mode, 'External'
137 | end
138 | end
139 | end
140 | end
141 | end
142 |
143 |
144 |
145 | private
146 |
147 | def document_content
148 | <<-STR
149 |
150 | Works!
151 |
152 | STR
153 | end
154 |
155 | def web_settings_content
156 | "\r\n"
157 | end
158 |
159 | def expand_path(path)
160 | File.expand_path(File.join(File.dirname(__FILE__), path))
161 | end
162 |
163 | end
164 |
--------------------------------------------------------------------------------
/test/part_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 | require "fileutils"
3 | require "set"
4 |
5 | class PartTest < Minitest::Test
6 | attr_reader :part, :builder
7 |
8 |
9 |
10 | context "Building" do
11 | setup do
12 | @part = OpenXml::Part.new
13 | end
14 |
15 | context "Given simple xml for one part" do
16 | setup do
17 | @builder = @part.build_xml { |xml|
18 | xml.document do |inner_xml|
19 | 2.times { inner_xml.child(attribute: "value", other_attr: "other value") }
20 | end
21 | }
22 | end
23 |
24 | should "build the expected xml" do
25 | assert_equal basic_xml, builder.to_s(debug: true)
26 | end
27 | end
28 |
29 | context "Given namespaced xml for one part" do
30 | setup do
31 | @builder = @part.build_xml do |xml|
32 | xml.document("xmlns:ns" => "some:namespace:uri") do
33 | 2.times { xml["ns"].child(attribute: "value", other_attr: "other value") }
34 | end
35 | end
36 | end
37 |
38 | should "build the expected xml" do
39 | assert_equal namespaced_xml, builder.to_s(debug: true)
40 | end
41 | end
42 |
43 | context "For a standalone XML element" do
44 | setup do
45 | @builder = @part.build_standalone_xml do |xml|
46 | xml.document do
47 | xml.child(attribute: "value", other_attr: "other value")
48 | end
49 | end
50 | end
51 |
52 | should "build the expected xml" do
53 | assert_equal standalone_xml, builder.to_s(debug: true)
54 | end
55 | end
56 | end
57 |
58 |
59 |
60 | private
61 |
62 | def basic_xml
63 | <<-STR.strip
64 |
65 |
66 |
67 |
68 |
69 | STR
70 | end
71 |
72 | def namespaced_xml
73 | <<-STR.strip
74 |
75 |
76 |
77 |
78 |
79 | STR
80 | end
81 |
82 | def standalone_xml
83 | <<-STR.strip
84 |
85 |
86 |
87 |
88 | STR
89 | end
90 |
91 | end
92 |
--------------------------------------------------------------------------------
/test/rels_test.rb:
--------------------------------------------------------------------------------
1 | require "test_helper"
2 |
3 | class RelsTest < Minitest::Test
4 | context "with no defaults" do
5 | should "be empty" do
6 | assert OpenXml::Parts::Rels.new.empty?
7 | end
8 | end
9 |
10 | context "#to_xml" do
11 | context "given a document with an external hyperlink" do
12 | should "write the TargetMode attribute of the Relationship element" do
13 | path = File.expand_path "../support/external_hyperlink.docx", __FILE__
14 | OpenXml::Package.open(path) do |package|
15 | part = package.parts["word/_rels/document.xml.rels"]
16 | assert_match(/