serif 0.2.2 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. data/Gemfile.lock +17 -17
  2. data/README.md +31 -3
  3. data/bin/serif +6 -0
  4. data/lib/serif/admin_server.rb +30 -8
  5. data/lib/serif/config.rb +4 -0
  6. data/lib/serif/content_file.rb +7 -22
  7. data/lib/serif/post.rb +0 -6
  8. data/lib/serif/server.rb +3 -1
  9. data/lib/serif/site.rb +57 -1
  10. data/lib/serif.rb +2 -0
  11. data/serif.gemspec +1 -1
  12. data/statics/assets/js/attachment.js +91 -0
  13. data/statics/assets/js/jquery.drop.js +33 -0
  14. data/statics/assets/js/jquery.insert.js +39 -0
  15. data/statics/skeleton/_config.yml +39 -1
  16. data/statics/templates/admin/edit_draft.liquid +9 -10
  17. data/statics/templates/admin/edit_post.liquid +6 -10
  18. data/statics/templates/admin/layout.liquid +47 -3
  19. data/statics/templates/admin/new_draft.liquid +5 -9
  20. data/test/content_file_spec.rb +39 -0
  21. data/test/file_digest_tag_spec.rb +4 -0
  22. data/test/filters_spec.rb +4 -0
  23. data/test/post_spec.rb +6 -0
  24. data/test/site_dir/_site/drafts/another-sample-draft/cdc8037ed098e34a19fc6671ab652f908dd94009ff642d4e5ee80fa566fa.html +13 -0
  25. data/test/site_dir/_site/drafts/sample-draft/f828e5f22d76b04c586e90680ac814e6b233d3d380f6be6975beba75081b.html +13 -0
  26. data/test/site_dir/_site/test-blog/post-to-be-published-on-generate.html +1 -0
  27. data/test/site_dir/_site/test-blog/sample-post.html +1 -0
  28. data/test/site_dir/_site/test-blog/second-post.html +1 -0
  29. data/test/site_dir/_templates/post.html +1 -0
  30. data/test/site_dir/_trash/1361047797-autopublish-draft +5 -0
  31. data/test/site_dir/_trash/{1360450928-autopublish-draft → 1361047797-test-draft} +1 -1
  32. data/test/site_generation_spec.rb +36 -0
  33. data/test/site_spec.rb +8 -0
  34. metadata +11 -4
  35. data/test/site_dir/_trash/1360450928-test-draft +0 -3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- serif (0.2.1)
4
+ serif (0.2.3)
5
5
  liquid (~> 2.4)
6
6
  pygments.rb (~> 0.3)
7
7
  rack (~> 1.0)
@@ -14,39 +14,39 @@ PATH
14
14
  GEM
15
15
  remote: http://rubygems.org/
16
16
  specs:
17
- diff-lcs (1.1.2)
17
+ diff-lcs (1.1.3)
18
18
  liquid (2.4.1)
19
- multi_json (1.5.0)
19
+ multi_json (1.6.1)
20
20
  posix-spawn (0.3.6)
21
21
  pygments.rb (0.3.7)
22
22
  posix-spawn (~> 0.3.6)
23
23
  yajl-ruby (~> 1.1.0)
24
- rack (1.4.4)
24
+ rack (1.5.2)
25
25
  rack-protection (1.3.2)
26
26
  rack
27
27
  rack-rewrite (1.3.3)
28
- rake (0.9.2.2)
28
+ rake (0.9.6)
29
29
  redcarpet (2.2.2)
30
30
  redhead (0.0.8)
31
- rspec (2.5.0)
32
- rspec-core (~> 2.5.0)
33
- rspec-expectations (~> 2.5.0)
34
- rspec-mocks (~> 2.5.0)
35
- rspec-core (2.5.1)
36
- rspec-expectations (2.5.0)
37
- diff-lcs (~> 1.1.2)
38
- rspec-mocks (2.5.0)
31
+ rspec (2.12.0)
32
+ rspec-core (~> 2.12.0)
33
+ rspec-expectations (~> 2.12.0)
34
+ rspec-mocks (~> 2.12.0)
35
+ rspec-core (2.12.2)
36
+ rspec-expectations (2.12.1)
37
+ diff-lcs (~> 1.1.3)
38
+ rspec-mocks (2.12.2)
39
39
  simplecov (0.7.1)
40
40
  multi_json (~> 1.0)
41
41
  simplecov-html (~> 0.7.1)
42
42
  simplecov-html (0.7.1)
43
- sinatra (1.3.3)
44
- rack (~> 1.3, >= 1.3.6)
45
- rack-protection (~> 1.2)
43
+ sinatra (1.3.4)
44
+ rack (~> 1.4)
45
+ rack-protection (~> 1.3)
46
46
  tilt (~> 1.3, >= 1.3.3)
47
47
  slop (3.4.3)
48
48
  tilt (1.3.3)
49
- timecop (0.5.5)
49
+ timecop (0.5.9.2)
50
50
  yajl-ruby (1.1.0)
51
51
 
52
52
  PLATFORMS
data/README.md CHANGED
@@ -6,7 +6,15 @@ Serif is a file-based blogging engine intended for simple sites. It compiles Mar
6
6
 
7
7
  # Changes and what's new
8
8
 
9
- See `CHANGELOG` for what's new.
9
+ ## Latest release
10
+
11
+ * Support drag-and-drop image uploading in the admin interface, with customisable paths. (#18)
12
+ * Generate private preview files for drafts, and generate the site on every draft change. (#19, #24)
13
+ * `serif dev` server serves 404s on missing files instead of 500 exceptions. (#22)
14
+ * Warn about _config.yml auth details after `serif new` skeleton (#23)
15
+ * Smarter onbeforeunload warnings that only fire if changes have been made. (#17)
16
+
17
+ See `CHANGELOG` for more.
10
18
 
11
19
  # Contents of this README
12
20
 
@@ -210,6 +218,7 @@ admin:
210
218
  username: username
211
219
  password: password
212
220
  permalink: /blog/:year/:month/:title
221
+ images_upload_path: /images/:timestamp_:name
213
222
  ```
214
223
 
215
224
  If a permalink setting is not given in the configuration, the default is `/:title`. There are the following options available for permalinks:
@@ -221,9 +230,28 @@ Placeholder | Value
221
230
  `:month` | Month as given in the filename, e.g., "01"
222
231
  `:day` | Day as given in the filename, e.g., "28"
223
232
 
233
+ ### Admin drag-and-drop upload path
234
+
235
+ The `images_upload_path` configuration setting is an _absolute path_ and will be relative to the base directory of your site, used in the admin interface to control where files are sent. The default value is `/images/:timestamp_:name`. Similar to permalinks, the following placeholders are available:
236
+
237
+ Placeholder | Value
238
+ ----------- |:-----
239
+ `:slug` | URL "slug" at the time of upload, e.g., "your-post-title"
240
+ `:year` | Year at the time of upload, e.g., "2013"
241
+ `:month` | Month at the time of upload, e.g., "02"
242
+ `:day` | Day at the time of upload, e.g., "16"
243
+ `:name` | Original filename string of the image being uploaded
244
+ `:timestamp`| Unix timestamp, e.g., "1361057832685"
245
+
246
+ Any slashes in `images_upload_path` are converted to directories.
247
+
224
248
  ## Other files
225
249
 
226
- Any other file in the directory's root will be copied over exactly as-is, with two caveats for any file ending in `.html` or `.xml`:
250
+ Any other file in the directory's root will be copied over exactly as-is, with two caveats.
251
+
252
+ First, `images/` is used for the drag-and-drop file uploads from the admin interface. Files are named with `<unix_timestamp>.<extension>`.
253
+
254
+ Second, for any file ending in `.html` or `.xml`:
227
255
 
228
256
  1. These files are assumed to contain [Liquid markup](http://liquidmarkup.org/) and will be processed as such.
229
257
  2. Any header data will not be included in the processed output.
@@ -382,7 +410,7 @@ In addition to those mentioned above, such as the archive page variables, there
382
410
 
383
411
  ## Post template variables
384
412
 
385
- These are available on individual post pages.
413
+ These are available on individual post pages, in `_template/post.html`.
386
414
 
387
415
  * `{{ post }}` --- the post being processed. Allows access to variables like `post.url`, `post.title`, `post.slug`, `post.created` and `post.content`.
388
416
  * `{{ prev_post }}` --- the post published chronologically before `post`.
data/bin/serif CHANGED
@@ -50,6 +50,12 @@ def produce_skeleton(dir)
50
50
  files.each do |f|
51
51
  FileUtils.cp_r(f, dir, verbose: true)
52
52
  end
53
+
54
+ puts
55
+ puts "*** NOTE ***"
56
+ puts
57
+ puts "You should now edit the username and password in _config.yml"
58
+ puts
53
59
  end
54
60
 
55
61
  commands = Slop::Commands.new do
@@ -42,7 +42,7 @@ class AdminServer
42
42
  get "/admin/new/draft" do
43
43
  content = Draft.new(site)
44
44
  autofocus = "slug"
45
- liquid :new_draft, locals: { post: content, autofocus: autofocus }
45
+ liquid :new_draft, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: autofocus }
46
46
  end
47
47
 
48
48
  post "/admin/new/draft" do
@@ -61,12 +61,13 @@ class AdminServer
61
61
  autofocus = "title" unless params[:title]
62
62
  autofocus = "slug" unless params[:slug]
63
63
 
64
- liquid :new_draft, locals: { error_message: error_message, post: content, autofocus: autofocus }
64
+ liquid :new_draft, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, autofocus: autofocus }
65
65
  else
66
66
  if Draft.exist?(site, params[:slug])
67
- liquid :new_draft, locals: { error_message: error_message, post: content, autofocus: autofocus }
67
+ liquid :new_draft, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, autofocus: autofocus }
68
68
  else
69
69
  content.save(params[:markdown])
70
+ site.generate
70
71
  redirect to("/admin")
71
72
  end
72
73
  end
@@ -106,7 +107,7 @@ class AdminServer
106
107
  error_message = "You must pick a URL to use"
107
108
  end
108
109
 
109
- liquid :edit_draft, locals: { error_message: error_message, post: content }
110
+ liquid :edit_draft, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, private_url: site.private_url(content) }
110
111
  else
111
112
  content.save(params[:markdown])
112
113
 
@@ -114,9 +115,10 @@ class AdminServer
114
115
  # a directory-change-level event.
115
116
  if params[:publish] == "yes"
116
117
  content.publish!
117
- site.generate
118
118
  end
119
119
 
120
+ site.generate
121
+
120
122
  redirect to("/admin")
121
123
  end
122
124
  end
@@ -133,7 +135,7 @@ class AdminServer
133
135
  error_message = "Content must not be blank." if params[:markdown].empty?
134
136
  error_message = "Title must not be blank." if params[:title].empty?
135
137
 
136
- liquid :edit_post, locals: { error_message: error_message, post: content }
138
+ liquid :edit_post, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content }
137
139
  else
138
140
  content.save(params[:markdown])
139
141
  site.generate
@@ -147,10 +149,10 @@ class AdminServer
147
149
 
148
150
  if params[:type] == "posts"
149
151
  content = site.posts.find { |p| p.slug == params[:slug] }
150
- liquid :edit_post, locals: { post: content, autofocus: "markdown" }
152
+ liquid :edit_post, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: "markdown" }
151
153
  elsif params[:type] == "drafts"
152
154
  content = Draft.from_slug(site, params[:slug])
153
- liquid :edit_draft, locals: { post: content, autofocus: "markdown" }
155
+ liquid :edit_draft, locals: { images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: "markdown", private_url: site.private_url(content) }
154
156
  else
155
157
  response.status = 404
156
158
  return "Nope"
@@ -171,6 +173,26 @@ class AdminServer
171
173
  Redcarpet::Markdown.new(Serif::MarkupRenderer, fenced_code_blocks: true).render(content).strip
172
174
  end
173
175
  end
176
+
177
+ post "/admin/attachment" do
178
+ attachment = params["attachment"]
179
+ filename = attachment["final_name"]
180
+ file = attachment["file"]
181
+ uid = attachment["uid"]
182
+
183
+ tempfile = file[:tempfile]
184
+
185
+ FileUtils.mkdir_p(File.join(site.directory, File.dirname(filename)))
186
+ FileUtils.mkdir_p(File.dirname(site.site_path(filename)))
187
+
188
+ # move to the source directory
189
+ FileUtils.mv(tempfile.path, File.join(site.directory, filename))
190
+
191
+ # copy to production to avoid the need to generate right now
192
+ FileUtils.copy(File.join(site.directory, filename), site.site_path(filename))
193
+
194
+ "File uploaded"
195
+ end
174
196
  end
175
197
 
176
198
  def initialize(source_directory)
data/lib/serif/config.rb CHANGED
@@ -14,6 +14,10 @@ class Config
14
14
  yaml["admin"]["password"]
15
15
  end
16
16
 
17
+ def image_upload_path
18
+ yaml["image_upload_path"] || "/images/:timestamp_:name"
19
+ end
20
+
17
21
  def permalink
18
22
  yaml["permalink"] || "/:title"
19
23
  end
@@ -51,10 +51,6 @@ class ContentFile
51
51
  end
52
52
  end
53
53
 
54
- def modified
55
- File.mtime(@path)
56
- end
57
-
58
54
  def draft?
59
55
  !published?
60
56
  end
@@ -104,15 +100,17 @@ class ContentFile
104
100
  converted_headers
105
101
  end
106
102
 
107
- def self.rename(original_slug, new_slug)
108
- raise if File.exist?("#{dirname}/#{new_slug}")
109
- File.rename("#{dirname}/#{original_slug}", "#{dirname}/#{new_slug}")
110
- end
111
-
112
103
  def save(markdown = nil)
113
104
  markdown ||= content if !new?
114
105
 
115
106
  save_path = path || "#{self.class.dirname}/#{@slug}"
107
+
108
+ if new?
109
+ set_publish_time(Time.now)
110
+ else
111
+ set_updated_time(Time.now)
112
+ end
113
+
116
114
  File.open(save_path, "w") do |f|
117
115
  f.puts %Q{#{raw_headers}
118
116
 
@@ -124,24 +122,11 @@ class ContentFile
124
122
 
125
123
  true # always return true for now
126
124
  end
127
-
128
- def [](header)
129
- h = headers[header]
130
- if h
131
- h
132
- else
133
- raise "no such header #{header}"
134
- end
135
- end
136
125
 
137
126
  def inspect
138
127
  %Q{<#{self.class} #{headers.inspect}>}
139
128
  end
140
129
 
141
- def self.all
142
- Post.all + Draft.all
143
- end
144
-
145
130
  protected
146
131
 
147
132
  def set_publish_time(time)
data/lib/serif/post.rb CHANGED
@@ -36,12 +36,6 @@ class Post < ContentFile
36
36
  all(site).find { |p| p.slug == slug }
37
37
  end
38
38
 
39
- def save(markdown)
40
- # update the timestamp if the content has actually changed
41
- set_updated_time(Time.now) unless markdown.strip == content.strip
42
- super
43
- end
44
-
45
39
  def to_liquid
46
40
  h = {
47
41
  "title" => title,
data/lib/serif/server.rb CHANGED
@@ -7,6 +7,8 @@ class DevelopmentServer
7
7
  class DevApp < Sinatra::Base
8
8
  set :public_folder, Dir.pwd
9
9
 
10
+ not_found { "Resource not found" }
11
+
10
12
  get "/" do
11
13
  File.read(File.expand_path("_site/index.html"))
12
14
  end
@@ -22,7 +24,7 @@ class DevelopmentServer
22
24
  # make a naive assumption that there's a 404 file at 404.html
23
25
  file ||= Dir[File.expand_path("_site/404.html")].first
24
26
 
25
- File.read(file)
27
+ file ? File.read(file) : 404
26
28
  end
27
29
  end
28
30
 
data/lib/serif/site.rb CHANGED
@@ -26,6 +26,7 @@ module Filters
26
26
  end
27
27
 
28
28
  def encode_uri_component(string)
29
+ return "" unless string
29
30
  CGI.escape(string)
30
31
  end
31
32
 
@@ -42,7 +43,7 @@ class FileDigest < Liquid::Tag
42
43
  DIGEST_CACHE = {}
43
44
 
44
45
  # file_digest "file.css" [prefix:.]
45
- Syntax = /^\s*(\S+)\s*(?:(prefix\s*:\s*\S+)\s*)?/
46
+ Syntax = /^\s*(\S+)\s*(?:(prefix\s*:\s*\S+)\s*)?$/
46
47
 
47
48
  def initialize(tag_name, markup, tokens)
48
49
  super
@@ -120,6 +121,18 @@ class Site
120
121
  most_recent ? most_recent.updated : Time.now
121
122
  end
122
123
 
124
+ # Gives the URL absolute path to a private draft preview.
125
+ #
126
+ # If the draft has no such preview available, returns nil.
127
+ def private_url(draft)
128
+ private_draft_pattern = site_path("/drafts/#{draft.slug}/*")
129
+ file = Dir[private_draft_pattern].first
130
+
131
+ return nil unless file
132
+
133
+ "/drafts/#{draft.slug}/#{File.basename(file, ".html")}"
134
+ end
135
+
123
136
  def bypass?(filename)
124
137
  !%w[.html .xml].include?(File.extname(filename))
125
138
  end
@@ -296,6 +309,8 @@ class Site
296
309
  next_post = post
297
310
  end
298
311
 
312
+ generate_draft_previews(default_layout)
313
+
299
314
  generate_archives(default_layout)
300
315
 
301
316
  if Dir.exist?("_site")
@@ -308,6 +323,47 @@ class Site
308
323
 
309
324
  private
310
325
 
326
+ # generates draft preview files for any unpublished drafts.
327
+ #
328
+ # uses the same template as live posts.
329
+ def generate_draft_previews(layout)
330
+ drafts = self.drafts
331
+
332
+ template = Liquid::Template.parse(File.read("_templates/post.html"))
333
+
334
+ # publish each draft under a randomly generated name, or use the
335
+ # existing file if one is present.
336
+ drafts.each do |draft|
337
+ url = private_url(draft)
338
+ if url
339
+ # take our existing URL like /drafts/foo/<random> (without .html)
340
+ # and give the filename
341
+ file = File.basename(url)
342
+ else
343
+ # create a new name
344
+ file = SecureRandom.hex(30)
345
+ end
346
+
347
+ # convert the name into a relative path
348
+ file = "drafts/#{draft.slug}/#{file}"
349
+
350
+ # the absolute path in the site's tmp path, where we create the file
351
+ # ready to be deployed.
352
+ live_preview_file = tmp_path(file)
353
+ FileUtils.mkdir_p(File.dirname(live_preview_file))
354
+
355
+ puts "#{url ? "Updating" : "Creating"} draft preview: #{file}"
356
+
357
+ File.open(live_preview_file + ".html", "w") do |f|
358
+ f.puts layout.render!(
359
+ "draft_preview" => true,
360
+ "page" => { "title" => [ "Draft Preview", draft.title ] },
361
+ "content" => template.render!("site" => self, "post" => draft)
362
+ )
363
+ end
364
+ end
365
+ end
366
+
311
367
  # goes through all draft posts that have "publish: now" headers and
312
368
  # calls #publish! on each one
313
369
  def preprocess_autopublish_drafts
data/lib/serif.rb CHANGED
@@ -8,6 +8,8 @@ require "redhead"
8
8
  require "cgi"
9
9
  require "digest"
10
10
 
11
+ require "securerandom"
12
+
11
13
  require "serif/content_file"
12
14
  require "serif/post"
13
15
  require "serif/draft"
data/serif.gemspec CHANGED
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = "serif"
3
- s.version = "0.2.2"
3
+ s.version = "0.2.3"
4
4
  s.authors = ["Adam Prescott"]
5
5
  s.email = ["adam@aprescott.com"]
6
6
  s.homepage = "https://github.com/aprescott/serif"
@@ -0,0 +1,91 @@
1
+ // Derived by Adam Prescott from a code snippet used
2
+ // with implied permission:
3
+ //
4
+ // http://blog.alexmaccaw.com/svbtle-image-uploading
5
+
6
+ var createAttachment = function(file, element) {
7
+ var data = new FormData();
8
+
9
+ var d = new Date();
10
+ var uid = d.getTime();
11
+
12
+ var month = d.getMonth().toString();
13
+ if (month.length < 2) { month = "0" + month; }
14
+
15
+ var day = d.getDate().toString();
16
+ if (day.length < 2) { day = "0" + day; }
17
+
18
+ var processedName = file.name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/__+/g, "_");
19
+
20
+ var s = Serif.variables["imageUploadPattern"];
21
+
22
+ if (/:slug/.test(s) && !(Serif.variables["currentSlug"] && Serif.variables["currentSlug"])) {
23
+ alert("Your image upload path is set to use a slug, but no such slug exists yet.");
24
+ return null;
25
+ }
26
+
27
+ var placeholderValues = {
28
+ ":slug": Serif.variables["currentSlug"],
29
+ ":timestamp": uid.toString(),
30
+ ":year": d.getFullYear().toString(),
31
+ ":month": month,
32
+ ":day": day,
33
+ ":name": processedName
34
+ };
35
+
36
+ $.each(placeholderValues, function(placeholder, value) {
37
+ s = s.replace(placeholder, value);
38
+ });
39
+
40
+ var extension = file.name.substring(file.name.lastIndexOf('.') + 1);
41
+
42
+ var finalName = s;
43
+
44
+ // if it doesn't already have the extension in the name, add it,
45
+ // otherwise, correct the, e.g., _png, to .png.
46
+ //
47
+ // this just avoids _png.png noise
48
+ if (finalName.substring(finalName.length - extension.length - 1) == ("_" + extension)) {
49
+ finalName = finalName.replace(new RegExp("_" + extension + "$"), "." + extension);
50
+ } else {
51
+ finalName = finalName + "." + extension;
52
+ }
53
+
54
+ data.append('attachment[file]', file);
55
+ data.append('attachment[uid]', uid);
56
+ data.append('attachment[final_name]', finalName);
57
+
58
+ $.ajax({
59
+ url: '/admin/attachment',
60
+ data: data,
61
+ cache: false,
62
+ contentType: false,
63
+ processData: false,
64
+ type: 'POST',
65
+ }).error(function() {
66
+ console.log("error uploading image");
67
+ });
68
+
69
+ var absText = '![' + file.name + '](' + finalName + ')';
70
+ $(element).insertAtCaret(absText);
71
+ };
72
+
73
+ $(function() {
74
+ if ($("[data-attachify]").length > 0) {
75
+ $(document).dropArea();
76
+
77
+ $(document).bind("drop", function(e) {
78
+ e.preventDefault();
79
+ e = e.originalEvent;
80
+
81
+ var files = e.dataTransfer.files;
82
+
83
+ for (var i=0; i < files.length; i++) {
84
+ // Only upload images
85
+ if (/image/.test(files[i].type)) {
86
+ createAttachment(files[i], $("[data-attachify]").first());
87
+ }
88
+ };
89
+ });
90
+ }
91
+ });
@@ -0,0 +1,33 @@
1
+ // All credit to Alex MacCaw:
2
+ //
3
+ // https://gist.github.com/maccman/2907187
4
+
5
+ (function($){
6
+ function dragEnter(e) {
7
+ $(e.target).addClass("dragOver");
8
+ e.stopPropagation();
9
+ e.preventDefault();
10
+ return false;
11
+ };
12
+
13
+ function dragOver(e) {
14
+ e.originalEvent.dataTransfer.dropEffect = "copy";
15
+ e.stopPropagation();
16
+ e.preventDefault();
17
+ return false;
18
+ };
19
+
20
+ function dragLeave(e) {
21
+ $(e.target).removeClass("dragOver");
22
+ e.stopPropagation();
23
+ e.preventDefault();
24
+ return false;
25
+ };
26
+
27
+ $.fn.dropArea = function(){
28
+ this.bind("dragenter", dragEnter).
29
+ bind("dragover", dragOver).
30
+ bind("dragleave", dragLeave);
31
+ return this;
32
+ };
33
+ })(jQuery);
@@ -0,0 +1,39 @@
1
+ // All credit to Alex MacCaw
2
+ //
3
+ // https://gist.github.com/maccman/2907189
4
+
5
+ (function($){
6
+ var insertAtCaret = function(value) {
7
+ if (document.selection) { // IE
8
+ this.focus();
9
+ sel = document.selection.createRange();
10
+ sel.text = value;
11
+ this.focus();
12
+ }
13
+ else if (this.selectionStart || this.selectionStart == '0') {
14
+ var startPos = this.selectionStart;
15
+ var endPos = this.selectionEnd;
16
+ var scrollTop = this.scrollTop;
17
+
18
+ this.value = [
19
+ this.value.substring(0, startPos),
20
+ value,
21
+ this.value.substring(endPos, this.value.length)
22
+ ].join('');
23
+
24
+ this.focus();
25
+ this.selectionStart = startPos + value.length;
26
+ this.selectionEnd = startPos + value.length;
27
+ this.scrollTop = scrollTop;
28
+
29
+ } else {
30
+ throw new Error('insertAtCaret not supported');
31
+ }
32
+ };
33
+
34
+ $.fn.insertAtCaret = function(value){
35
+ $(this).each(function(){
36
+ insertAtCaret.call(this, value);
37
+ })
38
+ };
39
+ })(jQuery);
@@ -8,7 +8,45 @@
8
8
  # permalink: [permalink format for generated posts]
9
9
  #
10
10
  # See the README for information on permalink formats.
11
-
11
+ #
12
+ # Image upload path configuration, used for any images added
13
+ # through the admin drag-and-drop interface:
14
+ #
15
+ # image_upload_path: [/some/absolute/path/:with/:placeholders]
16
+ #
17
+ # This should be an ABSOLUTE PATH and will be relative to your
18
+ # base site (e.g., mysite.com). The value used will have the
19
+ # relevant extension appended AUTOMATICALLY.
20
+ #
21
+ # The default value is: /images/:timestamp_:name
22
+ #
23
+ # Possible placeholders for the image upload path:
24
+ #
25
+ # :year
26
+ # current full year, e.g., "2013"
27
+ #
28
+ # :month
29
+ # current month as a two digit value, e.g., "02"
30
+ #
31
+ # :day
32
+ # current day as a two digit value, e.g., "01"
33
+ #
34
+ # :timestamp
35
+ # unix timestamp in seconds, e.g., "1361057832685"
36
+ #
37
+ # :name
38
+ # filename of the image being uploaded
39
+ #
40
+ # :slug
41
+ # the slug of the post at the time the image is added
42
+ #
43
+ # Any slashes in this string will become directories relative to the
44
+ # base site directory.
45
+ #
46
+ # Example: image_upload_path: /images/:slug/:year_:month_:day_:timestamp
47
+ #
48
+ # This would lead to an example path of
49
+ # /images/sample-post/2013_02_16_1361057832685.png
12
50
  admin:
13
51
  username: changethisusername
14
52
  password: changethispassword
@@ -14,6 +14,10 @@
14
14
  </code>
15
15
  </h2>
16
16
 
17
+ <h2 id="private-url">
18
+ <code><span>preview:</span> <a href="{{ private_url }}">{{ private_url | escape }}</a></code>
19
+ </h2>
20
+
17
21
  <div class="post{% if post.draft %} draft{% endif %}">
18
22
  <header>
19
23
  <h1 id="heading" class="post">
@@ -28,7 +32,7 @@
28
32
  </header>
29
33
  </div>
30
34
  <div id="content-areas">
31
- <textarea spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown"{% if autofocus == "markdown" %}{{ " autofocus" }}{% endif %}>{{ post.content | strip | escape }}</textarea>
35
+ <textarea data-attachify spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown"{% if autofocus == "markdown" %}{{ " autofocus" }}{% endif %}>{{ post.content | strip | escape }}</textarea>
32
36
 
33
37
  <article id="entry" class="post" data-state="hidden"></article>
34
38
  </div>
@@ -51,13 +55,8 @@
51
55
  </form>
52
56
  {% endif %}
53
57
 
54
- </div>
58
+ <aside>
59
+ <p>Drag and drop images into the form above to upload them.</p>
60
+ </aside>
55
61
 
56
-
57
- <script>
58
- $(function() {
59
- window.onbeforeunload = function() {
60
- return "You may lose unsaved changes if you leave this page.";
61
- }
62
- });
63
- </script>
62
+ </div>
@@ -17,14 +17,14 @@
17
17
  <div class="post draft">
18
18
  <header>
19
19
  <h1 id="heading" class="post">
20
- <textarea spellcheck="false" placeholder="It was a dark and stormy night" name="title" autofocus>{{ post.title | escape }}</textarea>
20
+ <textarea data-attachify spellcheck="false" placeholder="It was a dark and stormy night" name="title" autofocus>{{ post.title | escape }}</textarea>
21
21
  </h1>
22
22
 
23
23
  <time datetime="{{ post.created | date: "%Y-%m-%d" }}">{{ post.created | date: "%B %d %Y" }}</time>
24
24
  </header>
25
25
  </div>
26
26
  <div id="content-areas">
27
- <textarea spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown">{{ post.content | strip | escape }}</textarea>
27
+ <textarea data-attachify spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown">{{ post.content | strip | escape }}</textarea>
28
28
 
29
29
  <article id="entry" class="post" data-state="hidden"></article>
30
30
  </div>
@@ -33,12 +33,8 @@
33
33
 
34
34
  <input type="checkbox" id="render-preview"> <label class="preview" for="render-preview">Preview</label>
35
35
  </form>
36
- </div>
37
36
 
38
- <script>
39
- $(function() {
40
- window.onbeforeunload = function() {
41
- return "You may lose unsaved changes if you leave this page.";
42
- }
43
- });
44
- </script>
37
+ <aside>
38
+ <p>Drag and drop images into the form above to upload them.</p>
39
+ </aside>
40
+ </div>
@@ -43,7 +43,7 @@ body > nav ul li:last-child {
43
43
  background: none;
44
44
  }
45
45
 
46
- #edit #slug {
46
+ #edit #slug, #edit #private-url {
47
47
  color: #aaaaaa;
48
48
  text-transform: lowercase;
49
49
  margin-bottom: 0;
@@ -182,6 +182,12 @@ body > nav ul li:last-child {
182
182
  min-height: 20em;
183
183
  }
184
184
 
185
+ #edit > aside {
186
+ opacity: 0.5;
187
+ position: absolute;
188
+ bottom: -4em;
189
+ }
190
+
185
191
  #edit #entry {
186
192
  display: none;
187
193
  width: 100%;
@@ -336,11 +342,43 @@ body > nav ul li:last-child {
336
342
  <script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
337
343
  <script src="/admin/js/jquery.autosize.js"></script>
338
344
  <script src="/admin/js/mousetrap.min.js"></script>
345
+ <script src="/admin/js/jquery.drop.js"></script>
346
+ <script src="/admin/js/jquery.insert.js"></script>
347
+ <script src="/admin/js/attachment.js"></script>
339
348
 
340
349
  <script>
350
+ var Serif = {
351
+ "variables": {
352
+ "imageUploadPattern": "{{ images_path }}",
353
+ "currentSlug": {% if post.slug %}"{{ post.slug }}"{% else %}null{% endif %}
354
+ }
355
+ };
356
+ </script>
357
+
358
+ <script>
359
+ function setDepartureCheck(enable) {
360
+ if (enable) {
361
+ window.onbeforeunload = function() {
362
+ return "You may lose unsaved changes if you leave this page.";
363
+ }
364
+ } else {
365
+ window.onbeforeunload = null;
366
+ }
367
+ }
368
+
369
+ setDepartureCheck(false);
370
+
371
+ $(function() {
372
+ $("input, textarea").change(function(event) {
373
+ setDepartureCheck(true);
374
+ });
375
+ });
376
+
341
377
  $("#edit").ready(function() {
342
378
  $("#slug input").blur(function() {
343
- $(this).val($(this).val().trim().replace(/\s+/g, '-').replace(/^-+|-+$/g, '').toLowerCase());
379
+ $(this).val($(this).val().trim().replace(/\s+/g, '-').replace(/"/g, "-").replace(/^-+|-+$/g, '').toLowerCase());
380
+
381
+ Serif.variables["currentSlug"] = $(this).val();
344
382
  });
345
383
 
346
384
  $("#slug input").keyup(function(event) {
@@ -356,6 +394,10 @@ body > nav ul li:last-child {
356
394
 
357
395
  $("#confirm-delete + label").text("Delete");
358
396
 
397
+ $("#edit form input[type=submit]").click(function(event) {
398
+ setDepartureCheck(false);
399
+ });
400
+
359
401
  $("#delete input[type=submit]").click(function(event) {
360
402
  var el = $(this);
361
403
 
@@ -372,6 +414,7 @@ body > nav ul li:last-child {
372
414
 
373
415
  $("#confirm-delete + label").click(function(event) {
374
416
  event.preventDefault();
417
+ setDepartureCheck(false);
375
418
  $(this).parent("form").submit();
376
419
  });
377
420
 
@@ -425,6 +468,7 @@ body > nav ul li:last-child {
425
468
  });
426
469
 
427
470
  Mousetrap.bind(["ctrl+s", "command+s"], function(e) {
471
+ setDepartureCheck(false);
428
472
  $("#edit form input[type=submit]").click();
429
473
  $("#shortcuts").hide();
430
474
  return false;
@@ -445,7 +489,7 @@ body > nav ul li:last-child {
445
489
  });
446
490
 
447
491
  Mousetrap.bind(["ctrl+.", "command+."], function(e) {
448
- $("#delete, #save, #render-preview + label, #publish + label").fadeToggle();
492
+ $("#delete, #save, #render-preview + label, #publish + label, #edit > aside, #private-url").fadeToggle();
449
493
  $("#slug, .post > header time").slideToggle();
450
494
  $("#content").focus();
451
495
  $("body > nav").toggleClass("hidden");
@@ -22,7 +22,7 @@
22
22
  </header>
23
23
  </div>
24
24
  <div id="content-areas">
25
- <textarea spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown"{% if autofocus == "markdown" %}{{ " autofocus" }}{% endif %}>{{ post.content | strip | escape }}</textarea>
25
+ <textarea data-attachify spellcheck="false" id="content" placeholder="Lorem ipsum dolor sit amet." name="markdown"{% if autofocus == "markdown" %}{{ " autofocus" }}{% endif %}>{{ post.content | strip | escape }}</textarea>
26
26
 
27
27
  <article id="entry" class="post" data-state="hidden"></article>
28
28
  </div>
@@ -32,12 +32,8 @@
32
32
  <input type="checkbox" id="render-preview"> <label class="preview" for="render-preview">Preview</label>
33
33
  </form>
34
34
 
35
- </div>
35
+ <aside>
36
+ <p>Drag and drop images into the form above to upload them.</p>
37
+ </aside>
36
38
 
37
- <script>
38
- $(function() {
39
- window.onbeforeunload = function() {
40
- return "You may lose unsaved changes if you leave this page.";
41
- }
42
- });
43
- </script>
39
+ </div>
@@ -0,0 +1,39 @@
1
+ require "test_helper"
2
+
3
+ describe Serif::ContentFile do
4
+ subject do
5
+ Serif::Site.new(testing_dir)
6
+ end
7
+
8
+ describe "#title=" do
9
+ it "sets the underlying header value to the assigned title" do
10
+ (subject.drafts + subject.posts).each do |content_file|
11
+ content_file.title = "foobar"
12
+ content_file.headers[:title].should == "foobar"
13
+ end
14
+ end
15
+ end
16
+
17
+ describe "#save(markdown)" do
18
+ it "sets the underlying updated time value for posts" do
19
+ draft = Serif::Draft.new(subject)
20
+ draft.title = "Testing"
21
+ draft.slug = "hi"
22
+
23
+ begin
24
+ draft.save("# Some content")
25
+ draft.publish!
26
+
27
+ post = Serif::Post.from_slug(subject, draft.slug)
28
+
29
+ t = Time.now
30
+ Timecop.freeze(t + 30) do
31
+ post.save("# Heading content")
32
+ post.updated.to_i.should == (t + 30).to_i
33
+ end
34
+ ensure
35
+ FileUtils.rm(post.path)
36
+ end
37
+ end
38
+ end
39
+ end
@@ -30,5 +30,9 @@ describe Serif::FileDigest do
30
30
  it "ignores trailing whitespace on the prefix" do
31
31
  file_digest("test-stylesheet.css prefix:. ").render(@context).should == ".f8390232f0c354a871f9ba0ed306163c"
32
32
  end
33
+
34
+ it "raises a SyntaxError on invalid syntax" do
35
+ expect { file_digest("test-stylesheet.css pefoiejw").render(@context) }.to raise_error(SyntaxError)
36
+ end
33
37
  end
34
38
  end
data/test/filters_spec.rb CHANGED
@@ -43,6 +43,10 @@ describe Serif::Filters do
43
43
  subject.encode_uri_component(char).should == enc_char
44
44
  end
45
45
  end
46
+
47
+ it "returns an empty string on nil input" do
48
+ subject.encode_uri_component(nil).should == ""
49
+ end
46
50
  end
47
51
 
48
52
  describe "#xmlschema" do
data/test/post_spec.rb CHANGED
@@ -12,4 +12,10 @@ describe Serif::Post do
12
12
  it "uses the config file's permalink value" do
13
13
  @posts.all? { |p| p.url == "/test-blog/#{p.slug}" }.should be_true
14
14
  end
15
+
16
+ describe "#inspect" do
17
+ it "includes headers" do
18
+ @posts.all? { |p| p.inspect.should include(p.headers.inspect) }
19
+ end
20
+ end
15
21
  end
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <meta charset="UTF-8">
3
+ <title>My site: Draft Preview - another sample draft</title>
4
+ <h1>mysite.com</h1>
5
+
6
+
7
+ <h2>another sample draft</h2>
8
+
9
+ <p>another-sample-draft</p>
10
+
11
+ <p><a href="http://twitter.com/share?text=another+sample+draft&amp;url=http%3A%2F%2Fwww.mysite.com">Submit this to Twitter.</p>
12
+
13
+
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <meta charset="UTF-8">
3
+ <title>My site: Draft Preview - Sample draft</title>
4
+ <h1>mysite.com</h1>
5
+
6
+
7
+ <h2>Sample draft</h2>
8
+
9
+ <p>Just a sample draft.</p>
10
+
11
+ <p><a href="http://twitter.com/share?text=Sample+draft&amp;url=http%3A%2F%2Fwww.mysite.com">Submit this to Twitter.</p>
12
+
13
+
@@ -3,6 +3,7 @@
3
3
  <title>My site: Posts - Some draft title</title>
4
4
  <h1>mysite.com</h1>
5
5
 
6
+
6
7
  <h2>Some draft title</h2>
7
8
 
8
9
  <p>some content</p>
@@ -3,6 +3,7 @@
3
3
  <title>My site: Posts - Sample post</title>
4
4
  <h1>mysite.com</h1>
5
5
 
6
+
6
7
  <h2>Sample post</h2>
7
8
 
8
9
  <p>Just a sample post.</p>
@@ -3,6 +3,7 @@
3
3
  <title>My site: Posts - Second post</title>
4
4
  <h1>mysite.com</h1>
5
5
 
6
+
6
7
  <h2>Second post</h2>
7
8
 
8
9
  <p>Second post.</p>
@@ -1,3 +1,4 @@
1
+ {% if draft_preview %}<p>draftpreviewflagexists</p>{% endif %}
1
2
  <h2>{{ post.title }}</h2>
2
3
 
3
4
  {{ post.content | markdown }}
@@ -0,0 +1,5 @@
1
+ title: Some draft title
2
+ Updated: 2013-02-16T20:49:57+00:00
3
+ Created: 2013-02-16T20:49:57+00:00
4
+
5
+ some content
@@ -1,4 +1,4 @@
1
1
  title: Some draft title
2
- Created: 2013-02-09T23:02:08+00:00
2
+ Updated: 2013-02-16T20:49:57+00:00
3
3
 
4
4
  some content
@@ -49,6 +49,42 @@ describe Serif::Site do
49
49
  previous_title[/(?<=: ).+/].should == "Sample post"
50
50
  end
51
51
 
52
+ it "sets a draft_preview flag for preview urls" do
53
+ preview_flag_pattern = /draftpreviewflagexists/
54
+
55
+ subject.generate
56
+
57
+ d = Serif::Draft.from_slug(subject, "sample-draft")
58
+ preview_contents = File.read(testing_dir("_site/#{subject.private_url(d)}.html"))
59
+ preview_contents =~ preview_flag_pattern
60
+
61
+ # does not exist on live published pages
62
+ (File.read(testing_dir("_site/test-blog/second-post.html")) =~ preview_flag_pattern).should be_false
63
+ end
64
+
65
+ it "creates draft preview files" do
66
+ subject.generate
67
+
68
+ Dir.exist?(testing_dir("_site/drafts")).should be_true
69
+ Dir[File.join(testing_dir("_site/drafts/*"))].size.should == subject.drafts.size
70
+
71
+ Dir.exist?(testing_dir("_site/drafts/sample-draft")).should be_true
72
+ Dir[File.join(testing_dir("_site/drafts/sample-draft"), "*.html")].size.should == 1
73
+
74
+ d = Serif::Draft.from_slug(subject, "sample-draft")
75
+ subject.private_url(d).should_not be_nil
76
+
77
+ # absolute paths
78
+ (subject.private_url(d) =~ /\A\/drafts\/#{d.slug}\/.*\z/).should be_true
79
+
80
+ # 60 characters long (30 bytes as hex chars)
81
+ (subject.private_url(d) =~ /\A\/drafts\/#{d.slug}\/[a-z0-9]{60}\z/).should be_true
82
+
83
+ # does not create more than one
84
+ subject.generate
85
+ Dir[File.join(testing_dir("_site/drafts/sample-draft"), "*.html")].size.should == 1
86
+ end
87
+
52
88
  context "for drafts with a publish: now header" do
53
89
  before :all do
54
90
  @time = Time.utc(2012, 12, 21, 15, 30, 00)
data/test/site_spec.rb CHANGED
@@ -23,6 +23,14 @@ describe Serif::Site do
23
23
  end
24
24
  end
25
25
 
26
+ describe "#private_url" do
27
+ it "returns nil for a draft without an existing file" do
28
+ d = double("")
29
+ d.stub(:slug) { "foo" }
30
+ subject.private_url(d).should be_nil
31
+ end
32
+ end
33
+
26
34
  describe "#latest_update_time" do
27
35
  it "is the latest time that a post was updated" do
28
36
  subject.latest_update_time.should == Serif::Post.all(subject).max_by { |p| p.updated }.updated
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: serif
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.2.3
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2013-02-09 00:00:00.000000000 Z
12
+ date: 2013-02-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rack
@@ -235,13 +235,17 @@ files:
235
235
  - statics/skeleton/_layouts/default.html
236
236
  - statics/skeleton/archive.html
237
237
  - statics/assets/js/mousetrap.min.js
238
+ - statics/assets/js/jquery.insert.js
239
+ - statics/assets/js/attachment.js
238
240
  - statics/assets/js/jquery.autosize.js
241
+ - statics/assets/js/jquery.drop.js
239
242
  - bin/serif
240
243
  - test/test_helper.rb
241
244
  - test/filters_spec.rb
242
245
  - test/config_spec.rb
243
246
  - test/liquid_filter_date_extension_spec.rb
244
247
  - test/site_spec.rb
248
+ - test/content_file_spec.rb
245
249
  - test/draft_spec.rb
246
250
  - test/markup_renderer_spec.rb
247
251
  - test/site_dir/index.html
@@ -257,6 +261,8 @@ files:
257
261
  - test/site_dir/_site/test-archive/2013/01/index.html
258
262
  - test/site_dir/_site/test-archive/2012/12/index.html
259
263
  - test/site_dir/_site/test-archive/2012/11/index.html
264
+ - test/site_dir/_site/drafts/another-sample-draft/cdc8037ed098e34a19fc6671ab652f908dd94009ff642d4e5ee80fa566fa.html
265
+ - test/site_dir/_site/drafts/sample-draft/f828e5f22d76b04c586e90680ac814e6b233d3d380f6be6975beba75081b.html
260
266
  - test/site_dir/_site/index.html
261
267
  - test/site_dir/_site/file-digest-test.html
262
268
  - test/site_dir/_site/page-alt-layout.html
@@ -266,8 +272,8 @@ files:
266
272
  - test/site_dir/_site/test-blog/second-post.html
267
273
  - test/site_dir/_site/test-blog/sample-post.html
268
274
  - test/site_dir/_site/archive.html
269
- - test/site_dir/_trash/1360450928-autopublish-draft
270
- - test/site_dir/_trash/1360450928-test-draft
275
+ - test/site_dir/_trash/1361047797-test-draft
276
+ - test/site_dir/_trash/1361047797-autopublish-draft
271
277
  - test/site_dir/_posts/2012-01-05-sample-post
272
278
  - test/site_dir/_posts/2013-01-01-second-post
273
279
  - test/site_dir/_layouts/alt-layout.html
@@ -312,6 +318,7 @@ test_files:
312
318
  - test/config_spec.rb
313
319
  - test/liquid_filter_date_extension_spec.rb
314
320
  - test/site_spec.rb
321
+ - test/content_file_spec.rb
315
322
  - test/draft_spec.rb
316
323
  - test/markup_renderer_spec.rb
317
324
  - test/file_digest_tag_spec.rb
@@ -1,3 +0,0 @@
1
- title: Some draft title
2
-
3
- some content