enwrite 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/filters.rb ADDED
@@ -0,0 +1,64 @@
1
+ # coding: utf-8
2
+ require 'htmlentities'
3
+
4
+ module Filters
5
+
6
+ def run_filters(text)
7
+ newtext = text
8
+ text.scan(/(\[(\w+)([^\]]*)\])/) do |m|
9
+ match = m[0]
10
+ debug "match = #{match}"
11
+ filter = m[1]
12
+ args = m[2]
13
+ if Filters.method_defined?("filter_#{filter}")
14
+ fn=Filters.method("filter_#{filter}")
15
+ arg = {}
16
+ args = HTMLEntities.new.decode(args)
17
+ debug "args = #{args}"
18
+ args.scan(/\b(\w+)=["“]([^"]*)["”]/) { |a|
19
+ arg[a[0]] = a[1]
20
+ }
21
+ verbose "Calling filter_#{filter} with args #{arg}"
22
+ result = fn.(arg)
23
+ if not result.nil?
24
+ newtext = newtext.gsub(match, result)
25
+ end
26
+ else
27
+ # We only produce this message in verbose mode because it gets triggered
28
+ # every time [some text in brackets] is used
29
+ warn("Warning: nonexistent filter #{filter} used, leaving text as is") if $enwrite_verbose
30
+ end
31
+ end
32
+ debug "After running filters:"
33
+ debug newtext
34
+ return newtext
35
+ end
36
+
37
+ def filter_youtube(args)
38
+ if args.include?('url')
39
+ args['src'] = args['url']
40
+ args.delete('url')
41
+ elsif args.include?('id')
42
+ args['src'] = "https://www.youtube.com/embed/#{args['id']}"
43
+ end
44
+ if args['src'].nil?
45
+ return nil
46
+ end
47
+ args['src'].gsub!(/\/watch\?v=/, "/embed/")
48
+ return "<iframe "+args.each.map{ |k,v| "#{k}=\"#{v}\"" }.join(" ")+"></iframe>"
49
+ end
50
+
51
+ def filter_gist(args)
52
+ if args.include?('url')
53
+ args['src'] = args['url']
54
+ args.delete('url')
55
+ end
56
+ if args['src'].nil?
57
+ return nil
58
+ end
59
+ if not args['src'] =~ /\.js$/
60
+ args['src'] += ".js"
61
+ end
62
+ return "<script "+args.each.map{ |k,v| "#{k}=\"#{v}\"" }.join(" ")+"></script>"
63
+ end
64
+ end
data/lib/output.rb ADDED
@@ -0,0 +1,9 @@
1
+ # Base class to output pages
2
+ #
3
+ # Diego Zamboni, March 2015
4
+ # Time-stamp: <2015-03-29 00:51:04 diego>
5
+
6
+ class Output
7
+ def output_note(metadata, note)
8
+ end
9
+ end
@@ -0,0 +1,236 @@
1
+ #
2
+ # Output class for Hugo
3
+ #
4
+ # Diego Zamboni, March 2015
5
+ # Time-stamp: <2015-04-30 12:40:31 diego>
6
+
7
+ require 'output'
8
+ require 'filters'
9
+ require 'enml-utils'
10
+ require 'fileutils'
11
+ require 'yaml/store'
12
+ require 'digest'
13
+
14
+ include Filters
15
+
16
+ class Hugo < Output
17
+ def initialize(opts = {})
18
+ @opts = opts
19
+ @base_dir = opts['base_dir']
20
+ unless @base_dir
21
+ error "The 'base_dir' option of the Hugo plugin must be set!"
22
+ end
23
+ @use_filters = opts['use_filters'] || true
24
+ @rebuild_all = opts['rebuild_all'] || false
25
+
26
+ # Persistent store for this base_dir
27
+ datadir = "#{@base_dir}/data"
28
+ FileUtils.mkdir_p datadir
29
+ @config_store = YAML::Store.new("#{datadir}/enwrite_data.yaml")
30
+
31
+ # Initialize GUID-to-filename map if needed
32
+ @config_store.transaction { @config_store[:note_files] = {} unless @config_store[:note_files] }
33
+
34
+ # These are [ realpath, urlpath ]
35
+ @static_dir = [ "#{@base_dir}/#{opts['static_subdir'] || 'static' }", opts['static_url'] || "" ]
36
+ @static_subdirs = { 'image' => opts['image_subdir'] || 'img',
37
+ 'audio' => opts['audio_subdir'] || 'audio',
38
+ 'video' => opts['video_subdir'] || 'video',
39
+ 'files' => opts['files_subdir'] || 'files',
40
+ }
41
+
42
+ # Tag-to-type map
43
+ @tag_to_type = opts['tag_to_type'] || { "default" => "post/",
44
+ "post" => "post/",
45
+ "page" => "" }
46
+ @tag_to_type_order = opts['tag_to_type_order'] || [ "post", "page", "default" ]
47
+
48
+ @tag_to_type_order.each { |type|
49
+ @tag_to_type[type] = "" unless @tag_to_type.include?(type)
50
+ @tag_to_type[type] = "" if @tag_to_type[type].nil?
51
+ }
52
+
53
+ # Markdown tag
54
+ @markdown_tag = opts['markdown_tag'] || "markdown"
55
+
56
+ # Command to run hugo
57
+ @hugo_cmd = opts['hugo_cmd'] || "hugo"
58
+ end
59
+
60
+ def set_static_dirs(note)
61
+ @static_dirs = {}
62
+ @static_dirs['note'] = [ "#{@static_dir[0]}/note/#{note.guid}",
63
+ "#{@static_dir[1]}/note/#{note.guid}" ]
64
+ ['image', 'audio', 'video', 'files']. each do |type|
65
+ @static_dirs[type] = [ "#{@static_dirs['note'][0]}/#{@static_subdirs[type]}", # full path
66
+ "#{@static_dirs['note'][1]}/#{@static_subdirs[type]}" ]; # url path
67
+ end
68
+ end
69
+
70
+ def delete_note(note, fname)
71
+ set_static_dirs(note)
72
+
73
+ if File.exist?(fname)
74
+ msg " This note has been deleted from Evernote, deleting its file #{fname}"
75
+ File.delete(fname)
76
+ end
77
+ if Dir.exist?(@static_dirs['note'][0])
78
+ msg " Deleting static files for deleted note #{@static_dirs['note'][0]}"
79
+ FileUtils.rmtree(@static_dirs['note'][0])
80
+ end
81
+ end
82
+
83
+ def output_note(note)
84
+ set_static_dirs(note)
85
+
86
+ msg "Found note '#{note.title}'"
87
+ verbose "Created: #{Time.at(note.created/1000)}" if note.created
88
+ verbose "Deleted: #{Time.at(note.deleted/1000)}" if note.deleted
89
+ verbose "Content length: #{note.contentLength}" if note.contentLength
90
+ verbose "Clipped from: #{note.attributes.sourceURL}" if note.attributes.sourceURL
91
+
92
+ markdown = note.tagNames.include?(@markdown_tag)
93
+ if markdown
94
+ msg " It has the '#{ @markdown_tag }' tag, so I will interpret it as markdown"
95
+ note.tagNames -= [ @markdown_tag ]
96
+ end
97
+
98
+ type = nil
99
+ # Detect the type of post according to its tags
100
+ @tag_to_type_order.each do |tag|
101
+ if note.tagNames.include?(tag) or tag == "default"
102
+ type = @tag_to_type[tag]
103
+ break
104
+ end
105
+ end
106
+ if type.nil?
107
+ error " ### I couldn't determine the type for this post - skipping it"
108
+ return
109
+ end
110
+
111
+ # Determine if we should include the page in the main menu
112
+ inmainmenu = note.tagNames.include?('_mainmenu')
113
+ if inmainmenu
114
+ note.tagNames -= [ '_mainmenu' ]
115
+ end
116
+
117
+ # Determine if we should use a custom slug
118
+ slug = nil
119
+ note.tagNames.grep(/^_slug=(\S+)/) do |slugtag|
120
+ slug = $1
121
+ note.tagNames -= [ slugtag ]
122
+ verbose " Will use custom slug for this post: #{slug}"
123
+ end
124
+
125
+ # Get our note GUID-to-filename map
126
+ note_files = config(:note_files, {}, @config_store)
127
+
128
+ # Determine the name I would assign to this note when published to Hugo
129
+ date = Time.at(note.created/1000).strftime('%F')
130
+ post_filename = "#{type}#{date}-#{note.title}.#{markdown ? 'md' : 'html'}"
131
+ # Do we already have a post for this note (by GUID)? If so, we remove the
132
+ # old file since it will be regenerated anyway, which also takes care of the
133
+ # case when the note was renamed and the filename will change, to avoid
134
+ # post duplication. If the note has been deleted, we just delete the
135
+ # old filename and stop here.
136
+ oldfile = note_files[note.guid]
137
+ if oldfile
138
+ verbose " I already had a file for note #{note.guid}, removing #{oldfile}"
139
+ File.delete(oldfile) if File.exist?(oldfile)
140
+ note_files.delete(note.guid)
141
+ setconfig(:note_files, note_files, @config_store)
142
+ if note.deleted
143
+ delete_note(note, oldfile)
144
+ return
145
+ end
146
+ end
147
+
148
+ # Run hugo to create the file, then read it back it to update the front matter
149
+ # with our tags.
150
+ # We run "hugo new" also for deleted notes so that hugo gives us the filename
151
+ # to delete.
152
+ fname = nil
153
+ frontmatter = nil
154
+ Dir.chdir(@base_dir) do
155
+ # Force -f yaml because it's so much easier to process
156
+ while true
157
+ post_filename.gsub!(/"/, '\"')
158
+ cmd = %Q(#{@hugo_cmd} new -f yaml "#{post_filename}" 2>&1)
159
+ debug "Executing: #{cmd}"
160
+ output = %x(#{cmd})
161
+ if output =~ /^(.+) created$/
162
+ # Get the full filename as reported by Hugo
163
+ fname = $1
164
+ if note.deleted
165
+ delete_note(note, fname)
166
+ return
167
+ end
168
+ # Load the frontmatter
169
+ frontmatter = YAML.load_file(fname)
170
+ # Update title because Hugo gets it wrong sometimes depending on the characters in the title, and to get rid of the date we put in the filename
171
+ frontmatter['title'] = note.title
172
+ # Fix the date to the date when the note was created
173
+ frontmatter['date'] = date
174
+ # Update tags, for now set categories to the same
175
+ frontmatter['tags'] = note.tagNames
176
+ frontmatter['categories'] = note.tagNames
177
+ # Set slug to work around https://github.com/spf13/hugo/issues/1017
178
+ frontmatter['slug'] = slug ? slug : note.title.downcase.gsub(/\W+/, "-").gsub(/^-+/, "").gsub(/-+$/, "")
179
+ # Set main menu tag if needed
180
+ frontmatter['menu'] = 'main' if inmainmenu
181
+ break
182
+ elsif output =~ /ERROR: \S+ (.+) already exists/
183
+ # Get the full filename as reported by Hugo
184
+ fname = $1
185
+ # If the file existed already, remove it and regenerate it
186
+ File.delete(fname)
187
+ if note.deleted
188
+ delete_note(note, fname)
189
+ return
190
+ end
191
+ # This shouldn't happen due to the index check above
192
+ unless @rebuild_all
193
+ error " I found a file that should not be there (#{fname}). This might indicate"
194
+ error " an inconsistency in my internal note-to-file map. Please re-run with"
195
+ error " --rebuild-all to regenerate it. I am deleting the file and continuing"
196
+ error " for now, but please review the results carefully."
197
+ end
198
+ redo
199
+ else
200
+ error " Hugo returned unknown output when trying to create this post - skipping it: #{output}"
201
+ return
202
+ end
203
+ end
204
+ end
205
+
206
+ debug "Updated frontmatter: #{frontmatter.to_s}"
207
+
208
+ File.open(fname, "w") do |f|
209
+ f.write(frontmatter.to_yaml)
210
+ f.puts("---")
211
+ f.puts
212
+ enml = ENML_utils.new(note.content, note.resources, @static_dirs, note.guid)
213
+ output = markdown ? enml.to_text : enml.to_html
214
+ if @use_filters
215
+ verbose "Running filters on text"
216
+ output = run_filters(output)
217
+ end
218
+ if note.attributes.sourceURL
219
+ f.puts(%(<p class="clip-attribute">via <a href="#{note.attributes.sourceURL}">#{note.attributes.sourceURL}</a></p>))
220
+ end
221
+ f.puts(output)
222
+ enml.resource_files.each do |resfile|
223
+ FileUtils.mkdir_p File.dirname(resfile[:fname])
224
+ File.open(resfile[:fname], "w") do |r|
225
+ r.write(resfile[:data])
226
+ end
227
+ verbose "Wrote file #{resfile[:fname]}"
228
+ end
229
+ end
230
+
231
+ verbose "Wrote file #{fname}"
232
+ note_files[note.guid] = fname
233
+ setconfig(:note_files, note_files, @config_store)
234
+
235
+ end
236
+ end
data/lib/util.rb ADDED
@@ -0,0 +1,44 @@
1
+ require 'yaml/store'
2
+ require 'colorize'
3
+
4
+ # Message output
5
+
6
+ def verbose(msg)
7
+ puts ("Enwrite [VERBOSE]: " + msg).blue if $enwrite_verbose
8
+ end
9
+
10
+ def debug(msg)
11
+ puts ("Enwrite [DEBUG]: " + msg) if $enwrite_debug
12
+ end
13
+
14
+ def error(msg)
15
+ $stderr.puts ("Enwrite [ERROR]: " + msg).red
16
+ end
17
+
18
+ def msg(msg)
19
+ puts ("Enwrite [INFO]: " + msg).green
20
+ end
21
+
22
+ def warn(msg)
23
+ $stderr.puts ("Enwrite [WARN]: " + msg).light_yellow
24
+ end
25
+
26
+ # Config file storage
27
+
28
+ def config_file
29
+ return "#{ENV['HOME']}/.enwrite.config"
30
+ end
31
+
32
+ def config_store
33
+ return YAML::Store.new(config_file())
34
+ end
35
+
36
+ # Get a persistent config value
37
+ def config(key, defval=nil, store=config_store)
38
+ return store.transaction { store.fetch(key, defval) }
39
+ end
40
+
41
+ # Set a persistent config value
42
+ def setconfig(key, val, store=config_store)
43
+ return store.transaction { store[key] = val }
44
+ end
data/test/helper.rb ADDED
@@ -0,0 +1,34 @@
1
+ require 'simplecov'
2
+
3
+ module SimpleCov::Configuration
4
+ def clean_filters
5
+ @filters = []
6
+ end
7
+ end
8
+
9
+ SimpleCov.configure do
10
+ clean_filters
11
+ load_adapter 'test_frameworks'
12
+ end
13
+
14
+ ENV["COVERAGE"] && SimpleCov.start do
15
+ add_filter "/.rvm/"
16
+ end
17
+ require 'rubygems'
18
+ require 'bundler'
19
+ begin
20
+ Bundler.setup(:default, :development)
21
+ rescue Bundler::BundlerError => e
22
+ $stderr.puts e.message
23
+ $stderr.puts "Run `bundle install` to install missing gems"
24
+ exit e.status_code
25
+ end
26
+ require 'test/unit'
27
+ require 'shoulda'
28
+
29
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
30
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
31
+ require 'enwrite'
32
+
33
+ class Test::Unit::TestCase
34
+ end
@@ -0,0 +1,7 @@
1
+ require 'helper'
2
+
3
+ class TestEnwrite < Test::Unit::TestCase
4
+ should "probably rename this file and start testing for real" do
5
+ flunk "hey buddy, you should probably rename this file and start testing for real"
6
+ end
7
+ end
metadata ADDED
@@ -0,0 +1,179 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: enwrite
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Diego Zamboni
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-04-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: colorize
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.7'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.7'
27
+ - !ruby/object:Gem::Dependency
28
+ name: deep_merge
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: evernote-thrift
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.25'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.25'
55
+ - !ruby/object:Gem::Dependency
56
+ name: evernote_oauth
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.2'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.2'
69
+ - !ruby/object:Gem::Dependency
70
+ name: htmlentities
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '4.3'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '4.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rdoc
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.12'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.12'
97
+ - !ruby/object:Gem::Dependency
98
+ name: bundler
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: jeweler
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '2.0'
125
+ description: |-
126
+ Enwrite allows you to generate a website from contents stored in Evernote.
127
+ At the moment only Hugo (http://gohugo.io) is supported as an output format,
128
+ but others can be added through plugins.
129
+ email: diego@zzamboni.org
130
+ executables:
131
+ - enwrite
132
+ extensions: []
133
+ extra_rdoc_files:
134
+ - LICENSE
135
+ - LICENSE.txt
136
+ - README.md
137
+ files:
138
+ - ".document"
139
+ - Gemfile
140
+ - Gemfile.lock
141
+ - LICENSE
142
+ - LICENSE.txt
143
+ - README.md
144
+ - Rakefile
145
+ - bin/enwrite
146
+ - lib/enml-utils.rb
147
+ - lib/enwrite.rb
148
+ - lib/evernote-utils.rb
149
+ - lib/filters.rb
150
+ - lib/output.rb
151
+ - lib/output/hugo.rb
152
+ - lib/util.rb
153
+ - test/helper.rb
154
+ - test/test_enwrite.rb
155
+ homepage: http://github.com/zzamboni/enwrite
156
+ licenses:
157
+ - MIT
158
+ metadata: {}
159
+ post_install_message:
160
+ rdoc_options: []
161
+ require_paths:
162
+ - lib
163
+ required_ruby_version: !ruby/object:Gem::Requirement
164
+ requirements:
165
+ - - ">="
166
+ - !ruby/object:Gem::Version
167
+ version: '0'
168
+ required_rubygems_version: !ruby/object:Gem::Requirement
169
+ requirements:
170
+ - - ">="
171
+ - !ruby/object:Gem::Version
172
+ version: '0'
173
+ requirements: []
174
+ rubyforge_project:
175
+ rubygems_version: 2.4.5
176
+ signing_key:
177
+ specification_version: 4
178
+ summary: 'Enwrite: Power a web site using Evernote'
179
+ test_files: []