ballmer 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Build Status](https://travis-ci.org/polleverywhere/ballmer.png?branch=master)](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
|