ballmer 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +1 -0
- data/Gemfile +4 -0
- data/Guardfile +11 -0
- data/LICENSE.txt +22 -0
- data/README.md +54 -0
- data/Rakefile +8 -0
- data/ballmer.gemspec +29 -0
- data/bin/ballmer +10 -0
- data/bin/ballmer-console +9 -0
- data/bin/ballmer-duplicator +30 -0
- data/bin/ballmer-pack +19 -0
- data/bin/ballmer-unpack +14 -0
- data/lib/ballmer/document/archive.rb +89 -0
- data/lib/ballmer/document/content_types.rb +43 -0
- data/lib/ballmer/document/part.rb +26 -0
- data/lib/ballmer/document/rels.rb +79 -0
- data/lib/ballmer/document/xml_part.rb +29 -0
- data/lib/ballmer/document.rb +52 -0
- data/lib/ballmer/presentation/notes.rb +32 -0
- data/lib/ballmer/presentation/notes_parser.rb +57 -0
- data/lib/ballmer/presentation/slide.rb +19 -0
- data/lib/ballmer/presentation/slides.rb +172 -0
- data/lib/ballmer/presentation/tags.rb +50 -0
- data/lib/ballmer/presentation/tags.xml +3 -0
- data/lib/ballmer/presentation.rb +20 -0
- data/lib/ballmer/version.rb +3 -0
- data/lib/ballmer.rb +7 -0
- data/spec/fixtures/notes.pptx +0 -0
- data/spec/fixtures/presentation1.pptx +0 -0
- data/spec/fixtures/presentation2.pptx +0 -0
- data/spec/fixtures/presentation3.pptx +0 -0
- data/spec/lib/ballmer/document/content_types_spec.rb +35 -0
- data/spec/lib/ballmer/document/rels_spec.rb +43 -0
- data/spec/lib/ballmer/document_spec.rb +12 -0
- data/spec/lib/ballmer/presentation/notes_parser_spec.rb +19 -0
- data/spec/lib/ballmer/presentation/tags_spec.rb +22 -0
- data/spec/lib/ballmer/presentation_spec.rb +118 -0
- data/spec/spec_helper.rb +38 -0
- metadata +198 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: fcae2435035fbc72f36f7e7f793b1c952da690da
|
4
|
+
data.tar.gz: 884e3214b7a0b7acb1653a1ce40c7b3c52f3ef84
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 62c5311693846c3db3a07146b5df427b5146ebd017f370aab8b5eae98acfcc6bb94b5a8e390cf5f7999f341e28924ec07a43c1a3c5e86196dc73c49266957be2
|
7
|
+
data.tar.gz: 3190b41051a7c694d9574ea83d4d82b1b999a0f95d4e7266ea32a38298a0e81dbb6f699f46be1a69dd883bd53f0f5caf6334c984f9350c70b58b2997c41f2e57
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
script: "bundle exec rspec"
|
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
|
9
|
+
# Run the base spec since that's where a bulk of the tests hang out.
|
10
|
+
watch(%r{^lib/ballmer/(.+?)/.+\.rb$}) { |m| "spec/lib/ballmer/#{m[1]}_spec.rb" }
|
11
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Brad Gessler
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Ballmer
|
2
|
+
|
3
|
+
[](https://travis-ci.org/polleverywhere/ballmer)
|
4
|
+
|
5
|
+
The Ballmer gem provides the basis for modifying Office documents in Ruby. It provides access to low-level primitives including:
|
6
|
+
|
7
|
+
* Unzip/zip Office document formats.
|
8
|
+
* Low level "part" abstraction and rels resolution.
|
9
|
+
* Direct access to manipulating/munging XML.
|
10
|
+
|
11
|
+
PowerPoint is the only format with a higher-level, but basic abstraction that allows:
|
12
|
+
|
13
|
+
* Copying and inserting slides.
|
14
|
+
* Reading slide notes in the most basic sense.
|
15
|
+
* Writing to slidenotes via a subset of markdown (only paragraphs).
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Add this line to your application's Gemfile:
|
20
|
+
|
21
|
+
gem 'ballmer'
|
22
|
+
|
23
|
+
And then execute:
|
24
|
+
|
25
|
+
$ bundle
|
26
|
+
|
27
|
+
Or install it yourself as:
|
28
|
+
|
29
|
+
$ gem install ballmer
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
# Open a pptx file
|
35
|
+
p = Ballmer::Presentation.open("./fixtures/Presentation3.pptx")
|
36
|
+
# Copy the first slide into the last position
|
37
|
+
p.sides.push p.slides.first
|
38
|
+
# Now save the file.
|
39
|
+
p.save
|
40
|
+
```
|
41
|
+
|
42
|
+
## Contributing
|
43
|
+
|
44
|
+
Microsoft Office is a complicating beast. If you need to grock documents you can help!
|
45
|
+
|
46
|
+
1. Fork it
|
47
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
48
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
49
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
50
|
+
5. Create new Pull Request
|
51
|
+
|
52
|
+
## Helpful Information
|
53
|
+
|
54
|
+
* [PresentationML](http://msdn.microsoft.com/en-us/library/office/gg278335.aspx) - Directory and XML structure of an Office file.
|
data/Rakefile
ADDED
data/ballmer.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'ballmer/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "ballmer"
|
8
|
+
spec.version = Ballmer::VERSION
|
9
|
+
spec.authors = ["Brad Gessler"]
|
10
|
+
spec.email = ["brad@polleverywhere.com"]
|
11
|
+
spec.description = %q{Open and manipulate Office files.}
|
12
|
+
spec.summary = %q{Manipulate Office files in Ruby.}
|
13
|
+
spec.homepage = ""
|
14
|
+
spec.license = "MIT"
|
15
|
+
|
16
|
+
spec.files = `git ls-files`.split($/)
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "bundler", "~> 1.3"
|
22
|
+
spec.add_development_dependency "rake"
|
23
|
+
spec.add_development_dependency "guard-rspec"
|
24
|
+
spec.add_development_dependency "terminal-notifier-guard"
|
25
|
+
spec.add_development_dependency "pry"
|
26
|
+
|
27
|
+
spec.add_dependency "zipruby"
|
28
|
+
spec.add_dependency "nokogiri"
|
29
|
+
end
|
data/bin/ballmer
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
|
3
|
+
# Just run the command of a ballmer-bin. I'm faking out what should eventually be a Thor CLI.
|
4
|
+
COMMAND=$1
|
5
|
+
|
6
|
+
# Now 'shift' the $1 out of the args ($@) so we can pass it up into the CLI.
|
7
|
+
shift
|
8
|
+
|
9
|
+
# And run the command
|
10
|
+
$0/../ballmer-$COMMAND $@
|
data/bin/ballmer-console
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This console is usefulf or interacting with a presentation, which makes debugging
|
4
|
+
# a little easier since this is mostly munging with files.
|
5
|
+
|
6
|
+
require 'pry'
|
7
|
+
require 'ballmer'
|
8
|
+
|
9
|
+
Ballmer::Presentation.open(ARGV.first || './fixtures/Presentation3.pptx').pry
|
@@ -0,0 +1,30 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
# This console is usefulf or interacting with a presentation, which makes debugging
|
4
|
+
# a little easier since this is mostly munging with files.
|
5
|
+
|
6
|
+
require 'ballmer'
|
7
|
+
|
8
|
+
# Deal with piped input.
|
9
|
+
ppt = if $stdin.tty?
|
10
|
+
Ballmer::Presentation.open(ARGV.shift)
|
11
|
+
else
|
12
|
+
Ballmer::Presentation.read($stdin.read)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Now duplicate the slides.
|
16
|
+
ARGV.shift.to_i.times do |n|
|
17
|
+
# Copy the first slide to the end of the presentation.
|
18
|
+
ppt.slides.push(ppt.slides.first).tap do |slide|
|
19
|
+
slide.notes.body = "This is copy #{n + 1}.\n\nIt was created at #{Time.now}."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Now delete the first slide.
|
24
|
+
ppt.slides.delete ppt.slides.first
|
25
|
+
|
26
|
+
# Save to disk or buffer
|
27
|
+
ppt.commit
|
28
|
+
|
29
|
+
# Pipe this shiz out if it was piped in.
|
30
|
+
ppt.archive.zip.read { |chunk| $stdout.write chunk } unless $stdin.tty?
|
data/bin/ballmer-pack
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
# Unpacks a .pptx file and makes XML readable by diff tools.
|
3
|
+
|
4
|
+
# Remove the .pptx extension
|
5
|
+
DIR=$1
|
6
|
+
|
7
|
+
# First argument should be a path to the MS Office file
|
8
|
+
OFFICE_FILE=$PWD/$2
|
9
|
+
|
10
|
+
# No indents
|
11
|
+
$XMLLINT_INDENT=''
|
12
|
+
|
13
|
+
pushd $DIR
|
14
|
+
# TODO - Remove the whitespace
|
15
|
+
find $DIR \( -name "*.xml.rels" -o -name "*.xml" \) -type f -exec xmllint --output '{}' --format '{}' \; -print
|
16
|
+
|
17
|
+
# Unzip the presentation into a folder
|
18
|
+
zip -r $OFFICE_FILE .
|
19
|
+
popd
|
data/bin/ballmer-unpack
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env sh
|
2
|
+
# Unpacks a .pptx file and makes XML readable by diff tools.
|
3
|
+
|
4
|
+
# First argument should be a path to the MS Office file
|
5
|
+
OFFICE_FILE=$1
|
6
|
+
|
7
|
+
# Remove the .pptx extension
|
8
|
+
DIR=${OFFICE_FILE%.*}
|
9
|
+
|
10
|
+
# Unzip the presentation into a folder
|
11
|
+
unzip $OFFICE_FILE -d $DIR
|
12
|
+
|
13
|
+
# Prettify the XML so its easier to diff.
|
14
|
+
find $DIR \( -name "*.xml.rels" -o -name "*.xml" \) -type f -exec xmllint --output '{}' --format '{}' \; -print
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require "zipruby"
|
2
|
+
|
3
|
+
module Ballmer
|
4
|
+
class Document
|
5
|
+
# Abstraction that sits on top of ZipRuby because the original
|
6
|
+
# lib API is a bit cumbersom to use directly.
|
7
|
+
class Archive
|
8
|
+
attr_reader :zip
|
9
|
+
|
10
|
+
def initialize(zip)
|
11
|
+
@zip = zip
|
12
|
+
@original_files = (0...zip.num_files).map { |n| zip.get_name(n) }
|
13
|
+
end
|
14
|
+
|
15
|
+
# Save the office XML file to the file.
|
16
|
+
def commit
|
17
|
+
# TODO
|
18
|
+
# Update ./docProps
|
19
|
+
# app.xml slides, notes, counts, etc
|
20
|
+
# core.xml Times
|
21
|
+
entries.each do |path, buffer|
|
22
|
+
path = path.to_s
|
23
|
+
if @original_files.include? path
|
24
|
+
@zip.replace_buffer path, buffer
|
25
|
+
else
|
26
|
+
@zip.add_buffer path, buffer
|
27
|
+
end
|
28
|
+
end
|
29
|
+
@zip.commit
|
30
|
+
end
|
31
|
+
|
32
|
+
# Write to the zip file at the given path.
|
33
|
+
def write(path, buffer)
|
34
|
+
entries[self.class.path(path)] = buffer
|
35
|
+
end
|
36
|
+
|
37
|
+
# Read the blog from the Zifile
|
38
|
+
def read(path)
|
39
|
+
entries[self.class.path(path)]
|
40
|
+
end
|
41
|
+
|
42
|
+
# Copy a file in the zip from a path to a path.
|
43
|
+
def copy(target, source)
|
44
|
+
write target, read(source)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Remove a file from the archive.
|
48
|
+
def delete(path)
|
49
|
+
path = self.class.path(path).to_s
|
50
|
+
zip.fopen(path).delete
|
51
|
+
entries.delete(path)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Enumerates all of the entries in the zip file. Key
|
55
|
+
# is the path of the file, and the value is the contents.
|
56
|
+
def entries
|
57
|
+
@entries ||= Hash.new do |entries, path|
|
58
|
+
path = self.class.path(path).to_s
|
59
|
+
entries[path] = if @original_files.include? path
|
60
|
+
zip.fopen(path).read
|
61
|
+
else
|
62
|
+
""
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Open an XML office file from the given path.
|
68
|
+
def self.open(path)
|
69
|
+
new Zip::Archive.open(path, Zip::TRUNC)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Read zip data from a bufffer. Very useful when you want to load a template
|
73
|
+
# into a server environment, modify, and serve up without writing to disk.
|
74
|
+
def self.read(data)
|
75
|
+
new Zip::Archive.open_buffer(data)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
# All of the paths in the office file formats are expressed with a leading "/",
|
81
|
+
# but zip files are not since they are always considered relative the the archive
|
82
|
+
# root by ZipRuby. This method means that we can keep referring to paths in the
|
83
|
+
# Document class on up while maintaining relative paths within the archive.
|
84
|
+
def self.path(path)
|
85
|
+
Pathname.new(path).expand_path('/').relative_path_from(Pathname.new('/'))
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Document
|
3
|
+
# Deals with everything related to content paths.
|
4
|
+
class ContentTypes < XMLPart
|
5
|
+
PATH = "[Content_Types].xml"
|
6
|
+
|
7
|
+
def initialize(doc, path = PATH)
|
8
|
+
super doc, path
|
9
|
+
end
|
10
|
+
|
11
|
+
# Get all of the parts for a given type
|
12
|
+
# TODO - Have this return an enumerable of parts so we can fitler by part-type.
|
13
|
+
# We can filter this by type from whatever is calling it if we blow open some
|
14
|
+
# new types...
|
15
|
+
def parts(type)
|
16
|
+
xml.xpath("//xmlns:Override[@ContentType='#{type}']").map{ |n| n['PartName'] }
|
17
|
+
end
|
18
|
+
alias :[] :parts
|
19
|
+
|
20
|
+
# Append a part to ContentTypes
|
21
|
+
def append(part)
|
22
|
+
# Don't write the part again if it already exists ya dummy
|
23
|
+
return nil if exists? part
|
24
|
+
|
25
|
+
edit_xml do |xml|
|
26
|
+
xml.at_xpath('/xmlns:Types').tap do |types|
|
27
|
+
types << Nokogiri::XML::Node.new("Override", xml).tap do |n|
|
28
|
+
n['PartName'] = part.path
|
29
|
+
n['ContentType'] = part.class::CONTENT_TYPE
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
alias :<< :append
|
35
|
+
|
36
|
+
# Test if the part already exists so we don't write
|
37
|
+
# it multiple times.
|
38
|
+
def exists?(part)
|
39
|
+
!! xml.at_xpath("//xmlns:Override[@ContentType='#{part.class::CONTENT_TYPE}' and @PartName='#{part.path}']")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Document
|
3
|
+
# Basic behavior of a part. Whats a part? Could be an image, an XML file, or anything really. Most
|
4
|
+
# parts in a document will be an XMLPart, so be sure to take a look at that.
|
5
|
+
class Part
|
6
|
+
attr_reader :path, :doc
|
7
|
+
|
8
|
+
def initialize(doc, path)
|
9
|
+
@doc, @path = doc, Pathname.new(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Get the relative path for this part from another part. This funciton
|
13
|
+
# is mostly used by the Rel class to figure out relationships between parts.
|
14
|
+
def relative_path_from(part)
|
15
|
+
# I think the rel_part.path bidness is not returning and absolute path. Fix and maybe this will work (and
|
16
|
+
# the weird + '..' won't be needed).
|
17
|
+
part.path.relative_path_from(path + '..')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Commit the part to the buffer.
|
21
|
+
def commit(data = self.doc)
|
22
|
+
doc.write path, data
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Document
|
3
|
+
# CRUD and resolve relative documents to a part. These map to .xml.rel documents
|
4
|
+
# in the MS Office document format.
|
5
|
+
class Rels < XMLPart
|
6
|
+
attr_reader :path, :doc
|
7
|
+
|
8
|
+
# TODO - Refactor the part_path business out here.
|
9
|
+
def initialize(doc, path, part_path)
|
10
|
+
super doc, path
|
11
|
+
@part_path = Pathname.new(part_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Return a list of target paths given a type.
|
15
|
+
def targets(type)
|
16
|
+
xml.xpath("//xmlns:Relationship[@Type='#{type}']").map{ |n| Pathname.new(n['Target']) }
|
17
|
+
end
|
18
|
+
|
19
|
+
# TODO
|
20
|
+
# Returns the rID of the part.
|
21
|
+
def id(part)
|
22
|
+
rel(part)['Id']
|
23
|
+
end
|
24
|
+
|
25
|
+
# Append a part to a rel so that we can extract an ID from it, and be
|
26
|
+
# really cool like that.
|
27
|
+
def append(part)
|
28
|
+
return nil if exists? part
|
29
|
+
|
30
|
+
edit_xml do |xml|
|
31
|
+
xml.at_xpath('/xmlns:Relationships').tap do |relationships|
|
32
|
+
relationships << Nokogiri::XML::Node.new("Relationship", xml).tap do |n|
|
33
|
+
n['Id'] = next_id
|
34
|
+
n['Type'] = part.class::REL_TYPE
|
35
|
+
# Rels require a strange path... still haven't quite figured it out but I need to.
|
36
|
+
n['Target'] = rel_path(part)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO
|
43
|
+
# Check if the part exists
|
44
|
+
def exists?(part)
|
45
|
+
!! rel(part)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Create a Rels class from a given part.
|
49
|
+
def self.from(part)
|
50
|
+
new part.doc, rels_path(part.path), part.path
|
51
|
+
end
|
52
|
+
|
53
|
+
# Resolve the default rels asset for a given part path.
|
54
|
+
def self.rels_path(part_path)
|
55
|
+
Pathname.new(part_path).join('../_rels', Pathname.new(part_path).sub_ext('.xml.rels').basename)
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def rel(part)
|
61
|
+
xml.at_xpath("/xmlns:Relationships/xmlns:Relationship[@Type='#{part.class::REL_TYPE}' and @Target='#{rel_path(part)}']")
|
62
|
+
end
|
63
|
+
|
64
|
+
# TODO - This feels dirty, dropping into kinda sorta paths (instead of parts). Refactor
|
65
|
+
# this so that we're only dealing with parts up in here. Use Part#relative_path_from.
|
66
|
+
def rel_path(rel_part)
|
67
|
+
# I think the rel_part.path bidness is not returning and absolute path. Fix and maybe this will work (and
|
68
|
+
# the weird + '..' won't be needed).
|
69
|
+
rel_part.path.relative_path_from(@part_path + '..')
|
70
|
+
end
|
71
|
+
|
72
|
+
# TODO - Figure out how to make this more MS idiomatic up 9->10 instead of incrementing
|
73
|
+
# the character....
|
74
|
+
def next_id
|
75
|
+
xml.xpath('//xmlns:Relationship[@Id]').map{ |n| n['Id'] }.sort.last.succ
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Document
|
3
|
+
# Various helpers for editing Part XML data and resolving relative part paths.
|
4
|
+
class XMLPart < Part
|
5
|
+
# TODO - Figure out how to curry the path into this call and delegate.
|
6
|
+
# Also, if the DOM is what speaks the truth, this caching will cause some
|
7
|
+
# really stupid/weird bug down the line.
|
8
|
+
def xml
|
9
|
+
@xml ||= doc.xml(path)
|
10
|
+
end
|
11
|
+
|
12
|
+
# TODO - Figure out how to curry the path into this call and delegate
|
13
|
+
def edit_xml(&block)
|
14
|
+
block.call(xml)
|
15
|
+
commit
|
16
|
+
end
|
17
|
+
|
18
|
+
# Grab the rels file for this asset.
|
19
|
+
def rels
|
20
|
+
Rels.from(self)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Commit the part XML to the buffer.
|
24
|
+
def commit
|
25
|
+
super xml.to_s
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Ballmer
|
2
|
+
# Deals with file concerns between higher-level classes like
|
3
|
+
# Slides, Notes and file-system level work.
|
4
|
+
class Document
|
5
|
+
autoload :Archive, 'ballmer/document/archive'
|
6
|
+
autoload :Part, 'ballmer/document/part'
|
7
|
+
autoload :XMLPart, 'ballmer/document/xml_part'
|
8
|
+
autoload :Rels, 'ballmer/document/rels'
|
9
|
+
autoload :ContentTypes, 'ballmer/document/content_types'
|
10
|
+
|
11
|
+
# Forward method calls on document to the archive, mostly
|
12
|
+
# low-level read/write/copy file operations. The Document
|
13
|
+
# class should deal with decorating these read/writes with
|
14
|
+
# helper Parts.
|
15
|
+
extend Forwardable
|
16
|
+
def_delegators :archive, :read, :write, :copy, :delete, :commit
|
17
|
+
|
18
|
+
# Make the archive available in case the developer needs to perform
|
19
|
+
# even lower level IO functions.
|
20
|
+
attr_reader :archive
|
21
|
+
|
22
|
+
def initialize(archive)
|
23
|
+
@archive = archive
|
24
|
+
end
|
25
|
+
|
26
|
+
# Open an XML office file from the given path.
|
27
|
+
def self.open(path)
|
28
|
+
new Archive.open(path)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Read zip data from a bufffer. Very useful when you want to load a template
|
32
|
+
# into a server environment, modify, and serve up without writing to disk.
|
33
|
+
def self.read(data)
|
34
|
+
new Archive.read(data)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Open an XML document at the given path.
|
38
|
+
def xml(path)
|
39
|
+
Nokogiri::XML read path
|
40
|
+
end
|
41
|
+
|
42
|
+
# Modify XML within a block and write it back to the zip when done.
|
43
|
+
def edit_xml(path, &block)
|
44
|
+
write path, xml(path).tap(&block).to_s
|
45
|
+
end
|
46
|
+
|
47
|
+
# Content types XML file.
|
48
|
+
def content_types
|
49
|
+
ContentTypes.new(self)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Presentation
|
3
|
+
class Notes < Document::XMLPart
|
4
|
+
# TODO, there are three types of notes. We need to figure out
|
5
|
+
# how to resolve the slide number, notes, and whatever the hell else
|
6
|
+
# the first note type is.
|
7
|
+
|
8
|
+
# Key used to look up notes from [Content-Types].xml.
|
9
|
+
CONTENT_TYPE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml".freeze
|
10
|
+
|
11
|
+
# Key used to look up notes from .xml.rel documents
|
12
|
+
REL_TYPE = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide'.freeze
|
13
|
+
|
14
|
+
# TODO - Generate/update the notes with a mark-down-ish heuristic,
|
15
|
+
# being that two newlines translate into the weird note formats of PPT slides.
|
16
|
+
def body=(body)
|
17
|
+
nodes_parser.parse(body)
|
18
|
+
commit
|
19
|
+
end
|
20
|
+
|
21
|
+
def body
|
22
|
+
nodes_parser.to_s
|
23
|
+
end
|
24
|
+
alias :to_s :body
|
25
|
+
|
26
|
+
private
|
27
|
+
def nodes_parser
|
28
|
+
NotesParser.new(xml)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Ballmer
|
2
|
+
class Presentation
|
3
|
+
class NotesParser
|
4
|
+
# Make this read the language set by the OS or from a configuration.
|
5
|
+
DEFAULT_LANG = 'en-US'
|
6
|
+
|
7
|
+
def initialize(xml)
|
8
|
+
@xml = xml
|
9
|
+
end
|
10
|
+
|
11
|
+
# MSFT Thought it would be cool to drop a bunch of different bodies
|
12
|
+
# and id attributes in here that don't link to anything, so lets go
|
13
|
+
# loosey goosey on it and find the stupid "Notes Placeholder" content.
|
14
|
+
def node
|
15
|
+
@xml.at_xpath('
|
16
|
+
//p:nvSpPr[
|
17
|
+
p:cNvPr[starts-with(@name, "Notes Placeholder")]
|
18
|
+
]/following-sibling::p:txBody
|
19
|
+
')
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_s
|
23
|
+
node.xpath('.//a:p').map do |p| # Put each new paragraph on a line.
|
24
|
+
p.xpath('.//a:t').map(&:text).join # And join up each word... turd.
|
25
|
+
end.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
# Parses a text file with newline breaks into "paragraphs" per whatever weird markup
|
29
|
+
# the noteSlides is using. For now we're keeping this simple, no italics or other crazy stuff,
|
30
|
+
# but this is the class that would be extended, changed, or swapped out in the future.
|
31
|
+
def parse(body, lang = DEFAULT_LANG)
|
32
|
+
body_pr = Nokogiri::XML::Node.new("p:txBody", @xml)
|
33
|
+
# These should be blank... I don't know why, but they're always that way in the files.
|
34
|
+
# <a:bodyPr/>
|
35
|
+
# <a:lstStyle/>
|
36
|
+
body_pr << Nokogiri::XML::Node.new("a:bodyPr", @xml)
|
37
|
+
body_pr << Nokogiri::XML::Node.new("a:lstStyle", @xml)
|
38
|
+
# TODO - Reject blank lines after we chomp 'em
|
39
|
+
body_pr << Nokogiri::XML::Node.new("a:p", @xml).tap do |p|
|
40
|
+
p << Nokogiri::XML::Node.new("a:r", @xml).tap do |r|
|
41
|
+
r << Nokogiri::XML::Node.new("a:rPr", @xml).tap do |rpr|
|
42
|
+
# PPT just wants this, k?
|
43
|
+
rpr["lang"] = lang
|
44
|
+
rpr["dirty"] = "0"
|
45
|
+
rpr["smtClean"] = "0"
|
46
|
+
end
|
47
|
+
# This is where we finally inject content. w00.
|
48
|
+
r << Nokogiri::XML::Node.new("a:t", @xml).tap do |t|
|
49
|
+
t.content = body
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
node.replace body_pr
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|