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.
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