serif 0.2.2 → 0.2.3

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.
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