diff --git a/.gitignore b/.gitignore index a1d112a..4b1dc20 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ /serif-*.gem /coverage /spec/*/_site -/spec/*/_trash /spec/examples.txt /.rbx /docs diff --git a/.travis.yml b/.travis.yml index feb2cd2..acfb6d2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,15 @@ cache: language: ruby cache: bundler rvm: - - 2.0.0 - 2.1 - 2.2 - ruby-head script: - bundle exec rspec - "bundle exec bundle-audit update && bundle exec bundle-audit check" +env: + - USE_SHELL=no + - USE_SHELL=yes matrix: allow_failures: - rvm: ruby-head diff --git a/CHANGELOG.md b/CHANGELOG.md index 8db12c1..bc295c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,11 @@ # next release * Prevent /tmp from filling up over time. (#77) +* Remove /tmp/_site snapshots altogether. (#82) +* The admin interface is removed. (#82) When upgrading to this version, you should update your site. This includes: + * Remove any deployment webserver configuration (e.g., mapping `/admin` to the correct port). + * Update `_config.yml` since the entire `admin:` section is now unused. + * Remove `css/admin/admin.css` since it's now unused. # v0.6 diff --git a/Gemfile.lock b/Gemfile.lock index f6a0319..d0288bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -4,14 +4,10 @@ PATH serif (0.6) kramdown (>= 1.9.0) liquid (~> 2.0) - nokogiri - rack redhead - reverse_markdown rouge (>= 1.10.0) rubypants sinatra - timeout_cache GEM remote: https://rubygems.org/ @@ -20,13 +16,6 @@ GEM bundler (~> 1.2) thor (~> 0.18) byebug (8.2.0) - capybara (2.5.0) - mime-types (>= 1.16) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) - cliver (0.3.2) coderay (1.1.0) coveralls (0.8.9) json (~> 1.8) @@ -39,8 +28,6 @@ GEM docile (1.1.5) domain_name (0.5.25) unf (>= 0.0.5, < 1.0.0) - gherkin (2.12.2) - multi_json (~> 1.3) http-cookie (1.0.2) domain_name (~> 0.5) json (1.8.3) @@ -49,15 +36,9 @@ GEM method_source (0.8.2) mime-types (2.6.2) mini_portile (0.6.2) - multi_json (1.11.2) netrc (0.11.0) nokogiri (1.6.6.3) mini_portile (~> 0.6.0) - poltergeist (1.8.0) - capybara (~> 2.1) - cliver (~> 0.3.1) - multi_json (~> 1.0) - websocket-driver (>= 0.2.0) pry (0.10.3) coderay (~> 1.1.0) method_source (~> 0.8.1) @@ -68,17 +49,12 @@ GEM rack (1.6.4) rack-protection (1.5.3) rack - rack-test (0.6.3) - rack (>= 1.0) rake (10.4.2) - rdoc (4.2.0) redhead (0.0.9) rest-client (1.8.0) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 3.0) netrc (~> 0.7) - reverse_markdown (1.0.0) - nokogiri rouge (1.10.1) rspec (3.4.0) rspec-core (~> 3.4.0) @@ -89,6 +65,9 @@ GEM rspec-expectations (3.4.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.4.0) + rspec-its (1.2.0) + rspec-core (>= 3.0.0) + rspec-expectations (>= 3.0.0) rspec-mocks (3.4.0) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.4.0) @@ -109,36 +88,25 @@ GEM thor (0.19.1) tilt (2.0.1) timecop (0.8.0) - timeout_cache (0.0.2) tins (1.6.0) - turnip (1.3.1) - gherkin (>= 2.5) - rspec (>= 2.14.0, < 4.0) unf (0.1.4) unf_ext unf_ext (0.0.7.1) - websocket-driver (0.6.3) - websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.2) - xpath (2.0.0) - nokogiri (~> 1.3) PLATFORMS ruby DEPENDENCIES bundler-audit - capybara coveralls - poltergeist + nokogiri pry-byebug rake - rdoc rspec + rspec-its serif! simplecov timecop - turnip BUNDLED WITH 1.10.6 diff --git a/README.md b/README.md index 78ec334..ab5c7e2 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,7 @@ [![Build Status](https://travis-ci.org/aprescott/serif.png?branch=master)](https://travis-ci.org/aprescott/serif) [![Code Climate](https://codeclimate.com/github/aprescott/serif.png)](https://codeclimate.com/github/aprescott/serif) [![Coverage Status](https://coveralls.io/repos/aprescott/serif/badge.png?branch=master)](https://coveralls.io/r/aprescott/serif) -Serif is a static site generator and blogging system powered by markdown files and an optional admin interface complete with drag-and-drop image uploading. ([Check out the simple video demo](https://docs.google.com/open?id=0BxPQpxGSOOyKS1J4MmlnM3JIaXM).) - -Serif releases you from managing a file system so you can focus on writing content. - -Having problems with Serif? [Open an issue on GitHub](https://github.com/aprescott/serif/issues) or use the [Serif Google Group](https://groups.google.com/forum/#!forum/serif-rb). +Serif is a static site generator and blogging system powered by markdown files. ## First time use @@ -36,12 +32,10 @@ Now visit to view the site. * [Archive pages](#archive-pages) * [Configuration](#configuration) * [Deploying](#deploying) -* [Customising the admin interface](#customising-the-admin-interface) * [Custom tags and filters](#custom-tags-and-filters) * [Template variables](#template-variables) * [Developing Serif](#developing-serif) * [Changes and what's new](#changes-and-whats-new) -* [Planned features](#planned-features) # Intro @@ -82,17 +76,6 @@ $ cd path/to/site/directory $ serif generate ``` -## Starting the admin server - -```bash -$ cd path/to/site/directory -$ ENV=production serif admin -``` - -Once this is run, visit and log in with whatever is in `_config.yml` as auth credentials. - -Drop the `ENV=production` part if you're running it locally. - ## Serving up the site for development This runs a very simple web server that is mainly designed to test what the site will look like and let you make changes to stuff like CSS files without having to regenerate everything. Changes to post content will not be detected (yet). @@ -216,11 +199,10 @@ Used for configuration settings. Here's a sample configuration: ```yaml -admin: - username: username - password: password -permalink: /blog/:year/:month/:title -image_upload_path: /images/:timestamp_:name +permalink: /blog/:title +archive: + enabled: yes + url_format: /blog/:year/:month ``` If a permalink setting is not given in the configuration, the default is `/:title`. There are the following options available for permalinks: @@ -234,31 +216,11 @@ Placeholder | Value NOTE: if you change the permalink value, you will break existing URLs for published posts, in addition to, e.g., any feed ID values that depend on the post URL never changing. -### Admin drag-and-drop upload path - -The `image_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: - -Placeholder | Value ------------ |:----- -`:slug` | URL "slug" at the time of upload, e.g., "your-post-title" -`:year` | Year at the time of upload, e.g., "2013" -`:month` | Month at the time of upload, e.g., "02" -`:day` | Day at the time of upload, e.g., "16" -`:name` | Original filename string of the image being uploaded -`:timestamp`| Unix timestamp, e.g., "1361057832685" - -Any slashes in `image_upload_path` are converted to directories. - ## Other files -Any other file in the directory's root will be copied over exactly as-is, with two caveats. - -First, `images/` (by default) is used for the drag-and-drop file uploads from the admin interface. Files are named with `_ .`. This is configurable, see the section on configuration. - -Second, for any file ending in `.html` or `.xml`: +Any other file in the directory's root will mostly be copied over exactly as-is. -1. These files are assumed to contain [Liquid markup](http://liquidmarkup.org/) and will be processed as such. -2. Any header data will not be included in the processed output. +An exception is any file ending in `.html` or `.xml`. These files are assumed to contain [Liquid markup](http://liquidmarkup.org/) and will be processed as such. Header values will be available on `page`. For example, this would work as an `about.html`: @@ -270,24 +232,24 @@ For example, this would work as an `about.html`: And so would this: ```html -title: My about page +x: y

All about me

-

Where do I begin? Well...

+

Where do I begin? Well... First, x is {{ page.x }}.

``` -In both cases, the output is, of course: +If you have a file like `feed.xml` that you wish to _not_ be contained within a layout, specify `layout: none` in the header for the file: ```html -

All about me

-

Where do I begin? Well...

-``` +layout: none -If you have a file like `feed.xml` that you wish to _not_ be contained within a layout, specify `layout: none` in the header for the file. + + +``` # Publishing drafts -To publish a draft, either do so through the admin interface available with `serif admin`, or add a `publish: now` header to the draft: +To automatically publish a draft, add a `publish: now` header to the draft: ``` title: A draft that will be published @@ -316,8 +278,6 @@ Created: 2013-01-01T12:01:30+00:00 Updated: 2013-03-18T19:03:30+00:00 ``` -Admin users: this is all done for you. - # Archive pages By default, archive pages are made available at `/archive/:year/month`, e.g., `/archive/2012/11`. Individual archive pages can be customised by editing the `_templates/archive_page.html` file, which is used for each month. @@ -330,7 +290,7 @@ To disable archive pages, or configure the URL format, see the section on config To link to archive pages, there is a `site.archive` template variable available in all pages. The structure of `site.archive` is a nested map starting at years: -``` +```ruby { "posts" => [...], "years" => [ @@ -353,18 +313,13 @@ Using `site.archive`, you can iterate over `years`, then iterate over `months` a Configuration goes in `_config.yml` and must be valid YAML. Here's a sample configuration with available options: -``` -admin: - username: myusername - password: mypassword +```yaml permalink: /posts/:title archive: enabled: yes url_format: /archive/:year/:month ``` -`admin` contains the credentials used when accessing the admin interface. This information is private, of course. - `permalink` is the URL format for individual post pages. The default permalink value is `/:title`. For an explanation of the format of permalinks, see above. `archive` contains configuration options concerning archive pages. `enabled` can be used to toggle whether archive pages are generated. If set to `no` or `false`, no archive pages will be generated. By default, this value is `yes`. @@ -393,23 +348,6 @@ location @not_found_page { Use `ENV=production serif generate` to regenerate the site for production. -## Admin interface - -The admin server can be started on the live server the same way it's started locally (with `ENV=production`). To access it from anywhere on the web, you will need to proxy/forward `/admin` HTTP requests to port 4567 to let the admin web server handle it. As an alternative, you could forward a local port with SSH --- you might use this if you didn't want to rely on just HTTP basic auth, which isn't very secure over non-HTTPS connections. - -# Customising the admin interface - -The admin interface is intended to be a minimal place to focus on writing content. You are free to customise the admin interface by creating a stylesheet at `$your_site_directory/css/admin/admin.css`. As an example, if your main site's stylesheet is `/css/style.css`, you can use an `@import` rule to inherit the look-and-feel of your main site editing content and looking at rendered previews. - - -```css -/* Import the main site's CSS to provide a similar look-and-feel for the admin interface */ - -@import url("/css/style.css"); - -/* more customisation below */ -``` - # Custom tags and filters These tags can be used in templates, in addition to the [standard Liquid filters and tags](https://github.com/Shopify/liquid/wiki/Liquid-for-Designers). For example: @@ -424,11 +362,6 @@ These tags can be used in templates, in addition to the [standard Liquid filters ## List of filters -* `date` with `'now'` - - This is a standard filter, but there is a [workaround](https://github.com/Shopify/liquid/pull/117) for - `{{ 'now' | date: "%Y" }}` to work, so you can use this in templates. - * `markdown` e.g., `{{ post.content | markdown }}`. @@ -527,9 +460,8 @@ Variable | Value ## Broad outline -* `./bin/serif {dev,admin,generate}` to run Serif commands. -* `rake test` to run the tests. -* Unit tests are written in RSpec. +* `./bin/serif {dev,generate}` to run Serif commands. +* `rspec` to run the tests. * `rake docs` will generate HTML documentation in `docs/`. Open `docs/index.html` in a browser to start. ## Directory structure @@ -540,12 +472,3 @@ Variable | Value # Changes and what's new See `CHANGELOG`. - -# Planned features - -Some things I'm hoping to implement one day: - -1. Custom hooks to fire after particular events, such as minifying CSS after publish, or committing changes and pushing to a git repository. -2. Simple Markdown pages instead of plain HTML for non-post content. -3. Automatically detecting file changes and regenerating the site. -4. Adding custom Liquid filters and tags. diff --git a/lib/serif.rb b/lib/serif.rb index 99b36af..cad2c67 100644 --- a/lib/serif.rb +++ b/lib/serif.rb @@ -1,24 +1,14 @@ -require "time" - -require "liquid" -require "kramdown" -require "rubypants" -require "rouge" -require "redhead" -require "timeout_cache" - -require "cgi" -require "digest" - -require "securerandom" - require "serif/errors" +require "serif/config" require "serif/content_file" -require "serif/post" require "serif/draft" +require "serif/filters" +require "serif/generator" require "serif/markup_renderer" +require "serif/page" +require "serif/placeholder" +require "serif/post" require "serif/site" -require "serif/config" module Serif end diff --git a/lib/serif/admin_server.rb b/lib/serif/admin_server.rb deleted file mode 100755 index 2414773..0000000 --- a/lib/serif/admin_server.rb +++ /dev/null @@ -1,292 +0,0 @@ -require "sinatra/base" -require "fileutils" -require "nokogiri" -require "reverse_markdown" - -module Serif -class AdminServer - class AdminApp < Sinatra::Base - Tilt.register :html, Tilt[:liquid] - - - set :root, Dir.pwd - set :public_folder, settings.root + (ENV["ENV"] == "production" ? "/_site" : "") - set :views, File.expand_path(File.join(File.dirname(__FILE__), "..", "..", "statics", "templates", "admin")) - - site = Serif::Site.new(settings.root) - - use(Rack::Auth::Basic, "Login credentials") do |username, password| - [username, password] == [site.config.admin_username, site.config.admin_password] - end - - before do - @conflicts = site.conflicts - end - - # multiple public folders?? - get "/admin/js/:file" do |file| - assets_dir = File.join(File.dirname(__FILE__), "..", "..", "statics", "assets", "js") - - [200, { "Content-Type" => "text/javascript" }, File.read(File.join(assets_dir, file))] - end - - get "/" do - redirect to("/admin") - end - - get "/admin/?" do - posts = site.posts.sort_by { |p| p.created }.reverse - drafts = site.drafts.sort_by { |p| File.mtime(p.path) }.reverse - - liquid :index, locals: { conflicts: @conflicts, posts: posts, drafts: drafts } - end - - get "/admin/edit/?" do - redirect to("/admin"), 301 - end - - get "/admin/bookmarks" do - liquid :bookmarks, locals: { conflicts: @conflicts } - end - - get "/admin/quick-draft" do - url = params[:url] - html_content = params[:content].strip - - title = params[:title] - - # delete anything nonprintable - title = title.gsub(/[^\x20-\x7E]/, "") - - # sanitise the HTML title into something we can use as a temporary slug - slug = title.split(" ").first(5).join(" ").gsub(/[^\w-]/, "-").gsub(/--+/, '-').gsub(/^-|-$/, '') - slug.downcase! - - if html_content.empty? - markdown = "[#{title}](#{url.gsub(")", "\\)")})" - else - # parse the document fragment and remove any empty nodes. - document = Nokogiri::HTML::DocumentFragment.parse(html_content) - document.traverse { |p| p.remove if p.text && p.text.strip.empty? } - html_content = document.to_html - - html_content = "
#{html_content}
" - markdown = ReverseMarkdown.convert(html_content, github_flavored: true, unknown_tags: :bypass) - - # markdown URLs need to have any )s escaped - markdown = "[#{title}](#{url.gsub(")", "\\)")}):\n\n#{markdown}" - end - - draft = Draft.new(site) - draft.title = title - draft.slug = slug - - # if the draft itself has no conflict, save it, - # otherwise show the new draft page with an error. - # - # if, after saving the draft because of no conflict, - # there is actually an overall site conflict, then - # keep on trucking so it doesn't interrupt the user. - if site.conflicts(draft) - liquid :new_draft, locals: { draft_content: draft.content, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: draft, error_message: "There is a conflict on this draft." } - else - draft.save(markdown) - - begin - site.generate - rescue PostConflictError => e - puts "Site has conflicts, skipping generation for now." - end - - if params[:edit] == "1" - redirect to("/admin/edit/drafts/#{slug}") - else - redirect to(url) - end - end - end - - get "/admin/new/draft" do - content = Draft.new(site) - autofocus = "slug" - liquid :new_draft, locals: { draft_content: content.content.to_s, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: autofocus } - end - - post "/admin/new/draft" do - content = Draft.new(site) - content.slug = params[:slug].strip - content.title = params[:title].strip - - if params[:markdown].strip.empty? || params[:title].empty? || params[:slug].empty? - [:title, :slug, :markdown].each do |p| - params[p] = nil if params[p] && params[p].empty? - end - - error_message = "There must be a URL, a title, and content to save." - - autofocus = "markdown" unless params[:markdown] - autofocus = "title" unless params[:title] - autofocus = "slug" unless params[:slug] - - liquid :new_draft, locals: { draft_content: params[:markdown].to_s, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, autofocus: autofocus } - else - if Draft.exist?(site, params[:slug]) - error_message = "Draft already eixsts with the given slug #{params[:slug]}." - liquid :new_draft, locals: { draft_content: params[:markdown].to_s, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, autofocus: autofocus } - else - content.save(params[:markdown]) - begin - site.generate - rescue PostConflictError => e - puts "Cannot generate. Skipping for now." - end - redirect to("/admin") - end - end - end - - post "/admin/edit/drafts" do - content = Draft.from_slug(site, params[:original_slug]) - - params[:markdown] = params[:markdown].strip - - # check if the slug has been edited, i.e., if we're renaming. - if !params[:slug].empty? && params[:original_slug] && params[:original_slug] != params[:slug] - if Draft.exist?(site, params[:slug]) - conflicting_name = true - - # we need to re-edit, so reload but use the original slug name - # not the new one that was attempted to be saved. - content = Draft.from_slug(site, params[:original_slug]) - else - Draft.rename(site, params[:original_slug], params[:slug]) - - # re-load after the rename - content = Draft.from_slug(site, params[:slug]) - end - end - - # make sure the title is whatever was just submitted - content.title = params[:title] - - # any errors - if conflicting_name || params[:markdown].empty? || params[:slug].empty? - if conflicting_name - error_message = "This name is already being used for a draft." - elsif params[:markdown].empty? - error_message = "Content must not be blank." - elsif params[:slug].empty? - error_message = "You must pick a URL to use" - end - - liquid :edit_draft, locals: { draft_content: params[:markdown].to_s, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, private_url: site.private_url(content) } - elsif (conflicts = site.conflicts) - error_message = "The site has a conflict and cannot be generated." - liquid :edit_draft, locals: { draft_content: params[:markdown].to_s, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content, private_url: site.private_url(content) } - else - content.save(params[:markdown]) - - # TODO: move the entire notion of generating a site out into - # a directory-change-level event. - if params[:publish] == "yes" - content.publish! - end - - site.generate - - redirect to("/admin") - end - end - - post "/admin/edit/posts" do - content = Post.from_basename(site, params[:original_basename]) - - params[:markdown] = params[:markdown].strip - params[:title] = params[:title].strip - - content.title = params[:title] - - if params[:markdown].empty? || params[:title].empty? - error_message = "Content must not be blank." if params[:markdown].empty? - error_message = "Title must not be blank." if params[:title].empty? - - liquid :edit_post, locals: { conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content } - elsif (conflicts = site.conflicts) - error_message = "The site has a conflict and cannot be generated." - liquid :edit_post, locals: { conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), error_message: error_message, post: content } - else - content.save(params[:markdown]) - site.generate - - redirect to("/admin") - end - end - - get "/admin/edit/posts/:basename" do - redirect to("/admin") unless params[:basename] - - content = Post.from_basename(site, params[:basename]) - liquid :edit_post, locals: { conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: "markdown" } - end - - get "/admin/edit/drafts/:slug" do - redirect to("/admin") unless params[:slug] - - content = Draft.from_slug(site, params[:slug]) - liquid :edit_draft, locals: { draft_content: content.content, conflicts: @conflicts, images_path: site.config.image_upload_path.gsub(/"/, '\"'), post: content, autofocus: "markdown", private_url: site.private_url(content) } - end - - post "/admin/delete/?" do - content = Draft.from_slug(site, params[:original_slug]) - content.delete! - - redirect to("/admin") - end - - post "/admin/convert-markdown/?" do - content = params["content"] - - if request.xhr? - Serif::Markdown.render(content).strip - end - end - - post "/admin/attachment" do - attachment = params["attachment"] - filename = attachment["final_name"] - file = attachment["file"] - uid = attachment["uid"] - - tempfile = file[:tempfile] - - FileUtils.mkdir_p(File.join(site.directory, File.dirname(filename))) - FileUtils.mkdir_p(File.dirname(site.site_path(filename))) - - source_file = File.join(site.directory, filename) - deployed_file = site.site_path(filename) - - # move to the source directory - FileUtils.mv(tempfile.path, source_file) - - # copy to production to avoid the need to generate right now - FileUtils.copy(source_file, deployed_file) - - # no executable permissions, and whatever the umask is - perms = 0777 & ~0111 & ~File.umask - File.chmod(perms, source_file, deployed_file) - - "File uploaded" - end - end - - def initialize(source_directory) - @source_directory = File.expand_path(source_directory) - end - - def start - FileUtils.cd @source_directory - app = Sinatra.new(AdminApp) - app.run! - end -end -end diff --git a/lib/serif/commands.rb b/lib/serif/commands.rb index 7fab245..8b53065 100644 --- a/lib/serif/commands.rb +++ b/lib/serif/commands.rb @@ -3,99 +3,89 @@ require "serif/server" module Serif -class Commands - def initialize(argv) - @argv = argv.dup - end + class Commands + def initialize(argv) + @argv = argv.dup + end - def process - command = @argv.shift - case command - when "-h", "--help", nil - print_help - exit 0 - when "admin" - initialize_admin_server(Dir.pwd) - when "generate" - generate_site(Dir.pwd) - when "dev" - initialize_dev_server(Dir.pwd) - when "new" - produce_skeleton(Dir.pwd) + def process + command = @argv.shift + + case command + when "-h", "--help", nil + print_help + exit 0 + when "generate" + generate_site + exit 0 + when "dev" + initialize_dev_server + when "new" + setup_new_site + + puts + puts "New site created! Generating site for the first time into _site/" + puts + + generate_site + + puts + puts "Site generated." + exit 0 + else + abort "Unknown command: #{command}" + end end - end - def initialize_admin_server(source_dir) - # need to cd to the directory before requiring the admin - # server, because otherwise Dir.pwd won't be right when - # the admin server class is defined at require time. - FileUtils.cd(source_dir) - require "serif/admin_server" + def initialize_dev_server + server = Serif::DevelopmentServer.new(Dir.pwd) + server.start + end - server = Serif::AdminServer.new(source_dir) - server.start - end + def generate_site + site = Serif::Site.new(Dir.pwd) + + begin + site.generate + rescue Serif::PostConflictError => e + puts "Error! Unable to generate because there is a conflict." + puts + puts "Conflicts at:" + puts + + site.conflicts.each do |url, ary| + puts url + ary.each do |e| + puts "\t#{e.path}" + end + end - def initialize_dev_server(source_dir) - FileUtils.cd(source_dir) + exit 1 + end + end - server = Serif::DevelopmentServer.new(source_dir) - server.start - end + def setup_new_site + target_dir = Dir.pwd - def generate_site(source_dir) + if Dir[File.join(target_dir, "*")].length > 0 + abort "Directory is not empty." + end - site = Serif::Site.new(source_dir) + skeleton_dir = File.join(File.dirname(__FILE__), "..", "..", "site_template") - begin - site.generate - rescue Serif::PostConflictError => e - puts "Error! Unable to generate because there is a conflict." - puts - puts "Conflicts at:" - puts + puts "Creating _posts" + FileUtils.mkdir(File.join(target_dir, "_posts")) - site.conflicts.each do |url, ary| - puts url - ary.each do |e| - puts "\t#{e.path}" + Dir.chdir(skeleton_dir) do + Dir["*"].each do |f| + puts "Creating #{f}" + FileUtils.cp_r(f, target_dir, verbose: false) end end - - exit 1 - end - end - - def verify_directory(dir) - unless Dir.exist?(dir) - puts "No such directory: #{dir}'" - exit 1 end - end - - def produce_skeleton(dir) - if !Dir[File.join(dir, "*")].empty? - abort "Directory is not empty." - end - - FileUtils.cd(File.join(File.dirname(__FILE__), "..", "..", "statics", "skeleton")) - files = Dir["*"] - files.each do |f| - FileUtils.cp_r(f, dir, verbose: true) - end - FileUtils.mkdir(File.join(dir, "_posts")) - - generate_site(dir) - - puts - puts "*** NOTE ***" - puts - puts "You should now edit the username and password in _config.yml" - puts - end - def print_help - puts <<-END_HELP + def print_help + puts <<-END_HELP USAGE serif [-h | --help] @@ -112,8 +102,6 @@ def print_help serif new Create a site skeleton to get started. Will only run if the current directory is empty. - serif admin Start the admin server on localhost:4567. - serif dev Start a simple dev server on localhost:8000. Serves up the generated static files, but loads some files (like CSS) from source (instead of @@ -126,14 +114,8 @@ def print_help $ ENV=production serif generate - $ ENV=production serif admin - - Note that this by and large doesn't change much, - but in future it may provide extra features. - - The main benefit is that the `file_digest` tag - will return a hex digest of the given file's - contents only when ENV is set to production. + This causes the `file_digest` Liquid tag to + return a hex digest of the given file's contents. EXAMPLES @@ -141,11 +123,7 @@ def print_help Generate the site. - $ serif admin - - Start the admin server on localhost:4567. - END_HELP + end end end -end diff --git a/lib/serif/config.rb b/lib/serif/config.rb index a937ec2..68e438a 100644 --- a/lib/serif/config.rb +++ b/lib/serif/config.rb @@ -1,45 +1,31 @@ require "yaml" module Serif -class Config - def initialize(config_file) - @config_file = config_file - end - - def admin_username - yaml["admin"]["username"] - end - - def admin_password - yaml["admin"]["password"] - end - - def image_upload_path - yaml["image_upload_path"] || "/images/:timestamp_:name" - end + class Config + def initialize(config_file) + @config_file = config_file + end - def permalink - yaml["permalink"] || "/:title" - end + def permalink + yaml_config["permalink"] || "/:title" + end - def archive_enabled? - a = yaml["archive"] + def archive_enabled? + archive_config["enabled"] + end - if a - a["enabled"] - else - false + def archive_url_format + archive_config["url_format"] || "/archive/:year/:month" end - end - def archive_url_format - (yaml["archive"] || {})["url_format"] || "/archive/:year/:month" - end + private - private + def yaml_config + @yaml_config ||= YAML.load_file(@config_file) + end - def yaml - @yaml ||= YAML.load_file(@config_file) + def archive_config + yaml_config["archive"] || {} + end end end -end diff --git a/lib/serif/content_file.rb b/lib/serif/content_file.rb index 58727ae..3a58ff5 100644 --- a/lib/serif/content_file.rb +++ b/lib/serif/content_file.rb @@ -1,163 +1,112 @@ -# -# ContentFile represents a file on the filesystem -# which contains the contents of a post, be it in -# draft form or otherwise. -# +require "time" +require "redhead" module Serif -class ContentFile - attr_reader :path, :slug, :site - - def initialize(site, path = nil) - @site = site - @path = path - - if @path - # we have to parse out the source first so that we get necessary - # metadata like published vs. draft. - load_source - - dirname = File.basename(File.dirname(@path)) - basename = File.basename(@path) - @slug = draft? ? basename : basename.split("-")[3..-1].join("-") + class ContentFile + attr_reader :path, :site + + def self.all(site, dirname, klass) + Dir[site.source_path(dirname, "*")].select do |f| + File.file?(f) + end.map do |f| + File.expand_path(f) + end.map do |f| + klass.new(site, f) + end end - end - def basename - File.basename(@path) - end - - def slug=(str) - @slug = str - - # if we're adding a slug and there's no path yet, then create the path. - # this will run for new drafts - - @path ||= File.expand_path("#{site.directory}/#{self.class.dirname}/#{@slug}") - end + def initialize(site, path) + unless site && path + raise ArgumentError, "must provide both site and path" + end - def title - return nil if !@source - headers[:title] - end + @site = site + @path = path - def title=(new_title) - if !@source - @source = Redhead::String["title: #{new_title}\n\n"] - else - @source.headers[:title] = new_title + load_source end - @cached_headers = nil - end - - # Returns true if the file is in the directory for draft content, or - # has no saved path yet. - def draft? - return true if !path - - File.dirname(path) == File.join(site.directory, Draft.dirname) - end + def draft? + !published? + end - # Returns true if the file is in the directory for published posts, - # false otherwise. - # - # If there is no path at all, returns false. - def published? - return false if !path + def title + headers[:title] + end - File.dirname(path) == File.join(site.directory, Post.dirname) - end + def content + @source.to_s + end - def content(include_headers = false) - include_headers ? "#{@source.headers.to_s}\n\n#{@source.to_s}" : @source.to_s - end + def created + return nil unless headers[:created] - def created - return nil if !@source - headers[:created].utc - end + headers[:created].utc + end - def updated - return nil if !@source - (headers[:updated] || created).utc - end + def updated + time = headers[:updated] || created + return nil unless time - def headers - return @cached_headers if @cached_headers + time.utc + end - return (@cached_headers = {}) unless @source + def headers + @cached_headers ||= @source.headers.to_h.map do |key, value| + if key == :created || key == :updated + value = Time.parse(value) + end - headers = @source.headers - converted_headers = {} + [key, value] + end.to_h + end - headers.each do |header| - key, value = header.key, header.value + def save + set_updated_time(Time.now) - if key == :created || key == :updated - value = Time.parse(value) + File.open(path, "w") do |f| + f.puts %Q{#{@source.headers.to_s}\n\n#{@source.to_s}}.strip end - converted_headers[key] = value + load_source end - @cached_headers = converted_headers - end - - def save(markdown = nil) - markdown ||= content if @source - - save_path = path || "#{self.class.dirname}/#{@slug}" - - # TODO: when a draft is being saved, it will call set_publish_time - # and then the #save call will execute this line, which will mean - # there is a very, very slight difference (fraction of a second) - # between the update time of a brand new published post and the - # creation time. - set_updated_time(Time.now) - - File.open(save_path, "w") do |f| - f.puts %Q{#{@source.headers.to_s} - -#{markdown}}.strip + def to_liquid + headers.map { |k, v| [k.to_s, v] }.to_h.merge( + "content" => content, + "slug" => slug, + "url" => url, + "draft" => draft?, + "published" => published? + ) end - # after every save, ensure we've re-loaded the saved content - load_source - - true # always return true for now - end - - def inspect - %Q{<#{self.class} #{headers.inspect}>} - end + protected - protected + def set_updated_time(time) + @source.headers[:updated] = time.xmlschema + headers_changed! + end - def set_publish_time(time) - @source.headers[:created] = time.xmlschema - headers_changed! - end + def headers_changed! + @cached_headers = nil + end - def set_updated_time(time) - @source.headers[:updated] = time.xmlschema - headers_changed! - end + private - # Invalidates the cached headers entirely. - # - # Any methods which alter headers should call this. - def headers_changed! - @cached_headers = nil - end + def load_source + source = File.read(path).gsub(/\r?\n/, "\n") + source.force_encoding("UTF-8") + @source = Redhead::String[source] + headers_changed! + end - private + def layout + Liquid::Template.parse(File.read(site.source_path("_layouts", "#{headers[:layout] || "default"}.html"))) + end - def load_source - source = File.read(path).gsub(/\r?\n/, "\n") - source.force_encoding("UTF-8") - @source = Redhead::String[source] - @cached_headers = nil + def template + Liquid::Template.parse(File.read(site.source_path("_templates/post.html"))) + end end end -end diff --git a/lib/serif/draft.rb b/lib/serif/draft.rb index 5923f0a..ef781c3 100644 --- a/lib/serif/draft.rb +++ b/lib/serif/draft.rb @@ -1,114 +1,62 @@ module Serif -class Draft < ContentFile - attr_reader :autopublish - - def self.dirname - "_drafts" - end - - def self.rename(site, original_slug, new_slug) - raise "file exists" if File.exist?("#{site.directory}/#{dirname}/#{new_slug}") - File.rename("#{site.directory}/#{dirname}/#{original_slug}", "#{site.directory}/#{dirname}/#{new_slug}") - end - - # Returns the URL that would be used for this post if it were - # to be published now. - def url - permalink_style = headers[:permalink] || site.config.permalink - - parts = { - "title" => slug.to_s, - "year" => Time.now.year.to_s, - "month" => Time.now.month.to_s.rjust(2, "0"), - "day" => Time.now.day.to_s.rjust(2, "0") - } - - output = permalink_style - - parts.each do |placeholder, value| - output = output.gsub(Regexp.quote(":" + placeholder), value) + class Draft < ContentFile + def self.all(site) + super(site, "_drafts", self) end - output - end - - def delete! - FileUtils.mkdir_p("#{site.directory}/_trash") - File.rename(@path, File.expand_path("#{site.directory}/_trash/#{Time.now.to_i}-#{slug}")) - end + def slug + @slug ||= File.basename(path) + end - def publish! - publish_time = Time.now - date = Time.now.strftime("%Y-%m-%d") - filename = "#{date}-#{slug}" + def published? + false + end - FileUtils.mkdir_p("#{site.directory}/#{Post.dirname}") - full_published_path = File.expand_path("#{site.directory}/#{Post.dirname}/#{filename}") + def url + permalink_style = headers[:permalink] || site.config.permalink - raise "conflict, post exists already" if File.exist?(full_published_path) + parts = { + "title" => slug.to_s, + "year" => Time.now.year.to_s, + "month" => Time.now.month.to_s.rjust(2, "0"), + "day" => Time.now.day.to_s.rjust(2, "0") + } - set_publish_time(publish_time) + Serif::Placeholder.substitute(permalink_style, parts) + end - @source.headers.delete(:publish) if autopublish? + def publish! + publish_time = Time.now + date = publish_time.strftime("%Y-%m-%d") - save + FileUtils.mkdir_p(site.source_path("_posts")) - File.rename(path, full_published_path) + published_filename = "#{date}-#{slug}" + published_path = site.source_path("_posts/#{published_filename}") - # update the path since the file has now changed - @path = Post.new(site, full_published_path).path - end + if File.exist?(published_path) + raise "found a conflict when trying to publish #{published_filename}: a file with that name exists already" + end - # if the assigned value is truthy, the "publish" header - # is set to "now", otherwise the header is removed. - def autopublish=(value) - if value - @source.headers[:publish] = "now" - else + @source.headers[:created] = publish_time.xmlschema @source.headers.delete(:publish) - end - headers_changed! - end + save - # Checks the value of the "publish" header, and returns - # true if the value is "now", ignoring trailing and leading - # whitespace. Returns false, otherwise. - def autopublish? - publish_header = headers[:publish] - publish_header && publish_header.strip == "now" - end - - def to_liquid - h = { - "title" => title, - "content" => content, - "slug" => slug, - "type" => "draft", - "draft" => draft?, - "published" => published?, - "url" => url - } - - headers.each do |key, value| - h[key] = value + FileUtils.mv(path, published_path) end - h - end - - def self.exist?(site, slug) - all(site).any? { |d| d.slug == slug } - end - - def self.all(site) - files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) } - files.map { |f| new(site, f) } - end + def autopublish? + headers[:publish].to_s.strip == "now" + end - def self.from_slug(site, slug) - path = File.expand_path(File.join(site.directory, dirname, slug)) - new(site, path) + def render(site) + layout.render!( + "site" => site, + "draft_preview" => true, + "page" => { "title" => title }, + "content" => template.render!("site" => site, "post" => self, "draft_preview" => true) + ) + end end end -end diff --git a/lib/serif/errors.rb b/lib/serif/errors.rb index c6c2f86..93b17a2 100644 --- a/lib/serif/errors.rb +++ b/lib/serif/errors.rb @@ -1,10 +1,3 @@ module Serif - # General error class. Allows capturing general errors - # applicable only to Serif. - class Error < RuntimeError; end - - # Represents a conflict between published posts and drafts. - # This should be used whenever two posts would occupy the same - # URL / file path. - class PostConflictError < Error; end + class PostConflictError < StandardError; end end diff --git a/lib/serif/filters.rb b/lib/serif/filters.rb new file mode 100644 index 0000000..cca6230 --- /dev/null +++ b/lib/serif/filters.rb @@ -0,0 +1,75 @@ +require "cgi" +require "rubypants" +require "liquid" +require "time" +require "digest" + +module Serif + module Filters + def strip(input) + input.strip + end + + def encode_uri_component(string) + return "" unless string + + CGI.escape(string) + end + + def smarty(text) + RubyPants.new(text).to_html + end + + def markdown(body) + Serif::Markdown.render(body) + end + + def xmlschema(input) + input.xmlschema + end + end + + class FileDigest < Liquid::Tag + DIGEST_CACHE = {} + + # file_digest "file.css" [prefix:.] + Syntax = /^\s*(\S+)\s*(?:(prefix\s*:\s*\S+)\s*)?$/ + + def initialize(tag_name, markup, tokens) + super + + if markup =~ Syntax + @path = $1 + + if $2 + @prefix = $2.gsub(/\s*prefix\s*:\s*/, "") + else + @prefix = "" + end + else + raise SyntaxError.new("Syntax error for file_digest") + end + end + + # Takes the given path and returns the MD5 + # hex digest of the file's contents. + # + # The path argument is first stripped, and any leading + # "/" has no effect. + def render(context) + return "" unless ENV["ENV"] == "production" + + full_path = File.join(context["site"]["__directory"], @path.strip) + + return @prefix + DIGEST_CACHE[full_path] if DIGEST_CACHE[full_path] + + digest = Digest::MD5.hexdigest(File.read(full_path)) + DIGEST_CACHE[full_path] = digest + + @prefix + digest + end + end +end + +Liquid::Template.register_filter(Serif::Filters) +Liquid::Template.register_tag("file_digest", Serif::FileDigest) diff --git a/lib/serif/generator.rb b/lib/serif/generator.rb new file mode 100644 index 0000000..ecbcbc1 --- /dev/null +++ b/lib/serif/generator.rb @@ -0,0 +1,149 @@ +module Serif + class Generator + attr_reader :site + + def initialize(site) + @site = site + end + + def default_layout + Liquid::Template.parse(File.read(site.source_path("_layouts/default.html"))) + end + + def generate! + Dir.chdir(site.source_directory) do + FileUtils.rm_rf("tmp/_site") + FileUtils.mkdir_p("tmp/_site") + + if site.conflicts + raise PostConflictError, "Generating would cause a conflict." + end + + puts "Auto-publishing drafts..." + preprocess_autopublish_drafts + puts "Auto-updating posts..." + preprocess_autoupdate_posts + + puts "Processing general files..." + process_general_files + puts "Processing published posts..." + process_posts + + puts + puts "Draft previews created:" + puts generate_draft_previews.join("\n") + + if site.config.archive_enabled? + generate_archives(default_layout) + end + + update_site_path + end + end + + private + + def preprocess_autoupdate_posts + site.posts.each do |p| + if p.autoupdate? + p.update! + end + end + end + + def generate_draft_previews + site.drafts.map do |draft| + preview_path = draft_preview_path(draft) + + live_preview_file = site.tmp_path(preview_path) + FileUtils.mkdir_p(File.dirname(live_preview_file)) + + File.open(live_preview_file + ".html", "w") do |f| + f.puts draft.render(site) + end + + preview_path + end + end + + def draft_preview_path(draft) + private_draft_pattern = site.site_path("drafts/#{draft.slug}/*") + existing_file = Dir[private_draft_pattern].first + + file = existing_file ? File.basename(existing_file, ".html") : SecureRandom.hex(30) + + "drafts/#{draft.slug}/#{file}" + end + + def preprocess_autopublish_drafts + site.drafts.each do |d| + if d.autopublish? + d.publish! + end + end + end + + def process_general_files + files = Dir["**/*"].select { |f| f !~ /\A_/ && File.file?(f) } + + files.each do |path| + dirname = File.dirname(path) + filename = File.basename(path) + + FileUtils.mkdir_p(site.tmp_path(dirname)) + + if bypass?(filename) + FileUtils.cp(path, site.tmp_path(path)) + next + end + + page = Serif::Page.new(site, path) + + File.open(site.tmp_path(path), "w") do |f| + f.puts page.render + end + end + end + + def process_posts + [nil, *site.posts, nil].each_cons(3) do |next_post, post, prev_post| + FileUtils.mkdir_p(site.tmp_path(File.dirname(post.url))) + + File.open(site.tmp_path(post.url + ".html"), "w") do |f| + f.puts post.render(site, prev_post: prev_post, next_post: next_post) + end + end + end + + def generate_archives(layout) + template = Liquid::Template.parse(File.read(site.source_path("_templates/archive_page.html"))) + + months = site.posts.group_by { |post| Date.new(post.created.year, post.created.month) } + + months.each do |month, posts| + archive_path = site.tmp_path(site.archive_url_for_date(month)) + + FileUtils.mkdir_p(File.dirname(archive_path)) + + File.open(File.join(archive_path + ".html"), "w") do |f| + f.puts layout.render!( + "archive_page" => true, + "month" => month, + "site" => site, + "content" => template.render!("archive_page" => true, "site" => site, "month" => month, "posts" => posts) + ) + end + end + end + + def update_site_path + FileUtils.rm_rf(site.source_path("_site")) && + FileUtils.mv(site.source_path("tmp/_site"), site.source_directory) && + FileUtils.rm_rf(site.source_path("tmp")) + end + + def bypass?(filename) + !%w[.html .xml].include?(File.extname(filename)) + end + end +end diff --git a/lib/serif/markup_renderer.rb b/lib/serif/markup_renderer.rb index 725c3a4..a542aba 100644 --- a/lib/serif/markup_renderer.rb +++ b/lib/serif/markup_renderer.rb @@ -1,9 +1,28 @@ +require "kramdown" +require "rouge" + +module Serif + class Markdown + def self.render(markdown) + options = { + input: "GFM", + auto_id_stripping: true, + enable_coderay: false, + hard_wrap: false, + parse_block_html: false + } + Kramdown::Document.new(markdown, options).to_serif_custom + end + end +end + module Kramdown module Converter class SerifCustom < Html def convert_codeblock(el, indent) attr = el.attr.dup language = extract_code_language!(attr) + if language Rouge.highlight(el.value, language, "html") else @@ -13,18 +32,3 @@ def convert_codeblock(el, indent) end end end - -module Serif -class Markdown - def self.render(markdown) - options = { - input: "GFM", - auto_id_stripping: true, - enable_coderay: false, - hard_wrap: false, - parse_block_html: false - } - Kramdown::Document.new(markdown, options).to_serif_custom - end -end -end diff --git a/lib/serif/page.rb b/lib/serif/page.rb new file mode 100644 index 0000000..4a4d112 --- /dev/null +++ b/lib/serif/page.rb @@ -0,0 +1,48 @@ +module Serif + class Page + attr_reader :site, :path + + def initialize(site, path) + @site = site + @path = path + end + + def render + template = Liquid::Template.parse(source.to_s) + + if layout_option == "none" + return template.render!("site" => site, "page" => headers) + end + + layout.render!( + "site" => site, + "page" => headers, + "content" => template.render!("site" => site, "page" => headers) + ) + end + + private + + def source + @source ||= Redhead::String[File.read(path)] + end + + def headers + source.headers.to_h.map { |k, v| [k.to_s, v] }.to_h + end + + def title + headers["title"] + end + + def layout_option + headers["layout"] || "default" + end + + def layout + layout_file = site.source_path("_layouts", "#{layout_option}.html") + + Liquid::Template.parse(File.read(layout_file)) + end + end +end diff --git a/lib/serif/placeholder.rb b/lib/serif/placeholder.rb new file mode 100644 index 0000000..04f17f4 --- /dev/null +++ b/lib/serif/placeholder.rb @@ -0,0 +1,13 @@ +module Serif + module Placeholder + def self.substitute(input, substitutions) + output = input + + substitutions.each do |placeholder_name, value| + output = output.gsub(Regexp.quote(":" + placeholder_name), value) + end + + output + end + end +end diff --git a/lib/serif/post.rb b/lib/serif/post.rb index bb31294..5320438 100755 --- a/lib/serif/post.rb +++ b/lib/serif/post.rb @@ -1,90 +1,64 @@ require "fileutils" module Serif -class Post < ContentFile - def self.dirname - "_posts" - end - - def url - permalink_style = headers[:permalink] || site.config.permalink - - filename_parts = File.basename(path).split("-") - - parts = { - "title" => slug, - "year" => filename_parts[0], - "month" => filename_parts[1], - "day" => filename_parts[2] - } - - output = permalink_style - - parts.each do |placeholder, value| - output = output.gsub(Regexp.quote(":" + placeholder), value) + class Post < ContentFile + def self.all(site) + super(site, "_posts", self) end - output - end + def slug + @slug ||= File.basename(path).split("-")[3..-1].join("-") + end - # if the assigned value is truthy, the "update" header - # is set to "now", otherwise the header is removed. - def autoupdate=(value) - if value - @source.headers[:update] = "now" - else - @source.headers.delete(:update) + def published? + true end - headers_changed! - end + def url + permalink_style = headers[:permalink] || site.config.permalink - # returns true if the post has been marked as needing a - # new updated timestamp header. - # - # this is based on the presence of an "update: now" header. - def autoupdate? - update_header = headers[:update] - update_header && update_header.strip == "now" - end + filename_parts = File.basename(path).split("-") - # Updates the updated timestamp and saves the contents. - # - # If there is an "update" header (see autoupdate?), it is deleted. - def update! - @source.headers.delete(:update) - set_updated_time(Time.now) - save - end + parts = { + "title" => slug, + "year" => filename_parts[0], + "month" => filename_parts[1], + "day" => filename_parts[2] + } - def self.all(site) - files = Dir[File.join(site.directory, dirname, "*")].select { |f| File.file?(f) }.map { |f| File.expand_path(f) } - files.map { |f| new(site, f) } - end + Serif::Placeholder.substitute(permalink_style, parts) + end - def self.from_basename(site, filename) - all(site).find { |p| p.basename == filename } - end + def autoupdate? + headers[:update].to_s.strip == "now" + end - def to_liquid - h = { - "title" => title, - "created" => created, - "updated" => updated, - "content" => content, - "slug" => slug, - "url" => url, - "type" => "post", - "draft" => draft?, - "published" => published?, - "basename" => basename - } + def update! + @source.headers.delete(:update) + save + end - headers.each do |key, value| - h[key.to_s] = value + def to_liquid + super.merge( + "created" => created, + "updated" => updated, + ) end - h + def render(site, prev_post:, next_post:) + template_variables = { + "post" => self, + "post_page" => true, + "prev_post" => prev_post, + "next_post" => next_post + } + + layout.render!( + "site" => site, + "page" => { "title" => title }, + "post_page" => true, + "content" => template.render!(template_variables) + ) + end end end -end diff --git a/lib/serif/server.rb b/lib/serif/server.rb index c0d4cc9..385a99c 100644 --- a/lib/serif/server.rb +++ b/lib/serif/server.rb @@ -1,42 +1,44 @@ require "sinatra/base" -require "fileutils" module Serif -class DevelopmentServer - class DevApp < Sinatra::Base - set :public_folder, Dir.pwd + class DevelopmentServer + class DevApp < Sinatra::Base + set :public_folder, Dir.pwd - not_found { "Resource not found" } + not_found { "Resource not found" } - get "/" do - File.read(File.expand_path("_site/index.html")) - end + get "/" do + File.read(File.expand_path("_site/index.html")) + end - # it seems Rack::Rewrite doesn't like public_folder files, so here we are - get "*" do - # attempt the exact name + an extension - file = Dir[File.expand_path("_site#{params[:splat].join("/")}.*")].first + # it seems Rack::Rewrite doesn't like public_folder files, so here we are + get "*" do + # attempt the exact name + an extension + file = Dir[File.expand_path("_site#{params[:splat].join("/")}.*")].first - # try index.html under the directory if it failed. useful for archive directory requests. - file ||= Dir[File.expand_path("_site#{params[:splat].join("/")}/index.html")].first + # try index.html under the directory if it failed. useful for archive directory requests. + file ||= Dir[File.expand_path("_site#{params[:splat].join("/")}/index.html")].first - # make a naive assumption that there's a 404 file at 404.html - file ||= Dir[File.expand_path("_site/404.html")].first + # make a naive assumption that there's a 404 file at 404.html + file ||= Dir[File.expand_path("_site/404.html")].first - file ? File.read(file) : 404 + file ? File.read(file) : 404 + end end - end - attr_reader :source_directory + attr_reader :source_directory - def initialize(source_directory) - @source_directory = source_directory - end + def initialize(source_directory) + raise ArgumentError, "a source directory must be given" unless source_directory - def start - FileUtils.cd @source_directory - app = Sinatra.new(DevApp) - app.run!(:port => 8000) + @source_directory = source_directory + end + + def start + Dir.chdir(source_directory) do + app = Sinatra.new(DevApp) + app.run!(port: 8000) + end + end end end -end diff --git a/lib/serif/site.rb b/lib/serif/site.rb index a07b623..bed5772 100644 --- a/lib/serif/site.rb +++ b/lib/serif/site.rb @@ -1,497 +1,111 @@ -module Serif -module Filters - def strip(input) - input.strip - end - - def encode_uri_component(string) - return "" unless string - CGI.escape(string) - end - - def smarty(text) - RubyPants.new(text).to_html - end - - def markdown(body) - Serif::Markdown.render(body) - end - - def xmlschema(input) - input.xmlschema - end -end - -class FileDigest < Liquid::Tag - DIGEST_CACHE = {} - - # file_digest "file.css" [prefix:.] - Syntax = /^\s*(\S+)\s*(?:(prefix\s*:\s*\S+)\s*)?$/ - - def initialize(tag_name, markup, tokens) - super - - if markup =~ Syntax - @path = $1 - - if $2 - @prefix = $2.gsub(/\s*prefix\s*:\s*/, "") - else - @prefix = "" - end - else - raise SyntaxError.new("Syntax error for file_digest") - end - end - - # Takes the given path and returns the MD5 - # hex digest of the file's contents. - # - # The path argument is first stripped, and any leading - # "/" has no effect. - def render(context) - return "" unless ENV["ENV"] == "production" - - full_path = File.join(context["site"]["directory"], @path.strip) - - return @prefix + DIGEST_CACHE[full_path] if DIGEST_CACHE[full_path] - - digest = Digest::MD5.hexdigest(File.read(full_path)) - DIGEST_CACHE[full_path] = digest - - @prefix + digest - end -end -end - -Liquid::Template.register_filter(Serif::Filters) -Liquid::Template.register_tag("file_digest", Serif::FileDigest) +require "liquid" +require "time" +require "fileutils" +require "securerandom" module Serif -class Site - def initialize(source_directory) - @source_directory = source_directory - end + class Site + attr_reader :source_directory - def directory - @source_directory - end - - # Returns all of the site's posts, in reverse chronological order - # by creation time. - def posts - Post.all(self).sort_by { |entry| entry.created }.reverse - end + def initialize(directory) + raise ArgumentError, "a source directory must be given" unless directory - def drafts - Draft.all(self) - end - - def config - @config ||= Serif::Config.new(File.join(@source_directory, "_config.yml")) - end - - def site_path(path) - File.join("_site", path) - end - - def tmp_path(path) - File.join("tmp", site_path(path)) - end - - def latest_update_time - most_recent = posts.max_by { |p| p.updated } - most_recent ? most_recent.updated : Time.now - end - - # Gives the URL absolute path to a private draft preview. - # - # If the draft has no such preview available, returns nil. - def private_url(draft) - private_draft_pattern = site_path("/drafts/#{draft.slug}/*") - file = Dir[private_draft_pattern].first - - return nil unless file - - "/drafts/#{draft.slug}/#{File.basename(file, ".html")}" - end - - def bypass?(filename) - !%w[.html .xml].include?(File.extname(filename)) - end - - # Returns the relative archive URL for the given date, - # using the value of config.archive_url_format - def archive_url_for_date(date) - format = config.archive_url_format - - parts = { - "year" => date.year.to_s, - "month" => date.month.to_s.rjust(2, "0") - } - - output = format - - parts.each do |placeholder, value| - output = output.gsub(Regexp.quote(":" + placeholder), value) + @source_directory = directory end - output - end - - # Returns a nested hash with the following structure: - # - # { - # :posts => [], - # :years => [ - # { - # :date => Date.new(2012), - # :posts => [], - # :months => [ - # { :date => Date.new(2012, 12), :archive_url => "/archive/2012/12", :posts => [] }, - # { :date => Date.new(2012, 11), :archive_url => "/archive/2012/11", :posts => [] }, - # # ... - # ] - # }, - # - # # ... - # ] - # } - def archives - h = {} - h[:posts] = posts - - # group posts by Date instances for the first day of the year - year_groups = posts.group_by { |post| Date.new(post.created.year) }.to_a - - # collect all elements as maps for the year start date and the posts in that year - year_groups.map! do |year_start_date, posts_by_year| - { - :date => year_start_date, - :posts => posts_by_year.sort_by { |post| post.created }.reverse - } + def pages + Page.all(self) end - year_groups.sort_by! { |year_hash| year_hash[:date] } - year_groups.reverse! - - year_groups.each do |year_hash| - year_posts = year_hash[:posts] - - # group the posts in the year by month - month_groups = year_posts.group_by { |post| Date.new(post.created.year, post.created.month) }.to_a - - # collect the elements as maps for the month start date and the posts in that month - month_groups.map! do |month_start_date, posts_by_month| - { - :date => month_start_date, - :posts => posts_by_month.sort_by { |post| post.created }.reverse, - :archive_url => archive_url_for_date(month_start_date) - } - end - - month_groups.sort_by! { |month_hash| month_hash[:date] } - month_groups.reverse! - - # set the months for the current year - year_hash[:months] = month_groups + def posts + Post.all(self).sort_by { |entry| entry.created }.reverse end - h[:years] = year_groups - - # return the final hash - h - end - - # Returns a hash representing any conflicting URLs, - # in the form - # - # { "/url_1" => [e_1, e_2, ..., e_n], ... } - # - # The elements e_i are the conflicting Post and - # Draft instances that share the URL "/url_1". - # - # Note that if n = 1 (that is, the array value is - # [e_1], containing a single element), then it is - # not included in the Hash, since it does not - # contribute to a conflict. - # - # If there are no conflicts found, returns nil. - # - # If an argument is specified, its #url value is - # compared against all post and draft URLs, and - # the value returned is either: - # - # 1. an array of post/draft instances that - # conflict, _including_ the argument given; or, - # 2. nil if there is no conflict. - def conflicts(content_to_check = nil) - if content_to_check - content = drafts + posts + [content_to_check] - - # In the event that the given argument is actually one of the - # drafts + posts, we need to de-duplicate, otherwise our return - # value will contain two of the same Draft/Post, which isn't - # actually a conflict. - # - # So to do that, we can use the path on the filesystem. However, - # we can't just rely on calling #path, because if content_to_check - # doesn't have a #path value, it'll be nil and it's possible that - # we might expand checking to multiple files/Drafts/Posts. - # - # Thus, if #path is nil, simply rely on object_id. - # - # FIXME: Replace this with a proper implementation of - # ContentFile equality/hashing. - content.uniq! { |e| e.path ? e.path : e.object_id } - - conflicts = content.select { |e| e.url == content_to_check.url } - - if conflicts.length <= 1 - return nil - else - return conflicts - end + def drafts + Draft.all(self) end - conflicts = (drafts + posts).group_by { |e| e.url } - conflicts.reject! { |k, v| v.length == 1 } - - if conflicts.empty? - nil - else - conflicts + def config + @config ||= Serif::Config.new(source_path("_config.yml")) end - end - - def to_liquid - @liquid_cache_store ||= TimeoutCache.new(1) - - cached_value = @liquid_cache_store[:liquid] - return cached_value if cached_value - @liquid_cache_store[:liquid] = { - "posts" => posts, - "latest_update_time" => latest_update_time, - "archive" => self.class.stringify_keys(archives), - "directory" => directory - } - end - - def generate - FileUtils.cd(@source_directory) - - FileUtils.rm_rf("tmp/_site") - FileUtils.mkdir_p("tmp/_site") - - files = Dir["**/*"].select { |f| f !~ /\A_/ && File.file?(f) } - - default_layout = Liquid::Template.parse(File.read("_layouts/default.html")) - - if conflicts - raise PostConflictError, "Generating would cause a conflict." + def source_path(*path) + File.join(source_directory, *path) end - # preprocess any drafts marked for autopublish, before grabbing the posts - # to operate on. - preprocess_autopublish_drafts - - # preprocess any posts that might have had an update flag set in the header - preprocess_autoupdate_posts - - posts = self.posts - - files.each do |path| - puts "Processing file: #{path}" - - dirname = File.dirname(path) - filename = File.basename(path) - - FileUtils.mkdir_p(tmp_path(dirname)) - if bypass?(filename) - FileUtils.cp(path, tmp_path(path)) - else - File.open(tmp_path(path), "w") do |f| - file = File.read(path) - title = nil - layout_option = :default - - if Redhead::String.has_headers?(file) - file_with_headers = Redhead::String[file] - title = file_with_headers.headers[:title] && file_with_headers.headers[:title].value - layout_option = file_with_headers.headers[:layout] && file_with_headers.headers[:layout].value - layout_option ||= :default - - # all good? use the headered string - file = file_with_headers - end - - if layout_option == "none" - f.puts Liquid::Template.parse(file.to_s).render!("site" => self) - else - if layout_option == :default - layout = default_layout - else - layout_file = File.join(self.directory, "_layouts", "#{layout_option}.html") - layout = Liquid::Template.parse(File.read(layout_file)) - end - f.puts layout.render!("site" => self, "page" => { "title" => title }, "content" => Liquid::Template.parse(file.to_s).render!("site" => self)) - end - end - end + def site_path(path) + source_path("_site", path) end - # run through the posts + nil so we can keep |a, b| such that a hits every element - # while iterating. - posts.each.with_index do |post, i| - # the posts are iterated over in reverse chrological order, and - # next_post here is post published chronologically after than - # the post in the iteration. - # - # if i == 0, we don't want posts.last, so return nil if i - 1 == -1 - next_post = (i == 0 ? nil : posts[i - 1]) - prev_post = posts[i + 1] - - puts "Processing post: #{post.path}" - - FileUtils.mkdir_p(tmp_path(File.dirname(post.url))) - - post_layout = default_layout - - if post.headers[:layout] - post_layout = Liquid::Template.parse(File.read(File.join(self.directory, "_layouts", "#{post.headers[:layout]}.html"))) - end - - File.open(tmp_path(post.url + ".html"), "w") do |f| - # variables available in the post template - post_template_variables = { - "post" => post, - "post_page" => true, - "prev_post" => prev_post, - "next_post" => next_post - } - - f.puts post_layout.render!( - "site" => self, - "page" => { "title" => post.title }, - "post_page" => true, - "content" => Liquid::Template.parse(File.read("_templates/post.html")).render!(post_template_variables) - ) - end + def tmp_path(path) + File.join("tmp", File.join("_site", path)) end - generate_draft_previews(default_layout) - - generate_archives(default_layout) - - if Dir.exist?("_site") - FileUtils.mv("_site", "/tmp/_site.#{Time.now.strftime("%Y-%m-%d-%H-%M-%S.%6N")}") + def latest_update_time + most_recent = posts.max_by { |p| p.updated } + most_recent ? most_recent.updated : Time.now end - FileUtils.mv("tmp/_site", ".") && FileUtils.rm_rf("tmp/_site") - FileUtils.rmdir("tmp") - Dir["/tmp/_site.*"][0..-6].each { |d| FileUtils.rm_rf(d) } - end - - private + def archive_url_for_date(date) + parts = { + "year" => date.year.to_s, + "month" => date.month.to_s.rjust(2, "0") + } - def preprocess_autoupdate_posts - posts.each do |p| - if p.autoupdate? - puts "Auto-updating timestamp for: #{p.title} / #{p.slug}" - p.update! - end + Serif::Placeholder.substitute(config.archive_url_format, parts) end - end - # generates draft preview files for any unpublished drafts. - # - # uses the same template as live posts. - def generate_draft_previews(layout) - drafts = self.drafts + def archives + h = { + "posts" => posts, + "years" => posts.group_by { |p| Date.new(p.created.year) }.map do |year_date, year_posts| + year_posts = year_posts.sort_by(&:created).reverse - template = Liquid::Template.parse(File.read("_templates/post.html")) + { + "date" => year_date, + "posts" => year_posts, + "months" => year_posts.group_by { |p| Date.new(p.created.year, p.created.month) }.to_a.map do |month_date, month_posts| + month_posts = month_posts.sort_by(&:created).reverse - # publish each draft under a randomly generated name, or use the - # existing file if one is present. - drafts.each do |draft| - url = private_url(draft) - if url - # take our existing URL like /drafts/foo/ (without .html) - # and give the filename - file = File.basename(url) - else - # create a new name - file = SecureRandom.hex(30) - end - - # convert the name into a relative path - file = "drafts/#{draft.slug}/#{file}" - - # the absolute path in the site's tmp path, where we create the file - # ready to be deployed. - live_preview_file = tmp_path(file) - FileUtils.mkdir_p(File.dirname(live_preview_file)) + { + "date" => month_date, + "posts" => month_posts, + "archive_url" => archive_url_for_date(month_date) + } + end + } + end + } - puts "#{url ? "Updating" : "Creating"} draft preview: #{file}" + h["years"].sort_by! { |el| el["date"] } + h["years"].reverse! - File.open(live_preview_file + ".html", "w") do |f| - f.puts layout.render!( - "site" => self, - "draft_preview" => true, - "page" => { "title" => draft.title }, - "content" => template.render!("site" => self, "post" => draft, "draft_preview" => true) - ) - end + h end - end - - # goes through all draft posts that have "publish: now" headers and - # calls #publish! on each one - def preprocess_autopublish_drafts - puts "Beginning pre-process step for drafts." - drafts.each do |d| - if d.autopublish? - puts "Autopublishing draft: #{d.title} / #{d.slug}" - d.publish! - end - end - end - - # Uses config.archive_url_format to generate pages - # using the archive_page.html template. - def generate_archives(layout) - return unless config.archive_enabled? - - template = Liquid::Template.parse(File.read("_templates/archive_page.html")) - - months = posts.group_by { |post| Date.new(post.created.year, post.created.month) } - months.each do |month, posts| - archive_path = tmp_path(archive_url_for_date(month)) + def conflicts + conflicts = (drafts + posts).group_by(&:url) + conflicts.select! { |url, entries| entries.length > 1 } - FileUtils.mkdir_p(archive_path) - - File.open(File.join(archive_path + ".html"), "w") do |f| - f.puts layout.render!("archive_page" => true, "month" => month, "site" => self, "content" => template.render!("archive_page" => true, "site" => self, "month" => month, "posts" => posts)) + if conflicts.empty? + nil + else + conflicts end end - end - - def self.stringify_keys(obj) - return obj unless obj.is_a?(Hash) || obj.is_a?(Array) - if obj.is_a?(Array) - return obj.map do |el| - stringify_keys(el) - end + def to_liquid + { + "posts" => posts, + "latest_update_time" => latest_update_time, + "archive" => archives, + # exists to allow the file_digest tag to work + "__directory" => source_directory + } end - result = {} - obj.each do |key, value| - result[key.to_s] = stringify_keys(value) + def generate + Serif::Generator.new(self).generate! end - result end end -end diff --git a/rakefile b/rakefile index 7d76aa2..eecc7e0 100644 --- a/rakefile +++ b/rakefile @@ -1,23 +1,6 @@ require "rake" require "time" -require "rspec/core/rake_task" require "benchmark" -require "rdoc/task" - -RSpec::Core::RakeTask.new(:spec) do |t| - t.pattern = ["spec/**/*_spec.rb", "spec/acceptance/**/*.feature"] -end - -task :default => :spec - -Rake::RDocTask.new(:docs) do |rd| - rd.main = "README.md" - rd.rdoc_dir = "docs" - rd.rdoc_files.include("README.md", "lib/**/*.rb") - rd.options << "--markup=markdown" -end - -task :rdoc => :docs task :stress, [:n] do |t, args| iterations = args[:n] || 250 diff --git a/serif.gemspec b/serif.gemspec index 2e128e9..4770006 100644 --- a/serif.gemspec +++ b/serif.gemspec @@ -4,36 +4,31 @@ Gem::Specification.new do |s| s.authors = ["Adam Prescott"] s.email = ["adam@aprescott.com"] s.homepage = "https://github.com/aprescott/serif" - s.summary = "Static site generator and markdown-based blogging with an optional admin interface complete with drag-and-drop image uploading." - s.description = "Serif is a static site generator and blogging system powered by markdown files and an optional admin interface complete with drag-and-drop image uploading." - s.files = Dir["{lib/**/*,statics/**/*,bin/*,spec/**/*}"] + %w[serif.gemspec rakefile LICENSE Gemfile Gemfile.lock README.md] + s.summary = "Static site generator and markdown-based blogging." + s.description = "Serif is a static site generator and blogging system powered by markdown files." + s.files = Dir["{lib/**/*,site_template/**/*,bin/*,spec/**/*}"] + %w[serif.gemspec rakefile LICENSE Gemfile Gemfile.lock README.md] s.require_path = "lib" s.bindir = "bin" s.executables = "serif" s.test_files = Dir["spec/*"] - s.required_ruby_version = ">= 2.0.0" + s.required_ruby_version = ">= 2.1.0" s.licenses = ["MIT"] - s.add_runtime_dependency "rack" s.add_runtime_dependency "kramdown", ">= 1.9.0" s.add_runtime_dependency "rubypants" s.add_runtime_dependency "rouge", ">= 1.10.0" s.add_runtime_dependency "sinatra" s.add_runtime_dependency "redhead" s.add_runtime_dependency "liquid", "~> 2.0" - s.add_runtime_dependency "reverse_markdown" - s.add_runtime_dependency "nokogiri" - s.add_runtime_dependency "timeout_cache" + s.add_development_dependency "nokogiri" s.add_development_dependency "rake" s.add_development_dependency "rspec" - s.add_development_dependency "simplecov" - s.add_development_dependency "timecop" - s.add_development_dependency "rdoc" + s.add_development_dependency "rspec-its" s.add_development_dependency "coveralls" - s.add_development_dependency "turnip" - s.add_development_dependency "capybara" - s.add_development_dependency "poltergeist" + s.add_development_dependency "simplecov" s.add_development_dependency "pry-byebug" + s.add_development_dependency "timecop" + s.add_development_dependency "bundler-audit" end diff --git a/statics/skeleton/_config.yml b/site_template/_config.yml similarity index 76% rename from statics/skeleton/_config.yml rename to site_template/_config.yml index cfb434a..71c8677 100644 --- a/statics/skeleton/_config.yml +++ b/site_template/_config.yml @@ -3,9 +3,6 @@ # # For information on which values are available here, see the README. # -admin: - username: changethisusername - password: changethispassword permalink: /:title archive: enabled: yes diff --git a/spec/site_dir/_drafts/sample-draft b/site_template/_drafts/sample-draft similarity index 100% rename from spec/site_dir/_drafts/sample-draft rename to site_template/_drafts/sample-draft diff --git a/statics/skeleton/_layouts/default.html b/site_template/_layouts/default.html similarity index 100% rename from statics/skeleton/_layouts/default.html rename to site_template/_layouts/default.html diff --git a/spec/site_dir/_templates/archive_page.html b/site_template/_templates/archive_page.html similarity index 100% rename from spec/site_dir/_templates/archive_page.html rename to site_template/_templates/archive_page.html diff --git a/statics/skeleton/_templates/post.html b/site_template/_templates/post.html similarity index 100% rename from statics/skeleton/_templates/post.html rename to site_template/_templates/post.html diff --git a/spec/site_dir/archive.html b/site_template/archive.html similarity index 100% rename from spec/site_dir/archive.html rename to site_template/archive.html diff --git a/spec/site_dir/index.html b/site_template/index.html similarity index 100% rename from spec/site_dir/index.html rename to site_template/index.html diff --git a/spec/acceptance/features/admin/drafts.feature b/spec/acceptance/features/admin/drafts.feature deleted file mode 100644 index beedfff..0000000 --- a/spec/acceptance/features/admin/drafts.feature +++ /dev/null @@ -1,33 +0,0 @@ -Feature: Creating and saving drafts - -Background: - Given I am logged in as an admin user - -Scenario: Creating a new draft - When I go to the new draft page - And I type up a new post - And I save the draft - Then I should see the newly saved draft - -Scenario: Viewing a saved draft - When I've saved a draft - Then I should be able to see its contents - -Scenario Outline: Saving a partial draft - When I go to the new draft page - And I type up a post with the missing - And I save the draft - Then I should see an error message - But the draft body should still be there - - Examples: - | content | - | slug | - | title | - -Scenario: Editing an existing draft - When I create a new draft - And I view the draft for editing - And I save the post with new content but no slug - Then I should see an error about being unable to update - But my new content should be there diff --git a/spec/acceptance/features/admin/landing_page.feature b/spec/acceptance/features/admin/landing_page.feature deleted file mode 100644 index bedfbbb..0000000 --- a/spec/acceptance/features/admin/landing_page.feature +++ /dev/null @@ -1,8 +0,0 @@ -Feature: Admin landing page - -Background: - Given I am logged in as an admin user - -Scenario: Admin landing page - When I view the admin landing page - Then I should see relevant summary information diff --git a/spec/acceptance/features/admin/markdown_previews.feature b/spec/acceptance/features/admin/markdown_previews.feature deleted file mode 100644 index de9db4a..0000000 --- a/spec/acceptance/features/admin/markdown_previews.feature +++ /dev/null @@ -1,14 +0,0 @@ -@js -Feature: Ability to preview rendered versions of Markdown - -Background: - Given I am logged in as an admin user - -Scenario Outline: When I press "Preview" on a post or draft, I should see rendered content - When I press preview on a that has content - Then I should see the rendered preview - - Examples: - | post_or_draft | - | post | - | draft | diff --git a/spec/acceptance/macros/admin_navigation_macros.rb b/spec/acceptance/macros/admin_navigation_macros.rb deleted file mode 100644 index a887cda..0000000 --- a/spec/acceptance/macros/admin_navigation_macros.rb +++ /dev/null @@ -1,5 +0,0 @@ -module AdminNavigationMacros - step "I go to the new draft page" do - click_on "New draft" - end -end diff --git a/spec/acceptance/macros/auth_macros.rb b/spec/acceptance/macros/auth_macros.rb deleted file mode 100644 index 7df9713..0000000 --- a/spec/acceptance/macros/auth_macros.rb +++ /dev/null @@ -1,24 +0,0 @@ -module AuthMacros - step "I am logged in as an admin user" do - basic_auth "test-changethisusername", "test-changethispassword" - visit "/admin" - - expect(page).to have_content("Admin") - end - - private - - def basic_auth(user, password) - encoded_login = ["#{user}:#{password}"].pack("m*").gsub(/\r?\n/, "") - - if page.driver.respond_to?(:header) - page.driver.header "Authorization", "Basic #{encoded_login}" - else - page.driver.headers = { "Authorization" => "Basic #{encoded_login}" } - end - end -end - -RSpec.configure do |config| - config.include AuthMacros, type: :feature -end diff --git a/spec/acceptance/steps/admin/drafts_steps.rb b/spec/acceptance/steps/admin/drafts_steps.rb deleted file mode 100644 index dabe3f2..0000000 --- a/spec/acceptance/steps/admin/drafts_steps.rb +++ /dev/null @@ -1,83 +0,0 @@ -module DraftsSteps - include AdminNavigationMacros - - step "I type up a new post" do - type_out_draft - end - - step "I save the draft" do - click_on "Save draft" - end - - step "I've saved a draft" do - step "I go to the new draft page" - step "I type up a new post" - step "I save the draft" - end - - step "I should be able to see its contents" do - click_on "new-awesome-post" - expect(page).to have_content("My new post content.") - # TODO: ugh - FileUtils.rm_f testing_dir("_drafts/new-awesome-post") - end - - step "I should see the newly saved draft" do - expect(page).to have_content("new-awesome-post") - # TODO: ugh - FileUtils.rm_f testing_dir("_drafts/new-awesome-post") - end - - step "I type up a post with the slug missing" do - type_out_draft(:title, :markdown) - end - - step "I type up a post with the title missing" do - type_out_draft(:slug, :markdown) - end - - step "I should see an error message" do - expect(page).to have_content("There must be a URL, a title, and content to save.") - end - - step "the draft body should still be there" do - expect(page).to have_field(:markdown, with: "My new post content.") - end - - step "I create a new draft" do - step "I go to the new draft page" - step "I type up a new post" - step "I save the draft" - end - - step "I view the draft for editing" do - click_on "new-awesome-post" - end - - step "I save the post with new content but no slug" do - fill_in :markdown, with: "Changed content." - fill_in :slug, with: "" - click_on "Update draft" - end - - step "my new content should be there" do - expect(page).to have_field(:markdown, with: "Changed content.") - end - - step "I should see an error about being unable to update" do - expect(page).to have_content("You must pick a URL to use") - # TODO: ugh - FileUtils.rm_f testing_dir("_drafts/new-awesome-post") - end - - private - - def type_out_draft(*fields) - fields = [:slug, :title, :markdown] if fields && fields.empty? - fields = [] if !fields - - fill_in :slug, with: "new-awesome-post" if fields.include?(:slug) - fill_in :title, with: "New Post" if fields.include?(:title) - fill_in :markdown, with: "My new post content." if fields.include?(:markdown) - end -end diff --git a/spec/acceptance/steps/admin/landing_page_steps.rb b/spec/acceptance/steps/admin/landing_page_steps.rb deleted file mode 100644 index 710b028..0000000 --- a/spec/acceptance/steps/admin/landing_page_steps.rb +++ /dev/null @@ -1,11 +0,0 @@ -module LandingPageSteps - step "I view the admin landing page" do - visit "/admin" - end - - step "I should see relevant summary information" do - expect(page).to have_title("Admin") - expect(page).to have_content("Drafts") - expect(page).to have_content("Posts") - end -end diff --git a/spec/acceptance/steps/admin/markdown_previews_steps.rb b/spec/acceptance/steps/admin/markdown_previews_steps.rb deleted file mode 100644 index 3c5ae45..0000000 --- a/spec/acceptance/steps/admin/markdown_previews_steps.rb +++ /dev/null @@ -1,16 +0,0 @@ -module MarkdownPreviewsSteps - include AdminNavigationMacros - - step "I press preview on a :post_or_draft that has content" do |post_type| - step "I go to the new draft page" - fill_in :markdown, with: "# Here is my heading\n\n```ruby\ndef foo(*)\n :foo\nend\n```\n\nAll **done!**" - find("label", text: "Preview").click - end - - step "I should see the rendered preview" do - expect(page).to have_selector("h1", text: "Here is my heading") - expect(page).to have_selector("p", text: "All done!") - expect(page).to have_selector("strong", text: "done!") - expect(page).to have_selector("pre.highlight code", text: "def foo(*) :foo end") - end -end diff --git a/spec/commands_spec.rb b/spec/commands_spec.rb deleted file mode 100644 index d78a287..0000000 --- a/spec/commands_spec.rb +++ /dev/null @@ -1,75 +0,0 @@ -class Serif::Commands - def exit(code) - "Fake exit with code #{code}" - end -end - -RSpec.describe Serif::Commands do - def expect_method_call(arg, method) - c = Serif::Commands.new([arg]) - expect(c).to receive(method) - capture_stdout { c.process } - end - - describe "#process" do - it "takes -h and --help and calls print usage" do - %w[-h --help].each do |cmd| - expect_method_call(cmd, :print_help) - end - end - - { - "admin" => :initialize_admin_server, - "dev" => :initialize_dev_server, - "new" => :produce_skeleton, - "generate" => :generate_site - }.each do |command, meth| - it "takes the command '#{command}' and runs #{meth}" do - expect_method_call(command, meth) - end - end - - it "exits on help" do - expect_method_call("-h", :exit) - end - end - - describe "#generate_site" do - it "calls Site#generate" do - allow(Serif::Site).to receive(:generation_called) - allow_any_instance_of(Serif::Site).to receive(:generate) { Serif::Site.generation_called } - - # if this is called, it means any instance of Site had #generate called. - expect(Serif::Site).to receive(:generation_called) - - Serif::Commands.new([]).generate_site("anything") - end - - context "with a conflict" do - def conflicting_generate_command - a = b = double("") - allow(a).to receive(:url) { "/foo" } - allow(b).to receive(:url) { "/foo" } - allow(a).to receive(:path) { "/anything" } - allow(b).to receive(:path) { "/anything" } - - # any non-nil value will do - allow_any_instance_of(Serif::Site).to receive(:conflicts) { { "/foo" => [a, b] } } - - command = Serif::Commands.new([]) - command.generate_site(testing_dir) - expect(command).to receive(:exit) - command - end - - it "exits" do - capture_stdout { conflicting_generate_command.process } - end - - it "prints the urls that conflict" do - output = capture_stdout { conflicting_generate_command.process } - expect(output).to match(/Conflicts at:\n\n\/foo\n\t\/anything\n\t\/anything/) - end - end - end -end diff --git a/spec/config_spec.rb b/spec/config_spec.rb deleted file mode 100644 index 56df5cd..0000000 --- a/spec/config_spec.rb +++ /dev/null @@ -1,53 +0,0 @@ -RSpec.describe Serif::Config do - subject do - Serif::Config.new(testing_dir("_config.yml")) - end - - describe "#admin_username" do - it "is the admin username defined in the config file" do - expect(subject.admin_username).to eq("test-changethisusername") - end - end - - describe "#admin_password" do - it "is the admin password defined in the config file" do - expect(subject.admin_password).to eq("test-changethispassword") - end - end - - describe "#image_upload_path" do - it "defaults to /images/:timestamp/_name" do - allow(subject).to receive(:yaml) { {} } - expect(subject.image_upload_path).to eq("/images/:timestamp_:name") - end - end - - describe "#permalink" do - it "is the permalink format defined in the config file" do - expect(subject.permalink).to eq("/test-blog/:title") - end - - it "defaults to /:title" do - allow(subject).to receive(:yaml) { {} } - expect(subject.permalink).to eq("/:title") - end - end - - describe "#archive_url_format" do - it "defaults to /archive/:year/:month" do - allow(subject).to receive(:yaml) { {} } - expect(subject.archive_url_format).to eq("/archive/:year/:month") - end - - it "is the archive_url_format found in the config file" do - expect(subject.archive_url_format).to eq("/test-archive/:year/:month") - end - end - - describe "#archive_enabled?" do - it "defaults to false" do - allow(subject).to receive(:yaml) { {} } - expect(subject.archive_enabled?).to be_falsey - end - end -end diff --git a/spec/content_file_spec.rb b/spec/content_file_spec.rb deleted file mode 100644 index f3f5e44..0000000 --- a/spec/content_file_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -RSpec.describe Serif::ContentFile do - subject do - Serif::Site.new(testing_dir) - end - - describe "#basename" do - it "is the basename of the path" do - (subject.drafts + subject.posts).each do |content_file| - expect(content_file.basename).to eq(File.basename(content_file.path)) - end - - draft = Serif::Draft.new(subject) - draft.slug = "foo" - draft.title = "foo" - - # NOTE! Freezing! - Timecop.freeze(Time.parse("2013-04-03")) - - draft.save - draft.publish! - post = Serif::Post.new(subject, draft.path) - - begin - expect(draft.path).not_to be_nil - expect(post).not_to be_nil - expect(draft.basename).to eq(post.basename) - - # NOTE! Time frozen! - expect(post.basename).to eq("2013-04-03-foo") - ensure - Timecop.return - FileUtils.rm(post.path) - end - end - end - - describe "draft and published status" do - it "can handle a nil path" do - c = Serif::ContentFile.new(subject) - expect(c.path).to be_nil - expect(c.draft?).to be_truthy - expect(c.published?).to be_falsey - end - end - - describe "draft?" do - it "is true if the file is in the _drafts directory" do - subject.drafts.each do |d| - expect(d.draft?).to be_truthy - expect(d.published?).to be_falsey - end - - d = subject.drafts.sample - orig_path = d.path - allow(d).to receive(:path) { orig_path.gsub(/^#{Regexp.quote(testing_dir("_drafts"))}/, testing_dir("_anything")) } - expect(d.draft?).to be_falsey - end - end - - describe "published?" do - it "can handle a nil path" do - d = Serif::Post.new(subject) - expect(d.draft?).to be_truthy - expect(d.published?).to be_falsey - end - - it "is true if the file is in the _posts directory" do - subject.posts.each do |p| - expect(p.published?).to be_truthy - expect(p.draft?).to be_falsey - end - - p = subject.posts.sample - orig_path = p.path - allow(p).to receive(:path) { orig_path.gsub(/^#{Regexp.quote(testing_dir("_posts"))}/, testing_dir("_anything")) } - expect(p.published?).to be_falsey - end - end - - describe "#title=" do - it "sets the underlying header value to the assigned title" do - (subject.drafts + subject.posts).each do |content_file| - content_file.title = "foobar" - expect(content_file.headers[:title]).to eq("foobar") - end - end - end - - describe "#save(markdown)" do - it "sets the underlying updated time value for posts" do - draft = Serif::Draft.new(subject) - draft.title = "Testing" - draft.slug = "hi" - - begin - draft.save("# Some content") - draft.publish! - - post = Serif::Post.new(subject, draft.path) - - t = Time.now - Timecop.freeze(t + 30) do - post.save("# Heading content") - expect(post.updated.to_i).to eq((t + 30).to_i) - end - ensure - FileUtils.rm(post.path) - end - end - end -end diff --git a/spec/draft_spec.rb b/spec/draft_spec.rb deleted file mode 100644 index fd99f02..0000000 --- a/spec/draft_spec.rb +++ /dev/null @@ -1,274 +0,0 @@ -RSpec.describe Serif::Draft do - before :all do - @site = Serif::Site.new(testing_dir) - D = Serif::Draft - FileUtils.rm_rf(testing_dir("_trash")) - end - - describe "#url" do - it "uses the current time for its placeholder values" do - d = D.new(@site) - d.slug = "my-blar-blar" - orig_headers = d.headers - allow(d).to receive(:headers) { orig_headers.merge(:permalink => "/foo/:year/:month/:day/:title") } - - Timecop.freeze(Time.parse("2020-02-09")) do - expect(d.url).to eq("/foo/2020/02/09/my-blar-blar") - end - end - - it "can handle nil slug values" do - d = D.new(@site) - expect(d.slug).to be_nil - orig_headers = d.headers - allow(d).to receive(:headers) { orig_headers.merge(:permalink => "/foo/:year/:month/:day/:title") } - - Timecop.freeze(Time.parse("2020-02-09")) do - expect(d.url).to eq("/foo/2020/02/09/") - end - end - - it "defaults to the config file's permalink value" do - d = D.new(@site) - d.slug = "gablarhgle" - expect(d.url).to eq("/test-blog/gablarhgle") - end - - it "uses its permalink header value" do - d = D.new(@site) - d.slug = "anything" - allow(d).to receive(:headers) { { :permalink => "testage" } } - expect(d.url).to eq("testage") - end - end - - describe ".rename" do - it "moves the draft to a new file" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - - D.rename(@site, "test-draft", "foo-bar") - d = D.from_slug(@site, "foo-bar") - expect(d).not_to be_nil - expect(File.exist?(testing_dir("_drafts/foo-bar"))).to be_truthy - - d.delete! - end - - it "raises if there is an existing draft" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - - draft2 = D.new(@site) - draft2.slug = "test-draft-2" - draft2.title = "Some draft title" - draft2.save("some content") - - expect { D.rename(@site, draft2.slug, draft.slug) }.to raise_error(RuntimeError, "file exists") - - draft.delete! - draft2.delete! - end - end - - describe "#delete!" do - it "moves the file to _trash" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.delete! - expect(Dir[testing_dir("_trash/*-test-draft")].length).to eq(1) - end - - it "creates the _trash directory if it doesn't exist" do - FileUtils.rm_rf(testing_dir("_trash")) - - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.delete! - - expect(File.exist?(testing_dir("_trash"))).to be_truthy - end - end - - describe "publish!" do - it "moves the file to the _posts directory" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.publish! - - published_path = testing_dir("_posts/#{Date.today.to_s}-#{draft.slug}") - expect(File.exist?(published_path)).to be_truthy - - # clean up - FileUtils.rm_f(published_path) - end - - it "creates the posts directory if it doens't already exist" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - - expect(FileUtils).to receive(:mkdir_p).with(testing_dir("_posts")).and_call_original - - begin - draft.publish! - ensure - FileUtils.rm(draft.path) - end - end - - it "makes the post available in Site#posts and Site#to_liquid even straight after a generate" do - draft = D.new(@site) - draft.slug = "test-draft-to-go-into-liquid" - draft.title = "Some draft title" - draft.save("some content") - published_path = testing_dir("_posts/#{Date.today.to_s}-#{draft.slug}") - - begin - capture_stdout { @site.generate } - expect(@site.posts.map(&:slug)).to_not include(draft.slug) - expect(@site.to_liquid["posts"].map(&:slug)).to_not include(draft.slug) - draft.publish! - capture_stdout { @site.generate } - expect(@site.posts.map(&:slug)).to include(draft.slug) - sleep 1 # wait for the liquid cache to be invalidated - expect(@site.to_liquid["posts"].map(&:slug)).to include(draft.slug) - ensure - # clean up - FileUtils.rm_f(published_path) - end - end - - it "changes the #path to be _posts not _drafts" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.publish! - - expect(draft.path).to eq(testing_dir("_posts/#{Date.today.to_s}-#{draft.slug}")) - draft.delete! # still deleteable, even though it's been moved - end - - it "does not write out an autopublish header if autopublish? is true" do - draft = D.new(@site) - draft.slug = "autopublish-draft" - draft.title = "Some draft title" - draft.autopublish = true - draft.save("some content") - draft.publish! - - # check the header on the object has been removed - expect(draft.autopublish?).to be_falsey - - # check the actual file doesn't have the header - expect(Serif::Post.new(@site, draft.path).headers[:publish]).to be_nil - - draft.delete! - end - end - - describe "#autopublish=" do - it "sets the 'publish' header to 'now' if truthy assigned value" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.autopublish = true - - expect(draft.headers[:publish]).to eq("now") - - draft.delete! - end - - it "removes the 'publish' header entirely if falsey assigned value" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.save("some content") - draft.autopublish = false - - expect(draft.headers.key?(:publish)).to be_falsey - - draft.delete! - end - - it "carries its value through to #autopublish?" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - draft.autopublish = false - expect(draft.autopublish?).to be_falsey - - draft.autopublish = true - expect(draft.autopublish?).to be_truthy - - draft.autopublish = false - expect(draft.autopublish?).to be_falsey - end - end - - describe "#autopublish?" do - it "returns true if there is a 'publish: now' header, otherwise false" do - draft = D.new(@site) - expect(draft.autopublish?).to be_falsey - headers = draft.headers - allow(draft).to receive(:headers) { headers.merge(:publish => "now") } - expect(draft.autopublish?).to be_truthy - end - - it "ignores leading and trailing whitespace around the value of the 'publish' header" do - draft = D.new(@site) - expect(draft.autopublish?).to be_falsey - headers = draft.headers - allow(draft).to receive(:headers) { headers.merge(:publish => " now ") } - expect(draft.autopublish?).to be_truthy - end - end - - describe "#to_liquid" do - it "contains the relevant keys" do - liq = @site.drafts.sample.to_liquid - - ["title", "content", "slug", "type", "draft", "published", "url"].each do |e| - expect(liq.key?(e)).to be_truthy - end - end - - context "for an initial draft" do - it "works fine" do - expect { Serif::Draft.new(@site).to_liquid }.to_not raise_error - end - end - end - - describe "#save" do - it "saves the file to _drafts" do - draft = D.new(@site) - draft.slug = "test-draft" - draft.title = "Some draft title" - - expect(D.exist?(@site, draft.slug)).to be_falsey - expect(File.exist?(testing_dir("_drafts/test-draft"))).to be_falsey - - draft.save("some content") - - expect(D.exist?(@site, draft.slug)).to be_truthy - expect(File.exist?(testing_dir("_drafts/test-draft"))).to be_truthy - - # clean up the file - draft.delete! - end - end -end diff --git a/spec/features/archives_spec.rb b/spec/features/archives_spec.rb new file mode 100644 index 0000000..34b4baa --- /dev/null +++ b/spec/features/archives_spec.rb @@ -0,0 +1,52 @@ +RSpec.describe "Archives" do + before { generate_site } + + describe "Main archive page" do + subject { Nokogiri::HTML.parse(File.read(testing_dir("_site/archive.html"))) } + + it "includes one year for each year with a published post" do + expect(subject.search(".year-date").map(&:text)).to eq([ + "2400 (post count: 1)", + "2399 (post count: 1)", + "2015 (post count: 4)", + "1921 (post count: 1)", + "1920 (post count: 1)" + ]) + end + + it "includes months within each year" do + expect(subject.search(".year").map { |e| e.search(".month-date").map(&:text) }).to eq([ + ["2400 January (post count: 1)"], + ["2399 January (post count: 1)"], + ["2015 March (post count: 1)", "2015 January (post count: 3)"], + ["1921 January (post count: 1)"], + ["1920 January (post count: 1)"] + ]) + end + + it "provides an archive link" do + expect(subject.search(".archive-link").map { |e| e.attr("href") }).to eq([ + "/test-archive/2400/01", + "/test-archive/2399/01", + "/test-archive/2015/03", + "/test-archive/2015/01", + "/test-archive/1921/01", + "/test-archive/1920/01" + ]) + end + end + + describe "individual archive month pages" do + subject { Nokogiri::HTML.parse(File.read(testing_dir("_site/test-archive/2015/01.html"))) } + + it "includes the month and posts" do + expect(subject.search("h1").map(&:text)).to eq(["Jan 2015 (3)"]) + + expect(subject.search("ul li a").map { |e| [e.attr("href"), e.text] }).to eq([ + ["/test-blog/test--posts-get-post-page-flag-true", "Sample post"], + ["/test-blog/test--published-posts-get-draft-preview-false", "No draft preview flag test"], + ["/test-blog/test--permalinks-from-config-file", "A post"] + ]) + end + end +end diff --git a/spec/features/directory_creation_spec.rb b/spec/features/directory_creation_spec.rb new file mode 100644 index 0000000..3bbeeb8 --- /dev/null +++ b/spec/features/directory_creation_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe "Directory creation during site generation" do + before do + FileUtils.rm_rf(testing_dir("_site")) + end + + describe "_site" do + it "is created if it doesn't exist" do + expect { generate_site }.to change { File.exist?(testing_dir("_site")) }.from(false).to(true) + end + end + + describe "_site/drafts" do + it "is created if it doesn't exist" do + expect { generate_site }.to change { File.exist?(testing_dir("_site/drafts")) }.from(false).to(true) + end + + it "is not created if there are no drafts" do + begin + FileUtils.mv(testing_dir("_drafts"), testing_dir("_drafts.temp")) + + expect { generate_site }.to_not change { File.exist?(testing_dir("_site/drafts")) }.from(false) + ensure + FileUtils.mv(testing_dir("_drafts.temp"), testing_dir("_drafts")) + end + end + end +end diff --git a/spec/features/drafts_spec.rb b/spec/features/drafts_spec.rb new file mode 100644 index 0000000..7bd2183 --- /dev/null +++ b/spec/features/drafts_spec.rb @@ -0,0 +1,30 @@ +RSpec.describe "Draft post" do + it "remains unpublished without changes" do + generate_site + expect(Dir[testing_dir("_site/test-blog/*")].length).to eq(8) + expect(Dir[testing_dir("_drafts/*")].length).to eq(3) + + with_file_contents(testing_dir("_drafts/test--new-draft"), "title: new draft\n\nsome content") do + expect(Dir[testing_dir("_drafts/*")].length).to eq(4) + expect { generate_site }.to_not change { Dir[testing_dir("_site/test-blog/*")].length } + expect(Dir[testing_dir("_drafts/*")].length).to eq(4) + end + end + + describe "the 'publish: now' header" do + it "will auto-publish on generation" do + generate_site + + with_file_contents(testing_dir("_drafts/test--new-draft"), "title: new draft\npublish: now\n\nsome content", removal_path: testing_dir("_posts/#{Time.now.strftime("%Y-%m-%d")}-test--new-draft")) do + expect(Dir[testing_dir("_drafts/*")].length).to eq(4) + expect { generate_site }.to change { Dir[testing_dir("_site/test-blog/*")].length }.from(8).to(9) + expect(Dir[testing_dir("_drafts/*")].length).to eq(3) + + newly_published_content = File.read(testing_dir("_posts/#{Time.now.strftime("%Y-%m-%d")}-test--new-draft")) + timestamp = /#{Time.now.strftime("%Y-%m-%dT")}\d\d:\d\d:\d\d#{Regexp.quote Time.now.xmlschema.split(/\d\d:\d\d:\d\d/).last}/ + + expect(newly_published_content).to match(/\Atitle: new draft\nCreated: #{timestamp.source}\nUpdated: #{timestamp.source}\n\nsome content\n\z/) + end + end + end +end diff --git a/spec/features/generation_spec.rb b/spec/features/generation_spec.rb new file mode 100644 index 0000000..02a26df --- /dev/null +++ b/spec/features/generation_spec.rb @@ -0,0 +1,84 @@ +RSpec.describe "Site generation with serif generate" do + before :all do + generate_site + end + + it "uses the the permalink value in the config file by default" do + expect(File.exist?(testing_dir("_site/test-blog/test--permalinks-from-config-file.html"))).to be_truthy + end + + it "uses a custom layout: header value for a non-post file, if specified" do + expect(File.read(testing_dir("_site/test--page-with-a-custom-layout-header-value.html"))).to match(/Alternate layout<\/h1>/) + end + + it "uses a custom layout: header value for a post file, if specified" do + expect(File.read(testing_dir("_site/test-blog/test--post-with-custom-layout.html"))).to match(/Alternate layout<\/h1>/) + end + + it "generates links for the next and previous posts" do + first_post_content = File.read(testing_dir("_site/test-blog/test--page-links--very-first-post.html")) + second_post_content = File.read(testing_dir("_site/test-blog/test--page-links--second-post.html")) + penultimate_post_content = File.read(testing_dir("_site/test-blog/test--page-links--penultimate-post.html")) + final_post_content = File.read(testing_dir("_site/test-blog/test--page-links--final-post.html")) + + expect(first_post_content).to_not include("Previous post") + expect(first_post_content).to include("Next post: Second post") + + expect(second_post_content).to include("Previous post: Very first post") + expect(second_post_content).to include("Next post:") + + expect(penultimate_post_content).to include("Previous post:") + expect(penultimate_post_content).to include("Next post: Final post") + + expect(final_post_content).to include("Previous post: Penultimate post") + expect(final_post_content).to_not include("Next post") + end + + it "sets the draft_preview flag to true for drafts" do + draft_preview_path = Dir[testing_dir("_site/drafts/test--drafts-get-draft-preview-true/*.html")].first + preview_contents = File.read(draft_preview_path) + published_contents = File.read(testing_dir("_site/test-blog/test--published-posts-get-draft-preview-false.html")) + + draft_preview_flag_content = "this is a draft preview" + expect(preview_contents).to include(draft_preview_flag_content) + expect(published_contents).to_not include(draft_preview_flag_content) + end + + it "ses the post_page flag to true for published posts" do + draft_preview_path = Dir[testing_dir("_site/drafts/test--drafts-get-post-page-flag-false/*.html")].first + preview_contents = File.read(draft_preview_path) + published_contents = File.read(testing_dir("_site/test-blog/test--posts-get-post-page-flag-true.html")) + page_contents = File.read(testing_dir("_site/test--page-get-post-page-flag-false.html")) + + expect(published_contents).to include("post_page flag set for template") + expect(published_contents).to include("post_page flag set for layout") + + expect(preview_contents).to_not include("post_page flag set for template") + expect(preview_contents).to_not include("post_page flag set for layout") + + expect(page_contents).to_not include("post_page flag set for template") + expect(page_contents).to_not include("post_page flag set for layout") + end + + it "generates preview files for drafts" do + draft_directory = testing_dir("_site/drafts/test--drafts-get-a-preview") + + # it's a directory + expect(Dir.exist?(draft_directory)).to be_truthy + expect(File.file?(draft_directory)).to be_falsey + + draft_preview_path = Dir[File.join(draft_directory, "*.html")].first + + # its actual name is all hex characters + expect(draft_preview_path).to_not be_nil + expect(File.basename(draft_preview_path)).to match(/\A[a-f0-9]{60}.html\z/) + + # each draft gets its own file + expect(Dir[testing_dir("_site/drafts/*")].length).to eq(3) + + # hex filenames are consistent: they're re-used if they already exist + generate_site + expect(Dir[File.join(draft_directory, "*.html")].length).to eq(1) + expect(File.basename(Dir[File.join(draft_directory, "*.html")].first)).to eq(File.basename(draft_preview_path)) + end +end diff --git a/spec/features/liquid_filters_and_tags_spec.rb b/spec/features/liquid_filters_and_tags_spec.rb new file mode 100644 index 0000000..0c7edd3 --- /dev/null +++ b/spec/features/liquid_filters_and_tags_spec.rb @@ -0,0 +1,51 @@ +RSpec.describe "Liquid filters and tags" do + { + markdown: [ + %Q~{{ "*some markdown*" | markdown }}~, "

some markdown

" + ], + smarty: [ + %Q~{{ "testing's for a " | append: '"' | append: "heading's" | append: '"' | append: " `with code` in it..." | smarty }}~, "testing’s for a “heading’s” `with code` in it…" + ], + strip: [ + %Q~{{ " testing " | strip }}~, "testing" + ], + xmlschema: [ + %Q~{{ site.latest_update_time | xmlschema }}~, "2400-01-01T00:00:00Z" + ], + encode_uri_component: [ + %Q~{{ "x&y" | encode_uri_component }}~, "x%26y" + ], + file_digest: [ + %Q~{% file_digest test-stylesheet.css %}~, "f8390232f0c354a871f9ba0ed306163c" + ] + }.each do |tag, (input, expected_output)| + describe "| #{tag}" do + it "is supported in non-post pages, in the layout and the file" do + with_file_contents(testing_dir("_layouts/test--with-#{tag}.html"), "
\n\n#{input}\n\n
\n\n{{ content }}") do + with_file_contents(testing_dir("test--some-file-with-#{tag}.html"), "layout: test--with-#{tag}\n\n
\n\n#{input}
") do + generate_site + + expect(File.read(testing_dir("_site/test--some-file-with-#{tag}.html")).strip).to include("
\n\n#{expected_output}") + expect(File.read(testing_dir("_site/test--some-file-with-#{tag}.html")).strip).to include("
\n\n#{expected_output}") + end + end + end + end + end + + describe "file_digest" do + before do + generate_site + end + + it "is empty in non-prod environments" do + expect(File.read(testing_dir("_site/test--page-with-a-filedigest-filter.html")).strip).to eq("f8390232f0c354a871f9ba0ed306163c\nf8390232f0c354a871f9ba0ed306163c\n.f8390232f0c354a871f9ba0ed306163c") + + generate_site(env: nil) + expect(File.read(testing_dir("_site/test--page-with-a-filedigest-filter.html")).strip).to eq("") + + generate_site(env: "development") # really: just not-production + expect(File.read(testing_dir("_site/test--page-with-a-filedigest-filter.html")).strip).to eq("") + end + end +end diff --git a/spec/features/new_skeleton_spec.rb b/spec/features/new_skeleton_spec.rb new file mode 100644 index 0000000..b3177d4 --- /dev/null +++ b/spec/features/new_skeleton_spec.rb @@ -0,0 +1,42 @@ +RSpec.describe "Creating a new site" do + around do |example| + begin + create_new_site(testing_dir("../new_source")) + + example.run + ensure + FileUtils.rm_r(testing_dir("../new_source")) + end + end + + def directory_signatures(directory) + Dir[File.join(directory, "/**/*")].select do |f| + File.file?(f) + end.map do |x| + [x.sub(/\A#{directory}/, ""), Digest::SHA256.hexdigest(File.read(x))] + end + end + + it "should generate _site/ immediately" do + expect(File.exist?(testing_dir("../new_source/_site"))).to be_truthy + end + + it "should create a new site based on the fixed site template" do + FileUtils.rm_r(testing_dir("../new_source/_site")) + + original_skeleton_tree = directory_signatures(testing_dir("../../site_template")) + generated_tree = directory_signatures(testing_dir("../new_source")) + + expect(generated_tree).to eq(original_skeleton_tree) + end + + it "includes a default _config.yml" do + expect(YAML.load_file(testing_dir("../new_source/_config.yml"))).to eq( + "permalink" => "/:title", + "archive" => { + "enabled" => true, + "url_format" => "/archive/:year/:month" + } + ) + end +end diff --git a/spec/features/pages_spec.rb b/spec/features/pages_spec.rb new file mode 100644 index 0000000..55f82e1 --- /dev/null +++ b/spec/features/pages_spec.rb @@ -0,0 +1,43 @@ +RSpec.describe "Pages" do + it "renders Liquid markup" do + with_file_contents(testing_dir("regular-file.html"), "some content {{ 'goes here' }}") do + generate_site + + expect(File.read(testing_dir("_site/regular-file.html")).strip).to eq("
\n\nsome content goes here\n
") + end + end + + it "uses the default layout" do + with_file_contents(testing_dir("regular-file.html"), "title: a title\n\nsome content") do + generate_site + + rendered = File.read(testing_dir("_site/regular-file.html")) + expect(rendered).to eq("
\n\nsome content\n
\n") + end + end + + it "allows a custom layout" do + with_file_contents(testing_dir("_layouts/custom.html"), "custom layout\n\n{{ content }}") do + with_file_contents(testing_dir("regular-file.html"), "layout: custom\n\nsome content") do + generate_site + + rendered = File.read(testing_dir("_site/regular-file.html")) + expect(rendered).to eq("custom layout\n\nsome content\n\n") + end + end + end + + it "uses all given header values in both the layout and the template" do + with_file_contents(testing_dir("_layouts/custom.html"), "custom layout {{ site.posts | size }} {{ page.header_a }} {{ page.header_b | upcase }} ({{ page.layout }}) {{ page.title }}\n\n{{ content }}") do + with_file_contents(testing_dir("regular-file.html"), "layout: custom\nheader_a: header-a-val\nheader_b: header-b-val\ntitle: a title\n\nsome content {{ site.posts | size }} {{ page.header_a }} {{ page.header_b | upcase }} ({{ page.layout }}) {{ page.title }}") do + generate_site + + rendered = File.read(testing_dir("_site/regular-file.html")).split("\n\n") + expect(rendered).to eq([ + "custom layout 8 header-a-val #{"header-b-val".upcase} (custom) a title", + "some content 8 header-a-val #{"header-b-val".upcase} (custom) a title" + ]) + end + end + end +end diff --git a/spec/features/publishing_conflicts_spec.rb b/spec/features/publishing_conflicts_spec.rb new file mode 100644 index 0000000..a87bfe8 --- /dev/null +++ b/spec/features/publishing_conflicts_spec.rb @@ -0,0 +1,65 @@ +RSpec.describe "Site generation with publishing conflicts" do + it "fails because of conflicts" do + with_file_contents(testing_dir("_posts/#{Time.now.strftime("%Y-%m-%d")}-test--existing-post"), "title: Existing post\nCreated: 1960-12-31T05:06:07Z\n\nsome existing content") do + expect(Dir[testing_dir("_posts/*")].length).to eq(9) + expect { generate_site }.to_not raise_error + + # collision based on the default permalink config + with_file_contents(testing_dir("_drafts/test--existing-post"), "title: A brand new post\n\nsome existing content") do + expect { generate_site }.to raise_error(TestingSiteGenerationError, "failed to generate site") + expect(Dir[testing_dir("_posts/*")].length).to eq(9) + end + + # collision based on the same, but where the post would be published + with_file_contents(testing_dir("_drafts/test--existing-post"), "title: A brand new post\npublish: now\n\nsome existing content") do + expect { generate_site }.to raise_error(TestingSiteGenerationError, "failed to generate site") + expect(Dir[testing_dir("_posts/*")].length).to eq(9) + end + + # collision based on a permalink value, even though the path is different + with_file_contents(testing_dir("_drafts/test--totally-different-path"), "permalink: /test-blog/test--existing-post\ntitle: A brand new post\npublish: now\n\nsome existing content") do + expect { generate_site }.to raise_error(TestingSiteGenerationError, "failed to generate site") + expect(Dir[testing_dir("_posts/*")].length).to eq(9) + end + end + end + + it "will fail even if the base _config.yml permalink configuration changes" do + begin + FileUtils.mv(testing_dir("_config.yml"), testing_dir("_config.yml.temp")) + + with_file_contents(testing_dir("_config.yml"), "permalink: /:year/:title") do + with_file_contents(testing_dir("_posts/#{Time.now.year}-#{Time.now.strftime("%m-%d")}-test--existing-post"), "title: Existing post\nCreated: 1960-12-31T05:06:07Z\n\nsome existing content") do + with_file_contents(testing_dir("_posts/#{Time.now.year - 1}-#{Time.now.strftime("%m-%d")}-test--existing-post"), "title: Existing post\nCreated: 1960-12-31T05:06:07Z\n\nsome existing content") do + expect { generate_site }.to_not raise_error + + File.open(testing_dir("_config.yml"), "w") { |f| f.puts("permalink: /new-permalink/:title") } + + expect { generate_site }.to raise_error(TestingSiteGenerationError, "failed to generate site") + end + end + end + ensure + FileUtils.mv(testing_dir("_config.yml.temp"), testing_dir("_config.yml")) + end + end + + it "never touches _site/ if there would be a failure" do + generate_site + t1 = File.stat(testing_dir("_site")).ctime + sleep 2 + + generate_site + t2 = File.stat(testing_dir("_site")).ctime + expect(t2 - t1 > 1).to be_truthy + sleep 1 + + with_file_contents(testing_dir("_posts/#{Time.now.strftime("%Y-%m-%d")}-test--existing-post"), "title: Existing post\nCreated: 1960-12-31T05:06:07Z\n\nsome existing content") do + with_file_contents(testing_dir("_drafts/test--existing-post"), "title: A brand new post\npublish: now\n\nsome existing content") do + expect { generate_site }.to raise_error(TestingSiteGenerationError) + + expect(File.stat(testing_dir("_site")).ctime).to eq(t2) + end + end + end +end diff --git a/spec/features/updating_posts_spec.rb b/spec/features/updating_posts_spec.rb new file mode 100644 index 0000000..b91b733 --- /dev/null +++ b/spec/features/updating_posts_spec.rb @@ -0,0 +1,27 @@ +RSpec.describe "Post updating" do + describe "the 'update: now' header" do + it "sets the Updated header value to the current time" do + with_file_contents(testing_dir("_posts/2000-12-20-test--published-post"), "title: Some post title\nCreated: 2000-12-20T14:15:16Z\nupdate: now\n\nchanges made") do + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to_not match(/\nUpdated: #{Time.now.strftime("%Y")}-/) + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to end_with("\nupdate: now") + + generate_site + + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to match(/\nCreated: 2000-12-20T14:15:16Z\nUpdated: #{Time.now.strftime("%Y-%m-%dT")}\d\d:\d\d:\d\d#{Regexp.quote Time.now.xmlschema.split(/\d\d:\d\d:\d\d/).last}\z/) + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to_not include("update: now") + end + end + + it "overrides an existing Updated header value by taking precedence" do + with_file_contents(testing_dir("_posts/2000-12-20-test--published-post"), "title: Some post title\nCreated: 2000-12-20T14:15:16Z\nUpdated: 2001-01-01T14:15:16Z\nupdate: now\n\nchanges made") do + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to_not match(/\nUpdated: #{Time.now.strftime("%Y")}-/) + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to end_with("\nupdate: now") + + generate_site + + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to match(/\nCreated: 2000-12-20T14:15:16Z\nUpdated: #{Time.now.strftime("%Y-%m-%dT")}\d\d:\d\d:\d\d#{Regexp.quote Time.now.xmlschema.split(/\d\d:\d\d:\d\d/).last}\z/) + expect(File.read(testing_dir("_posts/2000-12-20-test--published-post")).split("\n\n").first).to_not include("update: now") + end + end + end +end diff --git a/spec/file_digest_tag_spec.rb b/spec/file_digest_tag_spec.rb deleted file mode 100644 index 487f5b7..0000000 --- a/spec/file_digest_tag_spec.rb +++ /dev/null @@ -1,36 +0,0 @@ -RSpec.describe Serif::FileDigest do - def file_digest(markup) - Serif::FileDigest.new("file_digest", markup, "no tokens needed") - end - - before :each do - site = Serif::Site.new(testing_dir) - @context = { "site" => { "directory" => site.directory }} - end - - describe "#render" do - it "returns the md5 hex digest of the finally deployed site path" do - expect(file_digest("test-stylesheet.css").render(@context)).to eq("f8390232f0c354a871f9ba0ed306163c") - end - - it "ignores leading slashes" do - expect(file_digest("/test-stylesheet.css").render(@context)).to eq("f8390232f0c354a871f9ba0ed306163c") - end - - it "ignores surrounding whitespace" do - expect(file_digest(" test-stylesheet.css ").render(@context)).to eq("f8390232f0c354a871f9ba0ed306163c") - end - - it "includes a prefix if one is specified" do - expect(file_digest("test-stylesheet.css prefix:.").render(@context)).to eq(".f8390232f0c354a871f9ba0ed306163c") - end - - it "ignores trailing whitespace on the prefix" do - expect(file_digest("test-stylesheet.css prefix:. ").render(@context)).to eq(".f8390232f0c354a871f9ba0ed306163c") - end - - it "raises a SyntaxError on invalid syntax" do - expect { file_digest("test-stylesheet.css pefoiejw").render(@context) }.to raise_error(SyntaxError) - end - end -end diff --git a/spec/lib/config_spec.rb b/spec/lib/config_spec.rb new file mode 100644 index 0000000..1a83f89 --- /dev/null +++ b/spec/lib/config_spec.rb @@ -0,0 +1,58 @@ +RSpec.describe Serif::Config do + let(:config_file) { double } + let(:config_hash) { { "x" => 1 } } + + subject(:config) { Serif::Config.new(config_file) } + + before do + allow(YAML).to receive(:load_file).with(config_file).and_return(config_hash) + end + + describe "#permalink" do + its(:permalink) { should eq("/:title") } + + context "with a specific value in the config file" do + let(:config_hash) { { "x" => 1, "permalink" => "/some/format/:title" } } + + its(:permalink) { should eq("/some/format/:title") } + end + end + + describe "#archive_enabled?" do + its(:archive_enabled?) { should be_falsey } + + context "when the config file specifies archived: enabled: true" do + let(:config_hash) { { "x" => 1, "archive" => { "enabled" => true } } } + + its(:archive_enabled?) { should be_truthy } + end + + context "when the config file specifies archived: enabled: false" do + let(:config_hash) { { "x" => 1, "archive" => { "enabled" => false } } } + + its(:archive_enabled?) { should be_falsey } + end + + context "when the config file specifies archived:, but not an enabled value" do + let(:config_hash) { { "x" => 1, "archive" => {} } } + + its(:archive_enabled?) { should be_falsey } + end + end + + describe "#archive_url_format" do + its(:archive_url_format) { should eq("/archive/:year/:month") } + + context "when the config file specifies archived: url_format:" do + let(:config_hash) { { "x" => 1, "archive" => { "url_format" => "/some/format" } } } + + its(:archive_url_format) { should be_truthy } + end + + context "when the config file specifies archived:, but not an url_format value" do + let(:config_hash) { { "x" => 1, "archive" => {} } } + + its(:archive_url_format) { should eq("/archive/:year/:month") } + end + end +end diff --git a/spec/lib/content_file_spec.rb b/spec/lib/content_file_spec.rb new file mode 100644 index 0000000..dd3c5b8 --- /dev/null +++ b/spec/lib/content_file_spec.rb @@ -0,0 +1,17 @@ +RSpec.describe Serif::ContentFile do + let(:site) { double } + let(:file_contents) { "x: 1\n\nsome content" } + let(:file_path) { "some-path" } + + include_examples "a content file" do + let(:expected_liquid_hash) do + { + "content" => "content value", + "slug" => "slug value", + "url" => "url value", + "draft" => "draft? value", + "published" => "published? value" + } + end + end +end diff --git a/spec/lib/draft_spec.rb b/spec/lib/draft_spec.rb new file mode 100644 index 0000000..78535a9 --- /dev/null +++ b/spec/lib/draft_spec.rb @@ -0,0 +1,171 @@ +RSpec.describe Serif::Draft do + let(:site) { double } + let(:file_contents) { "x: 1\n\nsome content" } + let(:file_path) { "some-path" } + + include_examples "a content file" do + let(:expected_liquid_hash) do + { + "content" => "content value", + "slug" => "slug value", + "url" => "url value", + "draft" => "draft? value", + "published" => "published? value" + } + end + end + + describe ".all" do + let(:draft_1) { double } + let(:draft_2) { double } + + before do + allow(site).to receive(:source_path).with("_drafts", "*").and_return("foo") + + allow(File).to receive(:file?).with("path-1").and_return(false) + allow(File).to receive(:file?).with("path-2").and_return(true) + allow(File).to receive(:file?).with("path-3").and_return(false) + allow(File).to receive(:file?).with("path-4").and_return(true) + + allow(Dir).to receive(:[]).with("foo").and_return([ + "path-1", + "path-2", + "path-3", + "path-4" + ]) + + allow(File).to receive(:expand_path) { |x| "expanded-#{x}" } + + allow(Serif::Draft).to receive(:new).with(site, "expanded-path-2").and_return(draft_1) + allow(Serif::Draft).to receive(:new).with(site, "expanded-path-4").and_return(draft_2) + end + + specify { expect(Serif::Draft.all(site)).to eq([draft_1, draft_2]) } + end + + describe "#slug" do + let(:file_path) { "some/path/to/a/file-goes-here" } + + its(:slug) { should eq("file-goes-here") } + end + + describe "#published?" do + its(:published?) { should eq(false) } + end + + describe "#url" do + let(:headers) { {} } + + before do + allow(site).to receive_message_chain(:config, :permalink).and_return("default-permalink/:title") + allow(subject).to receive(:headers).and_return(headers) + allow(subject).to receive(:slug).and_return("file-slug") + end + + its(:url) { should eq("default-permalink/file-slug") } + + context "when there is a specific permalink header value" do + let(:headers) { { permalink: "/foo/bar/:title" } } + + its(:url) { should eq("/foo/bar/file-slug") } + end + + context "with a permalink format that includes a year, month, and day" do + let(:headers) { { permalink: "/foo/:year/bar/:month/baz/:day/xyz/:title" } } + + before do + Timecop.freeze(Time.parse("2003-04-11 13:14:10 UTC")) + end + + its(:url) { should eq("/foo/2003/bar/04/baz/11/xyz/file-slug")} + end + end + + describe "#publish!" do + let(:published_file_conflict) { false } + let(:file_contents) { "x: 1\npublish: existing publish header value\n\nsome content"} + + before do + Timecop.freeze(Time.parse("2004-05-05 13:14:15 UTC")) + + allow(site).to receive(:source_path) { |x| "site source path for #{x}" } + allow(FileUtils).to receive(:mkdir_p).with("site source path for _posts") + allow(subject).to receive(:slug).and_return("file-slug") + allow(File).to receive(:exist?).with("site source path for _posts/2004-05-05-file-slug").and_return(published_file_conflict) + + # save stubbing, so we can verify that source headers are updated + allow(subject).to receive(:save).and_call_original + allow(File).to receive(:open).and_call_original + + yielded_file = double + allow(File).to receive(:open).with(file_path, "w").and_yield(yielded_file) + allow(yielded_file).to receive(:puts) { |new_content| allow(File).to receive(:read).with(file_path).and_return(new_content) } + + allow(FileUtils).to receive(:mv) + end + + it "creates the site's _posts directory" do + subject.publish! + + expect(FileUtils).to have_received(:mkdir_p).with("site source path for _posts") + end + + it "updates the Created header" do + expect { subject.publish! }.to change { subject.headers[:created] }.from(nil).to(Time.at(Time.now.to_i)) + end + + it "removes the Publish header" do + expect { subject.publish! }.to change { subject.headers[:publish] }.from("existing publish header value").to(nil) + end + + it "calls save" do + subject.publish! + + expect(subject).to have_received(:save) + end + + it "moves the file to _posts" do + subject.publish! + + expect(FileUtils).to have_received(:mv).with("some-path", "site source path for _posts/2004-05-05-file-slug") + end + + context "when there is a published file with the same path" do + let(:published_file_conflict) { true } + + specify { expect { subject.publish! }.to raise_error(RuntimeError, "found a conflict when trying to publish 2004-05-05-file-slug: a file with that name exists already") } + end + + describe "#autopublish?" do + let(:headers) { {} } + + before do + allow(subject).to receive(:headers).and_return(headers) + end + + its(:autopublish?) { should be_falsey } + + context "when there is an Update header value" do + let(:headers) { { publish: "some value" } } + + its(:autopublish?) { should be_falsey } + end + + context "when there is an Update header value with a value of 'now'" do + let(:headers) { { publish: "now" } } + + its(:autopublish?) { should be_truthy } + end + + context "when there is an Update header value with a value of ' now '" do + let(:headers) { { publish: " now " } } + + its(:autopublish?) { should be_truthy } + end + end + + describe "#render" do + pending + end + end +end diff --git a/spec/filters_spec.rb b/spec/lib/filters_spec.rb similarity index 60% rename from spec/filters_spec.rb rename to spec/lib/filters_spec.rb index d6ce71c..fd45764 100644 --- a/spec/filters_spec.rb +++ b/spec/lib/filters_spec.rb @@ -1,4 +1,6 @@ RSpec.describe Serif::Filters do + let(:input) { double } + subject do o = Object.new o.extend(Serif::Filters) @@ -7,35 +9,17 @@ describe "#strip" do it "calls strip on its argument" do - double = double("") - expect(double).to receive(:strip).once - subject.strip(double) + allow(input).to receive(:strip).and_return("result") - s = " foo " - expect(subject.strip(s)).to eq(s.strip) - end - end - - describe "#smarty" do - it "runs the input through a SmartyPants processor" do - expect(subject.smarty("Testing")).to eq("Testing") - expect(subject.smarty("Testing's")).to eq("Testing’s") - expect(subject.smarty(%{"Testing" some "text's" input...})).to eq("“Testing” some “text’s” input…") - end - - it "does not do any markdown processing" do - expect(subject.smarty("# Heading")).to eq("# Heading") - expect(subject.smarty("Testing `code blocks` input")).to eq("Testing `code blocks` input") - end - - it "deals with HTML appropriately" do - expect(subject.smarty("

Testing's span testing

")).to eq("

Testing’s span testing

") + expect(subject.strip(input)).to eq("result") end end describe "#encode_uri_component" do it "percent-encodes various characters for use in a URI" do { + # ambiguous cases, tested here to ensure we're talking about ?query=params. + # http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-core/29373 " " => "+", "!" => "%21", "$" => "%24", @@ -64,30 +48,36 @@ end end - describe "#xmlschema" do - it "calls xmlschema on its input" do - d = double("") - expect(d).to receive(:xmlschema).once - subject.xmlschema(d) + describe "#smarty" do + it "runs the input through a smartypants processor" do + expect(subject.smarty("Testing")).to eq("Testing") + expect(subject.smarty("Testing's")).to eq("Testing’s") + expect(subject.smarty(%q{"Testing" some "text's" input...})).to eq("“Testing” some “text’s” input…") + end - t = Time.parse("2012-01-01") - t_utc = Time.utc("2012-01-01") - # -0400 => -04:00 - offset = t.strftime("%z").gsub(/(\d\d)\z/, ':\1') + it "does not do any markdown processing" do + expect(subject.smarty("# Heading")).to eq("# Heading") + expect(subject.smarty("Testing `code blocks` input")).to eq("Testing `code blocks` input") + end - expect(subject.xmlschema(t)).to eq("2012-01-01T00:00:00#{offset}") - expect(subject.xmlschema(t_utc)).to eq("2012-01-01T00:00:00Z") + it "deals with HTML appropriately" do + expect(subject.smarty("

Testing's span testing

")).to eq("

Testing’s span testing

") end end describe "#markdown" do - it "processes its input as markdown" do - # bit of a stub test - expect(subject.markdown("# Hi!").strip).to eq(%{

Hi!

}) + it "converts the input to markdown" do + allow(Serif::Markdown).to receive(:render).with(input).and_return("result") + + expect(subject.markdown(input)).to eq("result") end + end + + describe "#xmlschema" do + it "calls xmlschema on its input" do + allow(input).to receive(:xmlschema).and_return("result") - it "uses curly single quotes properly" do - expect(subject.markdown("# something's test").strip).to eq(%{

something’s test

}) + expect(subject.xmlschema(input)).to eq("result") end end end diff --git a/spec/markup_renderer_spec.rb b/spec/lib/markup_renderer_spec.rb similarity index 77% rename from spec/markup_renderer_spec.rb rename to spec/lib/markup_renderer_spec.rb index 2bf37de..5b68312 100644 --- a/spec/markup_renderer_spec.rb +++ b/spec/lib/markup_renderer_spec.rb @@ -2,7 +2,7 @@ subject { Serif::Markdown } it "renders language-free code blocks correctly" do - expect(subject.render(<This “very” sentence’s structure “isn’t” necessary.

diff --git a/spec/lib/page_spec.rb b/spec/lib/page_spec.rb new file mode 100644 index 0000000..735c11a --- /dev/null +++ b/spec/lib/page_spec.rb @@ -0,0 +1,64 @@ +RSpec.describe Serif::Page do + let(:site) { double } + let(:file_contents) { "header_a: val-1\nheader_b: val-2\n\nthese are file contents" } + + subject { Serif::Page.new(site, "some-file.html") } + + before do + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with("some-file.html").and_return(file_contents) + end + + its(:site) { should eq(site) } + its(:path) { should eq("some-file.html") } + + describe "#render" do + before do + template = double + allow(Liquid::Template).to receive(:parse).with("these are file contents").and_return(template) + allow(template).to receive(:render!).with("site" => site, "page" => hash_including("header_a" => "val-1", "header_b" => "val-2")).and_return("rendered template contents") + + allow(site).to receive(:source_path).with("_layouts", "default.html").and_return("default layout") + allow(File).to receive(:read).with("default layout").and_return("default layout file contents") + default_layout = double + allow(Liquid::Template).to receive(:parse).with("default layout file contents").and_return(default_layout) + + allow(default_layout).to receive(:render!).with( + "site" => site, + "page" => hash_including("header_a" => "val-1", "header_b" => "val-2"), + "content" => "rendered template contents" + ).and_return("final rendered layout") + end + + it "is the fully rendered layout" do + expect(subject.render).to eq("final rendered layout") + end + + context "when the file has a header which specifies the layout is 'none'" do + let(:file_contents) { "header_a: val-1\nheader_b: val-2\nlayout: none\n\nthese are file contents" } + + it "renders only the file" do + expect(subject.render).to eq("rendered template contents") + end + end + + context "when the file specifies a different layout" do + let(:file_contents) { "header_a: val-1\nheader_b: val-2\nlayout: something-else\n\nthese are file contents" } + + before do + allow(site).to receive(:source_path).with("_layouts", "something-else.html").and_return("an alternative layout") + allow(File).to receive(:read).with("an alternative layout").and_return("alternative layout contents") + alternative_layout = double + allow(Liquid::Template).to receive(:parse).with("alternative layout contents").and_return(alternative_layout) + + allow(alternative_layout).to receive(:render!).with( + "site" => site, + "page" => { "header_a" => "val-1", "header_b" => "val-2", "layout" => "something-else" }, + "content" => "rendered template contents" + ).and_return("final rendered alternative layout") + end + + its(:render) { should eq("final rendered alternative layout") } + end + end +end diff --git a/spec/lib/placeholder_spec.rb b/spec/lib/placeholder_spec.rb new file mode 100644 index 0000000..85550cc --- /dev/null +++ b/spec/lib/placeholder_spec.rb @@ -0,0 +1,25 @@ +RSpec.describe Serif::Placeholder do + describe ".substitute" do + specify { expect(Serif::Placeholder.substitute("foo", {})).to eq("foo") } + specify { expect(Serif::Placeholder.substitute(nil, {})).to eq(nil) } + + it "makes substitutions using placeholders given by colons" do + expect(Serif::Placeholder.substitute("foo :bar baz", "bar" => "a new value")).to eq("foo a new value baz") + expect(Serif::Placeholder.substitute("foo:bar baz", "bar" => "a new value")).to eq("fooa new value baz") + expect(Serif::Placeholder.substitute(":bar :bar :bar", "bar" => "123")).to eq("123 123 123") + expect(Serif::Placeholder.substitute("bar :bar bar", "bar" => "123")).to eq("bar 123 bar") + expect(Serif::Placeholder.substitute("bar:barbar", "bar" => "123")).to eq("bar123bar") + expect(Serif::Placeholder.substitute(":x_y_z", "x_y_z" => "123")).to eq("123") + end + + it "does not make any in-place modifications to the input arguments" do + str = "some :foo string" + parts = { "foo" => "1" } + + Serif::Placeholder.substitute(str, parts) + + expect(str).to eq(str) + expect(parts).to eq(parts) + end + end +end diff --git a/spec/lib/post_spec.rb b/spec/lib/post_spec.rb new file mode 100644 index 0000000..3b51cb0 --- /dev/null +++ b/spec/lib/post_spec.rb @@ -0,0 +1,140 @@ +RSpec.describe Serif::Post do + let(:site) { double } + let(:file_contents) { "x: 1\n\nsome content" } + let(:file_path) { "x-y-z-some-path" } + + include_examples "a content file" do + let(:expected_liquid_hash) do + { + "content" => "content value", + "slug" => "slug value", + "url" => "url value", + "draft" => "draft? value", + "published" => "published? value", + "created" => "created value", + "updated" => "updated value" + } + end + end + + describe ".all" do + let(:draft_1) { double } + let(:draft_2) { double } + + before do + allow(site).to receive(:source_path).with("_posts", "*").and_return("foo") + + allow(File).to receive(:file?).with("path-1").and_return(false) + allow(File).to receive(:file?).with("path-2").and_return(true) + allow(File).to receive(:file?).with("path-3").and_return(false) + allow(File).to receive(:file?).with("path-4").and_return(true) + + allow(Dir).to receive(:[]).with("foo").and_return([ + "path-1", + "path-2", + "path-3", + "path-4" + ]) + + allow(File).to receive(:expand_path) { |x| "expanded-#{x}" } + + allow(Serif::Post).to receive(:new).with(site, "expanded-path-2").and_return(draft_1) + allow(Serif::Post).to receive(:new).with(site, "expanded-path-4").and_return(draft_2) + end + + specify { expect(Serif::Post.all(site)).to eq([draft_1, draft_2]) } + end + + describe "#slug" do + let(:file_path) { "some/path/to/a/x-y-z-some-file-content" } + + its(:slug) { should eq("some-file-content") } + end + + describe "#published?" do + its(:published?) { should eq(true) } + end + + describe "#url" do + let(:headers) { {} } + + before do + allow(site).to receive_message_chain(:config, :permalink).and_return("default-permalink/:title") + allow(subject).to receive(:headers).and_return(headers) + allow(subject).to receive(:slug).and_return("file-slug") + end + + its(:url) { should eq("default-permalink/file-slug") } + + context "when there is a specific permalink header value" do + let(:headers) { { permalink: "/foo/bar/:title" } } + + its(:url) { should eq("/foo/bar/file-slug") } + end + + context "with a permalink format that includes a year, month, and day" do + let(:headers) { { permalink: "/foo/:year/bar/:month/baz/:day/somethingsomething/:title" } } + + before do + Timecop.freeze(Time.parse("2003-04-11 13:14:10 UTC")) + end + + its(:url) { should eq("/foo/x/bar/y/baz/z/somethingsomething/file-slug")} + end + end + + describe "#autoupdate?" do + let(:headers) { {} } + + before do + allow(subject).to receive(:headers).and_return(headers) + end + + its(:autoupdate?) { should be_falsey } + + context "when there is an Update header value" do + let(:headers) { { update: "some value" } } + + its(:autoupdate?) { should be_falsey } + end + + context "when there is an Update header value with a value of 'now'" do + let(:headers) { { update: "now" } } + + its(:autoupdate?) { should be_truthy } + end + + context "when there is an Update header value with a value of ' now '" do + let(:headers) { { update: " now " } } + + its(:autoupdate?) { should be_truthy } + end + end + + describe "#update!" do + let(:file_contents) { "update: existing update header\n\nsome post contents" } + + before do + # save stubbing, so we can verify that source headers are updated + allow(subject).to receive(:save).and_call_original + allow(File).to receive(:open).and_call_original + + yielded_file = double + allow(File).to receive(:open).with(file_path, "w").and_yield(yielded_file) + allow(yielded_file).to receive(:puts) { |new_content| allow(File).to receive(:read).with(file_path).and_return(new_content) } + end + + it "calls save" do + subject.update! + expect(subject).to have_received(:save) + end + + it "deletes the existing Update header" do + expect { subject.update! }.to change { subject.headers[:update] }.from("existing update header").to(nil) + end + end + + describe "#render" do + pending + end +end diff --git a/spec/lib/site_spec.rb b/spec/lib/site_spec.rb new file mode 100644 index 0000000..274d2b6 --- /dev/null +++ b/spec/lib/site_spec.rb @@ -0,0 +1,26 @@ +RSpec.describe Serif::Site do + subject { Serif::Site.new("a/source/directory") } + + describe "#latest_update_time" do + let(:posts) do + [ + double(updated: 2), + double(updated: 1), + double(updated: 4), + double(updated: 3) + ] + end + + before do + allow(subject).to receive(:posts).and_return(posts) + end + + its(:latest_update_time) { should eq(4) } + + context "when there are no posts" do + let(:posts) { [] } + + its(:latest_update_time) { should be_within(1).of(Time.now) } + end + end +end diff --git a/spec/post_spec.rb b/spec/post_spec.rb deleted file mode 100644 index 4ad71fa..0000000 --- a/spec/post_spec.rb +++ /dev/null @@ -1,137 +0,0 @@ -RSpec.describe Serif::Post do - subject do - Serif::Site.new(testing_dir) - end - - before :each do - @posts = subject.posts - end - - around :each do |example| - begin - d = Serif::Draft.new(subject) - d.slug = "foo-bar-bar-temp" - d.title = "Testing title" - d.save("# some content") - d.publish! - @temporary_post = Serif::Post.new(subject, d.path) - - example.run - ensure - FileUtils.rm(@temporary_post.path) - end - end - - describe "#from_basename" do - it "is nil if there is nothing found" do - expect(Serif::Post.from_basename(subject, "eoijfwoifjweofej")).to be_nil - end - - it "takes full filename within _posts" do - expect(Serif::Post.from_basename(subject, @temporary_post.basename).path).to eq(@temporary_post.path) - end - end - - it "uses the config file's permalink value" do - expect(@posts.all? { |p| p.url == "/test-blog/#{p.slug}" }).to be_truthy - end - - describe "#inspect" do - it "includes headers" do - @posts.all? { |p| expect(p.inspect).to include(p.headers.inspect) } - end - end - - describe "#autoupdate=" do - it "sets the 'update' header to 'now' if truthy assigned value" do - @temporary_post.autoupdate = true - expect(@temporary_post.headers[:update]).to eq("now") - end - - it "removes the 'update' header entirely if falsey assigned value" do - @temporary_post.autoupdate = false - expect(@temporary_post.headers.key?(:update)).to be_falsey - end - - it "marks the post as autoupdate? == true" do - expect(@temporary_post.autoupdate?).to be_falsey - @temporary_post.autoupdate = true - expect(@temporary_post.autoupdate?).to be_truthy - end - end - - describe "#autoupdate?" do - it "returns true if there is an update: now header" do - allow(@temporary_post).to receive(:headers) { { :update => "foo" } } - expect(@temporary_post.autoupdate?).to be_falsey - allow(@temporary_post).to receive(:headers) { { :update => "now" } } - expect(@temporary_post.autoupdate?).to be_truthy - end - - it "is ignorant of whitespace in the update header value" do - allow(@temporary_post).to receive(:headers) { { :update => "now" } } - expect(@temporary_post.autoupdate?).to be_truthy - - (1..3).each do |left| - (1..3).each do |right| - allow(@temporary_post).to receive(:headers) { { :update => "#{" " * left}now#{" " * right}"} } - expect(@temporary_post.autoupdate?).to be_truthy - end - end - end - end - - describe "#update!" do - it "sets the updated header timestamp to the current time" do - old_update_time = @temporary_post.updated - t = Time.now + 50 - - Timecop.freeze(t) do - @temporary_post.update! - expect(@temporary_post.updated).not_to eq(old_update_time) - expect(@temporary_post.updated.to_i).to eq(t.to_i) - expect(@temporary_post.headers[:updated].to_i).to eq(t.to_i) - end - end - - it "calls save and writes out the new timestamp value, without a publish: now header" do - expect(@temporary_post).to receive(:save).once.and_call_original - - t = Time.now + 50 - Timecop.freeze(t) do - @temporary_post.update! - - file_content = Redhead::String[File.read(@temporary_post.path)] - expect(Time.parse(file_content.headers[:updated].value).to_i).to eq(t.to_i) - expect(file_content.headers[:publish]).to be_nil - end - end - - it "marks the post as no longer auto-updating" do - expect(@temporary_post.autoupdate?).to be_falsey - @temporary_post.autoupdate = true - expect(@temporary_post.autoupdate?).to be_truthy - @temporary_post.update! - expect(@temporary_post.autoupdate?).to be_falsey - end - end - - describe "#to_liquid" do - it "contains the relevant keys" do - liq = subject.posts.sample.to_liquid - - ["title", - "created", - "updated", - "content", - "slug", - "url", - "type", - "draft", - "published", - "basename"].each do |e| - expect(liq.key?(e)).to be_truthy - end - end - end -end diff --git a/spec/site_dir/_config.yml b/spec/site_dir/_config.yml deleted file mode 100644 index ba82b16..0000000 --- a/spec/site_dir/_config.yml +++ /dev/null @@ -1,18 +0,0 @@ -# This is the Serif config file. It must be a valid YAML document. -# -# Some configuration options: -# -# admin: -# username: [a username for the admin web interface] -# password: [a password for the admin web interface] -# permalink: [permalink format for generated posts] -# -# See the README for information on permalink formats. - -admin: - username: test-changethisusername - password: test-changethispassword -permalink: /test-blog/:title -archive: - enabled: yes - url_format: /test-archive/:year/:month diff --git a/spec/site_dir/_drafts/another-sample-draft b/spec/site_dir/_drafts/another-sample-draft deleted file mode 100644 index 84f8c2a..0000000 --- a/spec/site_dir/_drafts/another-sample-draft +++ /dev/null @@ -1,3 +0,0 @@ -title: another sample draft - -another-sample-draft diff --git a/spec/site_dir/_layouts/default.html b/spec/site_dir/_layouts/default.html deleted file mode 100644 index 31cb2c0..0000000 --- a/spec/site_dir/_layouts/default.html +++ /dev/null @@ -1,8 +0,0 @@ - - -My site: {% if page.title and page.title != empty %}{{ page.title | join:" - " }}{% endif %} -

mysite.com

- -{% if post_page %}

post_page flag set for layout

{% endif %} - -{{ content }} diff --git a/spec/site_dir/_posts/2012-01-05-sample-post b/spec/site_dir/_posts/2012-01-05-sample-post deleted file mode 100644 index 42d923f..0000000 --- a/spec/site_dir/_posts/2012-01-05-sample-post +++ /dev/null @@ -1,4 +0,0 @@ -title: Sample post -Created: 2012-11-21T17:07:09+00:00 - -Just a sample post. diff --git a/spec/site_dir/_posts/2013-01-01-second-post b/spec/site_dir/_posts/2013-01-01-second-post deleted file mode 100644 index a9d3d70..0000000 --- a/spec/site_dir/_posts/2013-01-01-second-post +++ /dev/null @@ -1,4 +0,0 @@ -title: Second post -Created: 2013-01-01T00:00:00+00:00 - -Second post. diff --git a/spec/site_dir/_posts/2013-03-07-post-with-custom-layout b/spec/site_dir/_posts/2013-03-07-post-with-custom-layout deleted file mode 100644 index 42d0e7a..0000000 --- a/spec/site_dir/_posts/2013-03-07-post-with-custom-layout +++ /dev/null @@ -1,5 +0,0 @@ -title: Custom layout -Created: 2013-03-07T00:00:00+00:00 -layout: alt-layout - -Second post. diff --git a/spec/site_dir/_posts/2399-01-01-penultimate-post b/spec/site_dir/_posts/2399-01-01-penultimate-post deleted file mode 100644 index d362491..0000000 --- a/spec/site_dir/_posts/2399-01-01-penultimate-post +++ /dev/null @@ -1,4 +0,0 @@ -title: Penultimate post -Created: 2399-01-01T00:00:00+00:00 - -Penultimate post diff --git a/spec/site_dir/_posts/2400-01-01-final-post b/spec/site_dir/_posts/2400-01-01-final-post deleted file mode 100644 index 2139f8e..0000000 --- a/spec/site_dir/_posts/2400-01-01-final-post +++ /dev/null @@ -1,4 +0,0 @@ -title: Final post -Created: 2400-01-01T00:00:00+00:00 - -The final post in the blog diff --git a/spec/site_dir/page-alt-layout.html b/spec/site_dir/page-alt-layout.html deleted file mode 100644 index 0dfde39..0000000 --- a/spec/site_dir/page-alt-layout.html +++ /dev/null @@ -1,3 +0,0 @@ -layout: alt-layout - -page alt layout diff --git a/spec/site_dir/test-smarty-filter.html b/spec/site_dir/test-smarty-filter.html deleted file mode 100644 index ddf897e..0000000 --- a/spec/site_dir/test-smarty-filter.html +++ /dev/null @@ -1,3 +0,0 @@ -Some content - -{{ "testing's for a " | append: '"' | append: "heading's" | append: '"' | append: " `with code` in it..." | smarty }} diff --git a/spec/site_generation_spec.rb b/spec/site_generation_spec.rb deleted file mode 100644 index 1ee9497..0000000 --- a/spec/site_generation_spec.rb +++ /dev/null @@ -1,202 +0,0 @@ -RSpec.describe Serif::Site do - subject do - Serif::Site.new(testing_dir) - end - - before(:each) do - FileUtils.rm_rf(testing_dir("_site")) - end - - describe "site generation" do - it "raises PostConflictError if there are conflicts" do - # not nil, the value is unimportant - allow(subject).to receive(:conflicts) { [] } - expect { capture_stdout { subject.generate } }.to raise_error(Serif::PostConflictError) - end - - it "uses the permalinks in the config file for site generation" do - capture_stdout { subject.generate } - expect(File.exist?(testing_dir("_site/test-blog/sample-post.html"))).to be_truthy - end - - it "reads the layout header for a non-post file and uses the appropriate layout file" do - capture_stdout { subject.generate } - - # check it actually got generated - expect(File.exist?(testing_dir("_site/page-alt-layout.html"))).to be_truthy - expect(File.read("_site/page-alt-layout.html").lines.first).to match(/Alternate layout<\/h1>/) - end - - it "reads the layout header for a post file and uses the appropriate layout file" do - capture_stdout { subject.generate } - - # check it actually got generated - expect(File.exist?(testing_dir("_site/test-blog/post-with-custom-layout.html"))).to be_truthy - expect(File.read("_site/test-blog/post-with-custom-layout.html").lines.first).to match(/Alternate layout<\/h1>/) - end - - it "supports a smarty filter" do - capture_stdout { subject.generate } - expect(File.read("_site/test-smarty-filter.html")).to match(/testing’s for a “heading’s” `with code` in it…/) - end - - it "correctly handles file_digest calls" do - capture_stdout { subject.generate } - - expect(File.read("_site/file-digest-test.html").strip).to eq("f8390232f0c354a871f9ba0ed306163c\n.f8390232f0c354a871f9ba0ed306163c") - end - - it "makes the previous and next posts available" do - capture_stdout { subject.generate } - - contents = File.read("_site/test-blog/sample-post.html") - previous_title = contents[/^Previous post: .+?$/] - next_title = contents[/^Next post: .+?$/] - - expect(previous_title).to be_nil - expect(next_title).not_to be_nil - expect(next_title[/(?<=: ).+/]).to eq("Second post") - - contents = File.read("_site/test-blog/final-post.html") - previous_title = contents[/Previous post: .+?$/] - next_title = contents[/Next post: .+?$/] - - expect(previous_title).not_to be_nil - expect(next_title).to be_nil - expect(previous_title[/(?<=: ).+/]).to eq("Penultimate post") - end - - it "sets a draft_preview flag for preview urls" do - preview_flag_pattern = /draftpreviewflagexists/ - - capture_stdout { subject.generate } - - d = Serif::Draft.from_slug(subject, "sample-draft") - preview_contents = File.read(testing_dir("_site/#{subject.private_url(d)}.html")) - expect(preview_contents =~ preview_flag_pattern).to be_truthy - - # does not exist on live published pages - expect(File.read(testing_dir("_site/test-blog/second-post.html")) =~ preview_flag_pattern).to be_falsey - end - - it "sets a post_page flag for regular posts" do - capture_stdout { subject.generate } - d = Serif::Post.from_basename(subject, "2013-01-01-second-post") - expect(d).not_to be_nil - contents = File.read(testing_dir("_site#{d.url}.html")) - - # available to the post layout file - expect(contents =~ /post_page flag set for template/).to be_truthy - - # available in the layout file itself - expect(contents =~ /post_page flag set for layout/).to be_truthy - - # not set for regular pages - expect(File.read(testing_dir("_site/index.html")) =~ /post_page flag set for template/).to be_falsey - expect(File.read(testing_dir("_site/index.html")) =~ /post_page flag set for layout/).to be_falsey - - # not set for drafts - d = Serif::Draft.from_slug(subject, "sample-draft") - preview_contents = File.read(testing_dir("_site/#{subject.private_url(d)}.html")) - expect(preview_contents =~ /post_page flag set for template/).to be_falsey - expect(preview_contents =~ /post_page flag set for layout/).to be_falsey - end - - it "creates draft preview files" do - capture_stdout { subject.generate } - - expect(Dir.exist?(testing_dir("_site/drafts"))).to be_truthy - expect(Dir[File.join(testing_dir("_site/drafts/*"))].size).to eq(subject.drafts.size) - - expect(Dir.exist?(testing_dir("_site/drafts/sample-draft"))).to be_truthy - expect(Dir[File.join(testing_dir("_site/drafts/sample-draft"), "*.html")].size).to eq(1) - - d = Serif::Draft.from_slug(subject, "sample-draft") - expect(subject.private_url(d)).not_to be_nil - - # absolute paths - expect(subject.private_url(d) =~ /\A\/drafts\/#{d.slug}\/.*\z/).to be_truthy - - # 60 characters long (30 bytes as hex chars) - expect(subject.private_url(d) =~ /\A\/drafts\/#{d.slug}\/[a-z0-9]{60}\z/).to be_truthy - - # does not create more than one - capture_stdout { subject.generate } - expect(Dir[File.join(testing_dir("_site/drafts/sample-draft"), "*.html")].size).to eq(1) - end - - context "for posts with an update: now header" do - around :each do |example| - begin - d = Serif::Draft.new(subject) - d.slug = "post-to-be-auto-updated" - d.title = "Testing title" - d.save("# some content") - d.publish! - - @temporary_post = Serif::Post.new(subject, d.path) - @temporary_post.autoupdate = true - @temporary_post.save - - example.run - ensure - FileUtils.rm(@temporary_post.path) - end - end - - it "sets the updated header to the current time" do - t = Time.now + 30 - Timecop.freeze(t) do - capture_stdout { subject.generate } - expect(Serif::Post.from_basename(subject, @temporary_post.basename).updated.to_i).to eq(t.to_i) - end - end - end - - context "for drafts with a publish: now header" do - before :each do - @time = Time.utc(2012, 12, 21, 15, 30, 00) - - draft = Serif::Draft.new(subject) - draft.slug = "post-to-be-published-on-generate" - draft.title = "Some draft title" - draft.autopublish = true - draft.save("some content") - - @post = Serif::Draft.from_slug(subject, draft.slug) - expect(@post).not_to be_nil - - # verifies that the header has actually been written to the file, since - # we round-trip the save and load. - expect(@post.autopublish?).to be_truthy - - # Site#generate creates a backup of the site directory in /tmp - # and uses a timestamp, which is now fixed across all tests, - # so we have to remove it first. - FileUtils.rm_rf("/tmp/_site.2012-12-21-15-30-00") - - Timecop.freeze(@time) - end - - after :each do - Timecop.return - - # the generate processes creates its own set of instances, and we're - # publishing a draft marked as autopublish, so our @post instance - # has a #path value which is for the draft, not for the newly published - # post. thus, we need to clobber. - FileUtils.rm(*Dir[testing_dir("_posts/*-#{@post.slug}")]) - end - - it "places the file in the published posts folder" do - capture_stdout { subject.generate } - expect(File.exist?(testing_dir("_site/test-blog/#{@post.slug}.html"))).to be_truthy - end - - it "marks the creation time as the current time" do - capture_stdout { subject.generate } - expect(subject.posts.find { |p| p.slug == @post.slug }.created.to_i).to eq(@time.to_i) - end - end - end -end diff --git a/spec/site_spec.rb b/spec/site_spec.rb deleted file mode 100644 index 4f2e986..0000000 --- a/spec/site_spec.rb +++ /dev/null @@ -1,187 +0,0 @@ -RSpec.describe Serif::Site do - subject do - Serif::Site.new(testing_dir) - end - - describe "#conflicts" do - context "with no arguments" do - it "is nil if there are no conflicts" do - expect(subject.conflicts).to be_nil - end - - it "is a map of url => conflicts_array if there are conflicts" do - d = Serif::Draft.new(subject) - conflicting_post = subject.posts.first - d.slug = conflicting_post.slug - d.title = "Anything you like" - d.save("# Some content") - - # need this to be true - expect(d.url).to eq(conflicting_post.url) - - begin - conflicts = subject.conflicts - expect(conflicts).not_to be_nil - expect(conflicts.class).to eq(Hash) - expect(conflicts.size).to eq(1) - expect(conflicts.keys).to eq([conflicting_post.url]) - expect(conflicts[conflicting_post.url].size).to eq(2) - ensure - FileUtils.rm(d.path) - end - end - end - - context "with an argument given" do - it "is nil if there are no conflicts" do - expect(subject.conflicts(subject.drafts.sample)).to be_nil - expect(subject.conflicts(subject.posts.sample)).to be_nil - - d = Serif::Draft.new(subject) - expect(subject.conflicts(d)).to be_nil - end - - it "is an array of conflicting content if there are conflicts" do - d = Serif::Draft.new(subject) - conflicting_post = subject.posts.first - d.slug = conflicting_post.slug - d.title = "Anything you like" - d.save("# Some content") - - # need this to be true - expect(d.url).to eq(conflicting_post.url) - - begin - conflicts = subject.conflicts(d) - expect(conflicts).not_to be_nil - expect(conflicts.class).to eq(Array) - expect(conflicts.size).to eq(2) - conflicts.each do |e| - expect(e.url).to eq(conflicting_post.url) - end - ensure - FileUtils.rm(d.path) - end - end - end - end - - describe "#source_directory" do - it "should be sane" do - expect(subject.directory).to eq(File.join(File.dirname(__FILE__), "site_dir")) - end - end - - describe "#posts" do - it "is the number of posts in the site" do - expect(subject.posts.length).to eq(5) - end - end - - describe "#drafts" do - it "is the number of drafts in the site" do - expect(subject.drafts.length).to eq(2) - end - end - - describe "#private_url" do - it "returns nil for a draft without an existing file" do - d = double("") - allow(d).to receive(:slug) { "foo" } - expect(subject.private_url(d)).to be_nil - end - end - - describe "#latest_update_time" do - it "is the latest time that a post was updated" do - expect(subject.latest_update_time).to eq(Serif::Post.all(subject).max_by { |p| p.updated }.updated) - end - end - - describe "#site_path" do - it "should be relative, not absolute" do - p = Pathname.new(subject.site_path("foo")) - expect(p.relative?).to be_truthy - expect(p.absolute?).to be_falsey - end - - it "takes a string and prepends _site to that path" do - %w[a b c d e f].each do |e| - expect(subject.site_path(e)).to eq("_site/#{e}") - end - end - end - - describe "#config" do - it "is a Serif::Config instance" do - expect(subject.config.class).to eq(Serif::Config) - end - - it "should have the permalink format available" do - expect(subject.config.permalink).not_to be_nil - end - end - - describe "#archives" do - it "contains posts given in reverse chronological order" do - archives = subject.archives - archives[:posts].each_cons(2) do |a, b| - expect(a.created >= b.created).to be_truthy - end - - archives[:years].each do |year| - year[:posts].each_cons(2) do |a, b| - expect(a.created >= b.created).to be_truthy - end - - year[:months].each do |month| - month[:posts].each_cons(2) do |a, b| - expect(a.created >= b.created).to be_truthy - end - end - end - end - end - - describe "#to_liquid" do - it "uses the value of #archives without modification" do - expect(subject).to receive(:archives).once - subject.to_liquid - end - end - - describe "#archive_url_for_date" do - it "uses the archive URL format from the config to construct an archive URL string" do - date = Date.parse("2012-01-02") - expect(subject.archive_url_for_date(date)).to eq("/test-archive/2012/01") - end - end - - describe "#bypass?" do - it "is false if the filename has a .html extension" do - expect(subject.bypass?("foo.html")).to be_falsey - end - - it "is false if the filename has an .xml extension" do - expect(subject.bypass?("foo.xml")).to be_falsey - end - - it "is true if the filename is neither xml nor html by extension" do - expect(subject.bypass?("foo.css")).to be_truthy - end - end - - describe "#tmp_path" do - it "takes a string and prepends tmp/_site to that path" do - %w[a b c d].each do |e| - expect(subject.tmp_path(e)).to eq("tmp/_site/#{e}") - end - end - - it "should be relative, not absolute" do - p = Pathname.new(subject.tmp_path("foo")) - expect(p.absolute?).to be_falsey - expect(p.relative?).to be_truthy - end - end -end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 36e1af6..acb1795 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -3,7 +3,11 @@ # if we're running on Travis, use Coveralls, otherwise # let us generate SimpleCov output as normal. -if ENV["CI"] +# +# Note that we only do this if USE_SHELL is off, because +# otherwise the coverage can't capture lines executed by +# shelling out to bin/serif. +if ENV["CI"] && ENV["USE_SHELL"] == "no" require "coveralls" SimpleCov.formatter = Coveralls::SimpleCov::Formatter end @@ -12,50 +16,20 @@ add_filter "/spec/" end -# run tests in production mode so that file digests are enabled -ENV["ENV"] = "production" - +require "nokogiri" +require "rspec" +require "rspec/its" require "pry-byebug" +require "timecop" require "serif" require "serif/commands" -require "fileutils" -require "pathname" -require "time" -require "date" -require "timecop" -require "turnip/rspec" -require "turnip/capybara" -require 'capybara/poltergeist' -Capybara.configure do |config| - config.javascript_driver = :poltergeist -end -Dir[File.join(File.dirname(__FILE__), "support", "*")].each do |f| - require f -end +ENV["USE_SHELL"] ||= "no" -Dir[File.join(File.dirname(__FILE__), "acceptance", "macros", "*")].each do |f| +Dir[File.join(File.dirname(__FILE__), "support", "**/*.rb")].each do |f| require f end -def testing_dir(path = nil) - full_path = File.join(File.dirname(__FILE__), "site_dir") - - path ? File.join(full_path, path) : full_path -end - -def capture_stdout - begin - $orig_stdout = $stdout - $stdout = StringIO.new - yield - $stdout.rewind - return $stdout.string - ensure - $stdout = $orig_stdout - end -end - RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest @@ -130,19 +104,11 @@ def capture_stdout # as the one that triggered the failure. Kernel.srand config.seed - # TODO: Stop doing FileUtils.cd at runtime to avoid the need for this. - config.around :each do |example| - root_dir = File.expand_path(File.join(__FILE__, "..", "..")) - - FileUtils.cd root_dir - example.run - FileUtils.cd root_dir + config.after(:suite) do + FileUtils.rm_rf testing_dir("_site") end - config.before :suite do - FileUtils.cd testing_dir - require "serif/admin_server" - Capybara.app = Serif::AdminServer::AdminApp - FileUtils.cd File.expand_path(File.join(__FILE__, "..", "..")) + config.after(:each) do + Timecop.return end end diff --git a/spec/support/auto_load_turnip.rb b/spec/support/auto_load_turnip.rb deleted file mode 100644 index d727635..0000000 --- a/spec/support/auto_load_turnip.rb +++ /dev/null @@ -1,21 +0,0 @@ -RSpec.configure do |config| - config.before(turnip: true) do - example = Turnip::RSpec.fetch_current_example(self) - feature_file = example.metadata[:file_path] - - turnip_file_path = Pathname.new(feature_file).realpath - - # sadly Dir.pwd might have changed because of aprescott/serif#71, so we need - # to find the equivalent of Rails.root - root_app_folder = Pathname.new(Dir.pwd) - root_app_folder = root_app_folder.parent until root_app_folder.children(false).map(&:to_s).include?("serif.gemspec") - - root_acceptance_folder = root_app_folder.join("spec", "acceptance") - - default_steps_file = root_acceptance_folder + turnip_file_path.relative_path_from(root_acceptance_folder).to_s.gsub(/^features/, "steps").gsub(/\.feature$/, "_steps.rb") - default_steps_module = [turnip_file_path.basename.to_s.sub(".feature", "").split("_").collect(&:capitalize), "Steps"].join - - require default_steps_file.to_s - extend Module.const_get(default_steps_module) - end -end diff --git a/spec/support/shared_examples/it_behaves_like_a_content_file.rb b/spec/support/shared_examples/it_behaves_like_a_content_file.rb new file mode 100644 index 0000000..8345455 --- /dev/null +++ b/spec/support/shared_examples/it_behaves_like_a_content_file.rb @@ -0,0 +1,225 @@ +RSpec.shared_examples "a content file" do + before do + allow(File).to receive(:read).and_call_original + allow(File).to receive(:read).with(file_path).and_return(file_contents) + end + + subject { described_class.new(site, file_path) } + + its(:site) { should eq(site) } + its(:path) { should eq(file_path) } + + describe "#initialize" do + it "errors if an argument is missing" do + expect { described_class.new(nil, nil).to raise_error(ArgumentError, "must provide both site and path") } + expect { described_class.new("value", nil).to raise_error(ArgumentError, "must provide both site and path") } + expect { described_class.new(nil, "value").to raise_error(ArgumentError, "must provide both site and path") } + end + + it "loads the contents of the file" do + subject + + expect(File).to have_received(:read).with(file_path) + end + end + + describe "#draft?" do + let(:is_published) { false } + + before do + allow(subject).to receive(:published?).and_return(is_published) + end + + its(:draft?) { should be_truthy } + + context "when the published? value is true" do + let(:is_published) { true } + + its(:draft?) { should be_falsey } + end + end + + describe "#title" do + let(:headers) { {} } + + before do + allow(subject).to receive(:headers).and_return(headers) + end + + its(:title) { should be_nil } + + context "when the headers have a title" do + let(:headers) { { x: 1, title: "some title" } } + + its(:title) { should eq("some title") } + end + end + + describe "#content" do + its(:content) { should eq("some content") } + + context "when the file has no content" do + let(:file_contents) { "" } + + its(:content) { should eq("") } + end + + context "when the file has only headers" do + let(:file_contents) { "x: 1 "} + + its(:content) { should eq("") } + + context "and the headers are followed by the header separator" do + its(:content) { should eq("") } + end + end + end + + describe "#created" do + let(:headers) { {} } + + before do + allow(subject).to receive(:headers).and_return(headers) + end + + its(:created) { should be_nil } + + context "when the headers have a Created value" do + let(:time) { Time.parse("2015-01-01 20:00:00 +0500") } + let(:headers) { { x: 1, created: time } } + + its(:created) { should eq(time) } + + it "is always in UTC" do + expect(subject.created.zone).to eq("UTC") + end + end + end + + describe "#updated" do + let(:headers) { { x: 1 } } + + before do + allow(subject).to receive(:headers).and_return(headers) + end + + its(:updated) { should be_nil } + + context "when the headers have a Updated value" do + let(:time) { Time.parse("2015-01-01 20:00:00 +0500") } + let(:headers) { { x: 1, updated: time } } + + its(:updated) { should eq(time) } + + it "is always in UTC" do + expect(subject.updated.zone).to eq("UTC") + end + end + + context "when there is no Updated header value but there is a Created value" do + let(:created_time) { Time.parse("2015-01-01 20:00:00 +0500") } + let(:headers) { { x: 1 } } + + before do + allow(subject).to receive(:created).and_return(created_time) + end + + its(:updated) { should eq(created_time) } + end + + context "when both an Updated header value and a Created time are given" do + let(:created_time) { Time.parse("2015-01-01 20:00:00 +0500") } + let(:updated_time) { Time.parse("2200-01-01 20:00:00 +0500") } + let(:headers) { { x: 1, updated: updated_time } } + + before do + allow(subject).to receive(:created).and_return(created_time) + end + + its(:updated) { should eq(updated_time) } + end + end + + describe "#headers" do + its(:headers) { should eq(x: "1") } + + context "with a variety of headers" do + let(:file_contents) { "x: 1\ntitle: some title\nsomething else: 123\n hello : there" } + + its(:headers) { should eq(x: "1", title: "some title", :something_else => "123", _hello: "there") } + end + + context "when the header name is 'created'" do + let(:file_contents) { "created: 2015-01-01 12:30:45 +0500" } + + its(:headers) { should eq(created: Time.parse("2015-01-01 12:30:45 +0500")) } + end + + context "when the header name is 'updated'" do + let(:file_contents) { "updated: 2015-01-01 12:30:45 +0500" } + + its(:headers) { should eq(updated: Time.parse("2015-01-01 12:30:45 +0500")) } + end + + context "when the header's capitalization is not all-lowercase" do + let(:file_contents) { "X: 1\nyYy: 2\nZZZZZ: 3" } + + its(:headers) { should eq(x: "1", yyy: "2", zzzzz: "3") } + end + end + + describe "#save" do + before do + Timecop.freeze + allow(File).to receive(:open).and_call_original + + yielded_file = double + allow(File).to receive(:open).with(file_path, "w").and_yield(yielded_file) + allow(yielded_file).to receive(:puts) { |new_content| allow(File).to receive(:read).with(file_path).and_return(new_content) } + end + + it "sets the header's Update: time to the current time" do + expect { subject.save }.to change { subject.headers[:updated] }.from(nil).to(Time.at(Time.now.to_i)) + end + end + + describe "#to_liquid" do + let(:headers) { { x: 1, y: 2 } } + + before do + allow(subject).to receive(:headers).and_return(headers) + allow(subject).to receive(:content).and_return("content value") + allow(subject).to receive(:slug).and_return("slug value") + allow(subject).to receive(:url).and_return("url value") + allow(subject).to receive(:draft?).and_return("draft? value") + allow(subject).to receive(:published?).and_return("published? value") + + # only applies to posts, but doesn't hurt the test for drafts + allow(subject).to receive(:created).and_return("created value") + allow(subject).to receive(:updated).and_return("updated value") + end + + its(:to_liquid) { should eq({ "x" => 1, "y" => 2 }.merge(expected_liquid_hash)) } + + context "when there is a header value which conflicts with one of the defined liquid keys" do + let(:headers) { + h = { + "x" => "header's 1", + "y" => "header's 2", + "content" => "header's content value", + "slug" => "header's slug value", + "url" => "header's url value", + "draft" => "header's draft? value", + "published" => "header's published? value" + } + + h["created"] = "header's created value" if expected_liquid_hash.key?("created") + h["updated"] = "header's updated value" if expected_liquid_hash.key?("updated") + + h + } + + its(:to_liquid) { should eq({ "x" => "header's 1", "y" => "header's 2" }.merge(expected_liquid_hash)) } + end + end +end diff --git a/spec/support/test_site_helpers.rb b/spec/support/test_site_helpers.rb new file mode 100644 index 0000000..8749588 --- /dev/null +++ b/spec/support/test_site_helpers.rb @@ -0,0 +1,85 @@ +class TestingSiteGenerationError < StandardError; end + +def testing_dir(path = nil) + full_path = File.expand_path(File.join(File.dirname(File.expand_path(__FILE__)), "..", "test_source")) + + path = path ? File.join(full_path, path) : full_path + + File.expand_path(path) +end + +def capture_stdout + if ENV["FULL_STDOUT"] == "1" + yield + else + begin + $orig_stdout = $stdout + $stdout = StringIO.new + result = yield + $stdout.rewind + $stdout.string + + result + ensure + $stdout = $orig_stdout + end + end +end + +def serif_bin + root_path = File.expand_path("../../..", __FILE__) + serif_bin = File.join(root_path, "bin/serif") +end + +def generate_site(env: "production") + root_path = File.expand_path("../../..", __FILE__) + + if ENV["USE_SHELL"] == "no" + Serif::Commands.class_eval do + define_method(:exit) do |code| + if code > 0 + raise TestingSiteGenerationError, "failed to generate site" + end + end + end + + ENV["ENV"] = env + capture_stdout { Dir.chdir(File.join(root_path, "spec/test_source")) { Serif::Commands.new(["generate"]).process } } + else + system("cd #{File.join(root_path, "spec/test_source")} && ENV=#{env} #{serif_bin} generate > /dev/null") || raise(TestingSiteGenerationError, "failed to generate site") + end +end + +def create_new_site(directory) + if ENV["USE_SHELL"] == "no" + Serif::Commands.class_eval do + define_method(:exit) do |code| + if code > 0 + raise TestingSiteGenerationError, "failed to generate site" + end + end + end + + FileUtils.mkdir(directory) + + Dir.chdir(directory) do + capture_stdout { Serif::Commands.new(["new"]).process } + end + else + system("cd #{File.dirname(directory)} && mkdir #{File.basename(directory)} && cd #{File.basename(directory)} && #{serif_bin} new > /dev/null") || raise(TestingSiteGenerationError, "failed to create new site") + end +end + +def with_file_contents(path, contents, removal_path: nil, &block) + raise "refusing to modify existing file: #{path}" if File.exist?(path) + + begin + File.open(path, "w") do |f| + f.puts contents + end + + block.call + ensure + FileUtils.rm(removal_path || path) + end +end diff --git a/spec/test_source/_config.yml b/spec/test_source/_config.yml new file mode 100644 index 0000000..2a474df --- /dev/null +++ b/spec/test_source/_config.yml @@ -0,0 +1,4 @@ +permalink: /test-blog/:title +archive: + enabled: yes + url_format: /test-archive/:year/:month diff --git a/spec/test_source/_drafts/test--drafts-get-a-preview b/spec/test_source/_drafts/test--drafts-get-a-preview new file mode 100644 index 0000000..7a55f9b --- /dev/null +++ b/spec/test_source/_drafts/test--drafts-get-a-preview @@ -0,0 +1 @@ +This is a draft that should appear as part of a preview file. diff --git a/spec/test_source/_drafts/test--drafts-get-draft-preview-true b/spec/test_source/_drafts/test--drafts-get-draft-preview-true new file mode 100644 index 0000000..4fb310a --- /dev/null +++ b/spec/test_source/_drafts/test--drafts-get-draft-preview-true @@ -0,0 +1 @@ +This draft file is used to test that the draft_preview flag is set correctly. diff --git a/spec/test_source/_drafts/test--drafts-get-post-page-flag-false b/spec/test_source/_drafts/test--drafts-get-post-page-flag-false new file mode 100644 index 0000000..589f124 --- /dev/null +++ b/spec/test_source/_drafts/test--drafts-get-post-page-flag-false @@ -0,0 +1 @@ +This draft file should be rendered in a preview with the post_page flag set to false. diff --git a/spec/site_dir/_layouts/alt-layout.html b/spec/test_source/_layouts/alt-layout.html similarity index 100% rename from spec/site_dir/_layouts/alt-layout.html rename to spec/test_source/_layouts/alt-layout.html diff --git a/spec/test_source/_layouts/default.html b/spec/test_source/_layouts/default.html new file mode 100644 index 0000000..4dac77f --- /dev/null +++ b/spec/test_source/_layouts/default.html @@ -0,0 +1,3 @@ +
{% if post_page %}

post_page flag set for layout

{% endif %} + +{{ content }}
diff --git a/spec/test_source/_posts/1920-01-01-test--page-links--very-first-post b/spec/test_source/_posts/1920-01-01-test--page-links--very-first-post new file mode 100644 index 0000000..e0ab5f5 --- /dev/null +++ b/spec/test_source/_posts/1920-01-01-test--page-links--very-first-post @@ -0,0 +1,4 @@ +title: Very first post +Created: 1920-01-01T12:30:30Z + +This is the very first post, by publish time, and its "previous post" link should not appear. diff --git a/spec/test_source/_posts/1921-01-01-test--page-links--second-post b/spec/test_source/_posts/1921-01-01-test--page-links--second-post new file mode 100644 index 0000000..06b932a --- /dev/null +++ b/spec/test_source/_posts/1921-01-01-test--page-links--second-post @@ -0,0 +1,4 @@ +title: Second post +Created: 1921-01-01T12:30:30Z + +This is the second post in the blog, and it should appear as the "next post" link for the very first post. diff --git a/spec/test_source/_posts/2015-01-01-test--permalinks-from-config-file b/spec/test_source/_posts/2015-01-01-test--permalinks-from-config-file new file mode 100644 index 0000000..8bae923 --- /dev/null +++ b/spec/test_source/_posts/2015-01-01-test--permalinks-from-config-file @@ -0,0 +1,4 @@ +title: A post +created: 2015-01-01T12:30:00Z + +This file should be generated based on the config file's permalink value. diff --git a/spec/test_source/_posts/2015-01-02-test--post-with-custom-layout b/spec/test_source/_posts/2015-01-02-test--post-with-custom-layout new file mode 100644 index 0000000..8be1b7e --- /dev/null +++ b/spec/test_source/_posts/2015-01-02-test--post-with-custom-layout @@ -0,0 +1,5 @@ +title: Custom layout post +created: 2015-03-07T00:00:00+00:00 +layout: alt-layout + +This post file should use the custom layout given above. diff --git a/spec/test_source/_posts/2015-01-03-test--published-posts-get-draft-preview-false b/spec/test_source/_posts/2015-01-03-test--published-posts-get-draft-preview-false new file mode 100644 index 0000000..6f2aed8 --- /dev/null +++ b/spec/test_source/_posts/2015-01-03-test--published-posts-get-draft-preview-false @@ -0,0 +1,4 @@ +title: No draft preview flag test +created: 2015-01-03T12:30:30Z + +This post should be rendered without the draft_preview flag set. diff --git a/spec/test_source/_posts/2015-01-04-test--posts-get-post-page-flag-true b/spec/test_source/_posts/2015-01-04-test--posts-get-post-page-flag-true new file mode 100644 index 0000000..d74a866 --- /dev/null +++ b/spec/test_source/_posts/2015-01-04-test--posts-get-post-page-flag-true @@ -0,0 +1,4 @@ +title: Sample post +created: 2015-01-04T12:30:00Z + +This post should be generated with the post_page flag set to true. diff --git a/spec/test_source/_posts/2399-01-01-test--page-links--penultimate-post b/spec/test_source/_posts/2399-01-01-test--page-links--penultimate-post new file mode 100644 index 0000000..0257faa --- /dev/null +++ b/spec/test_source/_posts/2399-01-01-test--page-links--penultimate-post @@ -0,0 +1,4 @@ +title: Penultimate post +Created: 2399-01-01T00:00:00+00:00 + +This is the penultimate post, and should appear as the "previous post" link for the final post. diff --git a/spec/test_source/_posts/2400-01-01-test--page-links--final-post b/spec/test_source/_posts/2400-01-01-test--page-links--final-post new file mode 100644 index 0000000..631b018 --- /dev/null +++ b/spec/test_source/_posts/2400-01-01-test--page-links--final-post @@ -0,0 +1,4 @@ +title: Final post +Created: 2400-01-01T00:00:00+00:00 + +This is the final post in the blog, by publish time, and its "next post" link should not appear. diff --git a/statics/skeleton/_templates/archive_page.html b/spec/test_source/_templates/archive_page.html similarity index 100% rename from statics/skeleton/_templates/archive_page.html rename to spec/test_source/_templates/archive_page.html diff --git a/spec/site_dir/_templates/post.html b/spec/test_source/_templates/post.html similarity index 87% rename from spec/site_dir/_templates/post.html rename to spec/test_source/_templates/post.html index b305d6c..7a8b821 100644 --- a/spec/site_dir/_templates/post.html +++ b/spec/test_source/_templates/post.html @@ -1,4 +1,4 @@ -{% if draft_preview %}

draftpreviewflagexists

{% endif %} +{% if draft_preview %}

this is a draft preview

{% endif %} {% if post_page %}

post_page flag set for template

{% endif %}

{{ post.title }}

diff --git a/spec/test_source/archive.html b/spec/test_source/archive.html new file mode 100644 index 0000000..ddc114f --- /dev/null +++ b/spec/test_source/archive.html @@ -0,0 +1,15 @@ +{% for year in site.archive.years %} +
+
{{ year.date | date: "%Y" }} (post count: {{ year.posts | size }})
+ + {% for month in year.months %} +
+
{{ month.date | date: "%Y %B" }} (post count: {{ month.posts | size }})
+ +

+ {{ month.date | date: "%B %Y" }} +

+
+ {% endfor %} +
+{% endfor %} diff --git a/statics/skeleton/index.html b/spec/test_source/index.html similarity index 100% rename from statics/skeleton/index.html rename to spec/test_source/index.html diff --git a/spec/site_dir/page-header-but-no-layout.html b/spec/test_source/page-header-but-no-layout.html similarity index 100% rename from spec/site_dir/page-header-but-no-layout.html rename to spec/test_source/page-header-but-no-layout.html diff --git a/spec/test_source/test--page-get-post-page-flag-false.html b/spec/test_source/test--page-get-post-page-flag-false.html new file mode 100644 index 0000000..21f475c --- /dev/null +++ b/spec/test_source/test--page-get-post-page-flag-false.html @@ -0,0 +1 @@ +

This page should be rendered without the post_page flag set to false.

diff --git a/spec/test_source/test--page-with-a-custom-layout-header-value.html b/spec/test_source/test--page-with-a-custom-layout-header-value.html new file mode 100644 index 0000000..03568b1 --- /dev/null +++ b/spec/test_source/test--page-with-a-custom-layout-header-value.html @@ -0,0 +1,3 @@ +layout: alt-layout + +This page should use the custom layout header value specified above. diff --git a/spec/site_dir/file-digest-test.html b/spec/test_source/test--page-with-a-filedigest-filter.html similarity index 71% rename from spec/site_dir/file-digest-test.html rename to spec/test_source/test--page-with-a-filedigest-filter.html index d3eefcf..5bd1284 100644 --- a/spec/site_dir/file-digest-test.html +++ b/spec/test_source/test--page-with-a-filedigest-filter.html @@ -1,4 +1,5 @@ layout: none {% file_digest test-stylesheet.css %} +{% file_digest /test-stylesheet.css %} {% file_digest test-stylesheet.css prefix:. %} diff --git a/spec/site_dir/test-stylesheet.css b/spec/test_source/test-stylesheet.css similarity index 100% rename from spec/site_dir/test-stylesheet.css rename to spec/test_source/test-stylesheet.css diff --git a/statics/assets/js/attachment.js b/statics/assets/js/attachment.js deleted file mode 100644 index b1723c0..0000000 --- a/statics/assets/js/attachment.js +++ /dev/null @@ -1,124 +0,0 @@ -// Derived by Adam Prescott from a code snippet used -// with implied permission: -// -// http://blog.alexmaccaw.com/svbtle-image-uploading - -var createAttachment = function(file, element) { - var data = new FormData(); - - var d = new Date(); - var uid = d.getTime(); - - var month = d.getMonth().toString(); - if (month.length < 2) { month = "0" + month; } - - var day = d.getDate().toString(); - if (day.length < 2) { day = "0" + day; } - - var processedName = file.name.replace(/[^a-zA-Z0-9_]/g, "_").replace(/__+/g, "_"); - - var s = Serif.variables["imageUploadPattern"]; - - if (/:slug/.test(s) && !(Serif.variables["currentSlug"] && Serif.variables["currentSlug"])) { - alert("Your image upload path is set to use a slug, but no such slug exists yet."); - return null; - } - - var placeholderValues = { - ":slug": Serif.variables["currentSlug"], - ":timestamp": uid.toString(), - ":year": d.getFullYear().toString(), - ":month": month, - ":day": day, - ":name": processedName - }; - - $.each(placeholderValues, function(placeholder, value) { - s = s.replace(placeholder, value); - }); - - var extension = file.name.substring(file.name.lastIndexOf('.') + 1); - - var finalName = s; - - // if it doesn't already have the extension in the name, add it, - // otherwise, correct the, e.g., _png, to .png. - // - // this just avoids _png.png noise - if (finalName.substring(finalName.length - extension.length - 1) == ("_" + extension)) { - finalName = finalName.replace(new RegExp("_" + extension + "$"), "." + extension); - } else { - finalName = finalName + "." + extension; - } - - data.append('attachment[file]', file); - data.append('attachment[uid]', uid); - data.append('attachment[final_name]', finalName); - - $.ajax({ - url: '/admin/attachment', - data: data, - cache: false, - contentType: false, - processData: false, - type: 'POST', - }).error(function() { - console.log("error uploading image"); - }); - - var absText = '![' + file.name + '](' + finalName + ')'; - - // for some reason this is necessary to avoid the following - // giving an undefined result: - // - // 1. load a new draft page with an empty textarea - // 2. at this point element.value is undefined. - // 3. drag an image, calling insertAtCaret - // 4. element.value (3) in this method is still undefined (confusing!) - // 5. in the browser console, $("[data-attachify]").get(0).value - // is correct. (confusing!) - // 6. on a _second drag_, element.value is undefined (very confusing) - // 7. in the same second drag event, $(element).get(0).value is - // correct, hence this "reloading" to allow element.value - // below to not be undefined. - element = $(element).get(0); - - if (typeof element.value != "undefined") { - var pos = element.selectionStart; - var text = element.value; - var before = text.slice(0, pos); - var after = text.slice(pos); - - // if there is only a single newline, add one more for a blank - // line. - if (/[^\n]\n$/.test(before)) { - absText = "\n" + absText; - // if there aren't two new lines, add a full two - } else if (! /\n\n$/.test(before)) { - absText = "\n\n" + absText; - } - - } - - $(element).insertAtCaret(absText); -}; - -$(function() { - if ($("[data-attachify]").length > 0) { - $(document).dropArea(); - - $(document).bind("drop", function(e) { - e.preventDefault(); - e = e.originalEvent; - - var files = e.dataTransfer.files; - - for (var i=0; i < files.length; i++) { - // Only upload images - if (/image/.test(files[i].type)) { - createAttachment(files[i], $("[data-attachify]").first()); - } - }; - }); - } -}); diff --git a/statics/assets/js/jquery.autosize.js b/statics/assets/js/jquery.autosize.js deleted file mode 100644 index 3900476..0000000 --- a/statics/assets/js/jquery.autosize.js +++ /dev/null @@ -1,175 +0,0 @@ -// Autosize 1.13 - jQuery plugin for textareas -// (c) 2012 Jack Moore - jacklmoore.com -// license: www.opensource.org/licenses/mit-license.php - -(function ($) { - var - hidden = 'hidden', - borderBox = 'border-box', - lineHeight = 'lineHeight', - copy = ' - - - {% if post.draft %} - - {% else %} - - {% endif %} - -
-
- - -
-
- - - - - - {% if post.draft %} - - {% endif %} - - - -{% if post.draft %} -
- {% if post.slug %}{% endif %} - - -
-{% endif %} - - - -
diff --git a/statics/templates/admin/edit_post.liquid b/statics/templates/admin/edit_post.liquid deleted file mode 100644 index b73214f..0000000 --- a/statics/templates/admin/edit_post.liquid +++ /dev/null @@ -1,40 +0,0 @@ -{% if error_message %} -
-

{{ error_message }}

-
-{% endif %} - -
-
- - -

- - posts/ - -

- -
-
-

- -

- - -
-
-
- - -
-
- - - - -
- - -
diff --git a/statics/templates/admin/index.liquid b/statics/templates/admin/index.liquid deleted file mode 100644 index 578010b..0000000 --- a/statics/templates/admin/index.liquid +++ /dev/null @@ -1,19 +0,0 @@ -

Drafts

- -

(Most recently modified first.)

- - - -

Posts

- -

(Most recently published first.)

- - diff --git a/statics/templates/admin/layout.liquid b/statics/templates/admin/layout.liquid deleted file mode 100644 index 4735d7a..0000000 --- a/statics/templates/admin/layout.liquid +++ /dev/null @@ -1,603 +0,0 @@ - - - - - Admin - - - - - - - - - - - - - - - - - - - - {% if conflicts %} -

The following URLs have conflicts and must be resolved:

-
    - {% for conflict in conflicts %} -
  • - {{ conflict[0] | escape }} -
      - {% for conflicting_content in conflict[1] %} -
    • {% if conflicting_content.draft %}Draft{% else %}Post{% endif %}: {{ conflicting_content.title | escape }}
    • - {% endfor %} -
    -
  • - {% endfor %} -
- {% endif %} - - {{ content }} - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Ctrl-/ / Command-/Show/hide keyboard shortcut
g hGo to the admin homepage
g nGo to the new draft page
ESC / Ctrl-[ / Command-[Remove any <input> focus
Ctrl-. / Command-.Toggle focused view
Ctrl-S / Command-SSave
Ctrl-U / Command-UJump to URL name
Ctrl-H / Command-HJump to title/heading
Ctrl-Enter / Command-EnterToggle rendered preview
-
- - diff --git a/statics/templates/admin/new_draft.liquid b/statics/templates/admin/new_draft.liquid deleted file mode 100644 index 1b3b98a..0000000 --- a/statics/templates/admin/new_draft.liquid +++ /dev/null @@ -1,39 +0,0 @@ -{% if error_message %} -
-

{{ error_message }}

-
-{% endif %} - -
-
- - -

- - drafts/ - -

- -
-
-

- -

-
-
-
- - -
-
- - - - -
- - - -
diff --git a/vendor/cache/capybara-2.5.0.gem b/vendor/cache/capybara-2.5.0.gem deleted file mode 100644 index 3b559dc..0000000 Binary files a/vendor/cache/capybara-2.5.0.gem and /dev/null differ diff --git a/vendor/cache/cliver-0.3.2.gem b/vendor/cache/cliver-0.3.2.gem deleted file mode 100644 index 3635c7e..0000000 Binary files a/vendor/cache/cliver-0.3.2.gem and /dev/null differ diff --git a/vendor/cache/gherkin-2.12.2.gem b/vendor/cache/gherkin-2.12.2.gem deleted file mode 100644 index b8da516..0000000 Binary files a/vendor/cache/gherkin-2.12.2.gem and /dev/null differ diff --git a/vendor/cache/multi_json-1.11.2.gem b/vendor/cache/multi_json-1.11.2.gem deleted file mode 100644 index b65fbbb..0000000 Binary files a/vendor/cache/multi_json-1.11.2.gem and /dev/null differ diff --git a/vendor/cache/poltergeist-1.8.0.gem b/vendor/cache/poltergeist-1.8.0.gem deleted file mode 100644 index efe9d7c..0000000 Binary files a/vendor/cache/poltergeist-1.8.0.gem and /dev/null differ diff --git a/vendor/cache/rack-test-0.6.3.gem b/vendor/cache/rack-test-0.6.3.gem deleted file mode 100644 index 914afe9..0000000 Binary files a/vendor/cache/rack-test-0.6.3.gem and /dev/null differ diff --git a/vendor/cache/rdoc-4.2.0.gem b/vendor/cache/rdoc-4.2.0.gem deleted file mode 100644 index 1cec4c7..0000000 Binary files a/vendor/cache/rdoc-4.2.0.gem and /dev/null differ diff --git a/vendor/cache/reverse_markdown-1.0.0.gem b/vendor/cache/reverse_markdown-1.0.0.gem deleted file mode 100644 index 3a6aa2b..0000000 Binary files a/vendor/cache/reverse_markdown-1.0.0.gem and /dev/null differ diff --git a/vendor/cache/rspec-its-1.2.0.gem b/vendor/cache/rspec-its-1.2.0.gem new file mode 100644 index 0000000..ef4ed50 Binary files /dev/null and b/vendor/cache/rspec-its-1.2.0.gem differ diff --git a/vendor/cache/timeout_cache-0.0.2.gem b/vendor/cache/timeout_cache-0.0.2.gem deleted file mode 100644 index 5e13379..0000000 Binary files a/vendor/cache/timeout_cache-0.0.2.gem and /dev/null differ diff --git a/vendor/cache/turnip-1.3.1.gem b/vendor/cache/turnip-1.3.1.gem deleted file mode 100644 index 5cc24d6..0000000 Binary files a/vendor/cache/turnip-1.3.1.gem and /dev/null differ diff --git a/vendor/cache/websocket-driver-0.6.3.gem b/vendor/cache/websocket-driver-0.6.3.gem deleted file mode 100644 index eea0f01..0000000 Binary files a/vendor/cache/websocket-driver-0.6.3.gem and /dev/null differ diff --git a/vendor/cache/websocket-extensions-0.1.2.gem b/vendor/cache/websocket-extensions-0.1.2.gem deleted file mode 100644 index 59ad4be..0000000 Binary files a/vendor/cache/websocket-extensions-0.1.2.gem and /dev/null differ diff --git a/vendor/cache/xpath-2.0.0.gem b/vendor/cache/xpath-2.0.0.gem deleted file mode 100644 index fedf0e2..0000000 Binary files a/vendor/cache/xpath-2.0.0.gem and /dev/null differ