├── lib ├── md2key.rb └── md2key │ ├── version.rb │ ├── nodes │ ├── code.rb │ ├── table.rb │ ├── presentation.rb │ ├── slide.rb │ └── line.rb │ ├── nodes.rb │ ├── highlight.rb │ ├── config_builder.rb │ ├── diagram.rb │ ├── config_loader.rb │ ├── configuration.rb │ ├── cli.rb │ ├── renderer.rb │ ├── parser.rb │ └── keynote.rb ├── Rakefile ├── assets ├── advanced.gif ├── default.key ├── background.png └── setup_master_slide.png ├── exe └── md2key ├── scripts ├── create_empty_slide.scpt.erb ├── paste_clipboard.scpt.erb ├── delete_slide.scpt.erb ├── slides_count.scpt.erb ├── fetch_master_slide_name.scpt.erb ├── insert_note.scpt.erb ├── fetch_master_slide_names.scpt.erb ├── activate_slide.scpt.erb ├── delete_extra_slides.scpt.erb ├── insert_code_background.scpt.erb ├── update_slide.scpt.erb ├── update_table.scpt.erb ├── create_slide_and_write_body.scpt.erb ├── create_slide_and_select_body.scpt.erb ├── insert_image.scpt.erb ├── paste_and_indent.scpt.erb └── create_slide_and_insert_table.scpt.erb ├── .gitignore ├── bin ├── setup └── console ├── Gemfile ├── .github └── workflows │ ├── main.yml │ └── release.yml ├── LICENSE.txt ├── md2key.gemspec ├── CHANGELOG.md └── README.md /lib/md2key.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/cli' 2 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | -------------------------------------------------------------------------------- /lib/md2key/version.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | VERSION = '0.10.2' 3 | end 4 | -------------------------------------------------------------------------------- /assets/advanced.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/md2key/HEAD/assets/advanced.gif -------------------------------------------------------------------------------- /assets/default.key: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/md2key/HEAD/assets/default.key -------------------------------------------------------------------------------- /assets/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/md2key/HEAD/assets/background.png -------------------------------------------------------------------------------- /assets/setup_master_slide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/md2key/HEAD/assets/setup_master_slide.png -------------------------------------------------------------------------------- /exe/md2key: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | $:.unshift File.expand_path('./lib', __dir__) 4 | require 'md2key' 5 | 6 | Md2key::CLI.start(ARGV) 7 | -------------------------------------------------------------------------------- /scripts/create_empty_slide.scpt.erb: -------------------------------------------------------------------------------- 1 | tell application "Keynote" 2 | tell the front document 3 | make new slide 4 | end 5 | end tell 6 | -------------------------------------------------------------------------------- /lib/md2key/nodes/code.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module Nodes 3 | class Code < Struct.new(:source, :extension) 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/md2key/nodes/table.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module Nodes 3 | class Table < Struct.new(:rows, :columns, :data) 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /lib/md2key/nodes/presentation.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module Nodes 3 | class Presentation < Struct.new(:cover, :slides) 4 | end 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | .DS_Store 11 | .md2key 12 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/md2key/nodes.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/nodes/code' 2 | require 'md2key/nodes/line' 3 | require 'md2key/nodes/presentation' 4 | require 'md2key/nodes/slide' 5 | require 'md2key/nodes/table' 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in md2key.gemspec 4 | gemspec 5 | 6 | if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.5") 7 | gem 'listen', '>= 3.0' 8 | end 9 | -------------------------------------------------------------------------------- /scripts/paste_clipboard.scpt.erb: -------------------------------------------------------------------------------- 1 | tell application "Keynote" 2 | tell application "System Events" 3 | tell application process "Keynote" 4 | keystroke "v" using { command down } 5 | end tell 6 | end tell 7 | end tell 8 | -------------------------------------------------------------------------------- /scripts/delete_slide.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideIndex to item 1 of argv as number 3 | 4 | tell application "Keynote" 5 | tell the front document 6 | delete slide slideIndex 7 | end tell 8 | end tell 9 | end run 10 | -------------------------------------------------------------------------------- /scripts/slides_count.scpt.erb: -------------------------------------------------------------------------------- 1 | tell application "Keynote" 2 | tell the front document 3 | set n to 0 4 | repeat with s in slides 5 | set n to n + 1 6 | end repeat 7 | end tell 8 | 9 | do shell script "echo " & n 10 | end tell 11 | -------------------------------------------------------------------------------- /lib/md2key/nodes/slide.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module Nodes 3 | class Slide < Struct.new(:title, :lines, :image, :code, :table, :note, :level) 4 | def initialize(*) 5 | super 6 | self.lines ||= [] 7 | end 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "md2key" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | require 'irb' 10 | IRB.start 11 | -------------------------------------------------------------------------------- /lib/md2key/nodes/line.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module Nodes 3 | class Line < Struct.new(:text, :indent) 4 | def initialize(*) 5 | super 6 | self.indent ||= 0 7 | end 8 | 9 | def indented? 10 | indent > 0 11 | end 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /scripts/fetch_master_slide_name.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideIndex to item 1 of argv as number 3 | 4 | tell application "Keynote" 5 | tell the front document 6 | set masterName to the name of the base slide of slide slideIndex 7 | return masterName 8 | end tell 9 | end tell 10 | end run 11 | -------------------------------------------------------------------------------- /scripts/insert_note.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set lastIndex to item 1 of argv as number 3 | set theNote to item 2 of argv 4 | 5 | tell application "Keynote" 6 | tell the front document 7 | tell slide lastIndex 8 | set presenter notes to theNote 9 | end tell 10 | end tell 11 | end tell 12 | end run 13 | -------------------------------------------------------------------------------- /scripts/fetch_master_slide_names.scpt.erb: -------------------------------------------------------------------------------- 1 | tell application "Keynote" 2 | tell the front document 3 | set the ret to "" 4 | 5 | set the masterSlideNames to the name of every master slide 6 | repeat with masterName in the masterSlideNames 7 | set the ret to ret & "\n" & masterName 8 | end repeat 9 | return ret 10 | end tell 11 | end tell 12 | -------------------------------------------------------------------------------- /scripts/activate_slide.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideIndex to item 1 of argv as number 3 | 4 | tell application "Keynote" 5 | -- This can't be removed because using Cmd-v to paste. 6 | activate 7 | 8 | tell the front document 9 | -- Workaround to focus on specified slide. 10 | move slide slideIndex to before slide slideIndex 11 | end tell 12 | end tell 13 | end run 14 | -------------------------------------------------------------------------------- /scripts/delete_extra_slides.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slidesCount to item 1 of argv as number 3 | 4 | tell application "Keynote" 5 | tell the front document 6 | set i to slidesCount 7 | 8 | repeat with s in slides 9 | if i > 2 then 10 | delete slide i 11 | end if 12 | 13 | set i to i - 1 14 | end repeat 15 | end tell 16 | end tell 17 | end run 18 | -------------------------------------------------------------------------------- /scripts/insert_code_background.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set lastIndex to item 1 of argv as number 3 | set theImage to item 2 of argv as POSIX file 4 | 5 | tell application "Keynote" 6 | tell the front document 7 | set docWidth to its width 8 | set docHeight to its height 9 | 10 | tell slide lastIndex 11 | make new image with properties { opacity: 80, file: theImage, width: docWidth - 180, position: { 90, 240 } } 12 | end tell 13 | end tell 14 | end tell 15 | end run 16 | -------------------------------------------------------------------------------- /scripts/update_slide.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideTitle to item 1 of argv 3 | set slideBody to item 2 of argv 4 | set masterName to item 3 of argv as string 5 | set slideIndex to item 4 of argv as number 6 | 7 | tell application "Keynote" 8 | tell the front document 9 | set the base slide of the slide slideIndex to master slide masterName 10 | tell slide slideIndex 11 | set object text of default title item to slideTitle 12 | set object text of default body item to slideBody 13 | end tell 14 | end tell 15 | end tell 16 | end run 17 | -------------------------------------------------------------------------------- /scripts/update_table.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideIndex to item 1 of argv as number 3 | set rowIndex to item 2 of argv as number 4 | 5 | tell application "Keynote" 6 | tell the front document 7 | tell slide slideIndex 8 | set thisTable to the first table 9 | tell thisTable 10 | set idx to 1 11 | repeat with i in items 3 thru end of argv 12 | tell cell rowIndex of column idx 13 | set value to i 14 | set the font size to 12 15 | end tell 16 | set idx to idx + 1 17 | end repeat 18 | end tell 19 | end tell 20 | end tell 21 | end tell 22 | end run 23 | -------------------------------------------------------------------------------- /scripts/create_slide_and_write_body.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideTitle to item 1 of argv 3 | set slideBody to item 2 of argv 4 | set masterName to item 3 of argv as string 5 | set templateIndex to item 4 of argv as number 6 | 7 | tell application "Keynote" 8 | tell the front document 9 | set newSlide to make new slide with properties {base slide:master slide masterName} 10 | tell newSlide 11 | set object text of default title item to slideTitle 12 | set object text of default body item to slideBody 13 | end tell 14 | end tell 15 | end tell 16 | end run 17 | -------------------------------------------------------------------------------- /lib/md2key/highlight.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | class Highlight 3 | DEFAULT_EXTENSION = "txt" 4 | class << self 5 | def pbcopy_highlighted_code(code) 6 | ensure_highlight_availability 7 | 8 | extension = code.extension || DEFAULT_EXTENSION 9 | 10 | IO.popen("highlight -O rtf -K 28 -s rdark -k Monaco -S #{extension} -u utf-8 | pbcopy", 'w+') do |highlight| 11 | highlight.write(code.source) 12 | highlight.close 13 | end 14 | end 15 | 16 | private 17 | 18 | def ensure_highlight_availability 19 | return if system('which -s highlight') 20 | 21 | abort "`highlight` is not available. Try `brew install highlight`." 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /scripts/create_slide_and_select_body.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideTitle to item 1 of argv 3 | set masterName to item 2 of argv as string 4 | set templateIndex to item 3 of argv as number 5 | 6 | tell application "Keynote" 7 | activate 8 | 9 | tell the front document 10 | set newSlide to make new slide with properties {base slide:master slide masterName} 11 | tell newSlide 12 | set object text of default title item to slideTitle 13 | 14 | -- select the default body item 15 | set position of default body item to position of default body item 16 | -- press the Enter key 17 | tell application "System Events" to key code 76 18 | end tell 19 | end tell 20 | end tell 21 | end run 22 | -------------------------------------------------------------------------------- /lib/md2key/config_builder.rb: -------------------------------------------------------------------------------- 1 | module Md2key 2 | module ConfigBuilder 3 | class << self 4 | def build(skip_options: true) 5 | master_names = Keynote.fetch_master_slide_names 6 | masters = master_names.map.with_index do |name, i| 7 | build_master_config(name, i, skip_options: skip_options) 8 | end 9 | ["masters:\n", *masters].join 10 | end 11 | 12 | private 13 | 14 | def build_master_config(master_name, index, skip_options: true) 15 | " - name: #{master_name.inspect}\n".tap do |config| 16 | next if skip_options 17 | 18 | if index == 0 19 | config << " cover: true\n" 20 | elsif (1..3).cover?(index) 21 | config << " template: #{index}\n" 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | tests: 7 | strategy: 8 | fail-fast: false 9 | 10 | matrix: 11 | os: 12 | - macos-14 13 | - macos-15 14 | 15 | ruby-version: 16 | - '2.6' 17 | - '2.7' 18 | - '3.0' 19 | - '3.1' 20 | - '3.2' 21 | - '3.3' 22 | - '3.4' 23 | - ruby-head 24 | 25 | runs-on: ${{ matrix.os }} 26 | steps: 27 | - uses: actions/checkout@v5 28 | 29 | - uses: ruby/setup-ruby@v1 30 | with: 31 | ruby-version: ${{ matrix.ruby-version }} 32 | 33 | - run: | 34 | bundle install 35 | bundle exec exe/md2key # Test only syntax and basic functionality 36 | continue-on-error: ${{ matrix.ruby-version == 'ruby-head' }} 37 | -------------------------------------------------------------------------------- /scripts/insert_image.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set lastIndex to item 1 of argv as number 3 | set theImage to item 2 of argv as POSIX file 4 | set templateIndex to item 3 of argv as number 5 | 6 | tell application "Keynote" 7 | tell the front document 8 | set docWidth to its width 9 | set docHeight to its height 10 | 11 | -- Create temporary slide to fix the image size 12 | tell slide templateIndex 13 | set imgFile to make new image with properties { file: theImage, width: docWidth / 3 } 14 | tell imgFile 15 | set imgWidth to its width 16 | set imgHeight to its height 17 | end tell 18 | end tell 19 | 20 | tell slide lastIndex 21 | make new image with properties { file: theImage, width: imgWidth, position: { docWidth - imgWidth - 60, docHeight / 2 - imgHeight / 2 } } 22 | end tell 23 | end tell 24 | end tell 25 | end run 26 | -------------------------------------------------------------------------------- /scripts/paste_and_indent.scpt.erb: -------------------------------------------------------------------------------- 1 | -- NOTE: Using delay to prevent pbcopy overlap before pasting event. 2 | -- We should think of a good way to prevent it or provide an option 3 | -- to change the delay... 4 | on run argv 5 | set indent to item 1 of argv as number 6 | set insertNewline to item 2 of argv as boolean 7 | 8 | tell application "System Events" 9 | delay 0.1 -- TODO: this should be configurable or removed 10 | 11 | keystroke "v" using { command down } 12 | 13 | if indent is greater than 0 then 14 | repeat indent times 15 | key code 48 -- tab 16 | end repeat 17 | else 18 | if indent is less than 0 then 19 | repeat indent * -1 times 20 | key code 48 using { shift down } -- tab 21 | end repeat 22 | end if 23 | end if 24 | 25 | if insertNewline is true then 26 | delay 0.05 -- TODO: this should be configurable or removed 27 | 28 | key code 36 -- return 29 | end if 30 | end tell 31 | end run 32 | -------------------------------------------------------------------------------- /scripts/create_slide_and_insert_table.scpt.erb: -------------------------------------------------------------------------------- 1 | on run argv 2 | set slideTitle to item 1 of argv 3 | set templateIndex to item 2 of argv as number 4 | set rowCount to item 3 of argv as number 5 | set columnCount to item 4 of argv as number 6 | set masterName to item 5 of argv as string 7 | 8 | set headerRowCount to 1 9 | set headerColumnCount to 0 10 | set footerRowCount to 0 11 | 12 | tell application "Keynote" 13 | set thisDocument to the front document 14 | tell thisDocument 15 | set newSlide to make new slide with properties {base slide:master slide masterName} 16 | tell newSlide 17 | try 18 | set base slide to master slide "Title - Top" of thisDocument 19 | end try 20 | set object text of default title item to slideTitle 21 | set newTable to make new table with properties {column count:columnCount, row count:rowCount, footer row count:footerRowCount,header column count:headerColumnCount,header row count:headerRowCount} 22 | end tell 23 | end tell 24 | end tell 25 | end run 26 | -------------------------------------------------------------------------------- /lib/md2key/diagram.rb: -------------------------------------------------------------------------------- 1 | require 'tempfile' 2 | 3 | module Md2key 4 | class Diagram 5 | class << self 6 | # SEQUENCE_CONFIG_PATH = File.expand_path('../../assets/mermaid_sequence.config', __dir__) 7 | # GANTT_CONFIG_PATH = File.expand_path('../../assets/mermaid_gantt.config', __dir__) 8 | def generate_image_file(code) 9 | ensure_mermaid_availability 10 | 11 | file = Tempfile.new("diagram-#{code.extension}") 12 | file.write(code.source) 13 | file.close 14 | 15 | output_dir = File.dirname(file.path) 16 | image_path = "" 17 | IO.popen("mmdc -i #{file.path} --output #{file.path}.png", 'r') do |info| 18 | info.read 19 | image_path = "#{file.path}.png" 20 | file.unlink 21 | end 22 | 23 | return image_path 24 | end 25 | 26 | private 27 | 28 | def ensure_mermaid_availability 29 | return if system('which -s mmdc') 30 | 31 | abort "`mmdc` is not available. Try `npm install -g mermaid.cli`." 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /lib/md2key/config_loader.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/configuration' 2 | 3 | module Md2key 4 | class ConfigLoader 5 | class << self 6 | # @param [Array] paths - paths of YAML configs. Latter config overwrites former ones. 7 | def load(*paths) 8 | hash = {} 9 | paths.each do |path| 10 | hash.merge!(load_if_available(File.expand_path(path))) 11 | end 12 | Configuration.new(**symbolize_keys(hash)) 13 | end 14 | 15 | private 16 | 17 | def load_if_available(path) 18 | if File.exist?(path) 19 | YAML.load(File.read(path)) 20 | else 21 | {} 22 | end 23 | end 24 | 25 | def symbolize_keys(object) 26 | case object 27 | when Hash 28 | Hash.new.tap do |result| 29 | object.each do |key, value| 30 | result[key.to_sym] = symbolize_keys(value) 31 | end 32 | end 33 | when Array 34 | object.map(&method(:symbolize_keys)) 35 | else 36 | object 37 | end 38 | end 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Takashi Kokubun 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /md2key.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'md2key/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'md2key' 8 | spec.version = Md2key::VERSION 9 | spec.authors = ['Takashi Kokubun'] 10 | spec.email = ['takashikkbn@gmail.com'] 11 | 12 | spec.summary = 'Convert markdown to keynote' 13 | spec.description = 'Convert markdown to keynote' 14 | spec.homepage = 'https://github.com/k0kubun/md2key' 15 | spec.license = 'MIT' 16 | 17 | spec.files = `git ls-files -z`.split("\x0").reject do |f| 18 | f.match(%r{^(test|spec|features)/}) 19 | end 20 | spec.bindir = 'exe' 21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } 22 | spec.require_paths = ['lib'] 23 | 24 | spec.metadata['changelog_uri'] = "#{spec.homepage}/blob/v#{spec.version}/CHANGELOG.md" 25 | 26 | # macOS system ruby is 2.6.10 as of darwin25 27 | spec.required_ruby_version = '>= 2.6.0' 28 | 29 | spec.add_dependency 'thor', '>= 0.19' 30 | spec.add_dependency 'redcarpet', '>= 3.3' 31 | spec.add_dependency 'oga', '>= 1.2' 32 | spec.add_development_dependency 'bundler' 33 | spec.add_development_dependency 'rake' 34 | end 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | permissions: 9 | contents: read 10 | 11 | jobs: 12 | push: 13 | if: github.repository_owner == 'k0kubun' 14 | runs-on: ubuntu-latest 15 | 16 | environment: 17 | name: rubygems.org 18 | url: https://rubygems.org/gems/md2key 19 | 20 | permissions: 21 | contents: write 22 | id-token: write 23 | 24 | steps: 25 | - name: Harden Runner 26 | uses: step-security/harden-runner@f4a75cfd619ee5ce8d5b864b0d183aff3c69b55a # v2.13.1 27 | with: 28 | egress-policy: audit 29 | 30 | - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 31 | 32 | - name: Set up Ruby 33 | uses: ruby/setup-ruby@eaecf785f6a34567a6d97f686bbb7bccc1ac1e5c # v1.237.0 34 | with: 35 | bundler-cache: true 36 | ruby-version: ruby 37 | 38 | - name: Publish to RubyGems 39 | uses: rubygems/release-gem@a25424ba2ba8b387abc8ef40807c2c85b96cbe32 # v1.1.1 40 | 41 | - name: Create GitHub release 42 | run: | 43 | tag_name="$(git describe --tags --abbrev=0)" 44 | gh release create "${tag_name}" --verify-tag --generate-notes 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | -------------------------------------------------------------------------------- /lib/md2key/configuration.rb: -------------------------------------------------------------------------------- 1 | require 'yaml' 2 | 3 | module Md2key 4 | class Configuration 5 | class Master 6 | # @param [String] name - TODO: reject `nil` after Ruby 2.0 support is dropped 7 | # @param [TrueClass,FalseClass,nil] cover 8 | # @param [Integer,nil] template 9 | def initialize(name: nil, cover: nil, template: nil) 10 | @name = name 11 | @cover = cover 12 | @template = template 13 | end 14 | 15 | attr_reader :name, :cover, :template 16 | end 17 | 18 | # @param [Array String,Integer,TrueClass,FalseClass }>] masters 19 | def initialize(masters: []) 20 | @masters = masters.map { |m| Master.new(**m) } 21 | validate! 22 | end 23 | 24 | # @return [String,nil] 25 | def cover_master 26 | master = @masters.find do |master| 27 | master.cover 28 | end 29 | master && master.name 30 | end 31 | 32 | # @param [Integer] level 33 | # @return [String,nil] 34 | def slide_master(level) 35 | master = @masters.find do |master| 36 | master.template == level 37 | end 38 | master && master.name 39 | end 40 | 41 | private 42 | 43 | def validate! 44 | if @masters.select(&:cover).length > 1 45 | abort "Config error!\n`cover: true` cannot be specified multiple times" 46 | end 47 | (1..5).each do |level| 48 | if @masters.select { |m| m.template == level }.length > 1 49 | abort "`Config error!\ntemplate: #{level}` cannot be specified multiple times" 50 | end 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /lib/md2key/cli.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/config_builder' 2 | require 'md2key/config_loader' 3 | require 'md2key/parser' 4 | require 'md2key/renderer' 5 | require 'thor' 6 | require 'yaml' 7 | 8 | module Md2key 9 | class CLI < Thor 10 | desc 'convert MARKDOWN', 'Convert markdown to keynote' 11 | def convert(path) 12 | abort "md2key: `#{path}` does not exist" unless File.exist?(path) 13 | 14 | markdown = File.read(path) 15 | config = ConfigLoader.load('~/.md2key', './.md2key') 16 | ast = Parser.new.parse(markdown) 17 | Renderer.new(config).render!(ast) 18 | end 19 | 20 | desc 'init', 'Put .md2key template to current directory' 21 | option :skip_options, type: :boolean, default: false, aliases: %w[-n] 22 | def init 23 | yaml = ConfigBuilder.build(skip_options: options[:skip_options]) 24 | File.write('.md2key', yaml) 25 | puts "# Successfully generated .md2key!\n#{yaml}" 26 | end 27 | 28 | desc 'listen', 'Update the *.key file when each saves' 29 | def listen(path) 30 | require 'listen' 31 | puts 'Watching the *.md file...' 32 | listener = Listen.to('./', only: /\.md$/) do |_| 33 | convert(path) && (puts "The *.key file has been updated. let's open *.key file!") 34 | end 35 | listener.start 36 | sleep 37 | rescue Interrupt 38 | puts 'Bye.' 39 | end 40 | 41 | private 42 | 43 | # Shorthand for `md2key convert *.md` 44 | def method_missing(*args) 45 | path = args.first.to_s 46 | if args.length == 1 && path.end_with?('.md') 47 | convert(path) 48 | else 49 | return super(*args) 50 | end 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/md2key/renderer.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/configuration' 2 | require 'md2key/keynote' 3 | 4 | module Md2key 5 | class Renderer 6 | # Magic number index for master slide named "cover" 7 | COVER_LEVEL = 0 8 | 9 | # @param [Md2key::Configuration] config 10 | def initialize(config) 11 | @config = config 12 | end 13 | 14 | # @param [Md2key::Nodes::Presentation] ast 15 | def render!(ast) 16 | prepare_document_base 17 | generate_contents(ast) 18 | ensure 19 | Keynote.delete_template_slide 20 | end 21 | 22 | private 23 | 24 | def prepare_document_base 25 | Keynote.ensure_template_slide_availability 26 | Keynote.delete_extra_slides 27 | end 28 | 29 | # @param [Md2key::Nodes::Presentation] ast 30 | def generate_contents(ast) 31 | first_master = Keynote.fetch_master_slide_name(1) 32 | second_master = Keynote.fetch_master_slide_name(2) 33 | master_by_level = fetch_master_by_level 34 | 35 | cover_master = @config.cover_master || master_by_level.fetch(COVER_LEVEL, first_master) 36 | Keynote.update_cover(ast.cover, cover_master) 37 | 38 | ast.slides.each do |slide| 39 | slide_master = @config.slide_master(slide.level) || master_by_level.fetch(slide.level, second_master) 40 | 41 | if slide.table 42 | Keynote.create_slide_with_table(slide, slide.table.rows, slide.table.columns, slide_master) 43 | Keynote.insert_table(slide.table.data) 44 | else 45 | Keynote.create_slide(slide, slide_master) 46 | Keynote.insert_image(slide.image) if slide.image 47 | Keynote.insert_code(slide.code) if slide.code 48 | end 49 | 50 | Keynote.insert_note(slide.note) if slide.note 51 | end 52 | end 53 | 54 | # Find master names like "h1", "h2", ... and return { 1 => "h1", 2 => "h2" } only for available ones. 55 | # @return [Hash{ Integer => String }] 56 | def fetch_master_by_level 57 | masters = Keynote.fetch_master_slide_names 58 | 59 | {}.tap do |result| 60 | masters.each do |master| 61 | if master.match(/\Ah(?[1-5])\z/) 62 | level = Integer(Regexp.last_match[:level]) 63 | result[level] = master 64 | elsif master == 'cover' 65 | result[COVER_LEVEL] = 'cover' 66 | end 67 | end 68 | end 69 | end 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.10.2 2 | 3 | - Bump required Ruby version to 2.6 4 | - Add `changelog_uri` to gem metadata 5 | 6 | # v0.10.1 7 | 8 | - Relax dependency version requirements 9 | 10 | # v0.10.0 11 | 12 | - Switch from `mermaid` to `mmdc` to support newer Mermaid 13 | https://github.com/k0kubun/md2key/pull/48 14 | 15 | # v0.9.5 16 | 17 | - Fix keyword argument errors with slide theme selection for Ruby 3 18 | 19 | # v0.9.4 20 | 21 | - Fix keyword argument warnings for Ruby 2.7 22 | 23 | # v0.9.3 24 | 25 | - Extended delay to insert indented list 26 | 27 | # v0.9.2 28 | 29 | - Fix error on rendering diagram 30 | 31 | # v0.9.1 32 | 33 | - Resurrect Ruby 2.0.0 support which is accidentally dropped in v0.9.0 34 | 35 | # v0.9.0 36 | 37 | - Use master slides named "cover", "h1", "h2", ... 38 | - See https://github.com/k0kubun/md2key/pull/46 for details 39 | 40 | # v0.8.4 41 | 42 | - Add `md2key listen` subcommand to update Keynote by watching file 43 | - https://github.com/k0kubun/md2key/pull/44 44 | - Thanks to @sachin21 45 | 46 | # v0.8.3 47 | 48 | - Resurrect Ruby 2.0.0 support to work on macOS's default environment 49 | 50 | # v0.8.2 51 | 52 | - Allow highlighting UTF-8 source code 53 | 54 | # v0.8.1 55 | 56 | - Fix bug in converting nested list 57 | 58 | # v0.8.0 59 | 60 | - Add `md2key init` subcommand to generate .md2key 61 | - Allow changing master slide by .md2key 62 | - See https://github.com/k0kubun/md2key/pull/32 for details 63 | 64 | # v0.7.0 65 | 66 | - Support presenter notes 67 | - https://github.com/k0kubun/md2key/pull/28 68 | - Thanks to @amatsuda 69 | 70 | # v0.6.1 71 | 72 | - Add support to draw flowchart and sequence 73 | - https://github.com/k0kubun/md2key/pull/27 74 | - Thanks to @codeant 75 | 76 | # v0.6.0 77 | 78 | - Add table support 79 | - https://github.com/k0kubun/md2key/pull/25 80 | - Thanks to @codeant 81 | 82 | # v0.5.2 83 | 84 | - Use delay before paste to prevent pasting wrong text 85 | 86 | # v0.5.1 87 | 88 | - Escape HTML in markdown 89 | - https://github.com/k0kubun/md2key/issues/15 90 | - Thanks to @liubin 91 | - Avoid using shell to execute AppleScript 92 | - https://github.com/k0kubun/md2key/issues/16 93 | - Thanks to @liubin 94 | 95 | # v0.5.0 96 | 97 | - Support nested list 98 | - https://github.com/k0kubun/md2key/pull/23 99 | 100 | # v0.4.4 101 | 102 | - Bugfix for code listing without extension 103 | - https://github.com/k0kubun/md2key/pull/17 104 | - Thanks to @ainoya 105 | 106 | # v0.4.3 107 | 108 | - Support relative path for images 109 | - https://github.com/k0kubun/md2key/pull/14 110 | - Thanks to @liubin 111 | 112 | # v0.4.2 113 | 114 | - Internal refactorings 115 | - Drop unindent gem dependency 116 | 117 | # v0.4.1 118 | 119 | - Performance improvement 120 | - Stop generating applescript every time 121 | - Thanks to @bogem 122 | 123 | # v0.4.0 124 | 125 | - Support syntax-highlighted source code insert 126 | 127 | # v0.3.3 128 | 129 | - Don't pull the Keynote window in the front 130 | - Always use the front document to convert 131 | 132 | # v0.3.2 133 | 134 | - You no longer need to select the second slide by hand 135 | 136 | # v0.3.1 137 | 138 | - Support inserting images 139 | 140 | # v0.3.0 141 | 142 | - Old extra slides before execution are deleted automatically 143 | - Changed not to fail even if a second slide does not exist 144 | 145 | # v0.2.2 146 | 147 | - Activate a template slide automatically 148 | 149 | # v0.2.1 150 | 151 | - Don't skip a nested list 152 | - But still flattened 153 | 154 | # v0.2.0 155 | 156 | - Use redcarpet as markdown parser 157 | - Support list starting with `*` 158 | - Regard `#`, `##` and `###` as the same 159 | 160 | # v0.1.2 161 | 162 | - Temporarily support skipping `---` 163 | - Still expecting users to write a format like Deckset 164 | - https://github.com/k0kubun/md2key/pull/2 165 | - Thanks to @liubin 166 | 167 | # v0.1.1 168 | 169 | - Fix strange indentation 170 | - https://github.com/k0kubun/md2key/issues/1 171 | 172 | # v0.1.0 173 | 174 | - Initial release 175 | - Parse only `#`, `-` and `---` 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # md2key [![Build Status](https://github.com/k0kubun/md2key/actions/workflows/main.yml/badge.svg)](https://github.com/k0kubun/md2key/actions) 2 | 3 | Convert your markdown to keynote. 4 | 5 | ## Installation 6 | 7 | ```bash 8 | gem install md2key 9 | ``` 10 | 11 | ## Usage 12 | 13 | 1. Create a keynote document 14 | 2. Create a first slide as a cover slide 15 | 3. Create a second slide to choose a slide layout 16 | 4. Then execute `md2key markdown.md` 17 | 18 | ![](https://i.gyazo.com/9d4d00164683f516d44b3e536b3dd3e9.gif) 19 | 20 | ## Advanced Usage 21 | 22 | 1. Open master slide editor 23 | 2. Name "cover", "h1", "h2" ... "h5" to master slides 24 | 3. They will be used for the first slide, and other slides with `#`, `##` ... `#####` 25 | 26 | ![](./assets/advanced.gif) 27 | 28 | ## Features 29 | 30 | ### Basic example 31 | 32 | The slides in the movie can be generated with following markdown. 33 | You can separate slides with `---` just for readability. 34 | 35 | ```markdown 36 | # The presentation 37 | @k0kubun 38 | 39 | ## Hello world 40 | - I'm takashi kokubun 41 | - This is a pen 42 | - Nested item is available 43 | 44 | ## How are you? 45 | - I'm fine thank you 46 | ``` 47 | 48 | ### Insert image 49 | 50 | ```markdown 51 | # image slide 52 | 53 | - This is an example 54 | - You can insert an image 55 | 56 | ![](/Applications/Keynote.app/Contents/Resources/keynote.help/Contents/Resources/GlobalArt/AppLanding_KeynoteP4.png) 57 | ``` 58 | 59 | ### Insert source code 60 | 61 | If you have `highlight` command, you can insert syntax-highlighted source code. 62 | If you don't have it, execute `brew install highlight`. 63 | 64 | 65 | 66 |
 67 | # ActiveRecord::Precount
 68 | 
 69 | ```rb
 70 | Tweet.all.precount(:favorites).each do |tweet|
 71 |   p tweet.favorites.count
 72 | end
 73 | # SELECT `tweets`.* FROM `tweets`
 74 | # SELECT COUNT(`favorites`.`tweet_id`), `favorites`.`tweet_id` FROM `favorites` ...
 75 | ```
 76 | 
77 | 78 | ### Insert table 79 | ```markdown 80 | ## table 81 | 82 | | a | b | c | 83 | |:--|:--|:--| 84 | | 1 | 2 | 3 | 85 | ``` 86 | 87 | 88 | 89 | 90 | ### Insert flowchart & sequence diagram 91 |
 92 | ## flowchart & sequence diagram slide
 93 | 
 94 | ```mermaid
 95 | sequenceDiagram
 96 |     Alice->Bob: Hello Bob, how are you?
 97 |     Note right of Bob: Bob thinks
 98 |     Bob-->Alice: I am good thanks!
 99 |     Bob-->John the Long: How about you John?
100 |     Bob-->Alice: Checking with John...
101 |     Alice->John the Long: Yes... John, how are you?
102 |     John the Long-->Alice: Better then you!
103 | ```
104 | 
105 | 106 | 107 | 108 | ### Insert presenter note 109 | 110 | ```markdown 111 | # Keynote Speech 112 | 113 | - OMG! I'm keynoting! :fearful: 114 | 115 | ^ Remember, what would Freddie Mercury do? Yes, I'm Freddie! We are the champions!! 116 | ``` 117 | 118 | ### Configure master slide by header level (experimental) 119 | 120 | You can change master slide by header level if you have `.md2key`. 121 | 122 | See [#32](https://github.com/k0kubun/md2key/pull/32) for details. 123 | 124 | ## Creating your own template 125 | 126 | Follow the following steps: 127 | 128 | 1. Open keynote application. 129 | 2. Create empty presentation. 130 | 3. Add **two master slides** (required) to the empty presentation: 131 | - First slide is the cover. 132 | - Second slide is the layout. 133 | 134 | ### Sample 135 | You can start to modify and learn from [assets/default.key](https://github.com/k0kubun/md2key/blob/master/assets/default.key?raw=true). 136 | 137 | ### Important tips 138 | In order to make md2key replaces the texts correctly you have to select `Title` and `Body` from the master template. 139 | 140 | 141 | 142 | ## License 143 | 144 | MIT License 145 | -------------------------------------------------------------------------------- /lib/md2key/parser.rb: -------------------------------------------------------------------------------- 1 | require 'md2key/nodes' 2 | require 'oga' 3 | require 'redcarpet' 4 | 5 | # Parse markdown and generate AST. 6 | # This is created to be compatible with Deckset as far as possible. 7 | # See: http://www.decksetapp.com/cheatsheet/ 8 | module Md2key 9 | class Parser 10 | # @param [String] markdown 11 | # @return [Md2key::Nodes::Presentation] ast 12 | def parse(markdown) 13 | slides = parse_slides(markdown) 14 | cover = slides.delete_at(0) 15 | Nodes::Presentation.new(cover, slides) 16 | end 17 | 18 | private 19 | 20 | def parse_slides(markdown) 21 | slides = [] 22 | slide = Nodes::Slide.new 23 | 24 | html_nodes = Oga.parse_xml(to_xhtml(markdown)) 25 | html_nodes.children.each do |node| 26 | next unless node.is_a?(Oga::XML::Element) 27 | 28 | case node.name 29 | when /^h(?[1-9])$/ 30 | # New slide by header. You can skip to write `---`. 31 | # This feature will be removed in the future to provide 32 | # more compatibility with Deckset. 33 | # See: https://github.com/k0kubun/md2key/pull/2 34 | if slides.any? 35 | slide = Nodes::Slide.new 36 | end 37 | 38 | slides << slide 39 | slide.title = node.text 40 | slide.level = Regexp.last_match[:level].to_i 41 | when 'ul' 42 | slide.lines.concat(li_lines(node)) 43 | when 'ol' 44 | slide.lines.concat(li_lines(node)) 45 | when 'table' 46 | row_data = [] 47 | rows = 0 48 | columns = 0 49 | row_text = [] 50 | node.children[0].children.each do |child| 51 | next if !child.is_a?(Oga::XML::Element) || child.name != 'tr' 52 | rows += 1 53 | child.children.each do |td| 54 | next if !td.is_a?(Oga::XML::Element) || td.name != 'th' 55 | columns += 1 56 | row_text << td.text 57 | end 58 | end 59 | row_data << row_text 60 | node.children[1].children.each do |child| 61 | next if !child.is_a?(Oga::XML::Element) || child.name != 'tr' 62 | row_text = [] 63 | child.children.each do |td| 64 | next if !td.is_a?(Oga::XML::Element) || td.name != 'td' 65 | row_text << td.text 66 | end 67 | row_data << row_text 68 | rows += 1 69 | end 70 | slide.table = Nodes::Table.new(rows, columns, row_data) 71 | when 'p' 72 | node.children.each do |child| 73 | if child.is_a?(Oga::XML::Element) && child.name == 'img' 74 | slide.image = child.attribute('src').value 75 | next 76 | elsif child.is_a?(Oga::XML::Text) && child.text.start_with?('^ ') 77 | slide.note = child.text.sub(/^\^ /, '') 78 | next 79 | end 80 | slide.lines << Nodes::Line.new(child.text) 81 | end 82 | when 'pre' 83 | node.children.each do |child| 84 | next if !child.is_a?(Oga::XML::Element) || child.name != 'code' 85 | extension = child.attribute('class') ? child.attribute('class').value : nil 86 | slide.code = Nodes::Code.new(child.text, extension) 87 | end 88 | when 'hr' 89 | # noop 90 | end 91 | end 92 | slides 93 | end 94 | 95 | # @return [Array] 96 | def li_lines(ul_node, indent: 0) 97 | return [] unless ul_node.is_a?(Oga::XML::Element) 98 | return [] if ul_node.name != 'ul' && ul_node.name != 'ol' 99 | 100 | lines = [] 101 | ul_node.children.each do |li_node| 102 | next unless li_node.is_a?(Oga::XML::Element) 103 | next if li_node.name != 'li' 104 | 105 | li_node.children.each do |node| 106 | case node 107 | when Oga::XML::Text 108 | text = node.text.strip 109 | lines << Nodes::Line.new(text, indent) unless text.empty? 110 | when Oga::XML::Element 111 | next if node.name != 'ul' 112 | lines.concat(li_lines(node, indent: indent + 1)) 113 | end 114 | end 115 | end 116 | lines 117 | end 118 | 119 | def to_xhtml(markdown) 120 | redcarpet = Redcarpet::Markdown.new( 121 | Redcarpet::Render::XHTML.new( 122 | escape_html: true, 123 | ), 124 | fenced_code_blocks: true, tables: true 125 | ) 126 | redcarpet.render(markdown) 127 | end 128 | end 129 | end 130 | -------------------------------------------------------------------------------- /lib/md2key/keynote.rb: -------------------------------------------------------------------------------- 1 | require 'erb' 2 | require 'md2key/diagram' 3 | require 'md2key/highlight' 4 | 5 | module Md2key 6 | class Keynote 7 | COVER_SLIDE_INDEX = 1 8 | TEMPLATE_SLIDE_INDEX = 2 9 | CODE_BACKGROUND_PATH = File.expand_path('../../assets/background.png', __dir__) 10 | 11 | class << self 12 | # You must provide a first slide as a cover slide. 13 | # @param [Md2key::Nodes::Slide] slide 14 | def update_cover(slide, master_name) 15 | execute_applescript('update_slide', slide.title, slide.lines.map(&:text).join("\n"), master_name, COVER_SLIDE_INDEX) 16 | end 17 | 18 | # @param [Md2key::Nodes::Slide] slide 19 | # @param [String] master_name 20 | def create_slide(slide, master_name) 21 | if slide.lines.any?(&:indented?) 22 | create_indented_slide(slide, master_name) 23 | else 24 | # Not using `create_indented_slide` because this is faster. 25 | execute_applescript('create_slide_and_write_body', slide.title, slide.lines.map(&:text).join("\n"), master_name, TEMPLATE_SLIDE_INDEX) 26 | end 27 | end 28 | 29 | # @param [Md2key::Nodes::Slide] slide 30 | def create_slide_with_table(slide, rows, columns, master_name) 31 | execute_applescript('create_slide_and_insert_table', slide.title, TEMPLATE_SLIDE_INDEX, rows, columns, master_name) 32 | end 33 | 34 | def ensure_template_slide_availability 35 | return if slides_count >= 2 36 | execute_applescript('create_empty_slide') 37 | end 38 | 39 | # All slides after a second slide are unnecessary and deleted. 40 | def delete_extra_slides 41 | execute_applescript('delete_extra_slides', slides_count) 42 | end 43 | 44 | def delete_template_slide 45 | execute_applescript('delete_slide', TEMPLATE_SLIDE_INDEX) 46 | end 47 | 48 | # Insert image to the last slide 49 | def insert_image(path) 50 | execute_applescript('insert_image', slides_count, File.absolute_path(path), TEMPLATE_SLIDE_INDEX) 51 | end 52 | 53 | def insert_code(code) 54 | draw_diagram = false 55 | prefix = "" 56 | case code.extension 57 | when 'mermaid' 58 | draw_diagram = true 59 | when 'seq', 'sequence' 60 | prefix = "sequenceDiagram\n" 61 | draw_diagram = true 62 | else 63 | Highlight.pbcopy_highlighted_code(code) 64 | insert_code_background 65 | activate_last_slide 66 | paste_clipboard 67 | end 68 | 69 | if draw_diagram 70 | code.source = prefix + code.source 71 | path = Diagram.generate_image_file(code) 72 | insert_image(path) 73 | File.unlink(path) 74 | end 75 | end 76 | 77 | def insert_table(data) 78 | row_index = 1 79 | data.each do |row| 80 | param = [slides_count, row_index] 81 | row.each do |cell| 82 | param << cell 83 | end 84 | execute_applescript('update_table', *param) 85 | row_index += 1 86 | end 87 | end 88 | 89 | # Insert presenter note 90 | def insert_note(note) 91 | execute_applescript('insert_note', slides_count, note) 92 | end 93 | 94 | # @param [Integer] slide_index 95 | # @return [String] 96 | def fetch_master_slide_name(slide_index) 97 | execute_applescript('fetch_master_slide_name', slide_index).rstrip 98 | end 99 | 100 | # @return [Array] 101 | def fetch_master_slide_names 102 | execute_applescript('fetch_master_slide_names').strip.split("\n") 103 | end 104 | 105 | private 106 | 107 | def insert_code_background 108 | execute_applescript('insert_code_background', slides_count, CODE_BACKGROUND_PATH) 109 | end 110 | 111 | def activate_last_slide 112 | execute_applescript('activate_slide', slides_count) 113 | end 114 | 115 | def paste_clipboard 116 | execute_applescript('paste_clipboard') 117 | end 118 | 119 | def slides_count 120 | execute_applescript('slides_count').to_i 121 | end 122 | 123 | # @param [Md2key::Nodes::Slide] slide 124 | # @param [String] master_name 125 | def create_indented_slide(slide, master_name) 126 | execute_applescript('create_slide_and_select_body', slide.title, master_name, TEMPLATE_SLIDE_INDEX) 127 | 128 | last_index = slide.lines.size - 1 129 | current_indent = 0 130 | slide.lines.each_with_index do |line, index| 131 | # Using copy and paste to input multibyte chars 132 | pbcopy(line.text) 133 | paste_and_indent(line.indent - current_indent, insert_newline: index < last_index) 134 | current_indent = line.indent 135 | end 136 | end 137 | 138 | def pbcopy(str) 139 | IO.popen('pbcopy', 'w') do |io| 140 | io.write(str) 141 | io.close_write 142 | end 143 | end 144 | 145 | def paste_and_indent(indent, insert_newline: true) 146 | execute_applescript('paste_and_indent', indent, insert_newline) 147 | end 148 | 149 | # @return [String] - script's output 150 | def execute_applescript(script_name, *args) 151 | path = script_path(script_name) 152 | script = ERB.new(File.read(path)).result 153 | IO.popen(['osascript', '-', *args.map(&:to_s)], 'r+') do |io| 154 | io.write(script) 155 | io.close_write 156 | io.read 157 | end 158 | end 159 | 160 | def script_path(script_name) 161 | scripts_path = File.expand_path('../../scripts', __dir__) 162 | File.join(scripts_path, "#{script_name}.scpt.erb") 163 | end 164 | end 165 | end 166 | end 167 | --------------------------------------------------------------------------------