ballmer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +1 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +11 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +54 -0
  9. data/Rakefile +8 -0
  10. data/ballmer.gemspec +29 -0
  11. data/bin/ballmer +10 -0
  12. data/bin/ballmer-console +9 -0
  13. data/bin/ballmer-duplicator +30 -0
  14. data/bin/ballmer-pack +19 -0
  15. data/bin/ballmer-unpack +14 -0
  16. data/lib/ballmer/document/archive.rb +89 -0
  17. data/lib/ballmer/document/content_types.rb +43 -0
  18. data/lib/ballmer/document/part.rb +26 -0
  19. data/lib/ballmer/document/rels.rb +79 -0
  20. data/lib/ballmer/document/xml_part.rb +29 -0
  21. data/lib/ballmer/document.rb +52 -0
  22. data/lib/ballmer/presentation/notes.rb +32 -0
  23. data/lib/ballmer/presentation/notes_parser.rb +57 -0
  24. data/lib/ballmer/presentation/slide.rb +19 -0
  25. data/lib/ballmer/presentation/slides.rb +172 -0
  26. data/lib/ballmer/presentation/tags.rb +50 -0
  27. data/lib/ballmer/presentation/tags.xml +3 -0
  28. data/lib/ballmer/presentation.rb +20 -0
  29. data/lib/ballmer/version.rb +3 -0
  30. data/lib/ballmer.rb +7 -0
  31. data/spec/fixtures/notes.pptx +0 -0
  32. data/spec/fixtures/presentation1.pptx +0 -0
  33. data/spec/fixtures/presentation2.pptx +0 -0
  34. data/spec/fixtures/presentation3.pptx +0 -0
  35. data/spec/lib/ballmer/document/content_types_spec.rb +35 -0
  36. data/spec/lib/ballmer/document/rels_spec.rb +43 -0
  37. data/spec/lib/ballmer/document_spec.rb +12 -0
  38. data/spec/lib/ballmer/presentation/notes_parser_spec.rb +19 -0
  39. data/spec/lib/ballmer/presentation/tags_spec.rb +22 -0
  40. data/spec/lib/ballmer/presentation_spec.rb +118 -0
  41. data/spec/spec_helper.rb +38 -0
  42. 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
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1 @@
1
+ script: "bundle exec rspec"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in ballmer.gemspec
4
+ gemspec
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
@@ -0,0 +1,8 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ namespace :fixtures do
4
+ desc "Unpack all of the fixtures for inspection with a diff tool"
5
+ task :unpack do
6
+ Dir['./fixtures/*.pptx'].each{ |f| sh "bin/unpack #{f}" }
7
+ end
8
+ end
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 $@
@@ -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
@@ -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