├── .rspec ├── lib ├── keynote │ ├── version.rb │ ├── util.rb │ ├── master_slide.rb │ ├── theme.rb │ ├── slide.rb │ ├── slide_array.rb │ └── document.rb └── keynote-client.rb ├── .travis.yml ├── Gemfile ├── .gitignore ├── Guardfile ├── Rakefile ├── spec ├── spec_helper.rb └── keynote │ └── slide_spec.rb ├── LICENSE.txt ├── keynote-client.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /lib/keynote/version.rb: -------------------------------------------------------------------------------- 1 | module Keynote 2 | module Client 3 | VERSION = "0.1.2" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | before_install: 3 | - gem update bundler 4 | rvm: 5 | - 2.2 6 | - 2.1 7 | - 2.0 8 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in keynote-client.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | *.bundle 11 | *.so 12 | *.o 13 | *.a 14 | mkmf.log 15 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | guard 'rspec', cmd: 'bundle exec rspec' do 2 | watch(%r{^spec/.+_spec\.rb$}) 3 | watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" } 4 | watch('spec/spec_helper.rb') { "spec" } 5 | end 6 | -------------------------------------------------------------------------------- /lib/keynote-client.rb: -------------------------------------------------------------------------------- 1 | require 'unindent' 2 | 3 | require "keynote/document" 4 | require "keynote/theme" 5 | require "keynote/master_slide" 6 | require "keynote/slide" 7 | require 'keynote/slide_array' 8 | require "keynote/version" 9 | 10 | module Keynote 11 | end 12 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | require "bundler/gem_tasks" 3 | 4 | require 'rspec/core' 5 | require 'rspec/core/rake_task' 6 | 7 | begin 8 | Bundler.setup(:default, :development) 9 | rescue Bundler::BundlerError => e 10 | $stderr.puts e.message 11 | $stderr.puts "Run `bundle install` to install missing gems" 12 | exit e.status_code 13 | end 14 | 15 | RSpec::Core::RakeTask.new(:spec) 16 | 17 | task :default => :spec 18 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__) 2 | 3 | require 'simplecov' 4 | require 'coveralls' 5 | require 'keynote-client' 6 | 7 | Bundler.setup(:default, :development) 8 | 9 | RSpec.configure do |config| 10 | config.color = true 11 | config.tty = true 12 | end 13 | 14 | SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[ 15 | SimpleCov::Formatter::HTMLFormatter, 16 | Coveralls::SimpleCov::Formatter 17 | ] 18 | 19 | SimpleCov.start do 20 | add_filter '/spec/' 21 | end 22 | -------------------------------------------------------------------------------- /lib/keynote/util.rb: -------------------------------------------------------------------------------- 1 | require 'open3' 2 | require 'json' 3 | require "tempfile" 4 | 5 | module Keynote 6 | module Util 7 | def eval_script(script) 8 | file = Tempfile.new(['osascript', '.js']) 9 | file.write(script) 10 | file.close 11 | command = "osascript -l JavaScript #{file.path}" 12 | execute_out, process_status = *Open3.capture2(command) 13 | execute_out.chomp! 14 | JSON.parse(execute_out) unless execute_out.empty? 15 | ensure 16 | file.delete 17 | end 18 | 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /lib/keynote/master_slide.rb: -------------------------------------------------------------------------------- 1 | module Keynote 2 | class MasterSlide 3 | attr_reader :name 4 | 5 | def default_names 6 | { 7 | title_and_sub_title: "タイトル & サブタイトル", # Title & Subtitle 8 | photo_horizontal: "画像(横長)", # Photo - Horizontal 9 | title_center: "タイトル(中央)", # Title - Center 10 | photo_vertical: "画像(縦長)", # Photo - Vertical 11 | title_top: "タイトル(上)", # Title - Top 12 | title_and_bullets: "タイトル & 箇条書き", # Title & Bullets 13 | title_bullets_and_photo: "タイトル、箇条書き、画像", # Title, Bullets & Photo 14 | bullets: "箇条書き", # Bullets 15 | quote: "引用", # Quote 16 | photo: "画像", # Photo 17 | blank: "空白", # Blank 18 | } 19 | end 20 | 21 | def initialize(name) 22 | @name = name 23 | end 24 | 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 ryo katsuma 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /lib/keynote/theme.rb: -------------------------------------------------------------------------------- 1 | require 'keynote/util' 2 | 3 | module Keynote 4 | class Theme 5 | extend Keynote::Util 6 | 7 | attr_reader :id, :name 8 | 9 | def initialize(id: nil, name: nil) 10 | @id = id 11 | @name = name 12 | end 13 | 14 | def self.default 15 | self.all.first 16 | end 17 | 18 | def self.all 19 | self.find_with_conditions 20 | end 21 | 22 | def self.find_by(args) 23 | raise ArgumentError.new('nil argument is given') unless args 24 | 25 | if args.is_a?(Hash) && args.has_key?(:name) 26 | conditions = ".whose({ name: '#{args[:name]}' })" 27 | else 28 | raise ArgumentError.new('Unsupported argument is given') 29 | end 30 | 31 | find_with_conditions(conditions) 32 | end 33 | 34 | private 35 | 36 | def self.find_with_conditions(conditions = '') 37 | results = eval_script <<-APPLE.unindent 38 | var themes = Application("Keynote").themes#{conditions}; 39 | var results = []; 40 | for(var i = 0, len = themes.length; i < len; i++) { 41 | var theme = themes[i]; 42 | results.push({ id: theme.id(), name: theme.name() }); 43 | } 44 | JSON.stringify(results); 45 | APPLE 46 | 47 | return [] unless results 48 | 49 | results.map do |result| 50 | self.new(id: result["id"], name: result["name"]) 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /keynote-client.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'keynote/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "keynote-client" 8 | spec.version = Keynote::Client::VERSION 9 | spec.authors = ["ryo katsuma"] 10 | spec.email = ["katsuma@gmail.com"] 11 | spec.summary = %q{keynote client with high level API.} 12 | spec.description = %q{keynote-client provides a high level API like ActiveRecord style to control your Keynote.} 13 | spec.homepage = "https://github.com/katsuma/keynote-client" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 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 "pry", "~> 0.10.1" 22 | spec.add_development_dependency "bundler", "~> 1.7" 23 | spec.add_development_dependency "rake", "~> 12.3.3" 24 | spec.add_development_dependency "rspec", "~> 3.3.0" 25 | spec.add_development_dependency "simplecov", "~> 0.10.0" 26 | spec.add_development_dependency "coveralls", "~> 0.8.2" 27 | spec.add_development_dependency "guard", "~> 2.13.0" 28 | spec.add_development_dependency "guard-rspec", "~> 4.6.2" 29 | spec.add_dependency "unindent", "~> 1.0" 30 | end 31 | -------------------------------------------------------------------------------- /lib/keynote/slide.rb: -------------------------------------------------------------------------------- 1 | require 'keynote/util' 2 | 3 | module Keynote 4 | class Slide 5 | extend Keynote::Util 6 | include Keynote::Util 7 | 8 | attr_accessor( 9 | :document, 10 | :base_slide, 11 | :body_showing, 12 | :skipped, 13 | :slide_number, 14 | :title_showing, 15 | :body, 16 | :title, 17 | :presenter_notes, 18 | :transition_properties, 19 | ) 20 | 21 | def initialize(base_slide = nil, arguments = {}) 22 | raise ArgumentError.new('base_slide is not given') unless base_slide 23 | 24 | @base_slide = base_slide 25 | arguments.each do |attr, val| 26 | send("#{attr}=", val) 27 | end 28 | end 29 | 30 | def title=(title) 31 | title = title.gsub(/(\r\n|\r|\n)/) { '\\n' } 32 | @title = title 33 | return unless @document && @slide_number 34 | 35 | result = eval_script <<-APPLE.unindent 36 | var Keynote = Application("Keynote") 37 | var doc = Keynote.documents.byId("#{@document.id}") 38 | var slide = doc.slides()[#{@slide_number - 1}] 39 | slide.defaultTitleItem.objectText = "#{title}" 40 | JSON.stringify({ result: true }) 41 | APPLE 42 | end 43 | 44 | def body=(body) 45 | body = body.gsub(/(\r\n|\r|\n)/) { '\\n' } 46 | @body = body 47 | return unless @document && @slide_number 48 | 49 | result = eval_script <<-APPLE.unindent 50 | var Keynote = Application("Keynote") 51 | var doc = Keynote.documents.byId("#{@document.id}") 52 | var slide = doc.slides()[#{@slide_number - 1}] 53 | slide.defaultBodyItem.objectText = "#{body}" 54 | JSON.stringify({ result: true }) 55 | APPLE 56 | end 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # keynote-client 2 | 3 | `keynote-client` will provide a high level API (like ActiveRecord style) to control your Keynote. 4 | 5 | Currently this project is in alpha stage. It supports these features. 6 | 7 | - Create a new document with specified theme 8 | - Append a new slide with specified master slide 9 | - Update slides 10 | - Save a document 11 | 12 | ## Install 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | ```sh 17 | gem 'keynote-client' 18 | ``` 19 | 20 | And then execute: 21 | 22 | ```sh 23 | $ bundle 24 | ``` 25 | 26 | Or install it yourself as: 27 | 28 | ```sh 29 | $ gem install keynote-client 30 | ``` 31 | 32 | 33 | ## Usage 34 | 35 | ```ruby 36 | require 'keynote-client' 37 | include Keynote 38 | 39 | # Fetch all themes 40 | themes = Theme.all 41 | 42 | # Fetch theme specified name 43 | theme = Theme.find_by(name: 'ブラック').first 44 | 45 | # Create a new document with theme 46 | doc = Document.create(theme: theme, file_path: '/path/to/foo.key') 47 | 48 | # Save a document at file_path 49 | doc.save 50 | 51 | # Initialize a new slide 52 | slide = Slide.new("タイトル & 箇条書き", title: 'Pen', body: ["This is a pen", "Is this a pen?"].join("\n")) 53 | 54 | # Append slides 55 | doc.slides << slide 56 | 57 | # Fetch last slide 58 | slide = doc.slides.last 59 | 60 | # Update a slide 61 | slide.title = "About pen" 62 | slide.body = "Hello, pen." 63 | ``` 64 | 65 | ## Supported OS 66 | - OS X El Capitan 67 | - OS X Yosemite 68 | 69 | 70 | ## License 71 | `keynote-client` is released under the MIT License. 72 | 73 | 74 | ## Contributing 75 | 76 | 1. Fork it ( https://github.com/katsuma/keynote-client/fork ) 77 | 2. Create your feature branch (`git checkout -b my-new-feature`) 78 | 3. Commit your changes (`git commit -am 'Add some feature'`) 79 | 4. Push to the branch (`git push origin my-new-feature`) 80 | 5. Create a new Pull Request 81 | -------------------------------------------------------------------------------- /lib/keynote/slide_array.rb: -------------------------------------------------------------------------------- 1 | require 'unindent' 2 | require 'keynote/util' 3 | 4 | module Keynote 5 | module ArrayMethods 6 | def <<(slide) 7 | raise ArgumentError.new "master_slide_name is not specified" unless slide.base_slide 8 | 9 | title = slide.title.gsub(/(\r\n|\r|\n)/) { '\\n' } 10 | body = slide.body.gsub(/(\r\n|\r|\n)/) { '\\n' } 11 | 12 | result = eval_script <<-APPLE.unindent 13 | var Keynote = Application("Keynote") 14 | var doc = Keynote.documents.byId("#{self.document.id}") 15 | var masterSlide = doc.masterSlides.whose({name: "#{slide.base_slide}"}).first 16 | var slide = Keynote.Slide({ baseSlide: masterSlide }) 17 | doc.slides.push(slide) 18 | slide = doc.slides()[doc.slides().length - 1] 19 | slide.defaultTitleItem.objectText = "#{title}" 20 | slide.defaultBodyItem.objectText = "#{body}" 21 | 22 | var slideResult = { 23 | body_showing: slide.bodyShowing(), 24 | skipped: slide.skipped(), 25 | slide_number: slide.slideNumber(), 26 | title_showing: slide.titleShowing(), 27 | body: slide.defaultBodyItem.objectText(), 28 | title: slide.defaultTitleItem.objectText(), 29 | presenter_notes: slide.presenterNotes(), 30 | transition_properties: slide.transitionProperties() 31 | } 32 | JSON.stringify(slideResult) 33 | APPLE 34 | 35 | slide.document = self.document 36 | slide.body_showing = result["body_showing"] 37 | slide.skipped = result["skipped"] 38 | slide.slide_number = result["slide_number"] 39 | slide.title_showing = result["title_showing"] 40 | slide.presenter_notes = result["presenter_notes"] 41 | slide.transition_properties = result["transition_properties"] 42 | 43 | super 44 | end 45 | end 46 | 47 | class SlideArray < Array 48 | prepend ArrayMethods 49 | attr_accessor :document 50 | 51 | include Keynote::Util 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /spec/keynote/slide_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Keynote::Slide do 4 | describe '#initialize' do 5 | context 'when base_slide is not given' do 6 | it 'raises an ArgumentError' do 7 | expect { described_class.new }.to raise_error(ArgumentError) 8 | end 9 | end 10 | 11 | context 'when base_slide is given' do 12 | it 'returns Slide instance' do 13 | slide = described_class.new('base_title', title: 'foo') 14 | expect(slide).to be_a(described_class) 15 | expect(slide.title).to eq('foo') 16 | end 17 | end 18 | end 19 | 20 | describe '#title=' do 21 | subject { slide.title=('new_title') } 22 | 23 | context 'when document is not set' do 24 | let(:slide) { described_class.new('base_slide') } 25 | 26 | it 'does not eval script to update Keynote' do 27 | expect(slide).not_to receive(:eval_script) 28 | subject 29 | end 30 | end 31 | 32 | context 'when slide_number is not set'do 33 | let(:slide) { described_class.new('base_slide', slide_number: 1) } 34 | 35 | it 'does not eval script to update Keynote' do 36 | expect(slide).not_to receive(:eval_script) 37 | subject 38 | end 39 | end 40 | 41 | context 'when both argument is set' do 42 | let(:slide) do 43 | described_class.new('base_slide', 44 | document: Keynote::Document.new(id: 'some-document-id'), 45 | slide_number: 1 46 | ) 47 | end 48 | 49 | it 'evals script to update Keynote' do 50 | allow(Open3).to receive(:capture2).with(/osascript -l JavaScript/).and_return(["", 1]) 51 | expect(slide).to receive(:eval_script).with(/new_title/) 52 | subject 53 | expect(slide.title).to eq('new_title') 54 | end 55 | end 56 | end 57 | 58 | describe '#body=' do 59 | subject { slide.body=(new_body) } 60 | 61 | let(:new_body) { 'new_body' } 62 | 63 | context 'when document is not set' do 64 | let(:slide) { described_class.new('base_slide') } 65 | 66 | it 'does not eval script to update Keynote' do 67 | expect(slide).not_to receive(:eval_script) 68 | subject 69 | end 70 | end 71 | 72 | context 'when slide_number is not set'do 73 | let(:slide) { described_class.new('base_slide', slide_number: 1) } 74 | 75 | it 'does not eval script to update Keynote' do 76 | expect(slide).not_to receive(:eval_script) 77 | subject 78 | end 79 | end 80 | 81 | context 'when document is set' do 82 | let(:slide) do 83 | described_class.new('base_slide', 84 | document: Keynote::Document.new(id: 'some-document-id'), 85 | slide_number: 1 86 | ) 87 | end 88 | 89 | it 'evals script to update Keynote' do 90 | allow(Open3).to receive(:capture2).with(/osascript -l JavaScript/).and_return(["", 1]) 91 | expect(slide).to receive(:eval_script).with(/new_body/) 92 | subject 93 | expect(slide.body).to eq('new_body') 94 | end 95 | 96 | context 'and when new_body includes new-line character' do 97 | let(:new_body) { 'new\nbody' } 98 | 99 | it 'evals script to update Keynote with escaped new body' do 100 | allow(Open3).to receive(:capture2).with(/osascript -l JavaScript/).and_return(["", 1]) 101 | expect(slide).to receive(:eval_script).with(/new\\nbody/) 102 | subject 103 | expect(slide.body).to eq('new\\nbody') 104 | end 105 | end 106 | end 107 | end 108 | end 109 | -------------------------------------------------------------------------------- /lib/keynote/document.rb: -------------------------------------------------------------------------------- 1 | require 'keynote/util' 2 | 3 | module Keynote 4 | class Document 5 | extend Keynote::Util 6 | include Keynote::Util 7 | 8 | attr_accessor( 9 | :name, 10 | :slides, 11 | :master_slides, 12 | :slide_numbers_showing, 13 | :document_theme, 14 | :auto_loop, 15 | :auto_play, 16 | :auto_restart, 17 | :maximum_idle_duration, 18 | :current_slide, 19 | :height, 20 | :width, 21 | :file_path, 22 | ) 23 | 24 | attr_reader :id 25 | 26 | DEFAULT_WIDTH = 1024 27 | DEFAULT_HEIGHT = 768 28 | WIDE_WIDTH = 1900 29 | WIDE_HEIGHT = 1080 30 | 31 | def initialize(arguments = {}) 32 | @document_theme = arguments[:theme] || Theme.default 33 | @width = arguments.has_key?(:wide) && arguments[:wide] ? WIDE_WIDTH : DEFAULT_WIDTH 34 | @height = arguments.has_key?(:wide) && arguments[:wide] ? WIDE_HEIGHT : DEFAULT_HEIGHT 35 | @file_path = arguments[:file_path] 36 | @id = arguments[:id] 37 | @maximum_idle_duration = arguments[:maximumIdleDuration] 38 | @current_slide = arguments[:currentSlide] 39 | @slide_numbers_showing = arguments[:slideNumbersShowing] 40 | @auto_loop = arguments[:autoLoop] 41 | @auto_play = arguments[:autoPlay] 42 | @auto_restart = arguments[:autoRestart] 43 | @maximum_idle_duration = arguments[:maximumIdleDuration] 44 | @name = arguments[:name] 45 | end 46 | 47 | def master_slides 48 | results = eval_script <<-APPLE.unindent 49 | var Keynote = Application("Keynote") 50 | var doc = Keynote.documents.byId("#{id}") 51 | var masterSlides = doc.masterSlides() 52 | var results = [] 53 | for(var i=0; i e 119 | false 120 | end 121 | 122 | class DocumentInvalid < RuntimeError; end 123 | 124 | def save! 125 | raise DocumentInvalid unless save 126 | end 127 | 128 | def export 129 | # TBD 130 | end 131 | 132 | def self.create(arguments = {}) 133 | theme = arguments[:theme] || Theme.default 134 | width = arguments[:wide] ? WIDE_WIDTH : DEFAULT_WIDTH 135 | height = arguments[:wide] ? WIDE_HEIGHT : DEFAULT_HEIGHT 136 | file_path = arguments[:file_path] 137 | 138 | result = eval_script <<-APPLE.unindent 139 | var Keynote = Application("Keynote") 140 | var theme = Keynote.themes.whose({ id: "#{theme.id}" }).first 141 | var doc = Keynote.Document({ documentTheme: theme, width: #{width}, height: #{height} }); 142 | Keynote.documents.push(doc); 143 | JSON.stringify(doc.properties()); 144 | APPLE 145 | 146 | self.new(symbolize_keys(result).merge(theme: theme, width: width, height: height, file_path: file_path)) 147 | end 148 | 149 | def self.all 150 | self.find_with_conditions 151 | end 152 | 153 | def self.find_by(args) 154 | raise ArgumentError.new('nil argument is given') unless args 155 | 156 | if args.is_a?(Hash) && args.has_key?(:id) 157 | conditions = ".whose({ id: '#{args[:id]}' })" 158 | else 159 | raise ArgumentError.new('Unsupported argument is given') 160 | end 161 | 162 | find_with_conditions(conditions) 163 | end 164 | 165 | def self.current 166 | self.all.first 167 | end 168 | 169 | private 170 | 171 | def self.symbolize_keys(hash) 172 | Hash[hash.map { |k, v| [k.to_sym, v] }] 173 | end 174 | 175 | def self.find_with_conditions(conditions = '') 176 | results = eval_script <<-APPLE.unindent 177 | var documents = Application("Keynote").documents#{conditions}; 178 | var results = []; 179 | for(var i = 0, len = documents.length; i < len; i++) { 180 | results.push(documents[i].properties()); 181 | } 182 | JSON.stringify(results); 183 | APPLE 184 | 185 | return [] unless results 186 | 187 | results.map do |result| 188 | self.new(symbolize_keys(result)) 189 | end 190 | end 191 | end 192 | end 193 | --------------------------------------------------------------------------------