mg2en 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE +20 -0
- data/README.md +38 -0
- data/Rakefile +60 -0
- data/bin/mg2en +56 -0
- data/lib/mg2en.rb +10 -0
- data/lib/mg2en/direction.rb +17 -0
- data/lib/mg2en/generator.rb +56 -0
- data/lib/mg2en/ingredient.rb +46 -0
- data/lib/mg2en/options.rb +16 -0
- data/lib/mg2en/parser.rb +19 -0
- data/lib/mg2en/recipe.rb +82 -0
- data/lib/mg2en/version.rb +4 -0
- data/mg2en.gemspec +34 -0
- data/spec/direction_spec.rb +23 -0
- data/spec/dtds/enml2.dtd +592 -0
- data/spec/dtds/evernote-export3.dtd +295 -0
- data/spec/dtds/xhtml-lat1.ent +196 -0
- data/spec/dtds/xhtml-special.ent +80 -0
- data/spec/dtds/xhtml-symbol.ent +237 -0
- data/spec/fixtures/1.mgourmet3 +2107 -0
- data/spec/fixtures/2.mgourmet3 +52 -0
- data/spec/generator_spec.rb +16 -0
- data/spec/ingredient_spec.rb +36 -0
- data/spec/parser_spec.rb +29 -0
- data/spec/recipe_spec.rb +53 -0
- data/spec/spec_helper.rb +35 -0
- data/templates/default.haml +65 -0
- metadata +243 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 811186babe943626c085e819708eea8c8cc8fdf2
|
4
|
+
data.tar.gz: 8c31fb2042764c2bed691631191bde88fb8051bb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5715130d51e157b789ac687308049c91a33b364af563d652b10c7c73cea30dddcce8de6066b9f3c37bd18f3e90e44d006334aaacac2de21014fffb66306e34af
|
7
|
+
data.tar.gz: 8ec629fc09e5bd91dfcb7e9ed1bfff69c5adc48a6381bdc6c349c2bd6f5924ec5a2fbef14da5ea000f18fc4fff064159c82c5544a3f84798c725539427105657
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/Guardfile
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# A sample Guardfile
|
2
|
+
# More info at https://github.com/guard/guard#readme
|
3
|
+
|
4
|
+
guard :rspec, cmd: 'bundle exec rspec' do
|
5
|
+
watch(%r{^spec/.+_spec\.rb$})
|
6
|
+
watch(%r{^lib/mg2en/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
|
7
|
+
watch('spec/spec_helper.rb') { "spec" }
|
8
|
+
end
|
9
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Jeff Hutchison
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
6
|
+
this software and associated documentation files (the "Software"), to deal in
|
7
|
+
the Software without restriction, including without limitation the rights to
|
8
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
9
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
10
|
+
subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
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, FITNESS
|
17
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
18
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
19
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
20
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
# MacGourmet to Evernote
|
2
|
+
|
3
|
+
A library to parse a MacGourmet 3 export file and convert into an Evernote
|
4
|
+
export file. The resulting Evernote export file (*.enex) can be imported into
|
5
|
+
Evernote.
|
6
|
+
|
7
|
+
This converts most, but not all, MacGourmet recipe attributes.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Download and install [FreeImage](http://sourceforge.net/projects/freeimage/).
|
12
|
+
[Homebrew](http://brew.sh) users can:
|
13
|
+
|
14
|
+
brew install freeimage
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
gem 'mg2en'
|
19
|
+
|
20
|
+
And then execute:
|
21
|
+
|
22
|
+
$ bundle
|
23
|
+
|
24
|
+
Or install it yourself as:
|
25
|
+
|
26
|
+
$ gem install mg2en
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
mg2en -h
|
31
|
+
|
32
|
+
## Contributing
|
33
|
+
|
34
|
+
1. Fork it
|
35
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
36
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
37
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
38
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require 'pathname'
|
3
|
+
require 'mg2en'
|
4
|
+
|
5
|
+
task :default => [:preview]
|
6
|
+
|
7
|
+
# defaults for environment variables:
|
8
|
+
# MG3=spec/fixtures/1.mgourmet3
|
9
|
+
# ENEX=tmp/<basename of MG3>.enex
|
10
|
+
# NOTEBOOK=Test
|
11
|
+
# TEMPLATE=default
|
12
|
+
|
13
|
+
notebook = ENV['NOTEBOOK'] || "Test"
|
14
|
+
Mg2en::Options.defaults[:template] = ENV['TEMPLATE'] || "default"
|
15
|
+
|
16
|
+
# paths to source (mg3) and dest (enex)
|
17
|
+
mg3 = ENV['MG3'] ? File.absolute_path(ENV['MG3']) :
|
18
|
+
File.expand_path( '../spec/fixtures/1.mgourmet3', __FILE__)
|
19
|
+
enex = ENV['ENEX'] ? File.absolute_path(ENV['ENEX']) :
|
20
|
+
File.join(File.expand_path( '../tmp', __FILE__), File.basename(mg3, '.mgourmet3') + '.enex')
|
21
|
+
|
22
|
+
# display task description paths as relative paths to fit rake -T line
|
23
|
+
pwd = Pathname.new(Dir.getwd)
|
24
|
+
enex_relpath = Pathname.new(enex).relative_path_from(pwd)
|
25
|
+
mg3_relpath = Pathname.new(mg3).relative_path_from(pwd)
|
26
|
+
|
27
|
+
# Convert to/from files specified by environment vairiables:
|
28
|
+
# - MG3: conversion source file, defaults as above
|
29
|
+
# - ENEX: conversion destination file, defaults as above
|
30
|
+
# e.g. MG3=/tmp/notes.macgourmet3 ENEX=/tmp/notes.enex rake convert
|
31
|
+
desc "Convert MG3=#{mg3_relpath} to ENEX=#{enex_relpath}"
|
32
|
+
task :convert do
|
33
|
+
parser = Mg2en::Parser.new(mg3)
|
34
|
+
generator = Mg2en::Generator.new(parser.recipes)
|
35
|
+
generator.write(enex)
|
36
|
+
end
|
37
|
+
|
38
|
+
# Tell Evernote to import file to notebook specified by environment variables:
|
39
|
+
# - ENEX: the file to import, defaults as above
|
40
|
+
# - NOTEBOOK: the target notebook, defaults as above, must exist
|
41
|
+
# e.g. ENEX=/tmp/notes.enex NOTEBOOK=Notes rake import
|
42
|
+
|
43
|
+
desc "Imports ENEX=#{enex_relpath} into NOTEBOOK=#{notebook} (OS X only)"
|
44
|
+
task :import do
|
45
|
+
raise "task requires AppleScript" unless RUBY_PLATFORM =~ /darwin/
|
46
|
+
|
47
|
+
system "osascript -e 'tell application \"Evernote\" to import POSIX \
|
48
|
+
file \"#{enex}\" to notebook \"#{notebook}\" tags true'"
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "Convert and preview MG3=#{mg3_relpath} in NOTEBOOK=#{notebook} (OS X only)"
|
52
|
+
task :preview => [:convert, :import]
|
53
|
+
|
54
|
+
desc "Render ENML for MG3=#{mg3_relpath}"
|
55
|
+
task :enml do
|
56
|
+
parser = Mg2en::Parser.new(mg3)
|
57
|
+
parser.recipes.each do |r|
|
58
|
+
puts r.enml
|
59
|
+
end
|
60
|
+
end
|
data/bin/mg2en
ADDED
@@ -0,0 +1,56 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
+
require 'optparse'
|
5
|
+
require 'ostruct'
|
6
|
+
require 'mg2en'
|
7
|
+
|
8
|
+
OptionParser.new do |opts|
|
9
|
+
opts.banner = 'Usage: mg2en [options] input_file [output_file]'
|
10
|
+
|
11
|
+
opts.on('-v', '--[no-]verbose', 'Run verbosely') do |v|
|
12
|
+
Mg2en::Options.defaults[:verbose] = v
|
13
|
+
end
|
14
|
+
|
15
|
+
opts.on('--version', 'Show version') do
|
16
|
+
puts Mg2en::VERSION
|
17
|
+
exit
|
18
|
+
end
|
19
|
+
|
20
|
+
opts.on('-t', '--template TEMPLATE',
|
21
|
+
'Name of template',
|
22
|
+
' (e.g. --template default)') do |t|
|
23
|
+
unless File.exists?(File.expand_path("../../templates/#{t}.haml",
|
24
|
+
__FILE__))
|
25
|
+
puts "ERROR: templates/#{t}.haml not found"
|
26
|
+
exit
|
27
|
+
end
|
28
|
+
Mg2en::Options.defaults[:template] = t
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on('-l', '--list-templates', 'List included templates') do
|
32
|
+
templates_dir = File.expand_path('../../templates', __FILE__)
|
33
|
+
Dir.foreach(templates_dir) do |t|
|
34
|
+
puts Regexp.last_match(1) if t =~ /^(.+)\.haml$/
|
35
|
+
end
|
36
|
+
exit
|
37
|
+
end
|
38
|
+
|
39
|
+
end.parse!
|
40
|
+
|
41
|
+
input_file = ARGV[0]
|
42
|
+
output_file = ARGV[1]
|
43
|
+
|
44
|
+
unless input_file && File.exists?(input_file)
|
45
|
+
puts 'ERROR: input file not found'
|
46
|
+
exit
|
47
|
+
end
|
48
|
+
|
49
|
+
parser = Mg2en::Parser.new(input_file)
|
50
|
+
generator = Mg2en::Generator.new(parser.recipes)
|
51
|
+
generator.write(output_file)
|
52
|
+
|
53
|
+
if Mg2en::Options.defaults[:verbose]
|
54
|
+
puts "Converted #{input_file} to #{output_file}" \
|
55
|
+
" using template #{Mg2en::Options.defaults[:template]}"
|
56
|
+
end
|
data/lib/mg2en.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
module Mg2en
|
2
|
+
# This class holds a recipe direction step.
|
3
|
+
class Direction
|
4
|
+
attr_reader :description, :label, :highlighted
|
5
|
+
alias_method :highlighted?, :highlighted
|
6
|
+
|
7
|
+
def initialize(d)
|
8
|
+
@description = d['DIRECTION_TEXT']
|
9
|
+
@label = d['LABEL_TEXT']
|
10
|
+
@highlighted = d['IS_HIGHLIGHTED']
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_s
|
14
|
+
"#{label} #{description} #{highlighted}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'builder'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Mg2en
|
5
|
+
# Writes out an array of recipe objects into ENEX file.
|
6
|
+
class Generator
|
7
|
+
def initialize(recipes)
|
8
|
+
@recipes = recipes
|
9
|
+
end
|
10
|
+
|
11
|
+
def write(destination = nil)
|
12
|
+
open_destination(destination)
|
13
|
+
@xm.instruct! :xml, version: '1.0', encoding: 'UTF-8'
|
14
|
+
@xm.declare! :DOCTYPE, :"en-export", :SYSTEM,
|
15
|
+
'http://xml.evernote.com/pub/evernote-export3.dtd'
|
16
|
+
@xm.tag!(:'en-export',
|
17
|
+
:'export-date' => Time.new.strftime('%Y%m%dT%H%M%S%z'),
|
18
|
+
application: 'mg2en',
|
19
|
+
version: Mg2en::VERSION) do
|
20
|
+
@recipes.each do |recipe|
|
21
|
+
@xm.note { write_recipe(recipe) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
@xm.target!.close if destination
|
25
|
+
end
|
26
|
+
|
27
|
+
def open_destination(destination)
|
28
|
+
if destination
|
29
|
+
dfile = open(destination, 'w')
|
30
|
+
@xm = Builder::XmlMarkup.new(target: dfile)
|
31
|
+
else
|
32
|
+
@xm = Builder::XmlMarkup.new(target: STDOUT)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def write_recipe(recipe)
|
37
|
+
@xm.title(recipe.name)
|
38
|
+
@xm.content { @xm.cdata!(recipe.enml) }
|
39
|
+
recipe.tags.each do |t|
|
40
|
+
@xm.tag(t)
|
41
|
+
end
|
42
|
+
@xm.tag!(:'note-attributes') do
|
43
|
+
@xm.source(recipe.source) if recipe.source
|
44
|
+
@xm.tag!(:'source-url') { @xm.text! recipe.url } if recipe.url
|
45
|
+
end
|
46
|
+
if recipe.image
|
47
|
+
@xm.resource do
|
48
|
+
@xm.data(encoding: 'base64') do |d|
|
49
|
+
d << Base64.encode64(recipe.image)
|
50
|
+
end
|
51
|
+
@xm.mime('image/jpeg')
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Mg2en
|
2
|
+
# This class holds an ingredient.
|
3
|
+
class Ingredient
|
4
|
+
attr_reader :measurement, :quantity, :description, :direction, :group,
|
5
|
+
:ingredients, :link
|
6
|
+
alias_method :group?, :group
|
7
|
+
alias_method :link?, :link
|
8
|
+
|
9
|
+
def initialize(i)
|
10
|
+
if i.key?('DESCRIPTION')
|
11
|
+
@group = false
|
12
|
+
@quantity = i['QUANTITY']
|
13
|
+
@measurement = i['MEASUREMENT']
|
14
|
+
@description = i['DESCRIPTION']
|
15
|
+
@direction = i['DIRECTION']
|
16
|
+
@link = i['INCLUDED_RECIPE_ID'] > 0
|
17
|
+
elsif i.key?('DIVIDER_INGREDIENT')
|
18
|
+
@group = true
|
19
|
+
@description = i['DIVIDER_INGREDIENT']['DESCRIPTION']
|
20
|
+
@ingredients = []
|
21
|
+
ingts = i['INGREDIENTS']
|
22
|
+
ingts.each do |ing|
|
23
|
+
ingredient = Mg2en::Ingredient.new(ing)
|
24
|
+
@ingredients.push ingredient
|
25
|
+
end
|
26
|
+
else
|
27
|
+
fail 'Did not recognize input'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def to_s
|
32
|
+
return @description if self.group?
|
33
|
+
output = ''
|
34
|
+
output << @quantity unless @quantity.empty?
|
35
|
+
output << without_quantity
|
36
|
+
end
|
37
|
+
|
38
|
+
def without_quantity
|
39
|
+
output = ''
|
40
|
+
output << ' ' << @measurement unless @measurement.empty?
|
41
|
+
output << ' ' << @description
|
42
|
+
output << ', ' << @direction unless @direction.empty?
|
43
|
+
output
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mg2en
|
2
|
+
# This class holds library options.
|
3
|
+
class Options
|
4
|
+
@defaults = {
|
5
|
+
verbose: false,
|
6
|
+
template: 'default',
|
7
|
+
image_size: 192,
|
8
|
+
}
|
9
|
+
|
10
|
+
# The default option values.
|
11
|
+
# @return Hash
|
12
|
+
def self.defaults # rubocop:disable TrivialAccessors
|
13
|
+
@defaults
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/mg2en/parser.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'mg2en/recipe'
|
2
|
+
require 'plist'
|
3
|
+
|
4
|
+
module Mg2en
|
5
|
+
# Parse a MacGourmet 3 Plist export file into array of recipe objects.
|
6
|
+
class Parser
|
7
|
+
attr_reader :recipes
|
8
|
+
def initialize(filename_or_xml)
|
9
|
+
recipe_input = Plist.parse_xml(filename_or_xml)
|
10
|
+
fail ArgumentError, 'Unable to parse input' unless recipe_input
|
11
|
+
|
12
|
+
@recipes = []
|
13
|
+
recipe_input.each do |r|
|
14
|
+
recipe = Mg2en::Recipe.new(r)
|
15
|
+
@recipes.push recipe
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/mg2en/recipe.rb
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'mg2en/ingredient'
|
2
|
+
require 'mg2en/direction'
|
3
|
+
require 'haml'
|
4
|
+
require 'digest'
|
5
|
+
require 'image_science'
|
6
|
+
require 'tempfile'
|
7
|
+
require 'uri'
|
8
|
+
require 'active_support/core_ext/object/blank'
|
9
|
+
|
10
|
+
module Mg2en
|
11
|
+
# This class holds a recipe.
|
12
|
+
class Recipe
|
13
|
+
attr_reader :name, :summary, :note, :source, :url, :image
|
14
|
+
attr_reader :ingredients, :directions, :notes, :tags, :cuisine
|
15
|
+
|
16
|
+
def initialize(r)
|
17
|
+
@name = r['NAME']
|
18
|
+
@summary = r['SUMMARY']
|
19
|
+
@note = r['NOTE']
|
20
|
+
@source = r['SOURCE']
|
21
|
+
@url = check_url(r['PUBLICATION_PAGE'])
|
22
|
+
@yield = r['YIELD']
|
23
|
+
@servings = r['SERVINGS']
|
24
|
+
scale_image(r['IMAGE'].read) if r['IMAGE']
|
25
|
+
|
26
|
+
@ingredients = []
|
27
|
+
r['INGREDIENTS_TREE'].each do |i|
|
28
|
+
ingredient = Mg2en::Ingredient.new(i)
|
29
|
+
@ingredients.push ingredient
|
30
|
+
end
|
31
|
+
|
32
|
+
@directions = []
|
33
|
+
r['DIRECTIONS_LIST'].each do |d|
|
34
|
+
direction = Mg2en::Direction.new(d)
|
35
|
+
@directions.push direction
|
36
|
+
end
|
37
|
+
|
38
|
+
@notes = []
|
39
|
+
r['NOTES_LIST'] && r['NOTES_LIST'].each { |n| @notes.push n['NOTE_TEXT'] }
|
40
|
+
|
41
|
+
@tags = []
|
42
|
+
r['CATEGORIES'] && r['CATEGORIES'].each { |c| @tags.push c['NAME'] }
|
43
|
+
|
44
|
+
@cuisine = []
|
45
|
+
r['CUISINE'] && r['CUISINE'].each { |c| @cuisine.push c['NAME']}
|
46
|
+
end
|
47
|
+
|
48
|
+
def enml
|
49
|
+
template_file = Mg2en::Options.defaults[:template] + '.haml'
|
50
|
+
template = File.expand_path("../../../templates/#{template_file}",
|
51
|
+
__FILE__)
|
52
|
+
engine = Haml::Engine.new(File.open(template).read)
|
53
|
+
engine.render(self)
|
54
|
+
end
|
55
|
+
|
56
|
+
def scale_image(image)
|
57
|
+
file = Tempfile.new(['thumb', '.jpg'])
|
58
|
+
begin
|
59
|
+
ImageScience.with_image_from_memory(image) do |i|
|
60
|
+
i.cropped_thumbnail(Mg2en::Options.defaults[:image_size]) do |thumb|
|
61
|
+
thumb.save(file.path)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@image = file.read
|
65
|
+
ensure
|
66
|
+
file.close
|
67
|
+
file.unlink
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def check_url(url)
|
72
|
+
uri = URI.parse(url)
|
73
|
+
if uri.scheme.eql?('http') || uri.scheme.eql?('https')
|
74
|
+
return uri.to_s
|
75
|
+
else
|
76
|
+
return nil
|
77
|
+
end
|
78
|
+
rescue
|
79
|
+
return nil
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|