enwrite 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: []